Container không phải máy ảo: mọi container chia sẻ kernel với host. Bảo mật container là thu nhỏ những gì một tiến trình bị chiếm quyền có thể làm: ít phần mềm trong image, không chạy root, không ghi được filesystem, bỏ capability thừa, không có secret nằm sẵn trong image. Cơ chế nền (namespace, cgroup) xem Linux namespaces và cgroups.
flowchart LR A["Build: base image nhỏ, multi-stage"] --> B["Scan: trivy image"] B --> C["Registry: tag bất biến / digest"] C --> D["Runtime: non-root, read-only, cap_drop ALL"] D --> E["Secret: mount lúc chạy"] E --> F["Network: chỉ mở port cần thiết"]
Các lớp và cấu hình
| Lớp | Rủi ro | Biện pháp | Docker | Kubernetes |
|---|---|---|---|---|
| Image | CVE trong package, tool thừa (curl, shell) cho attacker dùng | Base slim/distroless, multi-stage, scan | trivy image |
Admission policy chặn image chưa scan |
| User | Root trong container = root trên host nếu thoát container | USER không phải 0 |
USER 10001 trong Dockerfile, --user |
runAsNonRoot: true, runAsUser |
| Filesystem | Attacker ghi webshell, sửa binary | Rootfs chỉ đọc, ghi vào tmpfs/volume riêng | --read-only --tmpfs /tmp |
readOnlyRootFilesystem: true + emptyDir |
| Capabilities | NET_ADMIN, SYS_ADMIN... cho phép can thiệp host |
Bỏ hết, thêm lại đúng cái cần | --cap-drop ALL --cap-add NET_BIND_SERVICE |
capabilities: {drop: [ALL]} |
| Leo quyền | setuid binary leo lên root | Chặn privilege escalation | --security-opt no-new-privileges |
allowPrivilegeEscalation: false |
| Syscall | Khai thác kernel qua syscall hiếm | Seccomp profile | Docker có profile mặc định | seccompProfile: {type: RuntimeDefault} |
| Secret | Secret nằm trong layer image, lộ khi push/pull | Truyền lúc chạy, không COPY/ENV |
docker run --env-file, compose secrets: |
Secret mount volume, External Secrets |
| Host | Container điều khiển được Docker daemon | Không mount /var/run/docker.sock, không --privileged |
Pod Security Admission restricted |
|
| Network | Lateral movement giữa container | Chỉ publish port cần, phân mạng | network riêng cho từng stack | NetworkPolicy |
Ví dụ
services:
api:
image: registry.example.com/api@sha256:<digest>
user: "10001:10001"
read_only: true
tmpfs: [/tmp]
cap_drop: [ALL]
security_opt: ["no-new-privileges:true"]
secrets: [db_password] # mount tại /run/secrets/db_password
environment:
DB_PASSWORD_FILE: /run/secrets/db_password
secrets:
db_password:
file: ./secrets/db_password.txt # file nằm ngoài Gitspec:
securityContext:
runAsNonRoot: true
runAsUser: 10001
seccompProfile: { type: RuntimeDefault }
containers:
- name: api
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities: { drop: [ALL] }
volumeMounts:
- { name: tmp, mountPath: /tmp }
volumes:
- { name: tmp, emptyDir: {} }Bật Pod Security Admission theo namespace: kubectl label ns prod pod-security.kubernetes.io/enforce=restricted.
Secret không nằm trong image
ARG/ENVchứa token sẽ còn trongdocker historyvà metadata image.COPYrồiRUN rmvẫn còn ở layer trước.- Secret cần lúc build (token kéo package private): dùng BuildKit
RUN --mount=type=secret,id=npmrc ...vàdocker build --secret id=npmrc,src=$HOME/.npmrc. - Quét image đã build để chắc chắn:
trivy image --scanners secret <image>(xem Trivy cheatsheet). Secret lỡ commit vào Git: xử lý lộ secret trong Git.
Khi nào dùng
- Mọi service mới: non-root,
cap_drop: ALL,no-new-privileges, scan trong CI. Chi phí gần như bằng 0. - Read-only rootfs: hầu hết app stateless; cần liệt kê thư mục app phải ghi (
/tmp, cache) để mount tmpfs/emptyDir. - Ngoại lệ có kiểm soát: container mạng (VPN, routing daemon, collector cần raw socket) mới thêm
NET_ADMIN/NET_RAW, ghi lý do.--privilegedvà mountdocker.sockcoi như trao quyền root host. - Dockerfile chi tiết: Dockerfile best practices.