Bildiğimiz gibi modern yazılım geliştirme ortamında containerization’ın benimsenmesi, uygulamaların oluşturulma ve dağıtılma şekillerini oldukça değiştirdi. Container’ların lightweight ve self-contained birimler olması, uygulamalarımızı farklı ortamlar arasında consistent bir şekilde kolayca taşıyabilme ve hızlı bir şekilde scale edebilme gibi bir çok farklı anlamda avantajlar ve esneklikler sağlamaktadır.
Ayrıca container’lar scalable microservice architecture’ları için de, en önemli building block’larından birisidir. Kubernetes gibi container orchestration araçları ile birlikte de kaynaklarımızı daha efektif bir şekilde kullanmamıza olanak tanıyarak, uygulamalarımızı esnek bir şekilde scale edebilmemizi ve değişen ihtiyaçlar karşısında hızlı bir şekilde hareket edebilmemizi de sağlamaktadır.
Elbette, container’ların sunduğu avantajların yanı sıra, maalesef güvenlikle ilgili kapsamlı bir yaklaşım gerektiren karmaşıklıkları da beraberinde getirmektedir. Bir container’daki bir güvenlik sorununun tüm sistemi tehlikeye atabilme olasılığının var olabilmesinden dolayı, sahip olduğumuz tüm container ekosistemini uçtan uca kapsayacak düzgün bir güvenlik duruşuna sahip olmamız oldukça önem arz etmektedir. Bir başka değişle containerized uygulamaların güvenlik risk’lerinin azaltılması ve gerekli önlemlerin alınması, bir best practice olmaktan öte artık organizasyonlar için stratejik bir zorunluluk haline gelmiştir.
Geçtiğimiz yıllarda gerçekleştirilen SolarWinds, Log4j (hatırlarsanız özellikle Log4j gündemimizi baya meşgul etmişti) gibi saldırılara dikkat ettiğimizde, özellikle Software Supply Chain’in hedef alındığını görebiliriz. Ayrıca uygulamalarımızı geliştirirken oldukça fazla open-source kütüphanelerden yararlandığımızı da göz önüne aldığımızda, uygulamalarımızın kötü amaçlı kod enjeksiyonlarına ve güvenliklerinin ihlal edilmesine ne kadar hassas ve savunmasız olduklarını da fark edebiliriz.
Genel anlamda software supply chain’i güçlendirmek için SDLC süreçleri boyunca uygulayabileceğimiz çeşitli önleyici kontroller ve yaklaşımlar mevcuttur. Günümüzde farklı organizasyonlar bu konuda best practice’lere sahip olabilmek adına güvenlik konusunda shifting-left yaklaşımını benimseyerek, güvenlik unsurlarını olabildiğince SDLC süreçlerinin erken aşamalarına entegre etmeye çalışmaktadırlar. Bu kontroller ve yaklaşımlar, software supply chain süreçlerindeki güvenlik açıklarını en aza indirerek bütünlüğü sağlamak, potansiyel güvenlik risklerini minimize etmek ve hızlı hareket edebilmek adına önemli roller oynamaktadır.
Örneğin The National Institute of Standards and Technology (NIST) tarafından yayımlanan “Zero Trust Architecture” ve “Strategies for the Integration of Software Supply Chain Security in DevSecOps CI/CD Pipelines” gibi kaynaklar, ekosistemde en az ayrıcalık ilkesinin benimsenmesinden, CI/CD süreçlerinde çeşitli politikalar ile güvenli build’lerin gerçekleştirilmesine, software composition analysis (SCA) ve static application security testing (SAST) gibi çeşitli araçların erken aşamalarda kullanılmasına kadar çeşitli öneriler sunmaktadır.
Ayrıca Microsoft’un Containers Secure Supply Chain (CSSC) framework’ü ve Google tarafından geliştirilen Supply-chain Levels for Software Artifacts (SLSA) güvenlik framework’ü, güvenli bir software supply chain oluşturmak için oldukça kapsamlı yönergeler sağlamaktadır.
Bu makale kapsamında, CI/CD aşamaları boyunca containerized uygulamaların bütünlüğünü sağlamaya ve kaynağını izlemeye yönelik yaklaşımları inceleyerek software supply chain güvenliğini sağlamaya bir göz atacağız. Ayrıca yukarıda paylaştığım bazı yönergeleri dikkate alarak güvenlik risklerini azaltmaya yönelik bazı önlemlere de bakacağız.
Bu bağlamda sırasıyla aşağıdaki konulara değiniyor olacağız.
- Güvenlik taraması gerçekleştirilmesi ve raporunun oluşturulması
- SBOM dokümanının oluşturulması
- Oluşturulan artifact’lerin imzalanması
- Oluşturulan artifact’lerin ilgili container image’i ile ilişkilendirilmesi
- CD süreçleri sırasında, oluşturulan artifact’lerin ve imzalarının doğrulanması
- Bu makalenin ikinci bölümünde ise Kubernetes ortamına bir container dağıtmadan önce, OPA Gatekeeper ve Ratify Verification Engine kullanarak, CD süreçleri sırasında politikalarla farklı güvenlik kontrollerinin sağlanmasından bahsedeceğiz
Şimdi oluşturulma aşaması (CI) süreçleri ile başlayalım!
Container Security Scanning
Tahmin edebileceğimiz gibi güvenilir olmayan kaynaklardan kullanacağımız container image’leri veya çeşitli kütüphaneler, içerebilecekleri malware veya güvenlik açıklarından dolayı önemli güvenlik riskleri oluşturabilirler. Bu güvenlik risk’leri open-source kütüphaneler kullanarak geliştirmiş olduğumuz kodda veya kullanıyor olduğumuz container image’lerinde saklanıyor olabilir.
Potansiyel güvenlik risklerini ele alabilmek ve en aza indirebilmek adına DevSecOps prensiplerini de dikkate alarak, CI süreçlerimizde oluşturulan her bir container image’ini ilgili container registry’lerimize göndermeden önce, container security scanning araçları ile detaylı bir güvenlik doğrulamasından geçiriyor olmamız gerekmektedir. Otomatikleştirilecek olan bu kontrol, container image’lerimizdeki potansiyel güvenlik açıklarını tespit ederek, olası riskleri önceden ele alabilmemize olanak tanıyacak ve ayrıca proaktif bir container güvenlik duruşuna ve software supply chain güvenliği için ilk önemli adımı atacaktır.
Her ne kadar basit bir konu gibi görünse de, container’ların güvenlik için yaşam döngülerini kontrol edebilmek benim için her zaman bir karın ağrısı olmuştur. Çünkü bu süreç herhangi bir base container image’ini dış bir kaynaktan çekip kendi iç ortamımıza güvenli bir şekilde getirmekten başlayıp, oluşturulacak olan ilgili container’ın production ortamına dağıtılma aşamasına kadar olan süreçleri kapsamaktadır.
Konumuza geri dönecek olursak, güvenlik duruşumuzu güçlendirebilmek için CI süreçlerimize dahil edebileceğimiz “Trivy“, “Twistlock“, “Grype” ve “Snyk” gibi çeşitli on-demand container güvenlik taraması araçları mevcuttur. Hangi araç’ı kullanacağımız farketmeksizin, ben bu makale kapsamında Trivy üzerinden ilerleyeceğim.
Trivy için kısaca gelişmiş open-source bir güvenlik taraması aracı diyebiliriz. Container güvenlik açıklarının taranmasının yanı sıra “git repository’ler“, “filesystem” ve “kubernetes” gibi çeşitli hedefler üzerinde de güvenlik taraması gerçekleştirebilmektedir.
Ayrıca güvenlik açıklarının yanı sıra (CVE), uygulama bağımlılıkları, IaC üzerindeki hatalar ve secret’ler gibi hassas bilgiler için de tarama gerçekleştirebilmektedir.
Makale boyunca örnek uygulama olarak daha önceki eski makalelerimde geliştirip, kullanmış olduğum .NET 5 tabalı Order API ‘ını baz alacağım. Ilgili repository’e buradan erişebilirsiniz. Eski bir örnek uygulama olması sayesinde güvenlik açıkları bakımından da örneğimize tam olarak uygun. DevOps ortamı olarak ise Azure Pipelines üzerinden ilerleyeceğim. OCI image ve artifact’leri için de Azure Container Registry ‘i (ACR) kullanacağım.
Öncelikle ilk aşama olarak aşağıdaki gibi multi-stage bir pipeline tanımlayalım.
trigger: - master pool: vmImage: ubuntu-latest variables: acrServiceConnectionName: 'MyPOCRegistry' acrName: 'YOUR_ACR_NAME' orderAPIImageName: 'order-api' stages: # Other stages for SAST, SCA tools... - stage: BuildAndScanStage displayName: 'Build & Scan Stage' jobs: - job: BuildAndScanContainerImage displayName: 'Build & scan the container image' steps: - task: Docker@2 displayName: 'Build $(orderAPIImageName) container image' inputs: containerRegistry: '$(acrServiceConnectionName)' repository: '$(orderAPIImageName)' command: 'build' Dockerfile: './OrderAPI/Dockerfile' buildContext: '.' tags: '1.0.0'
Bu noktadaki ilk amacımız ilgili source kodumuzu SAST ve SCA gibi araçların kontrollerinden geçirdikten sonra, containerized bir hale getirmek. Burada ilgili SAST ve SCA araçlarımızın mevcut olduğunu varsayacağız. Ardından oluşturulacak olan container image’ini de ilgili registry’e göndermeden önce, güvenlik kontrolünden geçirmek. Tabi buradaki yaklaşım ve politikalar, kurumdan kuruma da farklılık gösterebilmektedir.
“BuildAndScanContainerImage” job’ı içerisinde gerçekleştirdiğimiz containerization işleminden sonra, Trivy ‘i CI süreçlerimize dahil edebilmek için aşağıdaki iki task’ı ve “trivyVersion” variable’ını pipeline’a dahil edelim.
variables: ... trivyVersion: '0.48.0'
- task: Bash@3 displayName: 'Download Trivy v$(trivyVersion)' inputs: targetType: 'inline' script: | wget https://github.com/aquasecurity/trivy/releases/download/v$(trivyVersion)/trivy_$(trivyVersion)_Linux-64bit.deb sudo dpkg -i trivy_$(trivyVersion)_Linux-64bit.deb trivy -v - task: Bash@3 displayName: 'Scan the $(orderAPIImageName) container image for vulnerabilities' inputs: targetType: 'inline' script: | trivy image --exit-code 0 --severity HIGH,CRITICAL --scanners vuln $(acrName).azurecr.io/$(orderAPIImageName):1.0.0
Trivy ‘nin agent üzerine kurulum işlemini tamamladıktan sonra, tarama işlemi için kullanmış olduğumuz komutlara bir bakalım.
- İlgili pipeline’ın örneğimiz boyunca durdurulmaması için, “–exit-code 0” parametresini kullandım. Eğer “1” olarak belirlersek, “HIGH” veya “CRITICAL” olan güvenlik ihlalleri karşısında ilgili pipeline otomatik olarak durdurulacaktır. Ayrıca “LOW” ve “MEDIUM” seviyeleri de mevcuttur.
- Ayrıca “–scanners vuln” parametresi ile de sadece vulnerability taraması gerçekleştirmesini sağladım. Bunlara ek olarak “secret“, “misconfig” parametreleri ile de farklı taramalar da gerçekleştirebiliriz. Özellikle IaC template’leri için oldukla kullanışlı.
Trivy belirmiş olduğumuz bu konfigürasyon ile, varsayılan olarak CVE sonuçlarını tablo formatında aşağıdaki gibi konsol üzerine yazmaktadır.
Gördüğümüz gibi kullanmış olduğumuz örnek uygulama oldukça eski olduğu için, toplamda 70 adet farklı CVE ‘ler tespit etti.
Bu aşamada Trivy, hem ilgili OS hem de ilgili uygulamamızın bulundurmuş olduğu kütüphaneler üzerinde de bir güvenlik taraması gerçekleştirmiştir.
NOT: Order API .NET temelli olduğu için, “**/*.deps.json” dosyaları üzerinden ilgili kullanılan kütüphanelere erişim sağlamaktadır.
Şimdi, ilgili CVE sonuçlarını bir sonraki aşamada doğrulama süreçlerinde kullanabilmek ve software supply chain’imizin bir artifact’i olarak saklayabilmek için, aşağıdaki gibi Trivy ‘i JSON bir çıktı oluşturabilecek bir şekilde yapılandıralım. Ayrıca, ilgili container image’ini de registry’e gönderelim.
NOT: Makale boyunca kolaylık olması açısından container image versiyon tag’i olarak “1.0.0” üzerinden ilerleyeceğim.
- task: Bash@3 displayName: 'Scan the $(orderAPIImageName) container image for vulnerabilities' inputs: targetType: 'inline' script: | trivy image --exit-code 0 --severity HIGH,CRITICAL --security-checks vuln --format sarif --output ./trivy-sarif.json $(acrName).azurecr.io/$(orderAPIImageName):1.0.0 - task: Docker@2 displayName: 'Push $(orderAPIImageName) container image' inputs: containerRegistry: '$(acrServiceConnectionName)' repository: '$(orderAPIImageName)' command: 'push' tags: '1.0.0'
Bu noktada ilgili container’ı güvenlik taramasından geçirmiş ardından ilgili container image’ini registry’e göndermiş olacağız. Ayrıca, container güvenlik taraması sonucunu standart bir formatta olması ve farklı araçlarla kolay entegrasyon sağlaması nedeniyle SARIF olarak belirledik. Şimdi ise SARIF formatında elde edecek olduğumuz container güvenlik taraması sonucunu software supply chain’in bir parçası olarak saklayabilmek için, ORAS (OCI Registry As Storage) aracından yararlanacağız.
ORAS, kısaca OCI image’lerini ve supply chain artifact’lerini OCI registry’leri üzerinde yönetebilmemizi sağlayan bir araçtır.
- task: AzureCLI@2 displayName: 'Attach the scan result to the $(orderAPIImageName) container image' inputs: azureSubscription: 'DevOpsPoC' scriptType: 'bash' scriptLocation: 'inlineScript' inlineScript: | az acr login --name $(acrName) oras attach --artifact-type application/sarif+json $(acrName).azurecr.io/$(orderAPIImageName):1.0.0 ./trivy-sarif.json:application/json
Bu noktada “oras attach” komutunu kullanarak, ilgili container image’i ile “trivy-sarif.json” artifact’i arasında bir referans oluşmasını sağlıyoruz.
NOT: Ben bu noktada ORAS ‘ın ACR ‘a erişim sağlayabilmesi için, AAD credentials yöntemini kullandım. Ayrıca service principle kullanarak da bu işlemi gerçekleştirebilmekteyiz. Örneğin: “oras login myregistry.azurecr.io –username $SP_APP_ID –password $SP_PASSWD“
Ayrıca buradaki önemli olan bir nokta ise, “artifact-type” parametresi ve isimlendirme kuralı. Bu parametre kısaca bizlere farklı artifact tiplerine göre filtreler kullanabilmemizi sağlamaktadır. Değer olarak istediğimiz değeri verebilmekteyiz. Standart isimlendirme kurallarına uymak istersek ise, [org|company|entity].[objectType].[optional-subType].config.[version]+[optional-configFormat] şeklinde bir isimlendirmeyi takip edebiliriz. Detaylı bilgi için ise, buraya bir göz atabilirsiniz.
Ben bu makale serisi kapsamında verification engine olarak Ratify kullanacak olduğum ve onun built-in doğrulama plugin’lerinden yararlanacağım için, “artifact-type” olarak Ratify ‘ın belirtmiş olduğu “application/sarif+json” tipini kullandım.
Pipeline’ı çalıştırdıktan sonra ise ORAS ‘ın “discover” komutunu kullanarak, aşağıdaki gibi ilgili container’ın artifact graph’ını görüntüleyebiliriz. Ayrıca belirttiğimiz gibi spesifik artifact tipine göre sorgulama yapabilmek için “–artifact-type” parametresini kullanabilmekteyiz.
Software Bill of Materials (SBOM) Oluşturmak
Software supply chain güvenliği için kullanabileceğimiz bir diğer önemli artifact ise SBOM dokümanlarıdır. SBOM kısaca bir uygulamanın oluşabilmesi ve çalıştırılabilmesi için kullanılan tüm bileşenleri ve kütüphaneleri versiyonları ile birlikte ayrıntılı bir şekilde listelemek amacıyla oluşturulan bir dokümandır.
SBOM dokümanları software supply chain içerisinde bir şeffaflık ve görünürlük getirdiği için, uyum politikalarında (lisans yönetimi, uyumluluk denetimi vb.) ve güvenlik açıklarının erken keşfedilmesi gibi konularda oldukça önemli bir konuma sahiptir. Özetle, uygulamalarımızın güvenliğini yaşam döngüsü boyunca sağlayabilmek ve bağımlılıklarını, kaynaklarını izleyip doğrulayabilmek için, SBOM dokümanlarından yararlanabilmekteyiz.
Ayrıca SBOM ‘ları yönetebilmek ve sürekli geri bildirimler alabilmek için farklı araç ve platformlar da mevcuttur. Böylece herhangi bir risk durumunda neyin nerede etkilendiğini hızlı bir şekilde görebilir ve aksiyonlar alabiliriz.
SBOM dokümanı oluşturabilmek için farklı araçlar mevcuttur. Trivy ‘de bunlardan birisi. Trivy hem CycloneDX hem de SPDX formatında SBOM dokümanları oluşturabilmektedir. Ben bu makale kapsamında Ratify verification engine kullanacağım için, SPDX formatında bir SBOM dokümanı oluşturacağım.
Şimdi aşağıdaki iki task’ı pipeline’a dahil edelim ve ilgili container için bir SBOM dokümanı oluşturulmasını sağlayalım. Ardından oluşturulacak olan SBOM dokümanını da software supply chain’imizin bir artifact’i olarak ilgili container’a ORAS vasıtasıyla ekleyelim.
- task: Bash@3 displayName: 'Create a SBOM document for the $(orderAPIImageName) container image' inputs: targetType: 'inline' script: | trivy image --format spdx --output ./sbom.spdx.json $(acrName).azurecr.io/$(orderAPIImageName):1.0.0 - task: AzureCLI@2 displayName: 'Attach the SBOM document to the $(orderAPIImageName) container image' inputs: azureSubscription: 'DevOpsPoC' scriptType: 'bash' scriptLocation: 'inlineScript' inlineScript: | oras attach --artifact-type application/spdx+json $(acrName).azurecr.io/$(orderAPIImageName):1.0.0 ./sbom.spdx.json:application/json
Şimdi tekrardan ORAS ‘ın “discover” komutunu çalıştırdığımızda, aşağıdaki gibi SBOM artifact’inin de ilgili graph’a eklendiğini görebiliriz.
Artifact’lere Erişmek
ORAS kullanarak bir container ile ilişkilendirmiş olduğumuz herhangi bir artifact’i de çekebilmek mümkün. Bunun için aşağıdaki komutu ilgili artifact’in digest bilgisi ile çalıştırmamız yeterli olacaktır. Bir önceki aşamada yaptığımız gibi istediğimiz bir artifact’in digest bilgisine ise ORAS ‘ın “discover” komutu ile kolaylıkla erişebiliriz.
oras pull IMAGE_URL@DIGEST -o .
Örneğin Order API container image’i ile ilişkilendirmiş olduğumuz SBOM dokümanının digest bilgisine erişebilmek için, aşağıdaki komutu kullanabiliriz.
oras discover -o json --artifact-type 'application/spdx+json' $(acrName).azurecr.io/$(orderAPIImageName):1.0.0 | jq -r ".manifests[0].digest"
İndirmiş olduğumuz SBOM dokümanına baktığımızda ise, Order API container image’i içerisinde kullanılan NUGET paketlerinden OS içerisindeki bağımlılıklara kadar versiyonları ile birlikte listelendiklerini görebiliriz. SBOM dokümanlarının sağlamış olduğu bu görünürlük ve şeffaflık sayesinde, ilgili bağımlılıkların güvenliğini değerlendirebilmemiz ve güncellemelerini yönetebilmemiz daha kolay bir hale gelmektedir.
Container Image’ini ve Artifact’lerini İmzalamak
Bu noktaya kadar Order API ‘ının container image’ini oluşturduk ve ilgili container’ı güvenlik taramasından geçirdik. Ardıdan ilgili güvenlik sonuçlarını software supply chain’in bir artifact’i olarak ACR üzerinde ORAS vasıtasıyla sakladık. Ayrıca software supply chain’i daha etkili bir şekilde yönetebilmek, üzerinde şeffaflık ve görünürlüğe sahip olabilmek adına container’ın SBOM dokümanını da oluşturduk ve sakladık.
Şimdi bir diğer önemli aşama ise, oluşturmuş olduğumuz container image’ini ve artifact’lerini imzalamak. Bir software dağıtıcısı olarak imzalama aşaması veya bir software tüketicisi olarak verilen imzayı doğrulama aşaması, software supply chain içerisinde güveni oldukça arttıran önemli bir unsurdur.
Bu sayede bir container image’inin bütünlüğünü, kaynağını ve oluşturulmasından bu yana herhangi bir değişikliğe uğramadığını doğrulayabilir, yazılım geliştirme ve dağıtım süreçleri içerisine doğal olarak bir güvenilirlik getirebiliriz.
Ben bu noktada imzalama ve imzalanmış artifact’leri doğrulama işlemleri için, Notary Project ‘den yararlanacağım. Dilerseniz Sigstore projesinin Cosign aracına da bir göz atabilirsiniz. Sigstore, oldukça geniş kapsamlı bir ekosistem sunmaktadır.
Notation CLI, OCI artifact’lerini imzalayabilmemizi ve kolayca doğrulayabilmemizi sağlayan, Notary Project specification’larını implemente eden bir supply chain aracıdır. Şimdi ilk olarak aşağıdaki gibi pipeline’a “SigningStage” adında yeni bir stage ekleyelim ve Notation CLI ‘ı bu stage için hazırlayalım.
variables: ... notationVersion: '1.1.0' notationTestKeyName: 'order-api.io'
- stage: SigningStage displayName: 'Sign Artifacts' dependsOn: BuildAndScanStage jobs: - job: SignContainerArtifacts displayName: 'Sign container artifacts' steps: - task: Bash@3 displayName: 'Download & Prepare Notation v$(notationVersion)' inputs: targetType: 'inline' script: | wget https://github.com/notaryproject/notation/releases/download/v$(notationVersion)/notation_$(notationVersion)_linux_amd64.tar.gz tar xvzf notation_$(notationVersion)_linux_amd64.tar.gz sudo mv notation /usr/local/bin notation cert generate-test --default $(notationTestKeyName)
Notation ‘ın hazırlanmasından sonra ise, kolay bir örnek gerçekleştirebilmek adına Notation ‘ın imzalama işlemlerinde kullanabileceği “order-api.io” isimli test RSA key’ini ve doğrulama işlemleri için kullanıyor olacağı test self-signed X.509 certificate’inin oluşturulmasını sağlıyoruz. Key ve certificate’in oluşturulmasının yanı sıra, ilgili key’i varsayılan imzalama key’i olarak da ayarlamaktadır. Ayrıca certificate’i ise Certificate Authority (CA) olarak “order-api.io” isimli trust store’a eklemektedir. Elbette production ortamları için self-signed bir test certificate’i yerine güvenilir bir CA tarafından oluşturulmuş bir certificate ile ilerlememiz, güvenlik açısından faydamıza olacaktır. Ayrıca key’ler ve certificate’ler için Notation ‘ın Azure Key Vault ve AWS Signer entegrasyonu da bulunmaktadır.
Örnek pipeline’a geri dönecek olursak, imzalama işlemlerini gerçekleştireceğimiz task’ları aşağıdaki gibi ekleyeme başlayabiliriz.
- task: AzureCLI@2 displayName: 'Sign the $(orderAPIImageName) container image' inputs: azureSubscription: 'DevOpsPoC' scriptType: 'bash' scriptLocation: 'inlineScript' inlineScript: | az acr login --name $(acrName) docker pull $(acrName).azurecr.io/$(orderAPIImageName):1.0.0 CONTAINER_DIGEST=$(docker inspect --format='{{index .RepoDigests 0}}' $(acrName).azurecr.io/$(orderAPIImageName):1.0.0) && CONTAINER_DIGEST=${CONTAINER_DIGEST#*@} notation sign --signature-format cose --key $(notationTestKeyName) $(acrName).azurecr.io/$(orderAPIImageName)@$CONTAINER_DIGEST - task: AzureCLI@2 displayName: 'Sign the $(orderAPIImageName) container scan result' inputs: azureSubscription: 'DevOpsPoC' scriptType: 'bash' scriptLocation: 'inlineScript' inlineScript: | VULNERABILITY_SCAN_RESULT_DIGEST=$(oras discover -o json --artifact-type 'application/sarif+json' $(acrName).azurecr.io/$(orderAPIImageName):1.0.0 | jq -r ".manifests[0].digest") notation sign --signature-format cose --key $(notationTestKeyName) $(acrName).azurecr.io/$(orderAPIImageName)@$VULNERABILITY_SCAN_RESULT_DIGEST - task: AzureCLI@2 displayName: 'Sign the $(orderAPIImageName) container SBOM document' inputs: azureSubscription: 'DevOpsPoC' scriptType: 'bash' scriptLocation: 'inlineScript' inlineScript: | SBOM_DIGEST=$(oras discover -o json --artifact-type 'application/spdx+json' $(acrName).azurecr.io/$(orderAPIImageName):1.0.0 | jq -r ".manifests[0].digest") notation sign --signature-format cose --key $(notationTestKeyName) $(acrName).azurecr.io/$(orderAPIImageName)@$SBOM_DIGEST
Dahil etmiş olduğumuz bu üç task ile sırasıyla, Order API container’ını, container güvenlik taraması sonuçlarını ve container SBOM dokümanını Notation yardımıyla imzalıyor ACR üzerinde saklıyoruz. Ayrıca “tag” bilgileri değiştirilebilir ve farklı container image’lerine referans edebilir bir yapıda olduğu için, imzalama sırasında ilgili artifact’leri belirtebilmek için “digest” bilgilerini kullanıyoruz. Ayrıca, imza formatı olarak ise IETF tarafından standart olarak kabul edilen “cose” formatını kullanıyoruz. Daha önce ilgili container image’i ile ilişkilendirmiş olduğumuz artifact’lere erişebilmek ve digest bilgilerini elde edebilmek için ise, bir önceki aşamalarda bahsettiğimiz gibi ORAS CLI ‘ının “discover” komutundan yararlanıyoruz.
Şimdi ilgili container’ın tekrardan artifact graph’ına baktığımızda, aşağıdaki gibi herbir artifact için imza bilgilerinin dahil edildiğini görebiliriz.
Böylece, sadece container image’lerini imzalamanın yanı sıra diğer supply chain artifact’lerini de imzalayarak veya imzalanmışsa ilerleyen süreçlerde ilgili imzaları doğrulayarak, ilgili diğer artifact’lerin de güvenilir bir kaynak tarafından oluşturulduğunun doğrulanabilmesini sağlıyoruz. Dolayısıyla, ilgili containerized uygulamanın bütünlüğünü korumaya yönelik ekstra bir güvenilirlik getirmiş oluyoruz.
Artık bu noktada oluşturulma aşaması süreçlerindeki temel işlemleri tamamladık. Tabi ihtiyaçlar doğrultusunda siz bu süreçlerin içerisine code coverage sonuçları gibi farklı araçları da dahil edebilirsiniz.
Şimdi dağıtma aşaması süreçlerine geçebiliriz.
Dağıtılma Öncesi Artifact’lerin Doğrulanması
Dağıtma öncesi imzalanmış container image’lerinin ve bu image’lerle ilişkilendirilmiş artifact’lerin doğrulanması, software supply chain içinde güvenilirlik, bütünlük ve güvenlik sağlamak için kritik bir adımdır. Bu doğrulama süreçlerini, ilgili artifact’in güvenilir kaynaklardan geldiğini, bütünlüğünü koruduğunu ve ilgili güvenlik mekanizmalarından geçtiğini doğrulamak için kullanabilmekteyiz.
Bu noktada yine ORAS ve Notation CLI’ ından yararlanacağız.
Daha önce Notation ‘ın imzalama işlemlerinde kullanabileceği test RSA key’ini ve doğrulama işlemleri için kullanacağı test self-signed certificate’lerini “SigningStage” içerisinde local olarak oluşturmuştuk. Doğrulama işlemlerini gerçekleştirebilmek için ise, yeni oluşturacağımız stage içerisinde de bu certificate bilgilerine ihtiyacımız var. Öncelikle bunun için “SigningStage” içerisinde aşağıdaki task’ı son aşama olarak ekleyelim ve ilgili certificate path’ini pipeline artifact’i olarak paylaşalım.
- task: PublishPipelineArtifact@1 inputs: targetPath: '$(Agent.BuildDirectory)/../../.config/notation/localkeys' artifact: 'notation' publishLocation: 'pipeline'
Şimdi “Dev” adında aşağıdaki gibi yeni bir stage ekleyelim ve pipeline artifact’i olarak paylaşmış olduğumuz certificate path’ini, bu stage içerisinde de aynı şekilde konumlandıralım. Bu stage’i örnek uygulamamız olan Order API ‘ını development ortamına dağıtacağımız stage olarak düşünebiliriz.
- stage: Dev displayName: 'Deploy to Dev' dependsOn: SigningStage jobs: - job: VerifyArtifacts displayName: 'Verify Artifacts' steps: - task: DownloadPipelineArtifact@2 inputs: buildType: 'current' artifactName: 'notation' downloadPath: '$(Agent.BuildDirectory)/../../.config/notation/localkeys'
Bu noktada doğrulama işlemlerini gerçekleştirebilmek için “VerifyArtifacts” adında yeni bir job tanımladık ve ilk aşaması olarak, pipeline artifact’i olarak paylaşmış olduğumuz certificate path’ini aynı şekilde konumlandırdık. Bir sonraki aşama olarak ise yine Notation CLI ‘ını ilgili stage için hazırlayacağız.
Notation CLI ‘ını hazırlarken paylaşmış olduğumuz certificate’i de Notation içerisine dahil edip, ayrıca ek olarak bir güven politikası da tanımlayacağız. Notation ile bir container image’ini veya herhangi imzalanmış bir artifact’i doğrulayabilmek için, güvenlik politikaları tanımlamamız gerekmektedir. Bu politikalar ile, artifact’leri imzalayan güvenilir kaynakları ve uygulanacak olan doğrulama seviyelerini belirleyebilmekteyiz.
Şimdi projenin ana klasörü altında “trust-policy.json” adında aşağıdaki gibi bir güven politikası oluşturalım.
{ "version": "1.0", "trustPolicies": [ { "name": "mytodo-store-images", "registryScopes": [ "*" ], "signatureVerification": { "level" : "strict" }, "trustStores": [ "ca:order-api.io" ], "trustedIdentities": [ "*" ] } ] }
Bu noktada, “registryScopes” key’i ile, tanımlamış olduğumuz bu politikanın uygulanacağı registry artifact’lerini belirtebilmekteyiz. Ben örnek olması açısından spesifik bir registry artifact’i belirtmedim, genel olarak uyguladım. Uygulanacak olan doğrulama seviyesini ise “signatureVerification” key’i ile belirtebilmekteyiz. Ben bu noktada, “strict” seviyesini seçtim. Bunun dışında “permissive“, “audit” ve “skip” seçenekleri de bulunmaktadır.
Belirlemiş olduğumuz bu doğrulama seviyesi ile Notation, ilgili artifact’ler üzerinde “Integrity“, “Authenticity“, “Authentic timestamp“, “Expiry” ve “Revocation check” gibi farklı kontroller gerçekleştirecektir.
NOT: “Authenticity” kontrolü ile ilgili artifact’in güvendiğimiz bir kaynak tarafından mı oluşturulduğuna bakarken, “Authentic timestamp” kontrolü ile de ilgili artifact’in ilgili certificate geçerliyken imzalanıp imzalanmadığına da bakmaktadır.
Ayrıca “trustStores” key’i ile, bir sonraki aşamada CA olarak dahil edeceğimiz ilgili trusted root’u barındıracak olan “order-api.io” trusted store’unu belirtiyoruz. Artifact’leri imzalayan güvenebileceğimiz kaynakları ise, “trustedIdentities” key’i ile tanımlıyoruz. Ben bu noktada, “*” değerini belirterek, trusted store içerisine ekleyeceğimiz CA tarafından verilmiş tüm certificate’leri güvenilir kimlikler, kaynaklar olarak belirledim. Bu konu hakkında daha fazla bilgiye ise, buradan erişebilirsiniz.
Şimdi Notation CLI ‘ını “Dev” stage’i için hazırlayabiliriz.
- task: Bash@3 displayName: 'Prepare Notation v$(notationVersion)' inputs: targetType: 'inline' script: | ls wget https://github.com/notaryproject/notation/releases/download/v$(notationVersion)/notation_$(notationVersion)_linux_amd64.tar.gz tar xvzf notation_$(notationVersion)_linux_amd64.tar.gz sudo mv notation /usr/local/bin notation policy import ./trust-policy.json notation cert add --type ca --store $(notationTestKeyName) $(Agent.BuildDirectory)/../../.config/notation/localkeys/order-api.io.crt
Bu noktada daha önce “SigningStage” içerisinde yaptığımızdan farklı olarak, tanımlamış olduğumuz “trust-policy” isimli güven politikasını Notation içerisine dahil ediyoruz. Ardından bir önceki stage’den elde etmiş olduğumuz “order-api.io” isimli CA certificate’ini de yine aynı isimde bir trusted store’a ekliyoruz.
Notation CLI ‘ını güven politikası ve ilgili doğrulama certificate’i ile donattığımıza göre, artık doğrulama aşamasına geçebiliriz. Bu aşama, ilgili container image’inin ve ilişkili artifact’lerinin orijinal ve güvenilir bir kaynaktan geldiğini doğrulamamıza olanak tanımaktadır. Bu kaynak, bizim tarafımızdan olabileceği gibi, farklı bir ekip veya farklı bir yayımcı da olabilir. Ayrıca biz bu noktada tek bir pipeline üzerinden ilerledik ancak CD aşaması için farklı süreçler ve pipeline’lara da sahip olabiliriz. Bu doğrulama süreci ayrıca ilgili artifact’lerin oluşturulduğu, imzalandığı ve dağıtıldığı süreçler boyunca herhangi bir değişikliğe uğramadıklarından emin olabilmemizi de sağlamaktadır. Ayrıca bu aşamada dağıtılmak istenilen ilgili uygulamanın, yani örneğimiz gereği Order API ‘ının, farklı güvenlik aşamalarından geçip geçmediğini kontrol edebilir, buna göre farklı politikalar da zorunlu kılabilmekteyiz.
Imza doğrulama işlemini gerçekleştirebilmemiz için yapmamız gereken ise, aşağıdaki gibi Notation ‘ın “verify” komutunu çalıştırmak.
notation verify IMAGE_URL@DIGEST
- task: AzureCLI@2 displayName: 'Verify the $(orderAPIImageName) container image signature' inputs: azureSubscription: 'DevOpsPoC' scriptType: 'bash' scriptLocation: 'inlineScript' inlineScript: | az acr login --name $(acrName) docker pull $(acrName).azurecr.io/$(orderAPIImageName):1.0.0 CONTAINER_DIGEST=$(docker inspect --format='{{index .RepoDigests 0}}' $(acrName).azurecr.io/$(orderAPIImageName):1.0.0) && CONTAINER_DIGEST=${CONTAINER_DIGEST#*@} notation verify $(acrName).azurecr.io/$(orderAPIImageName)@$CONTAINER_DIGEST
Bu noktada ilgili container image’inin digest bilgisini kullanarak, Notation ile imza doğrulama işlemini gerçekleştiriyoruz. Ayrıca ilgili container image’inin içerisinde herhangi bir imza manifest’i barındırmaması veya barındırdığı imza trusted store içerisine dahil etmiş olduğumuz “order-api.io” isimli CA certificate’i tarafından imzalanmaması durumunda, doğrulamanın başarısız olmasını sağlıyoruz.
Başarılı durumda ise aşağıdaki gibi bir sonuç elde edeceğiz:
Şimdi aşağıdaki task’ları da ekleyerek, container ile ilişkilendirmiş olduğumuz diğer artifact’lerin imzalarını da doğrulayalım.
- task: AzureCLI@2 displayName: 'Verify the $(orderAPIImageName) container scan result' inputs: azureSubscription: 'DevOpsPoC' scriptType: 'bash' scriptLocation: 'inlineScript' inlineScript: | az acr login --name $(acrName) RESULT=$(oras discover -o json --artifact-type 'application/sarif+json' $(acrName).azurecr.io/$(orderAPIImageName):1.0.0 | jq -r ".manifests[0]") if [ "$RESULT" = "null" ]; then echo "Container scan result does not exist." exit 1 else VULNERABILITY_SCAN_RESULT_DIGEST=$(echo "$RESULT" | jq -r ".digest") notation verify $(acrName).azurecr.io/$(orderAPIImageName)@$VULNERABILITY_SCAN_RESULT_DIGEST fi - task: AzureCLI@2 displayName: 'Verify the $(orderAPIImageName) container SBOM document' inputs: azureSubscription: 'DevOpsPoC' scriptType: 'bash' scriptLocation: 'inlineScript' inlineScript: | az acr login --name $(acrName) RESULT=$(oras discover -o json --artifact-type 'application/spdx+json' $(acrName).azurecr.io/$(orderAPIImageName):1.0.0 | jq -r ".manifests[0]") if [ "$RESULT" = "null" ]; then echo "Container SBOM document does not exist." exit 1 else SBOM_DIGEST=$(echo "$RESULT" | jq -r ".digest") notation verify $(acrName).azurecr.io/$(orderAPIImageName)@$SBOM_DIGEST fi
Bu aşamada gördüğümüz gibi basit bir kontrol ekleyerek, ilgili container image’inin gerekli güvenlik kontrollerinden geçip geçmediğini belirliyor ve buna göre pipeline’ın başarılı veya başarısız olmasını sağlıyoruz. Devamında ise ilgili artifact’lerin imzalarının doğrulama işlemlerini Notation vasıtasıyla gerçekleştiriyoruz.
Böylece sadece container image’inin orijinalliğini doğrulamanın yanı sıra, onunla ilişkilendirilmiş olan diğer artifact’leri de imzalayarak ve doğrulayarak, software supply chain’in tamamında ek bir güven zinciri sağlamış oluyoruz.
Son Sözler
Makale oldukça uzun olsa da, umarım containerized uygulamalar bağlamında software supply chain’in güvenliğine yönelik bir bakış açısı sunabilmişimdir.
Makalenin bir sonraki bölümünde ise, OPA Gatekeeper ve Ratify Verification Engine kullanarak, container image’i ile ilişkilendirmiş olduğumuz SBOM dokümanı, container güvenlik taraması sonucu gibi artifact’lerin detaylı kontrollerini kubernetes ortamına yayımlamadan önce çeşitli politikalar ile nasıl gerçekleştirebileceğimize odaklanacağız.
Referanslar
Sign container images with Notation and Azure Key Vault using a self-signed certificate – Azure Container Registry | Microsoft Learn
GitHub – notaryproject/notation: A CLI tool to sign and verify artifacts
Quickstart: Sign and validate a container image | Notary Project | A set of specifications and tools intended to provide a cross-industry standard for securing software supply chains.