Ana içeriğe geç

Image Signing — Cosign + Keyless OIDC#

"İmajını imzalamadan registry'ye push'lamak, 'kim koymuş bilemem' demektir. Saldırgan registry'ye fake imaj push'larsa, cluster nasıl ayırt eder? Sadece imza ayırt eder."

Bu rehber container imaj imzalamayı — özellikle Cosign keyless — production'da kurmak için somut adımları, GitHub Actions entegrasyonunu ve admission verification'ı anlatır.


🎯 Niye Imzala?#

Tehdit#

[Attacker]
[Compromise registry]  ──► [Push fake image: <REGISTRY>/<APP>:1.4.0]
                                  [Cluster pulls]
                                  [Compromised pod runs]

Çözüm#

  • Build → sign with cosign
  • Cluster admission → verify signature
  • İmzasız imaj → deploy reddedilir

🔑 Keyless vs Key-Based#

Yöntem Pro Con
Keyless (OIDC) Anlık cert, kalıcı key yok, audit Rekor'da Internet erişimi gerek (Sigstore)
Key-based Offline, air-gapped uyumlu Private key güvenlik (Vault zorunlu)

🔑 2026 önerisi: Keyless. Public/internal repo'larda standardize.


🚀 Cosign Quick Start#

Install#

brew install cosign
# veya
go install github.com/sigstore/cosign/v2/cmd/cosign@<VERSION>

Keyless sign (GitHub Actions OIDC)#

# .github/workflows/release.yml
name: Build, Sign, Push

on:
  push:
    tags: ['v*']

permissions:
  id-token: write     # OIDC için zorunlu
  contents: read
  packages: write     # ghcr.io push

jobs:
  build:
    runs-on: ubuntu-latest
    outputs:
      digest: ${{ steps.build.outputs.digest }}
    steps:
      - uses: actions/checkout@<VERSION>

      - uses: docker/login-action@<VERSION>
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - id: build
        uses: docker/build-push-action@<VERSION>
        with:
          push: true
          tags: ghcr.io/<ORG>/<APP>:${{ github.ref_name }}

      - uses: sigstore/cosign-installer@<VERSION>

      - name: Sign image (keyless)
        env:
          COSIGN_EXPERIMENTAL: "true"
        run: |
          cosign sign --yes \
            ghcr.io/<ORG>/<APP>@${{ steps.build.outputs.digest }}

Cert GitHub OIDC'den alınır, imza Rekor (transparent log) kayıt olur.

Verify (CLI)#

COSIGN_EXPERIMENTAL=1 cosign verify \
  --certificate-identity-regexp="https://github.com/<ORG>/.*" \
  --certificate-oidc-issuer="https://token.actions.githubusercontent.com" \
  ghcr.io/<ORG>/<APP>:1.4.0

→ Output: imza valid, hangi GitHub Actions workflow'u tarafından imzalandı.


🛂 Cluster Admission — Kyverno#

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: verify-image-signatures
spec:
  validationFailureAction: Enforce
  webhookTimeoutSeconds: 30
  rules:
    - name: verify-cosign
      match:
        any:
          - resources:
              kinds: [Pod]
      verifyImages:
        - imageReferences:
            - "ghcr.io/<ORG>/*"
          attestors:
            - entries:
                - keyless:
                    subject: "https://github.com/<ORG>/*"
                    issuer: "https://token.actions.githubusercontent.com"
          mutateDigest: true     # tag → digest dönüştür
          required: true

→ İmzasız imaj deploy → reddedilir.


📦 Attestation — Metadata Imza#

İmza yetmez — metadata da imzala (SBOM, build provenance, scan):

- name: Generate SBOM
  uses: anchore/sbom-action@<VERSION>
  with:
    image: ghcr.io/<ORG>/<APP>@${{ steps.build.outputs.digest }}
    format: cyclonedx-json
    output-file: sbom.json

- name: Sign + attest SBOM
  env:
    COSIGN_EXPERIMENTAL: "true"
  run: |
    cosign attest --yes \
      --predicate sbom.json \
      --type cyclonedx \
      ghcr.io/<ORG>/<APP>@${{ steps.build.outputs.digest }}

Verify attestation#

COSIGN_EXPERIMENTAL=1 cosign verify-attestation \
  --type cyclonedx \
  --certificate-identity-regexp="https://github.com/<ORG>/.*" \
  --certificate-oidc-issuer="https://token.actions.githubusercontent.com" \
  ghcr.io/<ORG>/<APP>@<DIGEST>

🛡️ SLSA Provenance#

SLSA L3 = "Bu imaj kim tarafından, hangi commit'ten, hangi build'le üretildi" doğrulanabilir:

provenance:
  permissions:
    id-token: write
    contents: read
    actions: read
    packages: write
  uses: slsa-framework/slsa-github-generator/.github/workflows/builder_container_slsa3.yml@<VERSION>
  with:
    image: ghcr.io/<ORG>/<APP>
    registry-username: ${{ github.actor }}
  secrets:
    registry-password: ${{ secrets.GITHUB_TOKEN }}

→ İmaj + SLSA provenance + SBOM hepsi attached + verify-able.


🔒 Key-Based (Air-Gapped Senaryolar)#

Internet yok / offline lab:

# Key pair üret
cosign generate-key-pair
# → cosign.key (private), cosign.pub (public)

# Sign
cosign sign --key cosign.key <REGISTRY>/<APP>:<TAG>

# Verify
cosign verify --key cosign.pub <REGISTRY>/<APP>:<TAG>

🔑 cosign.key güvenli yerde dur (Vault, KMS). Compromise = tüm imzalar sahte olabilir.


🚫 Anti-Pattern Tablosu#

Anti-pattern Niye kötü Doğru
Imza yok Saldırgan fake image push edebilir cosign sign her release
Tag-based deploy (:latest) Tag mutable Digest pin
Key-based + key Git'te Compromise = total Keyless OIDC veya Vault'ta
Sadece sign, attestation yok SBOM verify yok cosign attest
Admission verify yok İmzasız imaj cluster'a girer Kyverno verifyImages
mutateDigest: false Tag'e bağlı, mutable risk mutateDigest: true
Internal registry yok, public her image Compromise vektörü Internal mirror + scan
Build runner paylaşımlı Side-channel Ephemeral runner
Pinned action tag (@v1) Tag taşınabilir SHA pin (@a1b2c3...)
Verify failure log'a düşer ama kabul edilir Imza anlamsız Enforce mode

📋 Image Signing Checklist#

[ ] CI: cosign sign her release (keyless OIDC)
[ ] CI: SBOM generate (Syft / Trivy)
[ ] CI: SBOM cosign attest
[ ] CI: SLSA provenance generator
[ ] Cluster: Kyverno verifyImages enforce
[ ] mutateDigest: true (tag → digest)
[ ] Internal registry (ghcr / Harbor)
[ ] Pinned action SHA (Renovate ile auto-update)
[ ] Build runner ephemeral
[ ] Verify CLI komutu runbook'ta
[ ] Quarterly: signing pipeline drill (fake imaj enjekte, yakalandı mı?)
[ ] Documentation: yeni servis nasıl sign eder
[ ] Admission failure → SIEM alert

📚 Referanslar#


"İmza 'paranoyak güvenlik' değil — modern supply chain'in imzasıdır. İmzalanmamış imaj = anonim koli: kim koydu bilinmez, açmak risklidir."