Skip to content

Module 5: Hardening & Optimization (Gia cố & Tối ưu hóa)

🎓 Instructor Profile

Kỹ sư Raizo (Phó CTO HPN) - Chuyên gia DevSecOps với tư duy "Zero Trust" (Không tin bất cứ ai). Cùng Giáo sư Tom xây dựng những pháo đài Container bất khả xâm phạm.

Chào mừng đến với Module quan trọng nhất đối với một kỹ sư Production. Bạn có thể build image nhanh, chạy compose giỏi, nhưng nếu để lộ một lỗ hổng bảo mật, toàn bộ hệ thống của công ty có thể sụp đổ chỉ sau một đêm.

Container rất mạnh, nhưng mặc định nó KHÔNG an toàn tuyệt đối. Chúng ta phải gia cố (Harden) nó.


🛡️ Phần 1: The Root Trap (Cạm bẫy quyền Root)

Sự thật trần trụi

Theo mặc định, mọi tiến trình trong Docker Container đều chạy với UID 0 (Root User).

Rủi ro chết người (Critical Risk) ☠️

Container chia sẻ Kernel với máy Host. Nếu Hacker khai thác được một lỗ hổng trong ứng dụng (VD: SQL Injection, Remote Code Execution) để chiếm quyền điều khiển Container -> Hắn lập tức có quyền Root trong Container. Nếu Kernel Linux có lỗ hổng chưa kịp vá (Kernel Exploit), Hacker có thể thực hiện kỹ thuật Container Breakout (Vượt ngục) và chiếm quyền Root của toàn bộ máy chủ vật lý.

👉 Quy tắc vàng: "Drop Root". Hãy biến Container thành một nhà tù không có chìa khóa.


⚖️ Phần 2: Implementing Least Privilege (Thực thi quyền tối thiểu)

Nguyên tắc Least Privilege: Chỉ cấp vừa đủ quyền để ứng dụng chạy, không hơn không kém.

Kỹ thuật trong Dockerfile

Thay vì để Docker tự quyết định, hãy chủ động tạo User thường.

dockerfile
# 1. Chọn Base Image
FROM node:18-alpine

# 2. Tạo Group và User hệ thống (System User)
# -S: System user (không cần login)
# -G: Thuộc group nào
RUN addgroup -S appgroup && adduser -S appuser -G appgroup

WORKDIR /app

# 3. Phân quyền sở hữu file cho user mới
# Nếu không có dòng này, file copy vào sẽ thuộc quyền root -> appuser không đọc/ghi được
COPY --chown=appuser:appgroup package*.json ./
RUN npm install
COPY --chown=appuser:appgroup . .

# 4. 🛑 DROP ROOT: Chuyển sang user thường từ dòng này
USER appuser

CMD ["node", "index.js"]

⚠️ Lưu ý về Port

User thường (Non-root) không thể bind vào các Privileged Ports (Cổng < 1024).

  • ❌ Không thể lắng nghe port 80.
  • ✅ Phải đổi ứng dụng sang lắng nghe port > 1024 (VD: 3000, 8080).

📉 Phần 3: Resource Quotas (Hạn ngạch tài nguyên)

Tư duy @[/observability]: Sẽ ra sao nếu code của bạn có bug Memory Leak (Rò rỉ bộ nhớ)? Container sẽ ăn dần RAM cho đến khi HẾT SẠCH RAM của máy chủ vật lý -> Máy chủ bị treo -> Tất cả dịch vụ khác chết theo (Domino Effect).

OOM Killer (Kẻ sát nhân giấu mặt)

Linux Kernel có một cơ chế tự vệ là Out Of Memory (OOM) Killer. Khi hết RAM, nó sẽ chọn tiến trình nào "ăn hại" nhất và bắn bỏ (Kill) không thương tiếc để cứu hệ thống.

Giải pháp: Limit Resource

Luôn đặt giới hạn cho Container để khoanh vùng thiệt hại.

yaml
# docker-compose.yml
services:
  web:
    image: my-app
    deploy:
      resources:
        limits:
          cpus: '0.50'    # Chỉ được dùng tối đa 50% của 1 core CPU
          memory: 512M    # Chỉ được dùng tối đa 512MB RAM
        reservations:
          memory: 128M    # Đảm bảo luôn có ít nhất 128MB để chạy

🧹 Phần 4: Image Hygiene (Vệ sinh Image)

Image càng chứa nhiều thứ, Bề mặt tấn công (Attack Surface) càng rộng.

