❤️ AZDIGI chính thức cập nhật hệ thống blog mới hoàn chỉnh. Tuy nhiên có thể một số bài viết bị sai lệch hình ảnh, hãy ấn nút Báo cáo bài viết ở cuối bài để AZDIGI cập nhật trong thời gian nhanh nhất. Chân thành cám ơn.

Docker giúp deploy nhanh, gọn, nhưng nếu không cấu hình cẩn thận thì nó cũng mở ra kha khá lỗ hổng. Docker daemon mặc định chạy với quyền root, container có thể bị escape, và image từ registry công khai không phải lúc nào cũng sạch. Bài này mình sẽ đi qua từng lớp bảo mật cho Docker trên VPS Linux, từ daemon, image cho đến runtime.

Docker Security

Tại sao Docker cần bảo mật riêng?

Nhiều bạn nghĩ container đã cách ly rồi thì an toàn. Thực tế thì không hẳn vậy. Có vài điểm cần hiểu rõ:

  • Docker daemon chạy root: Bất kỳ ai có quyền gọi Docker socket đều gián tiếp có quyền root trên host. Một container mount / của host là game over.
  • Container escape: Các lỗ hổng kernel hoặc cấu hình sai (privileged mode, mount sensitive path) có thể cho phép tiến trình trong container thoát ra host.
  • Chuỗi cung ứng attacks: Pull một image từ Docker Hub mà không verify, bạn có thể đang chạy code của người lạ trên server mình.

Hiểu được những rủi ro này rồi, mình sẽ đi vào từng phần cụ thể để khắc phục.

Bảo mật Docker daemon

Docker daemon lắng nghe qua Unix socket /var/run/docker.sock. Mặc định chỉ local access, nhưng nếu bạn bật TCP socket để remote thì cần cẩn thận.

Bind socket chỉ localhost

Nếu cần expose Docker API qua TCP, chỉ bind vào localhost:

# /etc/docker/daemon.json
{
  "hosts": ["unix:///var/run/docker.sock", "tcp://127.0.0.1:2376"]
}

Không bao giờ bind Docker daemon vào 0.0.0.0 mà không có TLS. Ai quét được port đó là có full quyền root trên server của bạn.

TLS cho remote access

Nếu bắt buộc phải truy cập Docker daemon từ xa (ví dụ CI/CD server khác), hãy bật TLS mutual authentication:

# /etc/docker/daemon.json
{
  "tls": true,
  "tlsverify": true,
  "tlscacert": "/etc/docker/tls/ca.pem",
  "tlscert": "/etc/docker/tls/server-cert.pem",
  "tlskey": "/etc/docker/tls/server-key.pem",
  "hosts": ["unix:///var/run/docker.sock", "tcp://0.0.0.0:2376"]
}

Sau khi chỉnh xong, restart Docker daemon:

sudo systemctl restart docker

Rootless Docker

Cách triệt để nhất để giảm rủi ro từ Docker daemon là chạy nó không cần root. Rootless Docker chạy toàn bộ daemon và container dưới user thường, nên dù container bị escape thì attacker cũng chỉ có quyền của user đó.

Cài đặt trên Ubuntu

# Cài các package cần thiết
sudo apt-get install -y uidmap dbus-user-session
# Chạy setup tool (với user thường, KHÔNG dùng sudo)
dockerd-rootless-setuptool.sh install

Sau khi cài xong, thêm các biến môi trường vào shell profile:

export PATH=/usr/bin:$PATH
export DOCKER_HOST=unix://$XDG_RUNTIME_DIR/docker.sock

Cài đặt trên AlmaLinux/Rocky Linux

# Cài package cần thiết
sudo dnf install -y fuse-overlayfs slirp4netns
# Chạy setup (với user thường)
dockerd-rootless-setuptool.sh install

Kiểm tra rootless Docker đang chạy:

docker info | grep -i "root"
# Kết quả mong đợi: rootless: true

Rootless Docker có một số hạn chế: không bind được port dưới 1024 (trừ khi cấu hình thêm), và một số volume mount có thể cần adjust permission. Với hầu hết ứng dụng web thông thường thì không ảnh hưởng gì.

Bảo mật image

Image là nền tảng của mọi container. Nếu image có vấn đề thì mọi thứ phía sau đều có vấn đề theo.

Chỉ dùng official images

Trên Docker Hub, các official images (có nhãn “Docker Official Image”) được Docker team review và cập nhật thường xuyên. Ưu tiên dùng các image này thay vì image từ nguồn không rõ.

# Tốt — official image
docker pull nginx
docker pull postgres
# Tránh — image từ nguồn không xác minh
docker pull randomuser123/nginx-custom

