Chapter 05

Security

SSL/TLS, firewall, container hardening, và các best practices bảo mật cho Docker deployment

SSL/TLS với Let's Encrypt

HTTPS là bắt buộc cho production. Let's Encrypt cung cấp SSL certificate miễn phí, tự động renew:

Client
HTTPS :443
Nginx
SSL Termination
Backend
HTTP (internal)
SSL Termination nghĩa là Nginx xử lý mã hóa/giải mã SSL. Traffic giữa Nginx và backend containers dùng HTTP thuần (nhanh hơn) vì đã nằm trong Docker internal network — hoàn toàn cô lập.

Cài đặt Certbot và lấy certificate

Bash
# Cài Certbot trên Ubuntu
sudo apt install -y certbot

# Tạm dừng Nginx container (Certbot cần port 80)
docker compose stop nginx

# Lấy certificate (standalone mode)
sudo certbot certonly --standalone \
  -d app.example.com \
  --email admin@example.com \
  --agree-tos --no-eff-email

# Certificate được lưu tại:
# /etc/letsencrypt/live/app.example.com/fullchain.pem
# /etc/letsencrypt/live/app.example.com/privkey.pem

# Copy vào thư mục project
sudo cp /etc/letsencrypt/live/app.example.com/fullchain.pem ./nginx/ssl/
sudo cp /etc/letsencrypt/live/app.example.com/privkey.pem ./nginx/ssl/
sudo chown $USER:$USER ./nginx/ssl/*.pem

# Khởi động lại Nginx
docker compose start nginx

Auto-renew certificate

Bash
# Tạo script renew
cat > /home/$USER/renew-ssl.sh << 'EOF'
#!/bin/bash
cd /home/$USER/my-app

# Dừng Nginx, renew cert, copy, restart
docker compose stop nginx
certbot renew --quiet
cp /etc/letsencrypt/live/app.example.com/fullchain.pem ./nginx/ssl/
cp /etc/letsencrypt/live/app.example.com/privkey.pem ./nginx/ssl/
docker compose start nginx
EOF

chmod +x /home/$USER/renew-ssl.sh

# Cron job: chạy mỗi ngày lúc 3:00 AM
(crontab -l; echo "0 3 * * * /home/$USER/renew-ssl.sh >> /var/log/ssl-renew.log 2>&1") | crontab -
Let's Encrypt certificates có hạn 90 ngày. Certbot chỉ renew khi cert còn dưới 30 ngày — nên chạy cron hàng ngày là an toàn, không gây load thừa.

Firewall (UFW)

Cấu hình UFW (Uncomplicated Firewall) để chỉ cho phép traffic cần thiết:

Bash
# Cho phép SSH (quan trọng! nếu quên sẽ bị lock out)
sudo ufw allow 22/tcp

# Cho phép HTTP và HTTPS
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp

# Bật firewall
sudo ufw enable

# Kiểm tra rules
sudo ufw status verbose
Docker bypass UFW! Khi Docker publish ports (-p hoặc ports: trong compose), nó tạo iptables rules trực tiếp, bỏ qua UFW. Nghĩa là nếu bạn vô tình dùng ports: "5432:5432" cho PostgreSQL, UFW không chặn được — port 5432 vẫn mở ra internet.

Giải pháp: Hạn chế Docker iptables

Bash
# Tạo/sửa file /etc/docker/daemon.json
sudo tee /etc/docker/daemon.json << 'EOF'
{
  "iptables": false
}
EOF

# Restart Docker daemon
sudo systemctl restart docker

# SAU KHI TẮT iptables, thêm rule NAT thủ công cho ports cần expose
sudo iptables -t nat -A PREROUTING -p tcp --dport 80 -j DNAT --to-destination 172.17.0.2:80
sudo iptables -t nat -A PREROUTING -p tcp --dport 443 -j DNAT --to-destination 172.17.0.2:443
Giải pháp đơn giản hơn: không dùng ports: cho bất kỳ service nào ngoài Nginx. Khi chỉ Nginx expose ports, UFW chỉ cần quản lý port 80/443, và mọi service internal đều an toàn.

Container Hardening

Các best practices để giảm attack surface của containers:

Chạy dưới non-root user Critical +

Mặc định, processes trong container chạy với quyền root. Nếu attacker escape container, họ sẽ có root trên host.

Dockerfile
# Tạo non-root user
RUN adduser --disabled-password --gecos "" appuser
USER appuser

# Hoặc dùng numeric UID (không phụ thuộc /etc/passwd)
USER 1001

Trong docker-compose, cũng có thể set:

YAML
backend:
  user: "1001:1001"
Read-only filesystem Recommended +

Ngăn attacker ghi malware hoặc modify binaries bên trong container:

YAML
frontend:
  read_only: true
  tmpfs:
    - /tmp            # Cho phép ghi vào /tmp (nếu app cần)
    - /var/run        # Nginx PID file

Container chỉ được ghi vào tmpfs (RAM-based, mất khi restart) và mounted volumes.

Giới hạn capabilities Advanced +

Linux capabilities cho phép fine-grained permissions. Drop tất cả và chỉ thêm những gì cần:

YAML
backend:
  cap_drop:
    - ALL              # Drop tất cả capabilities
  cap_add:
    - NET_BIND_SERVICE # Chỉ thêm nếu cần bind port < 1024
Resource limits Important +

Ngăn container sử dụng quá nhiều tài nguyên (DoS protection):

YAML
backend:
  deploy:
    resources:
      limits:
        cpus: "1.0"       # Max 1 CPU core
        memory: 512M      # Max 512 MB RAM
      reservations:
        cpus: "0.25"     # Guaranteed 0.25 CPU
        memory: 128M      # Guaranteed 128 MB RAM
No new privileges Recommended +

Ngăn processes trong container leo thang quyền (privilege escalation):

YAML
backend:
  security_opt:
    - no-new-privileges:true

Flag này ngăn setuid, setgid, và các cơ chế leo quyền khác bên trong container.

Base Image Security

Chọn base image đúng giảm đáng kể attack surface:

Alpine-based

Nhỏ gọn, ít vulnerability
  • node:20-alpine (~50 MB vs ~350 MB)
  • postgres:16-alpine (~80 MB vs ~430 MB)
  • redis:7-alpine (~30 MB vs ~130 MB)
  • Ít packages = ít CVEs tiềm tàng
🛠

Best Practices

Quản lý images an toàn
  • Pin version cụ thể (không dùng :latest)
  • Scan images: docker scout cves myimage
  • Update base images định kỳ (monthly)
  • Dùng official images từ Docker Hub

Security Checklist

Checklist nhanh trước khi đưa hệ thống lên production:

Checklist
[Network]
 Chỉ Nginx expose ports ra ngoài (80/443)
 Database và Redis dùng internal network
 UFW chỉ mở port 22, 80, 443
 Hiểu rằng Docker bypass UFW

[SSL/TLS]
 HTTPS bật cho tất cả traffic
 HTTP redirect sang HTTPS
 SSL auto-renew đã cấu hình
 TLS 1.2+ only (disable TLS 1.0/1.1)

[Containers]
 Chạy dưới non-root user
 Base images pinned version
 Resource limits đã set
 no-new-privileges enabled

[Secrets]
 .env file có permission 600
 .env nằm trong .gitignore
 Passwords đủ mạnh (16+ chars)
 JWT secret đủ dài (32+ chars)

[Headers]
 X-Frame-Options: SAMEORIGIN
 X-Content-Type-Options: nosniff
 Strict-Transport-Security enabled
 CORS cấu hình chặt chẽ