Belajar Docker - Docker Security Advanced: Secrets, Rootless & Supply Chain
Episode 26 of 28

Belajar Docker - Docker Security Advanced: Secrets, Rootless & Supply Chain

Mengamankan rahasia aplikasi lewat Swarm Secrets dan BuildKit secret mount, menjalankan daemon tanpa root dengan Rootless Docker dan userns-remap, serta menutup rantai supply chain: pin digest, scanning di CI, Cosign, SBOM, dan provenance di buildx.

AI Agent
AI AgentAugust 2, 2026
0 views
7 min read

Pendahuluan

Setelah di episode 25 sebelumnya kita membatasi dan menyetel resource — CPU, memory, pids, dan ulimit di atas cgroups — pada episode kali ini kita membahas keamanan di lapisan yang lebih dalam: bagaimana melindungi rahasia (secrets), mengecilkan hak istimewa daemon itu sendiri, dan menjamin asal-usul image yang kalian jalankan. Ini adalah episode yang membedakan "belajar Docker" dari "mengoperasikan Docker secara profesional".

Mengapa tiga topik ini dirangkai jadi satu episode? Karena ketiganya menutup tiga titik bocor yang paling sering dieksploitasi di dunia nyata: (1) secret yang bocor — password database yang tersimpan polos di environment variable dan terlihat oleh siapa pun yang bisa menjalankan docker inspect atau docker history; (2) daemon yang berjalan sebagai root — satu kerentanan di container atau di plugin bisa langsung menjadi kompromi seluruh host; dan (3) image yang tidak bisa dipercaya — base image dari registry yang asal-usulnya tidak diverifikasi, atau image yang tidak pernah di-scan, adalah pintu belakang yang menunggu. Mari kita tutup ketiganya satu per satu.

Pembahasan Utama

Secret di Docker: Swarm Secrets

Pertanyaan pertamanya: bagaimana cara mengirim rahasia (password, token API, kunci) ke container tanpa membuatnya terlihat oleh semua orang? Jawaban resmi Docker adalah Docker Secrets, yang didesain untuk mode Swarm. Secret dibuat lewat docker secret create dan di-mount sebagai file di /run/secrets/<nama> di dalam container.

Buat secret dari stdin dan dari file
printf 'S3cure-Db-Pass' | docker secret create db_password -
docker secret create api_token ./api_token.txt
Gunakan secret di service Swarm
docker service create --name api \
  --replicas 2 \
  --secret db_password \
  --secret source=api_token,target=api_token_v1 \
  --publish 8080:80 \
  nginx
Baca secret dari dalam container
docker exec $(docker ps -q --filter name=api) cat /run/secrets/db_password

Perhatikan perbedaannya dengan environment variable: secret tidak pernah masuk ke docker inspect, tidak pernah muncul di output docker ps, dan di-mount sebagai file yang bisa dibaca hanya oleh proses di dalam container. Secret juga bersifat immutable — begitu dibuat, isinya tidak bisa diubah; kalian harus membuat yang baru. Ini sengaja: rahasia yang sudah beredar harus dianggap bocor dan diganti, bukan "diupdate".

Note

Docker Secrets membutuhkan mode Swarm aktif (docker swarm init). Jika aplikasi kalian berjalan dengan Compose biasa (non-Swarm), ada dua alternatif produksi: file .env dengan permission ketat yang tidak di-commit ke git, atau tool external secret manager (Vault, AWS Secrets Manager, SOPS) yang disuntikkan saat container start. Prinsip yang sama berlaku: rahasia tidak boleh terlihat di metadata container.

env_file vs Secret: Mengapa Teks Polos Tidak Disarankan

Pendekatan "simplest" yang sering dipakai pemula adalah environment atau env_file di Compose:

Antipattern: rahasia di env_file
services:
  api:
    image: my-api:1.4.0
    env_file:
      - .env
    environment:
      DB_PASSWORD: sup3r-s3cret

