Tuần 1 - Ngày 6: Security và Image Optimization
Mục tiêu học tập
- Giảm image size bằng multi-stage build và slim base
- Quét vulnerability với Trivy, ECR Scanning và Inspector
- Chạy container với non-root user
- Quản lý secrets đúng cách (không bake vào image)
- Hiểu SBOM (Software Bill of Materials) và tại sao quan trọng
- Tổng hợp security checklist cho production container
1. Tối ưu image size
Tại sao quan trọng?
- Pull time: image nhỏ → deploy nhanh hơn (quan trọng khi scale out)
- Attack surface: ít package → ít CVE
- Storage cost: ECR tính tiền theo GB
- Cold start: Fargate pull image khi task start — image nhỏ giảm cold start
Đo image size
docker images myapp
# REPOSITORY TAG IMAGE ID CREATED SIZE
# myapp latest abc123 1 min ago 1.2GB ← TRƯỚC tối ưu
docker history myapp:latest # xem từng layer chiếm bao nhiêu
Kỹ thuật 1: Chọn base image nhỏ hơn
# TRƯỚC: image đầy đủ
FROM python:3.12 # ~1 GB
# SAU: slim
FROM python:3.12-slim # ~130 MB
# SAU: alpine
FROM python:3.12-alpine # ~55 MB (chú ý: musl libc thay vì glibc — một số lib C native có thể không tương thích)
# SAU: distroless (không có shell — rất an toàn)
FROM gcr.io/distroless/python3-debian12 # ~50 MB, không có /bin/sh
Kỹ thuật 2: Dọn cache apt/apk trong cùng RUN layer
# SAI: xoá ở layer khác — không tiết kiệm được space
RUN apt-get update && apt-get install -y curl
RUN rm -rf /var/lib/apt/lists/* # layer riêng — KHÔNG work
# ĐÚNG: tất cả trong một RUN
RUN apt-get update && \
apt-get install -y --no-install-recommends curl && \
rm -rf /var/lib/apt/lists/*
Kỹ thuật 3: Multi-stage build (xem lại Day 3)
# Builder: có đủ tools compile
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci # cài cả devDependencies
COPY . .
RUN npm run build # compile TypeScript
# Runtime: chỉ cần artifacts
FROM node:20-alpine AS runtime
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
# Không có source .ts, test files, devDeps
Kỹ thuật 4: .dockerignore kỹ lưỡng
# .dockerignore — liệt kê tất cả không cần cho runtime
.git
.github
.gitignore
node_modules
dist
build
*.log
.env*
tests/
docs/
*.md
Dockerfile*
docker-compose*
.eslintrc*
.prettierrc*
coverage/
.nyc_output/
So sánh kết quả tối ưu
| Technique | Size |
|---|---|
node:20 base | ~1.1 GB |
node:20-alpine base | ~180 MB |
node:20-alpine + cleanup | ~165 MB |
| Multi-stage (alpine runtime) | ~90 MB |
| Multi-stage + distroless | ~65 MB |
2. Vulnerability Scanning
Trivy — scanner phổ biến nhất (open source)
Amazon ECR Enhanced Scanning (Inspector)
# Enable enhanced scanning cho toàn bộ registry
aws ecr put-registry-scanning-configuration \
--scan-type ENHANCED \
--rules '[{
"repositoryFilters": [{"filter":"*","filterType":"WILDCARD"}],
"scanFrequency": "CONTINUOUS_SCAN"
}]' \
--region ap-southeast-1
# CONTINUOUS_SCAN: tự động scan khi push + scan lại khi CVE database update
# SCAN_ON_PUSH: chỉ scan khi push (rẻ hơn nhưng không realtime)
# Xem kết quả scan
aws ecr describe-image-scan-findings \
--repository-name myapp \
--image-id imageTag=latest \
--region ap-southeast-1
3. Non-root User
Tại sao quan trọng?
Nếu container escape xảy ra (hacker breakout khỏi container), process chạy với root trong container tương đương root trên host (với một số cấu hình). Non-root user giảm blast radius đáng kể.
Triển khai
# Node.js — image official đã có user "node" (uid=1000)
FROM node:20-alpine
WORKDIR /app
COPY --chown=node:node package*.json ./
RUN npm ci --omit=dev
COPY --chown=node:node . .
# Chuyển sang non-root TRƯỚC CMD
USER node
EXPOSE 3000
CMD ["node", "server.js"]
# Python — tạo user mới
FROM python:3.12-slim
WORKDIR /app
# Tạo user/group
RUN groupadd -r appgroup && useradd -r -g appgroup -u 1001 appuser
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY --chown=appuser:appgroup . .
USER appuser
EXPOSE 8000
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "app:app"]
# Alpine Linux — dùng adduser/addgroup (khác cú pháp với Debian)
FROM node:20-alpine
RUN addgroup -S appgroup && adduser -S -G appgroup -u 1001 appuser
WORKDIR /app
COPY --chown=appuser:appgroup . .
USER appuser
Verify non-root
docker run --rm myapp whoami
# appuser ← không phải root
docker run --rm myapp id
# uid=1001(appuser) gid=1001(appgroup)
4. Secrets Management — KHÔNG bake vào image
Vấn đề: secrets trong Dockerfile/image
# TUYỆT ĐỐI KHÔNG làm như này
ENV DATABASE_PASSWORD=mysecret123 # lưu trong image layers
ARG API_KEY=sk-abc123
RUN curl -H "Authorization: Bearer ${API_KEY}" ...
# ARG value vẫn lưu trong layer history!
# Ai cũng có thể extract secrets
docker history my-image --no-trunc
docker inspect my-image | grep -i password
Giải pháp 1: Environment variables lúc runtime
# Truyền vào lúc docker run
docker run -e DATABASE_URL="postgres://..." myapp
# Hoặc từ file (không commit file này)
docker run --env-file .env.production myapp
Giải pháp 2: AWS Secrets Manager + ECS
Task Definition lấy secret từ Secrets Manager:
"secrets": [
{
"name": "DATABASE_PASSWORD",
"valueFrom": "arn:aws:secretsmanager:ap-southeast-1:123456:secret:myapp/db-password"
},
{
"name": "API_KEY",
"valueFrom": "arn:aws:secretsmanager:ap-southeast-1:123456:secret:myapp/api-key:api_key::"
}
]
ECS agent tự inject secret vào env var của container khi task start. Không bao giờ lưu trong image hay task definition plaintext.
Giải pháp 3: Docker BuildKit secret mount (build-time only)
# Đọc secret trong RUN nhưng KHÔNG lưu vào layer
RUN --mount=type=secret,id=npm_token \
NPM_TOKEN=$(cat /run/secrets/npm_token) \
npm install
# Build với secret
docker build \
--secret id=npm_token,src=~/.npmrc \
.
Secret chỉ tồn tại trong quá trình build, không lưu trong image.
5. SBOM — Software Bill of Materials
SBOM là danh sách tất cả components/packages trong image: OS packages, language libraries, licenses. Yêu cầu ngày càng nhiều trong Enterprise/Government từ 2022+.
# Generate SBOM với Syft (từ Anchore)
brew install syft
syft myapp:latest
# output danh sách tất cả packages
# Export dạng SPDX (chuẩn phổ biến)
syft myapp:latest -o spdx-json > sbom.spdx.json
# Export dạng CycloneDX
syft myapp:latest -o cyclonedx-json > sbom.cyclonedx.json
# ECR có thể attach SBOM vào image artifact
aws ecr put-registry-policy ... # cấu hình artifact store
6. Security Checklist tổng hợp
IMAGE BUILD
[ ] Dùng base image cụ thể tag (không dùng :latest cho base)
[ ] Base image là slim/alpine khi có thể
[ ] .dockerignore loại trừ .env, .git, node_modules, test files
[ ] Multi-stage build — không để build tools trong runtime image
[ ] Không bake secrets (passwords, API keys, tokens) vào image
[ ] ARG secrets dùng BuildKit --mount=type=secret
[ ] RUN apt/apk cleanup trong cùng layer với install
[ ] COPY --chown= đúng ownership cho non-root user
RUNTIME
[ ] USER non-root (uid >= 1000)
[ ] EXPOSE document port — không expose port không cần thiết
[ ] read-only filesystem nếu app không cần write (-v data:/data:ro)
[ ] Healthcheck định nghĩa trong Task Definition hoặc Dockerfile
[ ] Resource limits (CPU/memory) trong Task Definition
REGISTRY / DEPLOYMENT
[ ] ECR image scanning enabled (ENHANCED nếu cần)
[ ] Lifecycle policy — tự xoá image cũ
[ ] Secrets qua Secrets Manager / Parameter Store, không plaintext env
[ ] ecsTaskRole có least-privilege IAM policy
[ ] CloudWatch Logs cho container stdout/stderr
[ ] Network: security group chỉ cho phép inbound cần thiết
Câu hỏi ôn tập
-
Tại sao
rm -rf /var/lib/apt/lists/*ở layer RUN riêng biệt KHÔNG giảm image size?Xem đáp án
Docker image là immutable layers chồng lên nhau. Layer trước đã ghi file
/var/lib/apt/lists/vào filesystem — layer sau có thể "xoá" theo view của container, nhưng data vẫn tồn tại trong layer cũ trong image. Để thực sự giảm size, phải xoá trong cùng mộtRUNinstruction:RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*. -
Sự khác biệt giữa ECR Basic Scanning và Enhanced Scanning (Inspector)?
Xem đáp án
Basic Scanning dùng open-source Clair, chỉ scan OS packages, chỉ khi push (scan on push). Enhanced Scanning (Amazon Inspector) scan cả OS packages lẫn language-specific packages (npm, pip, maven...), hỗ trợ
CONTINUOUS_SCAN(tự động rescan khi CVE database cập nhật), và tích hợp findings vào AWS Security Hub. Enhanced Scanning chi tiết và realtime hơn nhưng có thêm chi phí. -
Khi container chạy root process bị compromise, tại sao nguy hiểm hơn non-root?
Xem đáp án
Nếu attacker escape container (container breakout vulnerability), process root trong container có thể tương đương root trên host OS trong một số cấu hình (thiếu seccomp, AppArmor, user namespace). Root trong container cũng có thể đọc/ghi vào bind-mounted volumes, leo thang privilege qua SUID binaries, hoặc manipulate host network. Non-root process (uid >= 1000) giảm blast radius đáng kể dù vẫn cần thêm các lớp bảo mật khác.
-
ARG API_KEY=secrettrong Dockerfile — secret này có an toàn không? Tại sao?Xem đáp án
Không an toàn. Dù
ARGchỉ tồn tại trong build time, giá trị của ARG vẫn được lưu trong image metadata và build history.docker history my-image --no-trunchoặcdocker inspectsẽ hiện giá trị ARG. Bất kỳ ai có image đều có thể extract secret. Giải pháp đúng: dùng Docker BuildKit secret mounts (RUN --mount=type=secret,id=my_secret ...) — secret chỉ tồn tại trong quá trình RUN, không lưu trong layer. -
Kể 3 cách truyền secrets vào container runtime mà không lưu trong image.
Xem đáp án
(1) Environment variables lúc runtime:
docker run -e DB_PASSWORD=xxxhoặc--env-file .env.prod— không bake vào image, chỉ exist trong process environment.(2) AWS Secrets Manager + ECS secrets: Task Definition khai báo
"secrets": [{"name": "DB_PASSWORD", "valueFrom": "arn:aws:secretsmanager:..."}]— ECS agent inject vào container lúc start, không qua image.(3) Docker Compose secrets hoặc Docker Swarm secrets: Mount secret dưới dạng file vào
/run/secrets/trong container — file chỉ exist trong memory (tmpfs), không ghi vào image layer.
Bài tập thực hành
# 1. Cài Trivy và scan image
brew install trivy
docker pull python:3.12 # image lớn, nhiều CVE
trivy image --severity HIGH,CRITICAL python:3.12
docker pull python:3.12-slim
trivy image --severity HIGH,CRITICAL python:3.12-slim
# So sánh số lượng CVE
# 2. Test non-root
cat > Dockerfile.nonroot << 'EOF'
FROM node:20-alpine
RUN addgroup -S appgroup && adduser -S -G appgroup appuser
WORKDIR /app
COPY --chown=appuser:appgroup . .
USER appuser
CMD ["whoami"]
EOF
docker build -f Dockerfile.nonroot -t nonroot-test .
docker run --rm nonroot-test # in ra "appuser"
# 3. Kiểm tra secrets leak
cat > Dockerfile.badsecret << 'EOF'
FROM alpine
ARG MY_SECRET=default
RUN echo "Secret: ${MY_SECRET}"
EOF
docker build --build-arg MY_SECRET=supersecret -f Dockerfile.badsecret -t badsecret .
docker history badsecret --no-trunc | grep supersecret # THẤY secret trong history!
# 4. Trivy scan Dockerfile misconfiguration
trivy config Dockerfile.badsecret
# Trivy sẽ báo lỗi: secrets in build args
# Dọn dẹp
docker rmi nonroot-test badsecret
Tài liệu tham khảo chính thức
- Docker security best practices
- Trivy documentation
- Amazon ECR image scanning
- Amazon Inspector for ECR
- AWS Secrets Manager with ECS
- Docker BuildKit secret mounts
- SBOM with Syft
Tiếp theo: Ngày 7 — Quiz Tổng Kết Tuần 1