1. Quét lỗ hổng (Vulnerability Scanning)

Sử dụng Docker Scout (công cụ mới thay thế Docker Scan) hoặc Trivy.

bash
# Xem nhanh các lỗi bảo mật
docker scout quickview node:14-alpine

Bạn sẽ thấy danh sách các lỗ hổng CVE (Common Vulnerabilities and Exposures) được phân loại: Critical, High, Medium. 👉 Hành động: Nếu thấy Critical, hãy nâng cấp Base Image ngay lập tức (VD: từ node:14 lên node:18).

2. Distroless Images (Cấp độ Paranoid) 🕵️

Ngay cả alpine cũng chứa các công cụ như sh, ls, cat, vi. Nếu Hacker vào được, hắn có thể dùng các công cụ này để thám thính và phá hoại.

Google Distroless Images là các image đã bị lột bỏ hoàn toàn Shell và các tiện ích Linux. Chỉ chứa đúng Runtime (Java, Node, Python) và App của bạn.

  • Ưu điểm: Hacker vào được shell -> Không gõ được lệnh gì cả.
  • Nhược điểm: Khó debug (vì không exec vào được).

🔒 Phần 5: Read-Only Filesystem

Để ngăn chặn Hacker cài Backdoor (Cửa hậu) hoặc sửa đổi mã nguồn ngay trên môi trường Production, hãy khóa quyền ghi.

yaml
services:
  web:
    image: my-app
    read_only: true  # 🧱 Biến container thành pháo đài "Nội bất xuất, ngoại bất nhập"
    volumes:
      - /tmp         # Chỉ cho phép ghi vào các vùng đệm cần thiết

🏆 Challenge: The Hardening (Thử thách Gia cố)

Nhiệm vụ:

  1. Tạo một Dockerfile chạy Nginx.
  2. Cấu hình để Nginx chạy dưới quyền user nginx (có sẵn trong image), thay vì root.
  3. Đổi port Nginx từ 80 sang 8080 (trong file config Nginx và Dockerfile).
  4. Giới hạn RAM 64MB.
  5. Thêm healthcheck để đảm bảo server còn sống.

Nếu bạn làm được điều này, bạn đã lọt vào top 10% Developer quan tâm đến bảo mật thực sự! 🎖️


🔐 Phần 6: Secrets Management (Quản lý bí mật)

Sai lầm phổ biến: Environment Variables

dockerfile
# ❌ KHÔNG BAO GIỜ làm thế này
ENV DATABASE_PASSWORD=SuperSecret123
ENV API_KEY=sk-1234567890abcdef

Vấn đề: Env vars bị lưu vào Image layers. Bất kỳ ai pull image đều có thể xem:

bash
# Hacker xem secrets trong image
docker history my-app --no-trunc
docker inspect my-app | jq '.[0].Config.Env'

Giải pháp đúng: Docker Secrets (Swarm Mode)

yaml
# docker-compose.yml (Swarm mode)
services:
  api:
    image: my-app
    secrets:
      - db_password
      - api_key
    environment:
      # Đọc secret từ file (không từ env var)
      DB_PASSWORD_FILE: /run/secrets/db_password

secrets:
  db_password:
    file: ./secrets/db_password.txt  # Local file
  api_key:
    external: true  # Quản lý bởi Swarm manager

Giải pháp cho non-Swarm: .env + .dockerignore

bash
# .env (KHÔNG commit vào git)
DB_PASSWORD=SuperSecret123

# .dockerignore (PHẢI có)
.env
*.key
*.pem
secrets/
yaml
# docker-compose.yml
services:
  api:
    env_file: .env  # Inject lúc runtime, không bake vào image

🚫 Golden Rule

Secrets KHÔNG BAO GIỜ được bake vào Docker Image. Luôn inject lúc runtime qua env_file, Docker Secrets, hoặc external secret manager (Vault, AWS Secrets Manager).


Phần 7: Build Optimization (Tối ưu hóa Build)

Multi-stage Build (Kiến trúc đa tầng)

Giảm kích thước image từ 1GB xuống còn 50MB bằng cách tách build stage và runtime stage:

dockerfile
# === Stage 1: BUILD (có đầy đủ build tools) ===
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build

# === Stage 2: RUNTIME (chỉ có thứ cần thiết) ===
FROM node:20-alpine AS runtime
WORKDIR /app

# Chỉ copy artifact từ stage build
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules

# Non-root user
RUN addgroup -S app && adduser -S app -G app
USER app

