Bulan Maret lalu, gw audit security setup di salah satu client yang jalanin 23 container Docker di production. Hasil audit Trivy: 187 CVEs, 12 di antaranya HIGH, 3 CRITICAL. Penyebab utamanya: mereka pake image node:latest yang gak pernah diupdate, jalanin semua container sebagai root, gak ada seccomp profile, dan expose port gak perlu.
Yang bikin gw miris: 95% dari vulnerability itu bisa dihindari dengan basic hardening yang cuma butuh 2-3 jam setup. Gak perlu jadi security expert, gak perlu beli tools mahal. Cuma perlu tau apa yang harus dilakukan dan disiplin nerapinnya.
Artikel ini bakal breakdown semua layer container security — dari image build sampai runtime — lengkap dengan konfigurasi yang bisa lo copy-paste langsung. Untuk fondasi Docker-nya, baca dulu panduan Docker multi-stage build dan setup logging container.
Layer 1: Image Security — Dimulai dari Dockerfile
Security dimulai dari image. Image yang gak di-hardening = container yang rentan. Ini non-negotiable rules yang gw terapin di semua project:
1. Pake base image minimal
Image node:latest ukurannya ~900MB dan berisi ratusan packages yang gak lo butuhin di production. Setiap package tambahan = attack surface tambahan. Ganti dengan node:22-alpine (~120MB) atau node:22-slim (~150MB). Bedanya signifikan: Alpine pake musl libc (bukan glibc) yang berarti beda C library — beberapa native modules mungkin gak kompatibel. Slim lebih aman dari sisi kompatibilitas.
# BURUK: node:latest — 900MB, 200+ packages, 47 CVEs
FROM node:latest
# BAIK: node:22-alpine — 120MB, ~40 packages, 3 CVEs
FROM node:22-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production && npm cache clean --force
FROM node:22-alpine
WORKDIR /app
COPY --from=build /app/node_modules ./node_modules
COPY . .
USER node
CMD ["node", "server.js"]
2. Multi-stage build — wajib.
Multi-stage build udah dibahas detail di artikel terpisah, tapi dari sisi security: build tools (gcc, make, python-dev) gak boleh ada di production image. Kalau ada vulnerability di kompiler atau dev dependencies, attacker gak bisa exploit karena tools-nya gak ada. Pisah stage: builder (dengan semua tools) → production (cuma runtime + artifacts).
3. Jangan jalanin container sebagai root
Ini mungkin kesalahan keamanan Docker yang paling umum. Default Docker jalanin container sebagai root — artinya kalau attacker berhasil break out dari container, mereka dapet root access ke host. Solusinya simple: pake USER directive di Dockerfile.
# Di Dockerfile, tambahin user non-root
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
# Atau pake USER numeric UID (lebih secure — gak bergantung pada /etc/passwd)
USER 10001:10001
4. Update base image secara rutin
Image yang lo pull 3 bulan yang lalu mungkin udah punya 50+ CVEs baru. Gw schedule weekly rebuild: cron job yang pull ulang base image, rebuild, dan deploy ulang container. Otomatis pake CI/CD — setup-nya pake Gitea + Drone CI.
Layer 2: Image Scanning — Otomatis di CI/CD Pipeline
Gak cukup cuma pake base image yang aman — lo perlu scan image secara otomatis setiap build. Pake Trivy (Aqua Security) — gratis, cepat, dan update database CVE setiap jam.
# Install Trivy
docker pull aquasec/trivy:latest
# Scan image lokal
docker build -t myapp:latest .
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock aquasec/trivy image myapp:latest
# Output: daftar CVEs dengan severity
# Total: 3 (UNKNOWN: 0, LOW: 1, MEDIUM: 2, HIGH: 0, CRITICAL: 0)
# Integrasi di CI/CD — fail build kalau ada HIGH+ CVEs
docker run --rm aquasec/trivy image --severity HIGH,CRITICAL --exit-code 1 myapp:latest
Di CI/CD pipeline, gw set threshold: fail build kalau ada CRITICAL atau HIGH severity CVE. LOW dan MEDIUM di-track tapi gak blocking — karena realistic-nya, base image Alpine punya 2-5 LOW/MEDIUM CVEs yang gak bisa dihindari. Yang penting: CRITICAL = 0, HIGH = 0.
Layer 3: Runtime Security — Docker Run Flags yang Wajib
Ini layer paling impactful: konfigurasi runtime yang nge-limit apa yang bisa dilakukan container. Banyak dari ini gak perlu perubahan kode — cuma tambah flag di docker run atau docker-compose.yml.
1. Read-only root filesystem
Kebanyakan aplikasi gak perlu nulis ke filesystem — mereka write ke database atau external storage. Set read_only: true — kalau container lo mencoba nulis ke /, dia akan error. Ini prevent attacker dari modify binary atau write malware.
# docker run flag
docker run --read-only --tmpfs /tmp myapp:latest
# docker-compose
services:
app:
image: myapp:latest
read_only: true
tmpfs:
- /tmp:noexec,nosuid,size=128M
volumes:
- app_data:/data/uploads # explicit write path
2. Drop all capabilities, add only yang diperlukan
Linux capabilities adalah segmen-segmen dari root privilege. Default Docker container punya ~40 capabilities. Kebanyakan gak diperlukan. Prinsip: drop all, add minimal.
# Minimal capabilities buat web server:
# CAP_NET_BIND_SERVICE — bind ke port <1024
# Gak perlu: CAP_SYS_ADMIN, CAP_NET_ADMIN, CAP_SYS_PTRACE, dll.
docker run --cap-drop ALL --cap-add NET_BIND_SERVICE myapp:latest
3. Seccomp profile — system call filter
Linux punya ~450 system calls. Web server typical cuma butuh ~50. Seccomp (Secure Computing Mode) memfilter system calls yang bisa dilakukan container — kalau container panggil syscall yang gak ada di profile, dia di-kill oleh kernel.
# Default Docker seccomp profile (allow ~300 syscalls) — masih longgar
# Custom profile — strict, cuma allow yang benar-benar diperlukan
# Simpan sebagai seccomp-web.json
{
"defaultAction": "SCMP_ACT_ERRNO",
"architectures": ["SCMP_ARCH_X86_64"],
"syscalls": [
{ "names": ["accept", "bind", "brk", "clock_gettime", "clone",
"close", "connect", "epoll_create", "epoll_wait", "exit_group",
"fstat", "futex", "getdents", "getpeername", "getsockname",
"getsockopt", "listen", "lseek", "mkdir", "mmap", "mprotect",
"munmap", "nanosleep", "newfstatat", "openat", "poll", "read",
"readv", "recvfrom", "recvmsg", "rseq", "rt_sigaction",
"rt_sigprocmask", "sched_getattr", "sched_yield", "sendmmsg",
"sendto", "set_robust_list", "setsockopt", "shutdown",
"socket", "statx", "tgkill", "write", "writev"],
"action": "SCMP_ACT_ALLOW"
}
]
}
# Use it:
docker run --security-opt seccomp=seccomp-web.json myapp:latest
4. AppArmor profile
Kalau lo pake Ubuntu/Debian, AppArmor adalah mandatory access control (MAC) yang nge-restrict file access, network, dan capabilities container. Docker include profile default: docker-default. Tapi lo bisa bikin custom profile yang lebih strict.
# /etc/apparmor.d/docker-web
#include
profile docker-web flags=(attach_disconnected) {
#include
#include
network tcp,
network udp,
/ r,
/app/** r,
/app/data/** rw,
deny /proc/** rw,
deny /sys/** rw,
deny /etc/shadow rw,
deny capability sys_admin,
}
# Load profile
apparmor_parser -r /etc/apparmor.d/docker-web
# Gunakan di container
docker run --security-opt apparmor=docker-web myapp:latest
Layer 4: User Namespaces — Root di Luar Container Bukan Root di Host
Root inside container secara default adalah UID 0 yang sama dengan root di host. Artinya: kalau attacker break out dari container via kernel exploit, mereka langsung punya root di host. Rootless Docker dan user namespace remapping fix ini dengan map UID 0 di container ke UID non-privileged di host.
# Enable user namespace remapping di /etc/docker/daemon.json
{
"userns-remap": "default"
}
# Docker akan create user dockremap dan map UID/GID
# Container root -> UID 100000 (host)
# Container UID 1 -> UID 100001 (host)
# DLL
# Verify:
docker run --rm alpine id
# uid=0(root) gid=0(root) — inside container masih root
# Tapi di host, process ini jalan sebagai UID 100000
# Kalau break out, attacker dapet UID 100000, bukan root
Trade-off yang perlu lo tahu:
- User namespace remapping gak kompatibel dengan beberapa storage driver dan volume mounts. Kalau lo bind-mount direktori dari host, permission mapping bisa jadi rumit. Solusi: pake named volumes untuk persistent data, bukan bind mounts.
- Gak bisa expose privileged ports (<1024) langsung — karena container gak punya CAP_NET_BIND_SERVICE di level host. Solusi: pake reverse proxy di host yang forward ke port tinggi di container.
- Beberapa tools monitoring yang perlu akses host (docker socket) gak work. Solusi: pake cAdvisor atau Netdata yang jalan di host level, bukan di container.
Layer 5: Network Security — Minimal Exposure
Prinsip: jangan expose port yang gak perlu. Ini terdengar obvious, tapi gw masih sering liat container yang expose port 3000, 5432, 6379, 8080 ke public — padahal cuma port 80/443 yang perlu diakses dari luar.
# BURUK: expose semua port ke 0.0.0.0
ports:
- "3000:3000" # API — harusnya cuma internal
- "5432:5432" # PostgreSQL — LANGSUNG KE PUBLIC?!
# BAIK: cuma expose port yang diperlukan
ports:
- "127.0.0.1:4321:80" # reverse proxy akses dari localhost
# Database cuma accessible dari internal Docker network
networks:
- internal
- web
# Reverse proxy di network "web", database di network "internal"
# API server di kedua network
Buat network segmentation: pisah container ke network yang berbeda berdasarkan fungsi. Database cuma di internal network — gak bisa diakses dari luar. Reverse proxy di web network — satu-satunya titik masuk dari eksternal. Kalau API server di kedua network, dia jadi bridge yang terkontrol.
Layer 6: Vulnerability Monitoring Berkelanjutan
Security bukan one-time setup — ini proses berkelanjutan. Gw setup weekly vulnerability scan otomatis yang jalanin Trivy di semua image dan kirim alert kalau ada CRITICAL CVE baru.
#!/bin/bash
# /usr/local/bin/weekly-scan.sh — jalan tiap Minggu jam 3 pagi
IMAGES=("myapp:latest" "nginx:alpine" "postgres:16-alpine")
for IMAGE in "${IMAGES[@]}"; do
docker pull "$IMAGE"
RESULT=$(docker run --rm aquasec/trivy image --severity HIGH,CRITICAL --quiet "$IMAGE")
if [ -n "$RESULT" ]; then
echo "SECURITY ALERT: $IMAGE has HIGH/CRITICAL CVEs"
# Kirim notifikasi via webhook
fi
done
Kombinasikan security scanning dengan backup strategy 3-2-1 dan zero trust architecture untuk defense in depth. Container security hardening bukan proyek satu kali — ini budaya yang harus diterapin di setiap Dockerfile, setiap deployment, setiap CI/CD pipeline. Mulai dari satu container dulu, hardening dengan 3-4 layer di atas, baru scale ke container lainnya. Jangan overwhelm dengan semua checklist sekaligus.
💬 Komentar (0)
Belum ada komentar. Jadilah yang pertama! 💬