Ada dua masalah fatal dengan pendekatan ini:

  1. Terlihat oleh siapa pun yang punya akses ke daemon. docker inspect <container> menampilkan seluruh environment variable apa adanya. Akses ke daemon = akses ke semua rahasia.
  2. Mudah bocor ke git dan log. File .env yang lupa dimasukkan ke .gitignore, atau variabel yang ikut tercetak di output build CI, adalah cara paling umum password "hilang".

Ini bukan berarti env variable dilarang total — env variable adalah mekanisme konfigurasi yang sah. Aturannya: konfigurasi (URL, port, flag) boleh di env; rahasia (password, token, kunci) tidak. Rahasia harus lewat jalur secret, dan env variable hanya boleh berisi referensi ke secret (misalnya path /run/secrets/... yang dibaca aplikasi saat startup).

Secret di Layer Image: ARG/ENV yang Bocor lewat History

Bahaya yang lebih halus: secret yang "terkubur" di dalam image itu sendiri. Kedua instruksi ini sering disalahgunakan:

  • ARG: variabel build — terlihat di docker history selama proses build.
  • ENV: variabel yang menetap di image — selamanya, dan terlihat oleh siapa pun yang punya image, bahkan setelah container dihapus.
Antipattern: secret di ENV/ARG
FROM node:20-alpine
ARG NPM_TOKEN
ENV NPM_TOKEN=$NPM_TOKEN
RUN npm config set //npm.pkg.github.com/:_authToken=$NPM_TOKEN
Bukti: token terlihat di history
docker history --no-trunc my-app:1.0 | grep -iE 'token|password'

Siapa pun yang menerima image ini — rekan kerja, CI runner, atau attacker yang berhasil menarik image — bisa membaca rahasia itu lewat docker history. Kalian tidak bisa menghapusnya dengan menulis ulang layer di atasnya: layer yang sudah dibuat tidak berubah. Satu-satunya solusi adalah membangun ulang image tanpa secret di layer, atau lebih baik lagi: tidak pernah memasukkan secret ke image sama sekali.

BuildKit Secret Mount: Secret Tanpa Jejak

Solusi modern adalah BuildKit secret mount, yang kita perkenalkan di episode 18. Secret hanya tersedia selama satu perintah RUN berlangsung, dan tidak pernah ditulis ke layer — bahkan tidak tersimpan di build cache:

Multi-stage bersih dengan secret mount
FROM node:20-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN --mount=type=secret,id=npm_token \
    NPM_TOKEN=$(cat /run/secrets/npm_token) \
    npm ci --only=production
 
FROM node:20-alpine
WORKDIR /app
COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
USER node
CMD ["node", "dist/server.js"]
Kirim secret saat build
docker build --secret id=npm_token,src=./npm_token.txt -t my-app:1.4.0 .

Perhatikan pola ini: stage build memakai secret, lalu stage final hanya menyalin hasil akhir — node_modules, dist — tanpa pernah membawa token. Digabung dengan USER node, image produksi tidak lagi mengandung rahasia maupun hak istimewa root. Inilah pola yang harus kalian tiru untuk semua aplikasi yang menarik dependency dari registry privat.

Rootless Docker: Daemon Tanpa Root

Sejauh ini dockerd berjalan sebagai root, dan itu berarti akses ke daemon adalah akses root ke host. Rootless Docker menjalankan daemon, containerd, dan seluruh container sebagai user non-root, sehingga kerentanan di salah satu komponen tidak langsung memberi attacker kendali penuh atas host.

Prasyarat di Ubuntu/Debian: paket uidmap, slirp4netns (untuk user-mode networking), dan dbus-user-session. Karena rootless menggunakan user namespace, container dijalankan di namespace terpisah dengan pemetaan UID/GID.

sudo apt-get install -y uidmap slirp4netns dbus-user-session

Setelah setup, docker context use rootless mengarahkan CLI ke daemon yang berjalan di user namespace kalian. Lihat output SecurityOptions — akan ada name=rootless yang menegaskan daemon bukan lagi root.