EXPOSE 3000
CMD ["node", "dist/index.js"]

Cache Mount (BuildKit)

Tăng tốc build bằng cách cache package manager:

dockerfile
# syntax=docker/dockerfile:1
FROM node:20-alpine
WORKDIR /app

# 🚀 Cache npm downloads giữa các lần build
RUN --mount=type=cache,target=/root/.npm \
    npm ci --only=production

# 🚀 Cache apt-get cho Debian-based images
RUN --mount=type=cache,target=/var/cache/apt \
    apt-get update && apt-get install -y curl

.dockerignore Best Practices

text
# .dockerignore — Giảm build context từ 500MB xuống 5MB
node_modules
.git
.github
*.md
docker-compose*.yml
.env*
.vscode
coverage
.nyc_output
dist
*.log

So sánh kích thước Image

Base ImageKích thướcUse Case
node:20~1GB❌ Quá nặng cho production
node:20-slim~200MB✅ Production OK
node:20-alpine~180MB✅ Production tốt nhất
gcr.io/distroless/nodejs20~130MB🔒 Maximum security

🛡️ Phần 8: Runtime Security (Bảo mật Runtime)

AppArmor Profile

Giới hạn system calls mà container được phép thực hiện:

bash
# Kiểm tra AppArmor status
docker inspect --format='{{.AppArmorProfile}}' my-container
# Output: docker-default

# Chạy container với custom AppArmor profile
docker run --security-opt apparmor=my-custom-profile nginx

Seccomp Profile (Secure Computing)

Lọc syscalls ở mức kernel — tầng bảo vệ sâu nhất:

bash
# Xem default seccomp profile (block ~44 syscalls nguy hiểm)
docker run --rm --security-opt seccomp=unconfined nginx  # ❌ Tắt seccomp
docker run --rm --security-opt seccomp=custom.json nginx  # ✅ Custom profile
json
{
  "defaultAction": "SCMP_ACT_ERRNO",
  "syscalls": [
    {
      "names": ["read", "write", "open", "close", "stat", "fstat"],
      "action": "SCMP_ACT_ALLOW"
    }
  ]
}

Read-Only + tmpfs Pattern

Kết hợp read-only filesystem với tmpfs cho các thư mục cần ghi tạm:

yaml
services:
  web:
    image: nginx:alpine
    read_only: true
    tmpfs:
      - /tmp:size=64M
      - /var/run:size=1M
      - /var/cache/nginx:size=32M
    security_opt:
      - no-new-privileges:true  # Ngăn privilege escalation

PID Limits (Chống Fork Bomb)

yaml
services:
  web:
    image: my-app
    deploy:
      resources:
        limits:
          cpus: '1.0'
          memory: 512M
          pids: 100  # Tối đa 100 processes — chống fork bomb
bash
# Fork bomb test (ĐỪNG chạy trên production!)
# :(){ :|:& };:
# Không có PID limit → máy chủ treo cứng
# Có PID limit → chỉ container bị kill, host an toàn

🔍 Phần 9: Image Scanning Deep Dive (Quét bảo mật chuyên sâu)

Trivy (Open Source — khuyên dùng)

bash
# Scan image với Trivy
trivy image my-app:latest

# Chỉ hiện Critical và High
trivy image --severity CRITICAL,HIGH my-app:latest

# Scan và fail CI/CD nếu có Critical
trivy image --exit-code 1 --severity CRITICAL my-app:latest

# Scan cả misconfiguration trong Dockerfile
trivy config ./Dockerfile

Docker Scout (Official)

bash
# Quick overview
docker scout quickview my-app:latest

# Chi tiết CVEs
docker scout cves my-app:latest

# So sánh 2 image (trước và sau khi fix)
docker scout compare my-app:v1 --to my-app:v2

Tích hợp vào CI/CD Pipeline


⚠️ Production Pitfalls (Bẫy thực chiến)

⚠️ Cạm bẫy

Vấn đề: Không thêm USER directive trong Dockerfile → container chạy root (UID 0).

Hậu quả thực tế: CVE-2019-5736 (runc vulnerability) cho phép attacker escape container → chiếm root host. Mọi container chạy root đều bị ảnh hưởng. Thiệt hại: hàng triệu server trên toàn cầu phải patch khẩn cấp.

Giải pháp:

  • Luôn thêm USER nonroot hoặc tạo user riêng
  • Scan image để detect root user: docker inspect --format='{{.Config.User}}' my-app
  • Enforce bằng PodSecurityPolicy (Kubernetes) hoặc runtime policy

