❤️ 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.

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 1024CHOWN: Đổ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 rootbackend: internal: true: Network backend không có đường ra internetsecrets: 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:
- Daemon: Không expose socket, dùng TLS nếu remote, cân nhắc rootless mode
- Image: Dùng official image, pin version, scan trước khi deploy, multi-stage build
- Runtime: Read-only filesystem, drop capabilities, giới hạn resource, chặn privilege escalation
- Network: Tách network, chỉ expose port cần thiết, tránh host network
- Secrets: Không hardcode, dùng Docker secrets hoặc bảo vệ .env file
- Logging: Cấu hình rotation, không để log vô hạn
- 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.
Có thể bạn cần xem thêm
Về tác giả
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.