Keamanan Image

Keamanan Image

Bitnesia Sep 12, 2026 8 EN

Keamanan container saat berjalan cuma sekuat image yang jadi asalnya. Sebuah image yang dibangun dari base image gemuk berisi ratusan package yang tidak terpakai, punya secret yang tanpa sengaja ikut ter-bake ke dalam layer, atau ditarik dari sumber yang tidak jelas asal-usulnya, sudah membawa risiko sejak sebelum baris kode aplikasi sempat dieksekusi. Developer yang menulis Dockerfile dan Sysadmin/DevOps Engineer yang mengelola pipeline build sama-sama berperan menutup celah ini, karena keputusan yang diambil saat proses build punya dampak langsung ke permukaan serangan yang dihadapi attacker nantinya. Bab ini membahas empat area keamanan image: memilih base image minimal, menghindari kebocoran secret lewat mekanisme layer caching, membangun rantai kepercayaan supply chain dari base image sampai registry, dan menjadikan pemindaian kerentanan sebagai siklus berkelanjutan, bukan pemeriksaan sekali jalan.

23.1 Minimal Base Image

Setiap package, binary, dan shell yang ikut terbawa di dalam base image menambah permukaan serangan yang bisa dimanfaatkan attacker, meskipun package tersebut tidak pernah benar-benar dipakai aplikasi. Bagian ini membahas kenapa base image gemuk berisiko dan bagaimana memilih alternatif yang lebih ramping.

23.1.1 Masalah Base Image Gemuk

Base image umum seperti ubuntu atau debian versi penuh membawa package manager, shell interaktif, dan ratusan utility sistem yang dirancang untuk kebutuhan general purpose, bukan khusus untuk menjalankan satu aplikasi tertentu. Dari sudut pandang keamanan, setiap package tambahan ini berarti tambahan kemungkinan CVE yang perlu ditambal, dan kalau attacker berhasil masuk ke dalam container lewat vulnerability aplikasi, keberadaan shell dan tool jaringan seperti curl atau wget di dalam image justru memudahkan attacker melakukan eksplorasi lebih lanjut atau mengunduh payload tambahan. Base image yang minimal membalik keadaan ini: kalau shell atau package manager tidak ada di dalam image, attacker yang berhasil mengeksekusi kode arbitrer pun kehilangan banyak tool yang biasanya dipakai untuk melanjutkan serangan.

23.1.2 Alpine, Distroless, dan Scratch

Tiga pilihan base image minimal yang paling umum dipakai di lapangan punya karakteristik dan trade-off yang berbeda.

Alpine Linux memakai musl libc dan busybox sebagai pengganti glibc dan GNU coreutils, menghasilkan image dasar yang jauh lebih kecil dibanding distro umum sambil tetap menyediakan package manager apk dan shell sh untuk kebutuhan debugging.

FROM alpine:3.20

RUN apk add --no-cache curl

CMD ["sh"]

Distroless dari proyek gcr.io/distroless melangkah lebih jauh dengan menghilangkan package manager, shell, dan hampir seluruh utility sistem, hanya menyisakan runtime minimal yang dibutuhkan bahasa pemrograman tertentu, misalnya varian distroless/nodejs yang cuma berisi runtime Node.js tanpa npm ataupun shell sama sekali.

FROM node:20 AS build
WORKDIR /app
COPY . .
RUN npm ci --omit=dev

FROM gcr.io/distroless/nodejs20-debian12
COPY --from=build /app /app
WORKDIR /app
CMD ["server.js"]

Scratch adalah base image kosong sepenuhnya, tanpa filesystem apa pun di dalamnya, cocok untuk aplikasi yang di-compile menjadi binary statis tanpa dependency eksternal, misalnya aplikasi Go yang dikompilasi dengan CGO_ENABLED=0.

FROM golang:1.23 AS build
WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o server .

FROM scratch
COPY --from=build /app/server /server
ENTRYPOINT ["/server"]

23.1.3 Memilih Base Image yang Tepat