Pin version cụ thể

Tag latest thay đổi liên tục. Hôm nay build ngon, mai rebuild có thể lỗi vì base image đã thay đổi. Luôn pin version cụ thể:

# Không nên
FROM nginx:latest
# Nên
FROM nginx:1.25-alpine

Dùng Alpine variant khi có thể, vì image nhỏ hơn đồng nghĩa ít package hơn, ít lỗ hổng tiềm ẩn hơn.

Scan image tìm lỗ hổng

Trước khi đưa image lên production, hãy scan nó. Có hai công cụ phổ biến:

Docker Scout (tích hợp sẵn với Docker Desktop và CLI mới):

docker scout cve nginx:1.25
docker scout recommendations nginx:1.25

Trivy (open source, chạy được trên mọi môi trường):

# Cài Trivy trên Ubuntu
sudo apt-get install -y wget apt-transport-https gnupg
wget -qO - https://aquasecurity.github.io/trivy-repo/deb/public.key | sudo apt-key add -
echo "deb https://aquasecurity.github.io/trivy-repo/deb generic main" | sudo tee /etc/apt/sources.list.d/trivy.list
sudo apt-get update && sudo apt-get install -y trivy

# Cài Trivy trên AlmaLinux/Rocky Linux sudo rpm -ivh https://github.com/aquasecurity/trivy/releases/latest/download/trivy_*_Linux-64bit.rpm

# Scan image trivy image nginx:1.25 trivy image --severity HIGH,CRITICAL nginx:1.25

Nên tích hợp bước scan vào CI/CD pipeline. Image nào có lỗ hổng CRITICAL thì không cho deploy.

Multi-stage build giảm bề mặt tấn công

Multi-stage build cho phép bạn tách giai đoạn build (cần compiler, tools) khỏi giai đoạn chạy (chỉ cần runtime). Image cuối cùng sẽ nhỏ hơn và ít thành phần thừa hơn.

# Stage 1: Build
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build
# Stage 2: Production — chỉ copy artifacts cần thiết
FROM node:20-alpine
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
USER appuser
EXPOSE 3000
CMD ["node", "dist/server.js"]

Lưu ý dòng USER appuser ở cuối. Luôn chạy ứng dụng trong container với user không phải root.

Runtime security

Kể cả khi image sạch, cách bạn chạy container cũng quan trọng không kém. Dưới đây là những flag bạn nên dùng.

Read-only filesystem

Ngăn container ghi vào filesystem. Nếu ứng dụng cần ghi thì mount riêng thư mục cần ghi:

docker run -d \
  --read-only \
  --tmpfs /tmp \
  --tmpfs /var/cache/nginx \
  nginx:1.25-alpine

Chặn privilege escalation

Flag --no-new-privileges ngăn các tiến trình trong container leo thang quyền qua setuid/setgid:

docker run -d \
  --security-opt no-new-privileges:true \
  nginx:1.25-alpine

Drop Linux capabilities

Mặc định Docker cấp cho container một số Linux capabilities. Cách an toàn nhất là drop hết rồi chỉ thêm lại những gì cần:

# Drop tất cả, chỉ thêm lại NET_BIND_SERVICE (bind port < 1024)
docker run -d \
  --cap-drop ALL \
  --cap-add NET_BIND_SERVICE \
  nginx:1.25-alpine

Một số capabilities phổ biến cần thêm lại tùy ứng dụng:

  • NET_BIND_SERVICE: Bind port dưới 1024
  • CHOWN: Đổi owner file (một số app cần khi khởi động)
  • SETUID, SETGID: Chuyển user (nếu entrypoint cần chạy init rồi switch user)

Giới hạn tài nguyên

Không giới hạn resource thì một container bị lỗi có thể ăn hết RAM/CPU của cả host:

docker run -d \
  --memory 256m \
  --memory-swap 256m \
  --cpus 0.5 \
  --pids-limit 100 \
  nginx:1.25-alpine

  • --memory 256m: Giới hạn RAM 256MB
  • --memory-swap 256m: Bằng memory, nghĩa là không dùng swap
  • --cpus 0.5: Tối đa 50% một CPU core
  • --pids-limit 100: Tối đa 100 process, chống fork bomb

Kết hợp tất cả

Một lệnh docker run với đầy đủ security options trông như thế này:

docker run -d \
  --name secure-nginx \
  --read-only \
  --tmpfs /tmp \
  --tmpfs /var/cache/nginx \
  --cap-drop ALL \
  --cap-add NET_BIND_SERVICE \
  --security-opt no-new-privileges:true \
  --memory 256m \
  --cpus 0.5 \
  --pids-limit 100 \
  -p 80:80 \
  nginx:1.25-alpine

