Secure Ubuntu Server Cho Docker: Firewall, User Namespace, Secret Management
Docker giúp triển khai ứng dụng nhanh, nhất quán, dễ mở rộng. Nhưng Docker cũng mở thêm bề mặt tấn công: daemon quyền cao, container thoát ly chưa đủ, port public quá rộng, secret nằm lộ trong image/env/log. Một server Ubuntu chạy Docker “mặc định” thường chạy được, nhưng chưa chắc an toàn.
Bài viết này tập trung 3 lớp quan trọng: firewall, user namespace, secret management. Mục tiêu: giảm quyền, giảm lộ port, giảm rò secret → nếu app/container bị compromise, thiệt hại vẫn bị giới hạn.
1. Tư duy bảo mật Docker trên Ubuntu
Trước khi cấu hình, cần hiểu vài nguyên tắc:
– Docker daemon rất quyền lực: user thuộc group docker gần như có quyền root trên host.
– Container không phải VM: dùng chung kernel host → kernel attack surface quan trọng.
– Port published = public exposure nếu firewall không chặn.
– Secret trong image/env dễ rò qua docker inspect, log, CI/CD, layer cache.
– Defense-in-depth: firewall + least privilege + secret isolation + update định kỳ.
Checklist nền:
sudo apt update && sudo apt upgrade -y
sudo apt install -y ca-certificates curl gnupg ufwKiểm tra Docker:
docker version
docker info2. Firewall: kiểm soát traffic trước khi tới container
Vì sao UFW chưa đủ với Docker?
Docker tự thêm rule iptables để publish port. Vì vậy, nhiều người bật UFW nhưng container vẫn public port ngoài ý muốn.
Ví dụ:
docker run -p 8080:80 nginxPort 8080 có thể mở ra Internet, dù UFW không allow rõ ràng. Lý do: Docker thao tác chain iptables riêng, traffic forwarded có thể đi qua rule Docker trước.
Nguyên tắc publish port an toàn
Không publish mọi thứ ra0.0.0.0 nếu không cần.
Sai:
docker run -p 5432:5432 postgresĐúng hơn, chỉ bind localhost:
docker run -p 127.0.0.1:5432:5432 postgresKhi đó database chỉ accessible từ host, không public Internet.
Với Docker Compose:
services:
db:
image: postgres:16
ports:
- "127.0.0.1:5432:5432"Nếu app cùng network nội bộ Docker, thường không cần ports, dùng expose hoặc chỉ service name:
services:
app:
image: myapp
depends_on:
- db
db:
image: postgres:16
expose:
- "5432"
Cấu hình UFW cơ bản
Allow SSH trước:
sudo ufw allow OpenSSH
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw enable
sudo ufw status verboseAllow HTTP/HTTPS nếu server chạy reverse proxy:
sudo ufw allow 80/tcp
sudo ufw allow 443/tcpKhông allow DB/cache/internal service trừ khi thật sự cần.
Chặn Docker published ports bằng DOCKER-USER chain
Docker cung cấp chain DOCKER-USER: rule ở đây được apply trước rule Docker. Đây là nơi phù hợp để enforce firewall.
Ví dụ: chỉ cho IP 203.0.113.10 truy cập port 8080 container-published:
sudo iptables -I DOCKER-USER -i eth0 -p tcp --dport 8080 ! -s 203.0.113.10 -j DROPCho phép established traffic:
sudo iptables -I DOCKER-USER -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPTChặn toàn bộ inbound vào Docker từ interface public, trừ port cần thiết:
sudo iptables -I DOCKER-USER -i eth0 -j DROPNhưng cẩn thận: rule này có thể làm app mất public access. Nên test trong maintenance window.
Để persist iptables:
sudo apt install -y iptables-persistent
sudo netfilter-persistent saveReverse proxy pattern
Pattern an toàn hơn:
– Chỉ expose 80/443 public.
– App container không publish public port.
– Nginx/Traefik/Caddy reverse proxy route nội bộ.
– DB/cache chỉ nằm trong Docker network private.
Ví dụ Compose:
services:
proxy:
image: caddy:2
ports:
- "80:80"
- "443:443"
networks:
- frontend
- backend
app:
image: myapp
networks:
- backend
db:
image: postgres:16
networks:
- backend
networks:
frontend:
backend:
internal: true
internal: true giúp network không có external connectivity trực tiếp.
3. User Namespace: giảm tác hại nếu container bị chiếm
Vấn đề root trong container
Mặc định, root trong container thường map tới root trên host namespace liên quan. Dù Docker có isolation, nhiều lỗ hổng container escape từng khai thác quyền này. Vì vậy: container root nên map sang unprivileged user trên host.
Bật user namespace remapping
Docker hỗ trợ userns-remap. Khi bật, UID 0 trong container được map sang UID không đặc quyền trên host, ví dụ dockremap.
Tạo hoặc dùng user mặc định:
sudo dockerd --userns-remap=defaultCách chuẩn: cấu hình daemon.
Tạo file:
sudo nano /etc/docker/daemon.jsonNội dung:
{
"userns-remap": "default"
}Restart Docker:
sudo systemctl restart dockerKiểm tra:
docker info | grep -i usernsDocker sẽ tạo user/group dockremap, dùng /etc/subuid, /etc/subgid.
Kiểm tra:
grep dockremap /etc/subuid /etc/subgidLưu ý khi bật userns-remap
Bật userns-remap có thể ảnh hưởng:
– Volume bind mount bị permission denied. – Container cần quyền đặc biệt có thể lỗi. – Image/app chạy root nhưng ghi file host mount sẽ gặp mismatch UID. – Existing containers/images có thể cần recreate.
Ví dụ fix permission cho volume path:
sudo chown -R 100000:100000 /srv/myapp/dataUID thực tế phụ thuộc /etc/subuid. Kiểm tra trước khi chown.
Chạy container bằng non-root user
User namespace tốt, nhưng vẫn nên chạy process trong container bằng user thường.
Dockerfile:
FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
RUN addgroup -S app && adduser -S app -G app
USER app
CMD ["node", "server.js"]Compose:
services:
app:
image: myapp
user: "1000:1000"Giảm capability
Container không cần mọi Linux capability.
Compose:
services:
app:
image: myapp
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICENếu không cần bind port thấp, bỏ cả NET_BIND_SERVICE.
Thêm hardening:
security_opt:
- no-new-privileges:true
read_only: true
tmpfs:
- /tmpno-new-privileges chặn process gain privilege qua setuid/setgid. read_only giảm persistence nếu attacker ghi file.
Tránh privileged mode
Tránh:
privileged: trueprivileged gần như phá nhiều lớp isolation. Chỉ dùng cho case đặc biệt, hiểu rõ risk.
4. Secret Management: không để mật khẩu trôi nổi
Secret không nên nằm ở đâu?
Không đặt secret trong:
– Dockerfile:
ENV DB_PASSWORD=supersecret– Image layer.
– Git repo.
– .env commit nhầm.
– Command line:
docker run -e DB_PASSWORD=supersecret app– Log app. – CI output.
Vì có thể lộ qua:
docker inspect container_name
docker history image_name
history
journalctlDùng Docker secrets khi có Swarm
Docker secrets hoạt động tốt trong Swarm. Secret được mount dạng file trong container, thường tại /run/secrets/....
Khởi tạo Swarm:
docker swarm initTạo secret:
echo "strong-password" | docker secret create db_password -Compose cho Swarm:
version: "3.8"
services:
app:
image: myapp
secrets:
- db_password
environment:
DB_PASSWORD_FILE: /run/secrets/db_password
secrets:
db_password:
external: true
Deploy:
docker stack deploy -c docker-compose.yml mystackApp nên đọc secret từ file, không từ env trực tiếp.
Compose không Swarm: dùng file secret cẩn thận
Docker Compose hỗ trợ secrets, nhưng không bảo mật mạnh như Swarm; thường là bind mount file. Vẫn tốt hơn env nếu permission đúng.
services:
app:
image: myapp
secrets:
- db_password
secrets:
db_password:
file: ./secrets/db_password.txt
Set permission:
mkdir -p secrets
chmod 700 secrets
chmod 600 secrets/db_password.txtThêm .gitignore:
secrets/
.envDùng external secret manager
Với production nghiêm túc, dùng:
– HashiCorp Vault – AWS Secrets Manager – GCP Secret Manager – Azure Key Vault – SOPS + age – Doppler, 1Password Secrets Automation
Pattern tốt:
1. CI/CD lấy secret từ manager. 2. Runtime inject qua file tạm hoặc secret backend. 3. Secret rotate được. 4. App reload hoặc restart an toàn.
Rotate secret
Secret không bất tử. Cần quy trình rotate:
– Tạo secret mới. – Deploy app đọc secret mới. – Update DB/API credential. – Thu hồi secret cũ. – Audit log access.
Ví dụ PostgreSQL:
ALTER USER app_user WITH PASSWORD 'new-strong-password';Sau đó update Docker secret/secret manager, redeploy.
Giảm rò secret qua log
App phải mask dữ liệu nhạy cảm:
– Password – Token – Authorization header – Cookie – Private key – Connection string
Không log full config. Không dump env ở startup.
5. Hardening bổ sung cho Ubuntu + Docker
Giới hạn quyền Docker group
User thuộc group docker có quyền rất cao:
sudo usermod -aG docker usernameChỉ cấp cho admin thật sự cần. Với automation, ưu tiên deploy qua CI runner hạn chế quyền, hoặc SSH command giới hạn.
Bật auto security updates
sudo apt install -y unattended-upgrades
sudo dpkg-reconfigure unattended-upgradesDùng image nhỏ, cập nhật, scan định kỳ
Ưu tiên image official, minimal:
– alpine
– distroless
– slim
Scan:
docker scout cves image_nameHoặc:
trivy image image_nameResource limit
Ngăn container lỗi/attack ăn hết RAM/CPU:
services:
app:
image: myapp
mem_limit: 512m
cpus: "1.0"
pids_limit: 200Logging hợp lý
Giới hạn log Docker:
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
}
}File:
sudo nano /etc/docker/daemon.json
sudo systemctl restart docker6. Checklist cấu hình thực tế
Firewall
– UFW deny incoming.
– Chỉ allow 22, 80, 443.
– DB/cache bind 127.0.0.1 hoặc internal network.
– Dùng DOCKER-USER chain để chặn traffic ngoài ý muốn.
– Không publish port nếu service chỉ dùng nội bộ.
User namespace
– Bật userns-remap.
– Container chạy non-root.
– Drop capabilities.
– no-new-privileges:true.
– Tránh privileged:true.
– Dùng read-only filesystem nếu khả thi.
Secret
– Không commit .env, secrets/.
– Không bake secret vào image.
– Không truyền secret qua command line.
– Ưu tiên Docker secrets/secret manager.
– Đọc secret từ file.
– Rotate định kỳ.
– Mask log.
Kết luận: an toàn hơn bằng vài quyết định đúng
Secure Docker trên Ubuntu không phải cài một tool rồi xong. Đó là tập hợp quyết định nhỏ nhưng quan trọng: chỉ mở port cần thiết, không để container root quá mạnh, không để secret nằm lộ thiên.
Nếu cần ưu tiên, hãy làm theo thứ tự:
1. Khóa firewall, chỉ public reverse proxy.
2. Bind service nội bộ vào 127.0.0.1 hoặc Docker network private.
3. Bật userns-remap, chạy app non-root.
4. Drop capability, tránh privileged.
5. Chuyển secret khỏi env/image sang file secret hoặc secret manager.
6. Cập nhật, scan image, rotate secret định kỳ.
Kết quả: khi một container bị lỗi hoặc bị khai thác, attacker khó pivot ra host, khó truy cập service nội bộ, khó lấy credential lâu dài. Đây chính là mục tiêu thực tế của hardening: không hứa “bất khả xâm phạm”, nhưng giảm mạnh blast radius.
Bình luận (0)
Chưa có bình luận. Hãy là người đầu tiên!