Ketiadaan shell pada distroless dan scratch memang meningkatkan keamanan, tapi juga berarti docker exec untuk debugging langsung ke dalam container tidak lagi bisa dipakai seperti biasa; troubleshooting harus mengandalkan log aplikasi atau ephemeral debug container yang di-attach terpisah ke namespace container yang bermasalah. Di lapangan, keputusan ini biasanya jadi trade-off yang perlu didiskusikan bersama tim: aplikasi yang benar-benar stateless dan sudah stabil di production cocok memakai distroless atau scratch demi permukaan serangan sekecil mungkin, sementara aplikasi yang masih dalam tahap pengembangan aktif dan sering butuh debugging langsung di container mungkin lebih cocok tetap memakai alpine untuk sementara. Bandingkan ukuran dan isi image sebelum memutuskan lewat perintah berikut.

docker images --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}"

23.2 Risiko Layer Caching saat Build

BuildKit menyimpan setiap instruksi Dockerfile sebagai layer yang di-cache untuk mempercepat build berikutnya, tapi mekanisme cache ini punya jebakan keamanan tersendiri kalau data sensitif ikut tertulis ke dalam layer tersebut.

23.2.1 Build Cache Bisa Membocorkan Secret

Kesalahan umum yang sering ditemukan Sysadmin/DevOps Engineer saat audit image adalah credential, API key, atau private key yang diteruskan lewat instruksi ARG atau ENV biasa, atau di-COPY sementara ke dalam image lalu dihapus di instruksi berikutnya. Masalahnya, setiap layer di dalam image bersifat immutable dan tetap tersimpan penuh di dalam image meski file di dalamnya "dihapus" pada layer selanjutnya; penghapusan cuma menandai file itu tidak terlihat di filesystem gabungan, bukan benar-benar menghilangkannya dari layer sebelumnya. Siapa pun yang punya akses ke image, baik lewat docker pull maupun lewat inspeksi layer langsung, tetap bisa mengekstrak secret tersebut dari layer lama yang masih tersimpan di dalam image.

docker history --no-trunc payment-service:1.4.2

Perintah docker history di atas memperlihatkan perintah yang dijalankan pada setiap layer, termasuk ARG yang diteruskan lewat docker build --build-arg, sehingga secret yang sempat lewat sebagai build argument pun bisa ketahuan lewat cara ini.

23.2.2 BuildKit Secret Mount sebagai Solusi

Secret mount pada BuildKit menyediakan cara meneruskan data sensitif ke proses build yang cuma tersedia sementara selama instruksi RUN tertentu berjalan, tanpa pernah ikut ditulis ke layer image final. Secret ini dipasang lewat opsi --mount=type=secret pada instruksi RUN, dengan isi secret disediakan terpisah lewat flag --secret saat menjalankan docker build. Fitur ini butuh BuildKit sebagai builder, yang sudah jadi default sejak Docker Engine 23.0; untuk Docker Engine versi lebih lama, BuildKit perlu diaktifkan manual lewat environment variable DOCKER_BUILDKIT=1 sebelum menjalankan docker build.

# syntax=docker/dockerfile:1
FROM alpine:3.20

RUN --mount=type=secret,id=npm_token \
    NPM_TOKEN=$(cat /run/secrets/npm_token) && \
    echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > .npmrc && \
    npm install && \
    rm .npmrc
docker build --secret id=npm_token,src=./npm_token.txt -t app:1.0 .

Perhatikan baris pertama Dockerfile yang menyatakan # syntax=docker/dockerfile:1, syntax directive ini wajib ada supaya BuildKit mengaktifkan dukungan opsi --mount pada instruksi RUN. File secret yang di-mount ke /run/secrets/npm_token hanya tersedia selama instruksi RUN tersebut dieksekusi dan tidak pernah ikut tersimpan permanen ke layer manapun, berbeda dari COPY biasa yang isinya tetap terekam di layer meski file itu dihapus pada instruksi setelahnya.

Untuk memverifikasi secret benar-benar tidak bocor ke layer image, periksa ulang lewat docker history dan pastikan tidak ada jejak isi secret pada output-nya, lalu bandingkan dengan mengekstrak filesystem tiap layer memakai docker save kalau butuh pemeriksaan yang lebih menyeluruh.

docker save app:1.0 -o app.tar
tar -tvf app.tar

23.3 Supply Chain Security

Image yang dijalankan di production sebenarnya adalah hasil rantai panjang: base image dari pihak ketiga, dependency yang di-install dari package registry publik, sampai proses build yang mungkin berjalan di runner CI/CD yang dipakai bersama banyak proyek. Attacker yang berhasil menyusup di salah satu titik pada rantai ini, bukan langsung menyerang aplikasi produksinya, adalah pola serangan supply chain yang belakangan makin sering ditemukan di dunia nyata.

