Docker Hardening: Chạy Container với Non-root User và giảm bề mặt tấn công
Hướng dẫn Docker Hardening thực tế với non-root user, multi-stage builds, minimal image, read-only filesystem, Linux capabilities, seccomp, scanning và supply-chain security.
ShanverseUpdated 11 min read— viewsLoading views
Docker
DevSecOps
Threat model: container không phải máy ảo
Container chia sẻ kernel với host. Namespace, cgroup, capabilities, seccomp và LSM tạo các lớp cô lập, nhưng không biến workload thành một security boundary tuyệt đối.
Nếu attacker đạt remote code execution trong container, họ có thể:
Đọc secret mà process hiện tại được phép đọc.
Gọi internal services bằng network identity của workload.
Sửa file hoặc binary nếu filesystem writable.
Cài/download công cụ hậu khai thác nếu image có shell, package manager và network egress.
Lạm dụng Linux capabilities, mount hoặc socket được cấp quá mức.
Tiêu thụ CPU, RAM, PID hoặc disk để gây denial of service.
Tìm đường thoát container qua kernel/runtime vulnerability.
Comments
Hardening không thay thế việc vá ứng dụng. Nó giảm blast radius khi lớp ứng dụng thất bại.
Ba phạm vi thường bị nhầm lẫn
Non-root process trong container: process chính có UID khác 0 nhờ USER hoặc runtime setting.
User namespace: container UID được ánh xạ sang UID không đặc quyền trên host.
Rootless Docker: cả daemon và container chạy không cần root trên host.
Chúng bổ sung cho nhau nhưng không giống nhau. USER 10001 không tự động biến Docker daemon thành rootless; rootless daemon cũng không miễn trừ việc cấu hình quyền workload đúng cách.
Vì sao root trong container vẫn nguy hiểm?
Root trong container bị namespace/capability giới hạn, nhưng vẫn có nhiều quyền hơn non-root trong cùng container. Khi có lỗi cấu hình như --privileged, host mount, Docker socket hoặc capability mạnh, UID 0 làm hậu quả nghiêm trọng hơn.
Non-root giúp:
Chặn ghi vào file thuộc root trong image.
Giảm khả năng cài package, đổi ownership hoặc thao tác device.
Làm exploit cần thêm bước privilege escalation.
Hỗ trợ policy runAsNonRoot và compliance.
Tuy nhiên, non-root không cứu được workload nếu attacker đọc được mọi secret, gọi được metadata service hoặc truy cập /var/run/docker.sock.
Dockerfile Node.js hardened bằng multi-stage build
Điểm quan trọng:
Build tools và source không đi vào runtime stage.
UID/GID cố định giúp behavior nhất quán trên platform.
User không có login shell hay writable home không cần thiết.
COPY --chown đặt ownership ngay khi copy, tránh layer chown -R lớn.
USER đứng trước CMD, nên process mặc định không chạy root.
Dùng exec-form CMD để process nhận signal đúng.
Chọn UID/GID và ownership
Không chỉ tạo user; hãy thiết kế đường dẫn writable rõ ràng.
Tránh:
777 loại bỏ gần như toàn bộ lợi ích permission. Chỉ cấp write cho thư mục thực sự cần, chẳng hạn /tmp, upload buffer hoặc cache.
Khi mount volume, ownership trên host/volume có thể ghi đè ownership trong image. Kiểm tra UID/GID thực tế thay vì sửa bằng cách chạy container dưới root.
Xác minh non-root, đừng chỉ tin Dockerfile
Trong CI, test image cuối cùng. Một stage khác hoặc runtime override có thể làm mất USER.
Có thể ép runtime:
Nhưng --user có thể phá ứng dụng nếu image giả định home directory, /etc/passwd entry hoặc file ownership khác. Sửa image là giải pháp bền vững hơn.
Multi-stage build giảm attack surface như thế nào?
Builder thường chứa compiler, Git, curl, package manager, headers và test tools. Runtime chỉ nên chứa:
Binary/build output.
Production dependencies.
CA certificates, timezone data hoặc native libraries thực sự cần.
User/group metadata cần cho runtime.
Mỗi package thừa là thêm CVE, file, binary và hành vi cần kiểm soát. Image nhỏ hơn không tự động an toàn, nhưng image tối thiểu và có chủ đích thường dễ scan, patch và review hơn.
Minimal base image: slim, Alpine hay distroless?
Không có lựa chọn đúng cho mọi app.
Đừng chọn Alpine chỉ vì số MB. Hãy kiểm tra compatibility, CVE profile, patch cadence và vận hành.
Không cài package thừa
Với Debian-based image:
Kết hợp update/install trong cùng RUN để tránh cache metadata cũ.
Dùng --no-install-recommends.
Không để compiler, curl, ping, editor hoặc package manager nếu runtime không cần.
Không “cleanup” ở layer sau; file trong layer cũ vẫn làm image lớn.
.dockerignore: giảm context và ngăn dữ liệu lọt vào build
.dockerignore giảm build context và khả năng COPY . . mang secret, Git history hoặc artifact thừa vào image. Tuy nhiên, nếu secret đã được truyền bằng ARG hoặc ENV, ignore file không giải quyết được.
Không đưa secret vào layer
Sai:
ARG, image history, provenance, cache và logs đều có thể làm lộ secret. Dùng BuildKit secret mount:
Runtime secret phải được platform inject/mount; không COPY.env hoặc credential vào image.
Pin base image nhưng vẫn phải cập nhật
Tag cụ thể tốt hơn latest:
Pin digest tăng reproducibility:
Nhưng digest pinning không tự nhận security fix. Cần bot/policy mở pull request cập nhật digest, rebuild định kỳ và scan lại. Reproducibility không được biến thành trì hoãn patch.
Read-only root filesystem
Chạy thử:
Read-only root filesystem ngăn attacker sửa binary/config ở layer chính. Cấp vùng ghi tạm rõ ràng bằng tmpfs hoặc volume chuyên dụng.
Ứng dụng cần được sửa để không ghi tùy tiện vào working directory, home hoặc /var/log. Log nên đi stdout/stderr.
Drop Linux capabilities
Docker cấp một tập capability mặc định. Nhiều web app không cần capability nào:
Chỉ add lại capability đã chứng minh cần thiết:
Tốt hơn là bind port lớn hơn 1024, ví dụ 3000, để không cần NET_BIND_SERVICE.
Không dùng --privileged như cách nhanh để sửa permission. Nó vô hiệu hóa nhiều lớp cô lập.
Seccomp, AppArmor và SELinux
Giữ Docker default seccomp profile trừ khi có lý do và review rõ ràng.
Dùng seccomp=unconfined chỉ để chẩn đoán tạm thời, không phải fix production.
AppArmor/SELinux giới hạn file, process và resource access ngoài Unix permissions.
Với workload nhạy cảm, xây custom profile từ behavior quan sát được và test failure mode.
Hardening là deny-by-default có chủ đích, không phải tắt profile mỗi khi app lỗi.
Tránh mount nguy hiểm
Đặc biệt nguy hiểm:
Docker socket gần như trao quyền điều khiển Docker daemon và thường tương đương quyền host cao. Nếu cần build image trong CI, cân nhắc isolated builder, rootless BuildKit hoặc dịch vụ build chuyên dụng thay vì mount socket vào workload không tin cậy.
Volume nên:
Chỉ mount path cần thiết.
Dùng :ro khi container chỉ đọc.
Tránh host path nếu named volume/object storage đáp ứng được.
Không mount secret directory rộng hơn nhu cầu.
Giới hạn tài nguyên và process
Resource limits giảm khả năng một process lỗi hoặc bị khai thác làm cạn host. Chọn limit dựa trên profiling và SLO; limit quá thấp gây restart loop và self-inflicted DoS.
Kubernetes securityContext tương ứng
runAsNonRoot: true là enforcement bổ sung: pod fail thay vì âm thầm chạy UID 0. Kết hợp Pod Security Admission ở mức restricted khi phù hợp.
Scan image, SBOM và supply chain
Ví dụ với Trivy:
Một pipeline trưởng thành nên:
Scan source/dependency trước build.
Build image deterministically.
Tạo SBOM ở định dạng SPDX/CycloneDX.
Scan image cuối, không chỉ builder stage.
Ký image/provenance bằng identity của CI.
Admission policy chỉ cho deploy image từ registry/identity tin cậy.
Re-scan image đang lưu vì CVE database thay đổi theo thời gian.
Không fail CI mù quáng với mọi CVE. Đặt policy theo severity, exploitability, package reachability, fix availability và SLA; mọi exception phải có owner và expiry.
Kiểm tra image thực tế
Kiểm tra:
Default user có phải UID 0 không?
Image có shell, package manager, compiler hoặc credential thừa không?
Layer nào lớn bất thường?
Base digest có được phê duyệt không?
Config có secret trong Env, labels hoặc history không?
Entrypoint có nhận signal và exit đúng không?
Những sai lầm phổ biến
Tạo user nhưng quên USER. Image vẫn chạy root.
Dùng tên user nhưng runtime không resolve được. Numeric UID thường portable hơn cho policy.
chmod 777 để hết lỗi. App chạy được nhưng permission model bị phá.
Chạy root để bind port 80. Dùng port 8080/3000 và map ở runtime.
Multi-stage nhưng copy toàn bộ builder sang runtime. Build tools và source vẫn lọt vào image.
Dùng latest. Build không tái lập và khó audit.
Pin digest mãi mãi. Image bỏ lỡ security updates.
Dùng Alpine bất chấp compatibility. Image nhỏ nhưng production khó đoán.
Mount Docker socket. Container có đường kiểm soát host.
Tắt seccomp/SELinux để sửa lỗi. Một lỗi permission biến thành security regression.
Scan một lần khi release. CVE mới xuất hiện sau đó.
Tin rằng non-root là sandbox hoàn chỉnh. Network, secrets và kernel vẫn cần bảo vệ.
Checklist hardening production
Build
Base image đến từ publisher/registry tin cậy.
Tag/version cụ thể; digest được quản lý và cập nhật tự động.
Multi-stage build chỉ copy runtime artifacts.
Không có dev dependencies, compiler hay debug tools không cần thiết.
.dockerignore loại Git, secrets, logs và local artifacts.
Build secret dùng BuildKit secret mount.
Image cuối được scan và tạo SBOM.
Identity và filesystem
USER là UID/GID khác 0.
Writable directories tối thiểu và ownership chính xác.
Không dùng chmod 777.
Root filesystem read-only khi có thể.
/tmp/cache dùng tmpfs hoặc volume có giới hạn.
Runtime
Drop ALL capabilities; add lại từng capability cần thiết.
Giữ seccomp RuntimeDefault; dùng AppArmor/SELinux khi phù hợp.
Không --privileged, host namespaces hay Docker socket.
Volume mount tối thiểu và read-only khi có thể.
CPU, memory, PID và file-descriptor limits được cấu hình.
Network access và egress được giới hạn theo nhu cầu.
Supply chain và vận hành
Image được ký và provenance có thể xác minh.
Admission/deployment policy kiểm tra registry, signature và digest.
Image được rebuild/re-scan định kỳ.
Exceptions có owner, lý do và ngày hết hạn.
Observability không phụ thuộc vào shell trong production image.
Kết luận
Một hardened container nên có hai đặc điểm: ít thứ để khai thác và ít quyền để lạm dụng. Multi-stage build, minimal base image và dependency pruning giảm phần mềm có mặt. Non-root user, read-only filesystem, capability dropping, seccomp và resource limits giảm quyền của process khi chạy.
Đừng đánh đổi khả năng vá lỗi và vận hành chỉ để có image nhỏ nhất. Mục tiêu không phải số MB đẹp, mà là artifact tối thiểu, tái lập, cập nhật được và chạy dưới policy least privilege có thể kiểm chứng.
Table of contents(27)
Codedocker
1# syntax=docker/dockerfile:1.723FROM node:22-bookworm-slim AS deps4WORKDIR /app5COPY package.json package-lock.json ./6RUN --mount=type=cache,target=/root/.npm \7 npm ci89FROM node:22-bookworm-slim AS build
Docker Compose khi nào không còn đủ? So sánh Kubernetes và Docker Swarm về multi-host, self-healing, scaling, rolling update, networking, secrets và lộ trình migration production.
Hướng dẫn tự động deploy ứng dụng Docker lên VPS Ubuntu và cloud providers bằng GitHub Actions: AWS ECS/App Runner/EC2, Vercel, Render, secrets, healthcheck, rollback và zero-downtime strategy.
Hướng dẫn tự động hóa Build và Push Docker Image bằng GitHub Actions: GHCR, Docker Hub, Buildx, metadata tags, cache, multi-platform, secrets, SBOM, provenance và troubleshooting.