Tại sao image Docker thông thường lại là rủi ro bảo mật?
Khi mới học Docker, hầu hết mọi người đều quen tay với ubuntu:22.04 hoặc debian:bookworm làm base image. Tiện, có đủ tool, apt install thoải mái. Nhưng sau một thời gian đưa vào production, mình bắt đầu thấy đây là thói quen cần xem lại.
Thử chạy lệnh này để xem một image Ubuntu “bình thường” thực ra chứa những gì:
docker run --rm ubuntu:22.04 ls /bin
Kết quả: hàng trăm binary — bash, sh, curl, wget, nc, python3… Không thứ nào trong số đó liên quan đến ứng dụng của bạn. Nhưng nếu container bị xâm nhập, kẻ tấn công có đủ công cụ để di chuyển ngang (lateral movement), exfiltrate data, hoặc cài reverse shell ngay bên trong.
Google tạo ra Distroless Images để giải quyết đúng vấn đề này.
Distroless Images là gì?
Distroless là bộ base image do Google duy trì, chứa đúng những gì runtime cần — không hơn không kém. Không có shell (/bin/sh), không có package manager, không có coreutils. Chỉ có:
- Thư viện C chuẩn (
glibchoặcmusl) - CA certificates (cho HTTPS)
- Runtime tương ứng (JRE, Python interpreter, Node.js…)
- Timezone data
Về mặt bảo mật, điều này có nghĩa: ngay cả khi bị RCE, kẻ tấn công vào được container rồi cũng không chạy được lệnh nào vì không có shell. Không có package manager thì cũng không thể tải thêm tool. Attack surface thu hẹp đáng kể so với image Ubuntu thông thường.
Trên production cluster chạy 30+ container, mình đã áp dụng cách này và giảm được 40% resource usage — phần lớn đến từ image nhỏ hơn, layer cache hiệu quả hơn, và thời gian pull image khi scale out ngắn hơn rõ rệt.
Cài đặt và chuẩn bị môi trường
Không cần cài gì thêm. Distroless image được host trực tiếp trên Google Container Registry và GitHub Container Registry — có Docker là dùng được ngay.
Danh sách các image phổ biến:
# Các image Distroless phổ biến
gcr.io/distroless/static-debian12 # Chỉ CA certs + timezone (cho Go static binary)
gcr.io/distroless/base-debian12 # glibc + OpenSSL (cho C/C++, Rust)
gcr.io/distroless/java21-debian12 # JRE 21
gcr.io/distroless/python3-debian12 # Python 3
gcr.io/distroless/nodejs20-debian12 # Node.js 20
Mỗi image còn có tag :debug — thêm busybox shell để debug khi cần, nhưng không bao giờ dùng :debug trên production.
Cấu hình chi tiết: Multi-stage build với Distroless
Bí quyết dùng Distroless là kết hợp với multi-stage build: giai đoạn đầu dùng image đầy đủ để build, giai đoạn cuối copy artifact sang Distroless — gọn, không cần thay đổi gì ở source code.
Ví dụ 1: Ứng dụng Go (static binary)
# Stage 1: Build
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o myapp ./cmd/server
# Stage 2: Runtime — dùng Distroless static
FROM gcr.io/distroless/static-debian12
COPY --from=builder /app/myapp /myapp
EXPOSE 8080
ENTRYPOINT ["/myapp"]
Image cuối chỉ ~3–5MB — nhỏ hơn 160 lần so với golang:1.22 (~800MB), và thậm chí nhỏ hơn cả alpine (~10MB) vì không kéo theo shell.
Ví dụ 2: Ứng dụng Python (FastAPI/Flask)
# Stage 1: Install dependencies
FROM python:3.12-slim AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --prefix=/install --no-cache-dir -r requirements.txt
# Stage 2: Runtime
FROM gcr.io/distroless/python3-debian12
COPY --from=builder /install /usr/local
COPY --from=builder /app /app
WORKDIR /app
COPY . .
EXPOSE 8000
ENTRYPOINT ["python3", "-m", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
Ví dụ 3: Java Spring Boot
# Stage 1: Build JAR
FROM maven:3.9-eclipse-temurin-21 AS builder
WORKDIR /app
COPY pom.xml .
RUN mvn dependency:go-offline
COPY src ./src
RUN mvn package -DskipTests
# Stage 2: Runtime với JRE minimal
FROM gcr.io/distroless/java21-debian12
COPY --from=builder /app/target/myapp.jar /app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "/app.jar"]
Lưu ý quan trọng khi cấu hình
- ENTRYPOINT phải là exec form
["binary", "arg"], không dùng shell formbinary arg— vì không có shell để parse. - Không dùng
CMDdạng string — cùng lý do trên. - Không có user management — Distroless có sẵn user
nonroot(UID 65532). ThêmUSER nonroottrướcENTRYPOINTđể tránh chạy với quyền root. - Không có
/tmpwritable theo mặc định — nếu app cần temp file, mount tmpfs hoặc dùng emptyDir trong Kubernetes.
FROM gcr.io/distroless/static-debian12
COPY --from=builder /app/myapp /myapp
# Chạy với user nonroot thay vì root
USER nonroot:nonroot
EXPOSE 8080
ENTRYPOINT ["/myapp"]
Kiểm tra và Monitoring
So sánh kích thước image
# Build và so sánh
docker build -t myapp:distroless -f Dockerfile.distroless .
docker build -t myapp:ubuntu -f Dockerfile.ubuntu .
docker images | grep myapp
# myapp distroless a1b2c3d4 2 minutes ago 8.2MB
# myapp ubuntu e5f6g7h8 3 minutes ago 182MB
Kiểm tra không có shell
# Thử exec vào container — sẽ thất bại
docker run -d --name test myapp:distroless
docker exec -it test /bin/sh
# Error: OCI runtime exec failed: exec failed: unable to start container process:
# exec: "/bin/sh": stat /bin/sh: no such file or directory
# Với debug image để troubleshoot khi cần
docker run -it gcr.io/distroless/static-debian12:debug /busybox/sh
Scan vulnerability với Trivy
So sánh số lượng CVE là cách nhanh nhất để thấy sự khác biệt:
# Cài Trivy
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh
# Scan image ubuntu-based
trivy image myapp:ubuntu
# Kết quả: ~180 vulnerabilities (HIGH: 12, CRITICAL: 3)
# Scan image distroless
trivy image myapp:distroless
# Kết quả: ~8 vulnerabilities (HIGH: 1, CRITICAL: 0)
Chênh lệch đó khá ấn tượng — từ 3 CRITICAL xuống còn 0, và tổng số CVE giảm hơn 20 lần. Đây là lý do thực tế nhất để cân nhắc chuyển sang Distroless trên production.
Tích hợp healthcheck
Không có curl hay wget trong image, nên healthcheck cần một trong hai hướng: dùng binary tự viết trong app, hoặc chuyển hoàn toàn sang Kubernetes probe. Cách thứ nhất:
HEALTHCHECK --interval=30s --timeout=3s \
CMD ["/healthcheck"]
Trong Kubernetes thì đơn giản hơn nhiều — httpGet probe chạy hoàn toàn ngoài container, không cần binary nào bên trong:
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 15
periodSeconds: 20
Khi nào NÊN và KHÔNG NÊN dùng Distroless?
Nên dùng khi: app đã stable, chạy production lâu dài, team đã quen với multi-stage build, đang dùng Kubernetes (healthcheck dễ hơn nhiều).
Cân nhắc kỹ khi: app cần chạy script shell trong entrypoint, app gọi system command bên ngoài (subprocess, exec.Command), hoặc team vẫn đang giai đoạn develop và cần debug thường xuyên.
Với những trường hợp cần shell tối thiểu, alpine vẫn là lựa chọn cân bằng tốt — ~5MB, có /bin/sh, ít CVE hơn Ubuntu nhiều.
Distroless không phải silver bullet. Nhưng với microservice stateless viết bằng Go, Java, hay Python thuần, đây là cách đơn giản nhất để cải thiện security posture mà không tốn thêm công sức vận hành.