23.3.1 Rantai Kepercayaan dari Base Image sampai Registry

Setiap FROM pada Dockerfile pada dasarnya adalah keputusan mempercayai maintainer image tersebut. Base image dari Docker Official Images di Docker Hub melewati proses review dan pemindaian keamanan berkelanjutan oleh Docker, sehingga jauh lebih bisa dipercaya dibanding image dari akun publik yang tidak dikenal dan tidak jelas siapa yang memelihara maupun bagaimana proses build-nya. Sysadmin/DevOps Engineer perlu meninjau ulang seluruh base image yang dipakai di organisasinya, memastikan setiap image berasal dari publisher yang jelas dan ter-maintain aktif, karena base image yang sudah lama tidak diperbarui cenderung membawa vulnerability yang menumpuk tanpa pernah ditambal.

23.3.2 Verifikasi Image dengan Sigstore Cosign

Sebelumnya, Docker menyediakan Docker Content Trust (DCT) berbasis Notary untuk menandatangani dan memverifikasi image lewat variabel DOCKER_CONTENT_TRUST, tapi menurut dokumentasi resmi Docker, DCT sudah masuk fase retirement dan layanan Notary v1 di notary.docker.io dijadwalkan berhenti beroperasi pada 8 Desember 2026. Karena itu, DCT tidak lagi jadi pilihan yang tepat untuk mekanisme signing baru; alternatif yang lebih relevan dan makin banyak diadopsi ekosistem container saat ini adalah Sigstore lewat tool cosign.

Cosign menyimpan signature langsung berdampingan dengan image di registry yang sama, dan mendukung dua model penandatanganan: memakai key pair yang dikelola sendiri, atau keyless signing yang memakai identitas OIDC (misalnya akun GitHub Actions) untuk meminta sertifikat jangka pendek dari certificate authority Sigstore bernama Fulcio, sehingga tidak perlu mengelola private key secara manual.

cosign generate-key-pair
cosign sign --key cosign.key registry.example.com/payment-service@sha256:3f2b...

Penandatanganan dilakukan berdasarkan digest image, bukan tag, karena tag bisa dipindahkan menunjuk ke image lain kapan saja sementara digest bersifat unik dan permanen untuk satu konten image. Verifikasi di sisi consumer, misalnya di dalam pipeline deployment, memakai public key yang sama untuk memastikan image benar-benar berasal dari pihak yang menandatanganinya dan belum diubah sejak ditandatangani.

cosign verify --key cosign.pub registry.example.com/payment-service:1.4.2

Untuk keyless signing yang dijalankan lewat CI/CD, verifikasi dilakukan dengan mencocokkan identitas OIDC penanda tangan alih-alih public key statis.

cosign verify registry.example.com/payment-service:1.4.2 \
  --certificate-identity="https://github.com/org/repo/.github/workflows/build.yml@refs/heads/main" \
  --certificate-oidc-issuer="https://token.actions.githubusercontent.com"

23.3.3 Software Bill of Materials (SBOM)

Software Bill of Materials (SBOM) adalah daftar lengkap seluruh komponen, package, dan dependency yang menyusun sebuah image, mencatat nama, versi, dan lisensi masing-masing komponen dalam format standar seperti SPDX. SBOM memberi Sysadmin/DevOps Engineer visibilitas penuh atas apa saja yang sebenarnya ikut terbawa di dalam image, sehingga ketika sebuah vulnerability baru diumumkan untuk package tertentu, tim bisa langsung memastikan image mana saja yang terdampak tanpa perlu membongkar ulang setiap image satu per satu.

BuildKit mendukung pembuatan SBOM secara otomatis saat proses build lewat opsi --sbom pada docker buildx build, memakai scanner Syft dari Anchore secara default. Fitur attestation ini butuh Buildx versi 0.10 ke atas dengan builder BuildKit versi 0.11 ke atas, dan tidak berjalan di atas builder default bawaan docker yang dipakai kalau docker buildx create belum pernah dijalankan; buat dan aktifkan builder baru lebih dulu lewat docker buildx create --use kalau perintah --sbom gagal dengan pesan error terkait attestation yang tidak didukung.

docker buildx build --sbom=true -t payment-service:1.4.2 --push .

SBOM yang dihasilkan tersimpan sebagai attestation yang menempel pada image di registry, dan bisa diperiksa kembali kapan saja lewat docker buildx imagetools inspect tanpa perlu mem-build ulang image tersebut.

