
Hari 19: Sign Docker Image Pakai Cosign, Verifikasi Integritas
Tanda tangan Docker image dengan Cosign supaya kalau diubah orang, langsung ketahuan. Push ke GHCR, sign, verify — 3/3 check lulus.
Kenapa Harus Sign Docker Image?
Bayangin kasus ini: seseorang berhasil masuk ke Docker registry kamu, lalu mengganti image securebank:v1 dengan versi yang sudah disisipin backdoor. Tim DevOps pull image itu, deploy ke production, dan boom — attacker punya akses ke sistem.
Tanpa image signing, tidak ada cara untuk membedakan image asli dari image yang sudah di-tamper.
Image signing itu seperti tanda tangan notaris di dokumen. Kalau isinya berubah, tanda tangannya gak cocok lagi. Verifikasi signature sebelum deploy = pastikan image belum diubah sejak di-sign.
Hari ini kita pakai Cosign dari Sigstore project untuk sign Docker image SecureBank API.
Install Cosign
Cosign dibuat oleh Sigstore (Linux Foundation project). Cara install-nya:
# Download binary langsung dari GitHub releases (lebih cepat dari Homebrew)
curl -sL -o /opt/homebrew/bin/cosign \
"https://github.com/sigstore/cosign/releases/download/v3.1.1/cosign-darwin-arm64"
chmod +x /opt/homebrew/bin/cosign
cosign version
Versi yang terinstall: Cosign v3.1.1, Go 1.26.3.
Generate Key Pair
Cosign pakai konsep asymmetric cryptography — sama seperti SSH key atau GPG key:
- Private key (
cosign.key) — dipakai untuk sign. Hanya yang punya key ini yang bisa sign image. - Public key (
cosign.pub) — dipakai untuk verify. Siapapun yang punya key ini bisa verifikasi signature.
$ COSIGN_PASSWORD="" cosign generate-key-pair
Private key written to cosign.key
Public key written to cosign.pub
COSIGN_PASSWORD="" supaya gak interaktif (gak minta input password). Untuk challenge ini, key tanpa password sudah cukup. Di production, private key harus pakai password dan disimpan di secret manager.
Penting: Private Key Tidak Boleh Masuk Repo
# .gitignore
cosign.key
cosign.key ditambahkan ke .gitignore. Private key itu kunci rumah — kalau hilang atau jatuh ke tangan orang, mereka bisa sign image palsu atas nama kamu.
Public key (cosign.pub) aman di-commit. Kayak nomor rekening — bisa dibagi, tapi gak bisa dipakai untuk tarik uang.
Push Image ke GHCR
Sebelum bisa sign, image harus ada di registry. Kita pakai GitHub Container Registry (GHCR):
# Tag image untuk GHCR
docker tag securebank:v1 ghcr.io/stayrelevantid/securebank:v1
# Login ke GHCR (butuh GitHub PAT scope write:packages)
echo $GITHUB_TOKEN | docker login ghcr.io -u stayrelevantid --password-stdin
# Push image
docker push ghcr.io/stayrelevantid/securebank:v1
Hasilnya: image ghcr.io/stayrelevantid/securebank:v1 sudah ada di registry, digest sha256:df0ecb33....
Sign Image
Sekarang sign image-nya:
$ COSIGN_PASSWORD="" cosign sign --key cosign.key ghcr.io/stayrelevantid/securebank:v1
Signing artifact...
Pushing signature to: ghcr.io/stayrelevantid/securebank
Apa yang terjadi di belakang layar?
- Cosign menghitung SHA256 hash dari image manifest
- Hash tersebut di-sign dengan private key menggunakan algoritma ECDSA
- Signature (bukan image-nya) di-push ke registry sebagai OCI artifact
- Signature juga dikirim ke Rekor — transparency log di Sigstore
Image aslinya tidak berubah. Signature tersimpan terpisah di registry.
Warning: Tag vs Digest
Cosign memberi warning:
WARNING: Image reference uses a tag, not a digest, to identify the image to sign.
Ini karena tag (:v1) bisa di-repoint ke image berbeda kapan saja. Best practice di production: pakai digest (@sha256:...) untuk signing. Tapi untuk challenge ini, tag v1 sudah cukup karena kita kontrol full lifecycle image.
Verify Signature
Sekarang verifikasi:
$ COSIGN_PASSWORD="" cosign verify --key cosign.pub ghcr.io/stayrelevantid/securebank:v1
Verification for ghcr.io/stayrelevantid/securebank:v1 --
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 signatures were verified against the specified public key
[{"critical":{"identity":{"docker-reference":"ghcr.io/stayrelevantid/securebank:v1"},
"image":{"docker-manifest-digest":"sha256:df0ecb33..."},
"type":"https://sigstore.dev/cosign/sign/v1"},"optional":{}}]
3 checks lulus:
- Cosign claims validated — metadata signature valid (image reference, digest, type)
- Transparency log verified — signature tercatat di Rekor (tidak bisa diyangkal)
- Public key match — signature cocok dengan public key yang kita punya
Kalau image di-tamper di registry, verifikasi akan gagal karena hash image tidak akan cocok dengan yang ada di signature.
Alurnya: Build → Push → Sign → Verify → Deploy
Developer CI/CD Registry Deployment
| | | |
|--- build ------->| | |
|--- push -------->|--- push -------->| |
|--- sign -------->|--- sign -------->| |
| |--- transparency->| (Rekor log) |
| | | |
| | |<--- pull -------|
| | |<--- verify -----|
| | | [deploy if valid]
Di deployment (Kubernetes, Fase 3), kita bisa pakai Cosign verifier untuk auto-reject image yang signature-nya tidak valid. Ini namanya policy enforcement — image yang tidak di-sign tidak boleh deploy.
Private Key vs Public Key
Private Key (cosign.key) |
Public Key (cosign.pub) |
|
|---|---|---|
| Fungsi | Sign image | Verify signature |
| Simpan di mana | GitHub Secret / Vault / KMS | Di-commit ke repo |
| Bisa di-share? | TIDAK. Rahasia. | Ya, aman untuk publik |
| Kalau bocor? | Attacker bisa sign image palsu | Tidak masalah — public key cuma untuk verify |
| Di .gitignore? | Ya | Tidak |
Konsep ini sama seperti SSH key pair: private key (~/.ssh/id_ed25519) tetap rahasia, public key (~/.ssh/id_ed25519.pub) di-share ke GitHub/server.
Transparency Log (Rekor)
Salah satu fitur Cosign yang menarik: transparency log via Rekor.
Setiap signature yang dibuat Cosign otomatis dikirim ke Rekor — log publik yang tidak bisa diubah. Ini berfungsi sebagai:
- Audit trail — siapa sign apa, kapan, dengan key mana
- Non-repudiation — signer tidak bisa bilang "saya gak pernah sign image itu"
- Detect compromise — kalau key bocor, semua signature yang dibuat dengan key itu bisa di-track
Saat verifikasi, Cosign cek apakah signature ada di transparency log. Kalau tidak ada, verifikasi gagal — kemungkinan signature palsu.
Lesson Learned
1. Image signing itu tentang integritas, bukan enkripsi. Cosign tidak mengubah image. Dia bikin signature terpisah yang bisa diverifikasi. Kalau image diubah, hash-nya berubah, signature tidak cocok.
2. Private key tidak pernah meninggalkan environment trusted. cosign.key di .gitignore, tidak di-commit. Di CI, private key di-store sebagai GitHub Secret. Public key aman di-commit — siapapun bisa verify, tapi cuma yang punya private key yang bisa sign.
3. Transparency log = non-repudiation. Cosign mengirim signature ke Rekor (transparency log). Verifikasi mencek signature ada di log. Signer tidak bisa menyangkal bahwa mereka sign image itu.
4. Tag vs digest itu tradeoff. Tag lebih mudah dibaca (v1, v2), tapi bisa di-repoint. Digest (@sha256:...) immutable, tapi panjang dan susah dibaca. Untuk production, pakai digest. Untuk dev, tag sudah cukup.
5. GHCR gratis untuk public repo. GitHub Container Registry tidak butuh subscription untuk public images. Pakai GitHub PAT dengan scope write:packages untuk push. Gratis dan terintegrasi dengan akun GitHub yang sudah ada.
Kesimpulan
Hari ini kita sign Docker image SecureBank API pakai Cosign. Image yang ada di GHCR sekarang punya tanda tangan kriptografis yang bisa diverifikasi kapan saja.
Prosesnya:
- Generate key pair (private + public)
- Push image ke registry
- Sign image dengan private key
- Verify signature dengan public key
Kalau seseorang mengganti image di registry, verifikasi akan gagal. Signature tidak cocok karena hash image sudah berubah.
Ini adalah fondasi untuk supply chain security — memastikan bahwa image yang di-deploy ke production benar-benar image yang kita build, bukan image yang sudah di-tamper.
Besok: Hari 20 — Terraform Setup + IaC Scan (Checkov) — mulai bikin infrastructure as code dengan Terraform dan scan misconfiguration-nya.
Diskusi & Komentar
Hari 18: Docker Hardening, 8 Lapis Pertahanan Container
Next ArticleHari 20: Terraform + Checkov, 15 Celah IaC Ketahuan
Artikel Terkait
Hari 5: Trivy SCA Scan Nemukan 4 CVE di Golang API
Hari kelima 60 hari DevSecOps! Scan dependensi Go pakai Trivy dan nemukan 4 CVE termasuk 1 CRITICAL — termasuk library deprecated jwt-go.
Hari 8: Semgrep SAST Scan Temukan Kode Tidak Aman
Hari kedelapan 60 hari DevSecOps! Install Semgrep, buat kode insecure (MD5), dan scan ketemu 2 finding — MD5 weak hash dan HTTP server tanpa TLS.
Hari 10: MD5 ke Bcrypt, Pipeline Hijau Lagi
Hari kesepuluh 60 hari DevSecOps! Fix SAST findings — ganti MD5 ke bcrypt, hapus custom rule HTTP TLS, dan pipeline CI kembali hijau setelah 4 job semua pass.