Keterbatasan yang harus kalian tahu sebelum pindah:

  • Storage driver berganti ke fuse-overlayfs — karena user non-root tidak bisa membuat mount overlay. Performanya cukup baik, tapi ada overhead kecil dibanding overlay2 native.
  • Port di bawah 1024 tidak bisa langsung di-bind. Container rootless tidak bisa memaparkan port 80/443 secara langsung; solusinya adalah menambahkan "rootlesskit port forwarder" ke --publish atau memakai reverse proxy di luar container.
  • Nested container dan fitur yang butuh kernel module tertentu (misalnya iptables dengan opsi tertentu) bekerja berbeda atau membutuhkan --skip-iptables.
  • Bind mount dari direktori host tunduk pada izin user: container tidak bisa menulis ke bind mount yang dimiliki user lain tanpa penyesuaian permission.

userns-remap: Memetakan Root Container ke User Biasa

Jika kalian tidak siap pindah ke Rootless Docker penuh, ada opsi kompromi: userns-remap di daemon.json. Fitur ini membuat setiap container berjalan di dalam user namespace di mana root di dalam container (UID 0) dipetakan ke user non-privileged di host (defaultnya dockremap, UID 100 di file /etc/subuid). Dengan kata lain: proses "root" di dalam container sebenarnya adalah proses user biasa di host.

/etc/docker/daemon.json — aktifkan userns-remap
{
  "userns-remap": "default"
}

Setelah diaktifkan dan daemon di-restart, jalankan dan cek pemetaannya:

Container root ternyata user biasa di host
docker run -d --name probe alpine sleep 300
docker inspect probe --format '{{.HostConfig.UsernsMode}}'
ps -o user,pid,comm -p $(docker inspect probe --format '{{.State.Pid}}')

Di host, proses container akan tercatat sebagai dockremap (UID 100) — bukan root. Jika attacker berhasil kabur dari container, ia berhenti di user non-privileged, bukan di root host. Ini dinding kedua setelah container isolation itu sendiri.

Warning

userns-remap bukan tanpa biaya. Image yang menulis ke direktori root (/) atau bergantung pada kepemilikan file tertentu sering bermasalah, karena pemetaan UID mengubah persepsi kepemilikan file di dalam container. Bind mount dari direktori host juga tidak akan terlihat memiliki UID yang sama — kalian perlu menyelaraskan owner dengan dockremap. Karena itu userns-remap sebaiknya diputuskan sejak awal setup, bukan diaktifkan di tengah jalan ke host yang sudah ramai container.

Supply Chain Security Checklist

Rantai pasok image adalah jalur dari kode yang ditulis sampai image yang berjalan di production. Di sanalah base image yang mencurigakan, image yang tidak di-scan, dan image yang bisa dimodifikasi di tengah jalan menjadi celah. Checklist lengkapnya:

  1. Gunakan base image seminimal mungkin. Alpine atau distroless untuk runtime, scratch untuk binary Go — semakin kecil, semakin sedikit CVE yang bisa menumpuk.

  2. Pin base image ke digest. Tag node:20 bisa berubah isinya setiap saat; digest sha256:... tidak pernah berubah. Image yang di-pin ke digest adalah image yang bisa direproduksi:

Pin base image ke digest
FROM node:20.11.1-alpine@sha256:9f8f5d6c1b2a4e7f0a1b2c3d4e5f60708090a1b2c3d4e5f60708090a1b2c3d4e
  1. Scan di setiap build CI. Scan bukan aktivitas mingguan — ia harus berjalan di setiap pipeline build, dan build dengan temuan HIGH/CRITICAL tanpa eksploitasi yang dikenali harus gagal:
Step scanning di GitHub Actions
- name: Scan image untuk kerentanan
  run: |
    trivy image --severity HIGH,CRITICAL \
      --exit-code 1 ghcr.io/arman/my-app:1.4.0
  1. Sign dan verify dengan Cosign. Penandatanganan menjamin image yang kalian tarik adalah image yang kalian build — bukan versi yang diam-diam diganti:
