Sürüm sabitleme, imzalama ve CI/CD güvenliği
- Kilit dosyası kurulan kodu, imza ve kaynak kanıtı onu kimin ürettiğini, tam commit SHA'sı da CI adımının değişmeyeceğini güvenceye alır.
- Önce package-lock alanlarını ve pip hash modunu oku. Ardından npm audit signatures ve cosign verify ile kaynağı doğrula. SLSA seviyelerini, GitHub Actions güvenlik ayarlarını ve tj-actions olayını da incele.
Puan bütün etkinlikleri cevaplayınca hesaba yazılır. Bu dersi tamamlayınca +55 XP alacaksın. Giriş yapmadan ilerlersen puan yalnız bu tarayıcıda durur.
Mart 2025'te binlerce ekip aynı haberle güne başladı. Yaygın bir GitHub Actions adımı ele geçirilmişti ve iş akışı günlüklerine sırlar basıyordu. Bu ekiplerin hiçbiri kendi kodunu değiştirmemişti.
Değişen, çağırdıkları adımın etiketinin arkasındaki koddu. Tedarik zinciri saldırısı yalnız paketlerden gelmez; derleme ve yayın hattının kendisi de bir hedeftir. Bu derste üç soruyu sırayla cevaplarsın.
Kurduğum kod dün kurduğumla aynı mı? Bunu kilit dosyası ve özet denetimi cevaplar. Bu kodu kim, nerede üretti? Bunu imza ve kaynak kanıtı cevaplar.
Derleme hattım ele geçirilirse ne olur? İş akışı sertleştirmesi bu riski küçültür. Bütün adımları Altay Kargo'nun iade servisinin yayın hattında izlersin.
Kilit dosyası nasıl kilitler
Manifest dosyası sürümü çoğu zaman bir aralıkla yazar. Bu esneklikle güvenlik yamalarını kolayca alırsın, ama aynı kapıyı saldırgan da kullanır. Bir bakımcı hesabı ele geçirilirse saldırgan zararlı kodu küçük bir yama sürümü olarak yayımlar.
Geniş aralık tanımlayan her proje, bir sonraki kurulumda bu sürümü fark etmeden çeker. Kilit dosyası bu kapıyı kapatır. package-lock.json içindeki her kayıt üç temel alan taşır.
version kurulan tam sürümdür. resolved paketin indirildiği adrestir. integrity ise paket arşivinin SHA-512 özetidir. npm arşivi indirince özetini hesaplar ve kayıttakiyle karşılaştırır; tek bir bayt değişmişse kurulum durur.
"node_modules/lodash": {
"version": "4.18.1",
"resolved": "https://npm.altaykargo.example/lodash/-/lodash-4.18.1.tgz",
"integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
"license": "MIT"
}Kilit dosyası ancak doğru komutla işe yarar. npm install gerekirse ağacı yeniden çözer ve kilit dosyasını sessizce günceller. npm ci ise kilidi olduğu gibi kurar, node_modules klasörünü sıfırdan oluşturur ve kilidi hiç değiştirmez.
package.json ile kilit dosyası uyuşmazsa hata verip durur. CI ortamında doğru komut budur. Güncelleme ayrı ve bilinçli bir iştir.
Değişiklik notlarını okursun, kilidi geliştirme makinende güncellersin, değişiklik ayrı bir commit olarak incelemeden geçer. Python tarafında aynı korumayı hash denetim modu kurar. requirements.txt dosyasındaki her satır bir --hash değeri taşır. pip install --require-hashes indirilen her dosyanın özetini bu değerle karşılaştırır.
$ npm ci npm error code EUSAGE npm error `npm ci` can only install packages when your package.json and package-lock.json or npm-shrinkwrap.json are in sync. Please update your lock file with `npm install` before continuing. npm error npm error Missing: [email protected] from lock file $ pip install --require-hashes -r requirements.txt ERROR: THESE PACKAGES DO NOT MATCH THE HASHES FROM THE REQUIREMENTS FILE. If you have updated the package versions, please update the hashes. Otherwise, examine the package contents carefully; someone may have tampered with them. markupsafe==3.0.2 from https://pypi.altaykargo.example/packages/markupsafe-3.0.2.tar.gz (from -r requirements.txt (line 1)): Expected sha256 ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0 Got 7a1c5e0b93d2f4486a0e1c9b7d3f25e8c6b0a4d1f9e7c3b5a2d8f0e6c4b1a3d9
İki çıktıyı oku. npm ci, package.json içinde olup kilit dosyasında olmayan semver paketini bulur ve kurulumu durdurur. Bir geliştirici paketi manifeste eklemiş, ama kilidi güncellememiştir. Doğru tepki kilidi geliştirme makinesinde güncelleyip incelemeye göndermektir.
pip çıktısında Expected satırı requirements.txt dosyasına yazılan özettir. Got satırı indirilen dosyanın gerçek özetidir. İkisi farklıysa arşiv, kayda geçen dosya değildir. Ayna bozulmuş, araya biri girmiş ya da paket değiştirilmiş olabilir.
pip bu durumda hiçbir şey kurmaz. Hash modunda özeti eksik bir satır da hata verir. Mod, tek bir satırda --hash görünce bütün dosya için kendiliğinden açılır.
Etkinlik 1 / 7
Sürüm belirtimlerini güvenli ve riskli pratiğe göre ayır.
Önce bir seçeneğe, sonra aşağıdaki uygun kutuya bas. İstersen seçeneği kutuya sürükleyip bırak. Yerini değiştirmek için aynı işlemi tekrarla.
0/4 seçenek yerleşti.
Etkinlik 2 / 7
CI'da kilit dosyasıyla manifest dosyası arasında tutarsızlık varsa kurulumu başarısız kılan, bağımlılıkları yeniden çözmeyen npm komutunu yaz.
İmza ve kaynak kanıtı: npm ve Sigstore
Kilit dosyası içeriğin değişmediğini kanıtlar, ama kaynağı kanıtlamaz. Kayıt sunucusu baştan zararlı bir arşiv sunduysa kayıttaki özet de o arşivin özetidir. Bu boşluğu imza ve kaynak kanıtı (provenance) kapatır. npm kayıt sunucusu her paket sürümünü kendi anahtarıyla imzalar; imza paket adını, sürümünü ve integrity özetini birlikte kapsar.
Güvenilir yayımlama ya da --provenance ile yayımlanan paketler ayrıca bir kaynak kanıtı taşır. Bu kanıt paketin hangi depodan, hangi commit'ten ve hangi CI işinden üretildiğini söyler. npm audit signatures komutu kurulu ağaçtaki her paketin kayıt imzasını ve varsa kaynak kanıtını doğrular.
$ npm audit signatures audited 214 packages in 3s 214 packages have verified registry signatures 38 packages have verified attestations
Çıktıyı oku. İlk satır denetlenen paket sayısıdır. İkinci satır, bütün paketlerin kayıt imzasının doğrulandığını söyler.
Üçüncü satır, 38 paketin ayrıca doğrulanmış bir kaynak kanıtı taşıdığını gösterir. Geri kalanlar kanıt üretmeden yayımlanmıştır; bu bir saldırı işareti değildir, ama güvenceleri daha düşüktür. İmzası geçersiz ya da eksik paket olsaydı çıktı onu ayrı bir satırda sayardı. Konteyner imajlarında aynı işi Sigstore'un cosign aracı yapar.
Sigstore anahtarsız (keyless) imza modelini kullanır. İmzalayan CI işi, OIDC kimliğiyle Fulcio'dan yalnız dakikalarca geçerli bir sertifika alır. İmzayı atar, kaydı Rekor şeffaflık günlüğüne yazar ve geçici anahtarı atar.
$ cosign verify kayit.altaykargo.example/iade-servisi@sha256:5c2e8f41a9d07b3e6c1f48a2d95b0e7c3a6f1d8b4e2c9a07f5d3b1e8c6a4f290 \
--certificate-identity=https://github.com/altaykargo/iade-servisi/.github/workflows/yayin.yml@refs/heads/main \
--certificate-oidc-issuer=https://token.actions.githubusercontent.com
Verification for kayit.altaykargo.example/iade-servisi@sha256:5c2e8f41a9d07b3e6c1f48a2d95b0e7c3a6f1d8b4e2c9a07f5d3b1e8c6a4f290 --
The following checks were performed on each of these signatures:
- The cosign claims were validated
- Existence of the claims in the transparency log was verified offline
- The code-signing certificate was verified using trusted certificate authority certificates
[{"critical":{"identity":{"docker-reference":"kayit.altaykargo.example/iade-servisi"},"image":{"docker-manifest-digest":"sha256:5c2e8f41a9d07b3e6c1f48a2d95b0e7c3a6f1d8b4e2c9a07f5d3b1e8c6a4f290"},"type":"cosign container image signature"},"optional":{"Issuer":"https://token.actions.githubusercontent.com","Subject":"https://github.com/altaykargo/iade-servisi/.github/workflows/yayin.yml@refs/heads/main"}}]Komutta iki bayrak kritiktir. --certificate-identity, imzayı atan kimliği tam olarak belirtir: iade-servisi deposundaki yayin.yml iş akışı, main dalında. --certificate-oidc-issuer, bu kimliği hangi kimlik sağlayıcının verdiğini söyler.
Kimlik vermeden doğrulama yapmak anlamsızdır, çünkü herkes kendi kimliğiyle geçerli bir imza atabilir. Çıktıdaki üç madde yapılan denetimleri sayar. İmza iddiaları doğrulandı, kaydın şeffaflık günlüğünde bulunduğu doğrulandı, sertifika güvenilir bir sertifika otoritesine bağlandı.
JSON içindeki docker-manifest-digest, imzanın bağlı olduğu imaj özetidir. optional altındaki Issuer ve Subject, sertifikadaki kimliği gösterir. Kimliği bir desenle vermek için --certificate-identity-regexp de vardır. Desen geniş tutulursa, örneğin yalnız ^https://github.com/ yazılırsa, herhangi bir deponun iş akışı geçer.
Derinleş:Şeffaflık günlüğü neden önemli
Rekor, Sigstore'un herkese açık şeffaflık günlüğüdür. Günlüğe yalnız ekleme yapılır; bir kayıt yazıldıktan sonra silinemez ve değiştirilemez. Her anahtarsız imza bu günlüğe sertifikasıyla birlikte düşer. Bunun savunmaya iki faydası vardır.
Birincisi zaman kanıtıdır. Sertifikanın süresi dakikalar içinde dolar, ama imza geçerli kalır, çünkü günlük imzanın sertifika geçerliyken atıldığını kayıt altına alır. İkincisi izlemedir. Altay Kargo, kendi iş akışı kimliğiyle atılan bütün imzaları günlükte arayabilir.
Ekibin başlatmadığı bir yayın için imza görünürse, kimlik ele geçirilmiş demektir. Gizli bir imza mümkün değildir. Saldırganın attığı imza da herkesin okuduğu günlüğe yazılır ve iz bırakır. Sigstore'un rekor-monitor projesi bu aramayı düzenli bir işe çevirir ve beklenmeyen bir kayıtta haber verir.
Etkinlik 3 / 7
Altay Kargo'nun dağıtım sistemi imajları cosign verify ile doğruluyor, ama kimliği --certificate-identity-regexp '^https://github.com/' deseniyle veriyor. Bu doğrulamanın zayıflığı nedir?
CI hattı bir hedeftir: SLSA ve tj-actions olayı
Derleme hattına ne kadar güvenebileceğini anlatan ortak bir dil vardır. SLSA (Supply-chain Levels for Software Artifacts), derleme kanıtlarının güvenilirliğini seviyelerle anlatır. Güncel sürüm 1.2'dir ve derleme izinde dört seviye vardır.
Build L0'da hiçbir güvence yoktur. Build L1'de paket nasıl derlendiğini anlatan bir kaynak kanıtı taşır, ama kanıt kolayca taklit edilir. Build L2'de derleme barındırılan bir platformda yapılır ve kanıtı platform imzalar; taklit için açık bir saldırı gerekir.
Build L3'te platform sertleştirilmiştir. İşler birbirinden yalıtılır ve imza anahtarı kullanıcı adımlarından uzak tutulur; kanıtı taklit etmek çoğu saldırganın gücünü aşar. 1.2 sürümü kaynak kod için ayrı bir iz de tanımlar.
Derleme hattı, kodu üretime taşıyan en yetkili sistemdir. Yayın sırrını, imza kimliğini, bulut erişimini ve dağıtım yetkisini aynı anda tutar. Hattı ele geçiren saldırgan kaynak koda dokunmadan üretime kod sürebilir ya da sırları toplayabilir.
Hattın en zayıf halkası çoğu zaman başkasının yazdığı adımlardır. GitHub Actions'ta bir adım uses: sahip/ad@ref biçiminde çağrılır. ref bir etiket, bir dal ya da bir commit SHA'sı olabilir.
Etiket ve dal taşınabilir işaretlerdir. Deponun sahibi ya da hesabı ele geçiren biri onları başka bir commit'e yeniden bağlayabilir. Tam uzunlukta commit SHA'sı ise içeriğin kendisinden türer ve değiştirilemez.
Mart 2025'te tj-actions/changed-files adımı tam bu yoldan ele geçirildi. Saldırgan deponun birçok sürüm etiketini geriye dönük olarak zararlı bir commit'e yönlendirdi. CISA'ya göre 12 ile 15 Mart arasında bu adımı çalıştıran iş akışları, çalıştırıcının belleğindeki sırları okuyup iş akışı günlüğüne yazdı.
Günlüğü herkese açık depolarda erişim jetonları, npm jetonları ve özel RSA anahtarları açıkta kaldı. Olay CVE-2025-30066 kimliğini aldı ve CISA onu KEV kataloğuna ekledi. Adımı 23 binden fazla depo kullanıyordu.
Etiketle çağıranlar zararlı kodu çalıştırdı. Önceki temiz bir SHA'ya sabitleyenler etkilenmedi. CISA'nın önerisi açıktı: etkilenen depoları bul, sırları döndür, düzeltilmiş sürüme geç.
steps: # Etiket: sahibi ya da hesabı ele geçiren biri yeniden bağlayabilir - uses: tj-actions/changed-files@v46 # Tam SHA: içerik değişmez, yorum sürümü insan için yazar - uses: tj-actions/changed-files@2f7c5bfce28377bc069a65ba478de0a74aa0ca32 # v46.0.1
SHA sabitlemenin bir bedeli vardır: güncelleme kendiliğinden gelmez. Etiketle çağırdığın adım yeni yamaları kendiliğinden alıyordu; SHA ile çağırdığın adım almaz. Sabitlemek unutmak demek değildir.
Dependabot ve Renovate, SHA ile sabitlenmiş adımlarda yeni sürüm çıktığında SHA'yı ve yanındaki sürüm yorumunu güncelleyen bir çekme isteği açar. İnceleme bu çekme isteğinde yapılır; değişiklik notunu ve kod farkını okursun. Hareketli etiketin kolaylığı ile değişmez kimliğin güvencesi böylece birlikte kalır.
Etkinlik 4 / 7
CI yapılandırmasında güvenilmeyen bir hareketli sürüme bağlı satırı bul.
Etkinlik 5 / 7
Bir CI boru hattı, üçüncü taraf bir adımı sabit bir commit özeti yerine hareketli bir sürüm etiketiyle çağırıyor. Bu tercihin asıl riski nedir?
İş akışını sertleştir: izinler, girdi ve tetikleyici
Sabitleme adımın kodunu korur; iş akışının kendi ayarları da en az onun kadar önemlidir. Birincisi izinlerdir. Her iş akışı GITHUB_TOKEN adında otomatik bir jeton alır. permissions: contents: read ayarı bu jetonu okumayla sınırlar; yazma gereken iş, izni yalnız kendi düzeyinde ister.
İkincisi girdidir. Çekme isteği başlığı, dal adı ya da yorum metni saldırganın yazdığı metindir. Bu değer run satırına doğrudan ${{ }} ifadesiyle yazılırsa kabuk komutunun parçası olur. Buna şablon enjeksiyonu denir.
Doğru yol, değeri env ile bir ara değişkene almak ve komutta tırnak içinde kullanmaktır. Üçüncüsü tetikleyicidir. pull_request_target, çatal depodan gelen istek için hedef deponun sırlarıyla çalışır.
name: pr-kontrol
on:
pull_request_target:
permissions: write-all
jobs:
kontrol:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
- uses: ornek-org/rapor-araci@v1
- name: Baslik yazdir
run: |
echo "PR: ${{ github.event.pull_request.title }}"
$ zizmor .github/workflows/
warning[artipacked]: credential persistence through GitHub Actions artifacts
--> .github/workflows/pr-kontrol.yml:9:9
error[dangerous-triggers]: use of fundamentally insecure workflow trigger
--> .github/workflows/pr-kontrol.yml:2:1
error[template-injection]: code injection via template expansion
--> .github/workflows/pr-kontrol.yml:15:25
error[unpinned-uses]: unpinned action reference
--> .github/workflows/pr-kontrol.yml:9:15
error[unpinned-uses]: unpinned action reference
--> .github/workflows/pr-kontrol.yml:12:15
8 findings (3 suppressed, 2 unsafe fixes): 0 informational, 0 low, 1 medium, 4 highzizmor, GitHub Actions iş akışlarını statik olarak denetleyen bir araçtır. Çıktıyı dosyayla yan yana oku. dangerous-triggers, 2. satırdaki on: bloğunu işaretler; sorun, altındaki pull_request_target tetikleyicisidir.
11. satır bu tetikleyiciyle çatalın kodunu checkout ettiği için durum daha da kötüdür. template-injection, 15. satırda çekme isteği başlığının doğrudan kabuğa yazıldığını gösterir.
unpinned-uses iki adımı SHA'sız çağrı olarak listeler. artipacked uyarısı, checkout adımının jetonu .git klasöründe bıraktığını söyler; persist-credentials: false bunu kapatır. write-all satırı --persona=auditor ile excessive-permissions bulgusu olarak görünür. OpenSSF Scorecard da aynı alanları Pinned-Dependencies, Token-Permissions ve Dangerous-Workflow kontrolleriyle puanlar.
name: pr-kontrol
on:
pull_request:
permissions:
contents: read
jobs:
kontrol:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Baslik yazdir
env:
BASLIK: ${{ github.event.pull_request.title }}
run: echo "PR: $BASLIK"Etkinlik 6 / 7
Altay Kargo'nun yorum etiketleme iş akışında, yorumu yazan kişinin metnini doğrudan kabuğa taşıyan satırı bul.
Konteyner imajları ve sabitlemenin bakımı
Aynı mantık konteyner imajlarında da geçerlidir. FROM node:22-alpine satırı bir etiketi çeker ve etiket zamanla başka bir imaja işaret eder. Bu bazen istenen bir şeydir, çünkü temel imaj güvenlik yamalarıyla yenilenir. Ama derlemeyi tekrarlanamaz kılar ve ele geçirilmiş bir kayıt hesabına kapı bırakır.
İmajı özetiyle çekmek içeriği sabitler. Özet verildiğinde çekilecek içeriği özet belirler; etiket yalnız okuyan insana bilgi verir. Altay Kargo temel imajlarını iç kayıtta tutar ve Dockerfile içinde özetle çağırır. Dependabot ve Renovate Dockerfile içindeki özetleri de izler ve yeni imaj çıktığında güncelleme isteği açar.
# Dockerfile FROM kayit.altaykargo.example/temel/node:22-alpine@sha256:8d4f2a61c0b7e93d5a1f6c28e4b09d7a3c5e1f82b6d40a9c7e3f1b5d2a8c6e04 WORKDIR /uygulama COPY package.json package-lock.json ./ RUN npm ci --omit=dev --ignore-scripts COPY . .
Etkinlik 7 / 7
Doğru mu, yanlış mı?
Bir konteyner imajını etiketle (ornek-imaj:latest) çekmek yerine sabit bir imaj özetiyle (digest) çekmek, etiketin farklı bir imaja yeniden bağlanmasına karşı koruma sağlar.
Okumayı istediğin sırayla sürdürebilirsin. Ders, bütün etkinlikleri cevapladığında tamamlanır.
- Etkinlik 1· Kilit dosyası nasıl kilitler
- Etkinlik 2· Kilit dosyası nasıl kilitler
- Etkinlik 3· İmza ve kaynak kanıtı: npm ve Sigstore
- Etkinlik 4· CI hattı bir hedeftir: SLSA ve tj-actions olayı
- Etkinlik 5· CI hattı bir hedeftir: SLSA ve tj-actions olayı
- Etkinlik 6· İş akışını sertleştir: izinler, girdi ve tetikleyici
- Etkinlik 7· Konteyner imajları ve sabitlemenin bakımı