⚠️ Cạm bẫy

Vấn đề: Truyền password qua docker run -e DB_PASS=secret123 hoặc ENV trong Dockerfile.

Hậu quả: Secrets lộ qua docker inspect, process listing (/proc/*/environ), logging, và image history. Một lần bị commit vào image = secrets leaked vĩnh viễn.

Giải pháp: Dùng Docker Secrets, env_file (không commit vào git), hoặc external secret manager (HashiCorp Vault, AWS Secrets Manager).

⚠️ Cạm bẫy

Vấn đề: Dùng node:20 (1GB) hoặc ubuntu:latest (70MB+) làm production image.

Hậu quả: Pull chậm, deploy chậm, attack surface rộng (nhiều package = nhiều CVE tiềm ẩn), tốn bandwidth và storage.

Giải pháp:

  • Multi-stage build: Tách build tools khỏi runtime
  • Alpine hoặc Distroless base images
  • .dockerignore loại bỏ file rác
  • Scan thường xuyên: trivy image --severity CRITICAL

📝 Quiz: Kiểm tra kiến thức

🧠 Quiz

Câu 1: Tại sao KHÔNG nên truyền secrets qua ENV directive trong Dockerfile?

  • [ ] A) Vì ENV chỉ hoạt động với string, không hỗ trợ file
  • [x] B) Vì ENV được lưu vào image layers, ai có image đều xem được
  • [ ] C) Vì ENV không thể truyền vào container khi runtime
  • [ ] D) Vì ENV bị xóa sau khi build xong

💡 Giải thích: ENV directive lưu giá trị vào metadata của image layer. Bất kỳ ai pull image đều có thể xem bằng docker history --no-trunc hoặc docker inspect. Secrets phải được inject lúc runtime (env_file, Docker Secrets, secret manager).

Câu 2: Multi-stage build giúp giảm kích thước image bằng cách nào?

  • [ ] A) Nén tất cả layers thành một layer duy nhất
  • [ ] B) Xóa cache sau mỗi lệnh RUN
  • [x] C) Chỉ copy artifacts cần thiết từ build stage sang runtime stage
  • [ ] D) Tự động chuyển sang Alpine base image

💡 Giải thích: Multi-stage build cho phép dùng image đầy đủ tools (gcc, npm, maven) ở build stage, rồi chỉ COPY --from=builder những artifact cuối cùng (binary, dist/) sang một runtime image nhỏ gọn. Build tools không xuất hiện trong final image.

Câu 3: Seccomp profile trong Docker có chức năng gì?

  • [ ] A) Mã hóa traffic giữa các container
  • [ ] B) Quản lý secrets và certificates
  • [x] C) Lọc và giới hạn system calls mà container được phép gọi
  • [ ] D) Giám sát CPU và RAM usage của container

💡 Giải thích: Seccomp (Secure Computing Mode) là tính năng Linux kernel cho phép lọc syscalls. Docker mặc định block ~44 syscalls nguy hiểm (như reboot, mount, clock_settime). Custom seccomp profile cho phép whitelist chỉ syscalls cần thiết, giảm attack surface đáng kể.


Production Readiness Checklist

✅ Checklist triển khai

Checklist Hardening & Optimization cho Production:

  • [ ] Container chạy với non-root user (USER directive)
  • [ ] Secrets KHÔNG bake vào image (dùng env_file / Docker Secrets)
  • [ ] .env và secret files có trong .gitignore.dockerignore
  • [ ] Multi-stage build đã được áp dụng (build ≠ runtime stage)
  • [ ] Base image là Alpine / Slim / Distroless
  • [ ] Resource limits đã set (CPU, Memory, PID)
  • [ ] Read-only filesystem + tmpfs cho thư mục tạm
  • [ ] no-new-privileges security option đã bật
  • [ ] Image đã scan bằng Trivy / Docker Scout (0 Critical)
  • [ ] CI/CD pipeline block deploy khi phát hiện Critical CVE
  • [ ] .dockerignore đã loại bỏ .git, node_modules, .env
  • [ ] Health check đã cấu hình trong Dockerfile hoặc Compose

💡 Next Steps

Bạn đã biết cách gia cố container như một pháo đài. Module cuối cùng sẽ đưa bạn vào lõi Linux Kernel để hiểu container hoạt động ở mức sâu nhất — namespaces, cgroups, và OCI runtime!