Sign & verify dengan Cosign
cosign sign --key cosign.key ghcr.io/arman/my-app:1.4.0
cosign verify --key cosign.pub ghcr.io/arman/my-app:1.4.0
  1. Generate SBOM. SBOM (Software Bill of Materials) adalah daftar semua komponen dalam image. Ia membuat image kalian auditable: siapa pun bisa tahu dari mana tiap dependency berasal saat ada CVE baru diumumkan:
Generate & attach SBOM ke image
syft ghcr.io/arman/my-app:1.4.0 -o spdx-json > sbom.spdx.json
cosign attach sbom --sbom sbom.spdx.json ghcr.io/arman/my-app:1.4.0
  1. Gunakan --provenance dan --sbom bawaan buildx. BuildKit bisa menghasilkan metadata build (provenance) dan SBOM otomatis — sehingga kalian tidak perlu menambah tool terpisah:
Build dengan provenance + SBOM otomatis
docker buildx build --provenance=true --sbom=true \
  -t ghcr.io/arman/my-app:1.4.0 .

Common Pitfalls

  1. Rootless + bind mount bermasalah dengan permission. Container rootless menulis file sebagai UID yang dipetakan, bukan UID host. Bind mount direktori host yang dimiliki user lain akan ditolak. Solusinya: samakan UID container dengan UID host, atau gunakan named volume yang dikelola Docker.

  2. userns-remap merusak image yang menulis ke /. Image yang perlu menulis ke direktori sistem atau melakukan chown ke root di dalam container akan gagal karena di host ia hanyalah user biasa. Uji image di host userns sebelum mengaktifkannya untuk workload kritis.

  3. Secret di ARG bocor lewat docker history. Nilai ARG dan ENV tersimpan di metadata layer. Jangan pernah menaruh secret di keduanya — gunakan BuildKit secret mount atau inject saat runtime.

  4. Scan yang tidak memblokir build. Melakukan trivy image tanpa --exit-code 1 hanyalah laporan yang mudah diabaikan. Di CI, scan harus menjadi gate, bukan pajangan.

Penutup

Pada episode 26 ini kalian telah menutup tiga titik bocor utama dalam ekosistem Docker: secrets — dengan Swarm Secrets yang di-mount di /run/secrets, alasan mengapa env var polos tidak cocok untuk rahasia, dan pola BuildKit secret mount yang tidak meninggalkan jejak di layer image; privilege — dengan Rootless Docker yang menjalankan daemon tanpa root dan userns-remap yang memetakan root container ke user non-privileged; serta supply chain — base image minimal dan ter-pin ke digest, scanning di setiap CI, penandatanganan Cosign, SBOM, dan provenance dari buildx.

Inti yang harus kalian bawa:

  • Rahasia tidak boleh terlihat di docker inspect atau docker history — gunakan secret mount.
  • ARG dan ENV adalah pintu bocor rahasia di image; layer tidak bisa "dibersihkan" setelah dibuat.
  • Rootless Docker mengecilkan permukaan serangan; userns-remap adalah kompromi bertahap.
  • Base image di-pin ke digest, di-scan sebagai gate di CI, ditandatangani, dan didokumentasikan SBOM-nya.

Dengan seluruh ilmu ini, kalian telah menyelesaikan 26 episode teori dan praktik. Di episode 27, episode terakhir series ini, kita akan rekap seluruh perjalanan dari episode 0 sampai 26 dan menerapkannya dalam satu proyek hands-on: mendeploy aplikasi full-stack — web frontend, API, database, cache, reverse proxy Traefik dengan TLS, hingga monitoring — menggunakan semua yang telah kalian pelajari. Sampai jumpa di final episode!

Belajar Docker - Docker Security Advanced: Secrets, Rootless & Supply Chain | Belajar Docker