docker buildx imagetools inspect payment-service:1.4.2 --format "{{ json .SBOM.SPDX }}"

23.4 Vulnerability Scanning sebagai Siklus Berkelanjutan

Tool pemindaian kerentanan seperti Docker Scout dan Trivy sudah lazim dipakai untuk memeriksa CVE pada image sebelum dirilis. Bagian ini tidak mengulang cara menjalankan tool tersebut, melainkan membahas bagaimana hasil pemindaian dikelola sebagai bagian dari siklus keamanan yang berjalan terus-menerus, bukan pemeriksaan sekali jalan yang selesai begitu image lolos build.

23.4.1 Menetapkan Kebijakan Ambang Batas Kerentanan

Tanpa kebijakan yang jelas, tim gampang terjebak dua ekstrem: mengabaikan seluruh hasil pemindaian karena terlalu banyak temuan untuk ditangani satu per satu, atau sebaliknya memblokir setiap rilis hanya karena ada satu temuan low yang sebenarnya tidak berdampak signifikan. Kebijakan yang wajar di lapangan biasanya menetapkan bahwa temuan critical memblokir promosi ke production sepenuhnya, temuan high perlu ditinjau manual sebelum rilis disetujui, sementara temuan medium dan low dicatat sebagai backlog perbaikan tanpa menghentikan pipeline. Kebijakan ini perlu didokumentasikan secara eksplisit dan disepakati bersama, supaya keputusan meloloskan atau memblokir sebuah image tidak bergantung pada penilaian subjektif orang yang kebetulan sedang bertugas saat itu.

23.4.2 Menangani False Positive dengan Exemption

Tidak semua CVE yang terdeteksi tool pemindaian benar-benar berdampak pada aplikasi yang bersangkutan; sebuah package bisa saja punya CVE pada fungsi tertentu yang sama sekali tidak dipanggil aplikasi, atau risikonya sudah dimitigasi lewat kontrol lain seperti isolasi jaringan. Trivy menyediakan file .trivyignore untuk mendaftarkan CVE yang sengaja dikecualikan dari hasil pemindaian, lengkap dengan komentar alasan pengecualian dan tanggal kedaluwarsa opsional supaya exemption tidak berlaku selamanya tanpa ditinjau ulang.

# CVE-2024-XXXX: fungsi yang rentan tidak dipanggil di codebase ini
CVE-2024-XXXX

# Mitigasi sementara lewat network policy, tinjau ulang sebelum tanggal ini
CVE-2024-YYYY exp:2026-12-31
trivy image --ignorefile .trivyignore payment-service:1.4.2

Setiap exemption yang ditambahkan ke .trivyignore sebaiknya melalui proses review, bukan ditambahkan sepihak oleh siapa pun yang ingin pipeline-nya cepat lolos, karena file ini gampang disalahgunakan untuk menyembunyikan temuan yang sebenarnya perlu ditangani serius.

23.4.3 Memindai Dockerfile Sebelum Build dengan Hadolint

Hadolint adalah linter khusus Dockerfile yang memeriksa best practice penulisan instruksi build sebelum image sempat di-build sama sekali, berbeda dari Docker Scout maupun Trivy yang memeriksa image setelah jadi. Memeriksa Dockerfile di tahap paling awal ini membantu menangkap kesalahan konfigurasi yang berpotensi menimbulkan masalah keamanan lebih dini, jauh sebelum masuk ke tahap pemindaian image maupun deployment.

docker run --rm -i hadolint/hadolint < Dockerfile

Beberapa aturan Hadolint yang relevan langsung dengan keamanan image, misalnya DL3007 yang memperingatkan penggunaan tag latest pada instruksi FROM karena base image bisa berubah isinya kapan saja tanpa pemberitahuan, dan DL3008 yang menganjurkan mengunci versi package secara eksplisit saat memakai apt-get install supaya build tetap reproducible dan tidak tiba-tiba menarik versi package baru yang belum sempat diuji. Menjalankan Hadolint sebagai tahap awal pipeline CI/CD, sebelum tahap build image maupun pemindaian kerentanan, membuat masalah pada Dockerfile tertangkap lebih cepat dan lebih murah untuk diperbaiki dibanding menunggu ditemukan lewat pemindaian image yang jauh lebih memakan waktu.