Network isolation

Mặc định tất cả container trên cùng bridge network có thể giao tiếp với nhau. Trong thực tế, không phải container nào cũng cần nói chuyện với nhau.

Tạo internal network cho backend

# Tạo network internal (không ra internet)
docker network create --internal backend-net

# Container database chỉ nằm trong internal network docker run -d --name db --network backend-net postgres:16-alpine

# Container app kết nối cả backend-net và frontend docker network create frontend-net docker run -d --name app \ --network backend-net \ nginx:1.25-alpine docker network connect frontend-net app

Với cấu hình này, container db không thể truy cập internet, và từ bên ngoài cũng không truy cập được trực tiếp vào database.

Chỉ expose port cần thiết

# Tốt — chỉ expose port 80 ra localhost
docker run -d -p 127.0.0.1:80:80 nginx:1.25-alpine

# Tránh — expose ra tất cả interface docker run -d -p 80:80 nginx:1.25-alpine

# Tránh — dùng host network docker run -d --net=host nginx:1.25-alpine

Nếu bạn dùng reverse proxy (Nginx, Caddy, Traefik) trước container, thì container backend không cần expose port ra ngoài. Để reverse proxy và backend cùng một Docker network, giao tiếp internal là đủ.

Docker Compose security patterns

Hầu hết mọi người dùng Docker Compose để quản lý multi-container. Dưới đây là cách áp dụng các security options trong Compose file:

version: "3.8"

services: web: image: nginx:1.25-alpine read_only: true tmpfs: - /tmp - /var/cache/nginx security_opt: - no-new-privileges:true cap_drop: - ALL cap_add: - NET_BIND_SERVICE deploy: resources: limits: memory: 256M cpus: "0.5" ports: - "127.0.0.1:80:80" networks: - frontend

app: image: myapp:1.0 user: "1000:1000" read_only: true tmpfs: - /tmp security_opt: - no-new-privileges:true cap_drop: - ALL deploy: resources: limits: memory: 512M cpus: "1.0" networks: - frontend - backend depends_on: - db

db: image: postgres:16-alpine user: "999:999" security_opt: - no-new-privileges:true cap_drop: - ALL deploy: resources: limits: memory: 1G cpus: "1.0" volumes: - db-data:/var/lib/postgresql/data networks: - backend environment: POSTGRES_PASSWORD_FILE: /run/secrets/db_password secrets: - db_password

networks: frontend: backend: internal: true

volumes: db-data:

secrets: db_password: file: ./secrets/db_password.txt

Một vài điểm đáng chú ý:

  • user: "1000:1000": Chạy container với UID/GID cụ thể, không phải root
  • backend: internal: true: Network backend không có đường ra internet
  • secrets: Dùng Docker secrets thay vì đặt password trực tiếp trong environment

Quản lý secrets

Đừng bao giờ đặt password, API key trực tiếp trong Dockerfile hay docker-compose.yml. Có mấy cách an toàn hơn:

Docker secrets (Compose)

Như ví dụ phía trên, Docker secrets mount file vào /run/secrets/ trong container. Ứng dụng đọc password từ file thay vì environment variable:

# Tạo file secret
echo "my-strong-password" > ./secrets/db_password.txt
chmod 600 ./secrets/db_password.txt

Bảo vệ .env file

Nếu vẫn dùng .env file, hãy đảm bảo permission chặt chẽ:

# Chỉ owner đọc được
chmod 600 .env

# Thêm vào .gitignore echo ".env" >> .gitignore

# Kiểm tra không có secret nào trong git history git log --all --full-history -- .env

Nếu bạn lỡ commit secret vào git, chỉ xóa file rồi commit lại là không đủ. Secret vẫn nằm trong git history. Bạn cần dùng git filter-repo hoặc BFG Repo-Cleaner để xóa hoàn toàn, rồi rotate tất cả credential đã bị lộ.

Cấu hình logging

Container log mặc định không giới hạn size. Chạy lâu ngày, log có thể ăn hết ổ cứng. Cấu hình log rotation ngay từ đầu:

Cho từng container

docker run -d \
  --log-driver json-file \
  --log-opt max-size=10m \
  --log-opt max-file=3 \
  nginx:1.25-alpine

Cấu hình mặc định cho toàn bộ daemon

Thêm vào /etc/docker/daemon.json để áp dụng cho tất cả container mới:

{
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "10m",
    "max-file": "3"
  }
}

sudo systemctl restart docker

Với cấu hình trên, mỗi container tối đa giữ 3 file log, mỗi file 10MB. Tổng cộng 30MB log cho mỗi container, đủ để debug mà không lo đầy ổ.

Docker Bench Security

Docker Bench Security là tool tự động kiểm tra cấu hình Docker theo CIS Benchmark. Chạy nó để xem server bạn đang thiếu gì:

docker run --rm --net host --pid host \
  --userns host --cap-add audit_control \
  -e DOCKER_CONTENT_TRUST=$DOCKER_CONTENT_TRUST \
  -v /var/lib:/var/lib:ro \
  -v /var/run/docker.sock:/var/run/docker.sock:ro \
  -v /usr/lib/systemd:/usr/lib/systemd:ro \
  -v /etc:/etc:ro \
  docker/docker-bench-security

Output sẽ liệt kê từng mục kiểm tra với kết quả PASS, WARN, hoặc INFO. Tập trung fix những mục WARN trước.

Ví dụ output dạng:

[PASS] 1.1 - Ensure a separate partition for containers has been created
[WARN] 1.2 - Ensure only trusted users are allowed to control Docker daemon
[PASS] 2.1 - Run the Docker daemon as a non-root user
[WARN] 4.1 - Ensure that a user for the container has been created
[PASS] 5.1 - Ensure that, if applicable, an AppArmor Profile is enabled

Nên chạy Docker Bench định kỳ (ví dụ mỗi tháng hoặc sau mỗi lần thay đổi cấu hình Docker) để đảm bảo không có gì bị bỏ sót.

Checkpoint: Kiểm tra lại

Sau khi áp dụng các bước trên, hãy kiểm tra lại bằng checklist này:

1. Scan image

# Scan image đang dùng
trivy image nginx:1.25-alpine
trivy image postgres:16-alpine

# Kiểm tra không có lỗ hổng CRITICAL trivy image --severity CRITICAL --exit-code 1 nginx:1.25-alpine

2. Chạy container với security options

# Chạy container test với đầy đủ security flags
docker run -d \
  --name security-test \
  --read-only \
  --tmpfs /tmp \
  --cap-drop ALL \
  --cap-add NET_BIND_SERVICE \
  --security-opt no-new-privileges:true \
  --memory 128m \
  --cpus 0.25 \
  --pids-limit 50 \
  nginx:1.25-alpine

# Verify container đang chạy docker ps | grep security-test

# Kiểm tra security settings docker inspect security-test | jq '.[0].HostConfig.CapDrop' docker inspect security-test | jq '.[0].HostConfig.ReadonlyRootfs' docker inspect security-test | jq '.[0].HostConfig.SecurityOpt'

# Cleanup docker rm -f security-test

3. Chạy Docker Bench Security

# Chạy audit
docker run --rm --net host --pid host \
  --userns host --cap-add audit_control \
  -v /var/lib:/var/lib:ro \
  -v /var/run/docker.sock:/var/run/docker.sock:ro \
  -v /usr/lib/systemd:/usr/lib/systemd:ro \
  -v /etc:/etc:ro \
  docker/docker-bench-security

# Đọc kết quả, focus vào các mục [WARN] # Fix từng mục theo recommendation

Nếu Docker Bench không báo WARN nào liên quan đến các mục mình đã cấu hình, nghĩa là bạn đã cover được phần lớn. Các mục còn lại tùy vào môi trường cụ thể mà bạn quyết định có cần fix hay không.

Tóm lại

Bảo mật Docker không phải chỉ một bước mà xong. Nó là nhiều lớp chồng lên nhau:

  1. Daemon: Không expose socket, dùng TLS nếu remote, cân nhắc rootless mode
  2. Image: Dùng official image, pin version, scan trước khi deploy, multi-stage build
  3. Runtime: Read-only filesystem, drop capabilities, giới hạn resource, chặn privilege escalation
  4. Network: Tách network, chỉ expose port cần thiết, tránh host network
  5. Secrets: Không hardcode, dùng Docker secrets hoặc bảo vệ .env file
  6. Logging: Cấu hình rotation, không để log vô hạn
  7. Audit: Chạy Docker Bench Security định kỳ

Không cần áp dụng hết cùng lúc. Bắt đầu từ những thứ dễ nhất (pin image version, cấu hình log rotation, drop capabilities) rồi dần dần nâng lên. Quan trọng là mỗi container chạy trên production đều có ít nhất một vài lớp bảo vệ, thay vì chạy mặc định hoàn toàn.

Chia sẻ:
Bài viết đã được kiểm duyệt bởi AZDIGI Team

Về tác giả

Trần Thắng

Trần Thắng

Chuyên gia tại AZDIGI với nhiều năm kinh nghiệm trong lĩnh vực web hosting và quản trị hệ thống.

Hơn 10 năm phục vụ 80.000+ khách hàng

Bắt đầu dự án web của bạn với AZDIGI