Bảo mật Docker Container: Chạy app với Non-root User và quản lý Docker Secrets
Hướng dẫn bảo mật Docker Container với Non-root User và Docker Secrets: USER, COPY --chown, BuildKit secret mounts, Compose secrets, rotation, least privilege và kiểm tra Image.
ShanverseUpdated 12 min read— viewsLoading views
Docker
Secret Management
Root trong Container nguy hiểm đến mức nào?
Root trong Container không tự động đồng nghĩa root trên host, vì process vẫn bị giới hạn bởi namespace, capability, seccomp và các cơ chế kernel khác. Tuy nhiên, root bên trong Container vẫn có nhiều quyền hơn cần thiết:
Ghi đè file hệ thống trong Container.
Cài tool hoặc thay đổi runtime nếu filesystem cho phép.
Lạm dụng capability được cấp.
Truy cập mount nhạy cảm bị cấu hình sai.
Tăng tác động khi có kernel/runtime vulnerability.
Tận dụng Docker socket hoặc device nếu chúng bị mount nhầm.
Mermaid
Loading diagram…
Comments
Chạy non-root không biến Container thành “an toàn tuyệt đối”, nhưng nó loại bỏ nhiều hành động hậu khai thác dễ dàng.
Kiểm tra Image đang chạy bằng user nào
Chạy Image và in identity:
Kết quả mong muốn:
Nếu thấy:
runtime process đang chạy root.
Có thể xem cấu hình Image:
Tìm trường user trong config và luôn xác minh bằng test runtime trong CI.
Cách đơn giản với official Node.js Image
Official Node.js Image đã có user node. Dockerfile production:
Điểm quan trọng:
Cài package lúc build có thể cần root.
File runtime được copy với ownership đúng.
USER node đặt gần cuối, trước CMD.
Process production chạy non-root.
EXPOSE 3000 chỉ mô tả port; nó không publish port và không cần root.
Multi-stage Dockerfile an toàn hơn
Builder có thể dùng tool và quyền cần cho quá trình build; final stage chỉ chứa runtime artifact và chạy non-root.
Tạo user riêng khi Base Image chưa có
Debian/Ubuntu-based Image:
Alpine:
Numeric UID/GID giúp runtime hiểu identity ngay cả khi /etc/passwd tối giản. Dùng ID ổn định theo policy của tổ chức và tránh trùng với ownership của volume dùng chung.
COPY --chown tốt hơn chown -R muộn
Không tối ưu:
Cách này tạo thêm work, layer metadata và có thể chậm với node_modules lớn.
Tốt hơn:
Hoặc chỉ chown đúng artifact runtime:
Không dùng:
777 cho mọi user ghi file, phá mục tiêu least privilege và có thể che lỗi ownership thực sự.
Non-root User và port thấp
Process non-root thường không nên bind trực tiếp port đặc quyền như 80.
Cho app listen 3000:
Publish host port:
Port host 80 được Docker map vào Container port 3000; app vẫn chạy non-root. Chỉ thêm capability bind port thấp khi có yêu cầu rõ ràng và threat model chấp nhận.
Quyền ghi file: chỉ mở đúng nơi cần thiết
Ứng dụng production thường chỉ cần ghi vào một vài path:
/tmp cho temporary files.
/app/data cho dữ liệu local nếu thiết kế yêu cầu.
Một volume upload/cache cụ thể.
Chuẩn bị directory trong Image:
Sau đó:
Không cho app ghi vào source code hoặc toàn root filesystem nếu không cần.
Hardening bằng Docker Compose
Các lớp bảo vệ:
read_only: true: root filesystem không ghi được.
tmpfs: chỉ mở vùng ghi tạm có giới hạn.
cap_drop: ALL: loại Linux capabilities không cần.
no-new-privileges: process không thể lấy thêm privilege qua exec.
pids_limit: giảm fork bomb/resource abuse.
Secret chỉ mount vào service cần dùng.
Test từng option với ứng dụng và platform thực tế. Một số native dependency hoặc observability agent có thể cần path/capability cụ thể; chỉ cấp lại đúng quyền đó thay vì bỏ toàn bộ hardening.
Có nên đặt user: trong Compose?
Nếu Dockerfile đã có:
thường không cần override.
Compose có thể ép numeric identity:
Nhưng override mù quáng có thể làm app mất quyền đọc file, ghi volume hoặc truy cập home/cache. Ưu tiên định nghĩa runtime user trong Image; dùng Compose override cho môi trường đặc biệt và test quyền đầy đủ.
Rootless Docker khác Non-root User
Hai lớp độc lập:
Non-root trong Container: process ứng dụng không chạy UID 0 trong namespace Container.
Rootless Docker: daemon và Container runtime chạy không cần root host theo mô hình rootless.
Có thể dùng cả hai. Rootless daemon không thay thế USER; USER node cũng không thay thế hardening daemon/host.
Docker Secret là gì?
Secret là dữ liệu nhạy cảm như:
Database password.
API token.
Private key.
Registry credential.
TLS key.
Package manager token lúc build.
Mục tiêu: secret chỉ xuất hiện ở đúng thời điểm, đúng service và đúng phạm vi quyền.
Mermaid
Loading diagram…
Những cách truyền secret không an toàn
ARG
Build argument có thể xuất hiện trong metadata, command history hoặc cache context.
ENV
Secret tồn tại trong Image config và mọi Container tạo từ Image.
COPY
Xóa ở layer sau không xóa dữ liệu khỏi layer trước.
Command line và log
Environment tiện dụng nhưng có thể lộ qua inspect, process debugging, crash report hoặc log. Không dùng như cơ chế bảo vệ chính cho credential giá trị cao.
Build-time secret với BuildKit
Ví dụ cài private npm package:
Build:
Secret mount chỉ tồn tại trong build step và không được copy mặc định vào layer kết quả.
Không chạy:
vì bạn đang chủ động đưa secret vào layer.
Build secret trong Compose
Dockerfile vẫn phải dùng RUN --mount=type=secret,id=npmrc. Kiểm tra Compose/BuildKit version bằng docker compose version và docker compose config.
Runtime secret với Docker Compose
Tạo source file ngoài Git:
.gitignore:
Compose:
Secret xuất hiện trong Container ở:
Chỉ các service khai báo secret mới nhận mount đó.
Local Compose file-backed secret giúp scope và mount credential tốt hơn, nhưng source file trên host vẫn phải được bảo vệ. Nó không tự biến laptop hoặc filesystem thành secret manager được mã hóa.
Đọc secret file trong Node.js
Không log password, không đưa vào error object và không trả qua health endpoint.
Chỉ bỏ newline cuối do secret file thường được tạo bởi editor/CLI. Không trim() mù quáng nếu leading/trailing whitespace có thể là một phần hợp lệ của secret.
File permission cho secret và Non-root User
Non-root process phải có quyền đọc secret mount nhưng không nên có quyền sửa source secret.
Kiểm tra trong môi trường test:
Khả năng đặt owner/mode chi tiết phụ thuộc secret backend và Compose implementation. Với file-backed local Compose, host file permission và mount behavior rất quan trọng. Không giả định cùng semantics giữa Linux, Docker Desktop và Swarm; kiểm tra thực tế.
Docker Compose Secrets và Docker Swarm Secrets không giống hệt nhau
Local Docker Compose
Thường lấy secret từ file host.
Mount vào service được khai báo.
Phù hợp local development hoặc single-host workflow có kiểm soát.
Không thay thế Vault/KMS/cloud secret manager.
Docker Swarm Secrets
Dành cho Swarm services.
Có lifecycle và distribution do Swarm quản lý.
Secret được cấp cho task được phép.
Cần thiết kế rotation/redeploy theo Swarm.
Trên managed platform, ưu tiên secret integration bản địa của platform.
Rotation: đổi secret mà không gây outage dài
Một quy trình thực tế:
Tạo credential mới ở database/provider.
Cấp quyền tối thiểu cho credential mới.
Cập nhật secret source.
Redeploy/recreate service để đọc secret mới.
Xác nhận traffic và background jobs hoạt động.
Revoke credential cũ.
Audit log và xóa bản sao tạm.
Nhiều ứng dụng chỉ đọc secret lúc startup. Thay file host không đảm bảo process tự reload; cần restart có kiểm soát hoặc implement reload rõ ràng.
Nếu secret từng vào Git, Image hoặc log, phải coi là đã lộ và rotate ngay. Xóa file khỏi commit/Image mới không thu hồi credential cũ.
Secret cho development và production
Không copy production .env xuống laptop chỉ để Compose chạy giống production.
Kết hợp Non-root, Secret và filesystem read-only
Một cấu hình hoàn chỉnh:
Image đã khai báo USER node, nên Compose không cần chạy root để đọc app code.
Xác minh hardening
User
Root filesystem
Nếu Image có shell:
Lệnh phải fail. Distroless Image có thể không có shell; dùng integration test khác thay vì cài shell vào production Image.
Capabilities và config
Review user, capabilities, read-only root filesystem, mounts và security options.
Secret không nằm trong history
Dùng thêm image/layer scanner trong CI. docker history không phát hiện mọi dạng secret đã được đóng gói vào binary hoặc artifact.
Chạy smoke test
Các lỗi phổ biến
EACCES: permission denied
App đang ghi vào path root-owned. Sửa ownership/path hoặc mở một volume/tmpfs mục tiêu; không quay lại root và không chmod 777.
Non-root không bind được port 80
Cho app listen trên 3000 rồi map 80:3000.
Secret file không đọc được
Kiểm tra id, permission, mount path và backend semantics. Không chuyển secret sang environment chỉ để né lỗi mà chưa hiểu nguyên nhân.
Secret thay đổi nhưng app vẫn dùng giá trị cũ
Process đọc lúc startup. Recreate service có kiểm soát.
Build vẫn dùng credential cũ
Cache hit có thể tái sử dụng output. Dùng dependency/version input rõ ràng và rebuild stage cần thiết, không ghi secret vào ARG để bust cache.
Volume tạo file root-owned
Chuẩn bị directory ownership trong Image hoặc init volume theo quy trình riêng, sau đó chạy app non-root.
Anti-pattern cần tránh
USER root ở final stage mà không có lý do.
chmod -R 777 để “sửa nhanh” permission.
--privileged hoặc mount /var/run/docker.sock cho application.
Copy .env, key hoặc token vào Image.
Dùng ARG/ENV cho build secret.
Log connection string có password.
Dùng cùng credential cho local, CI và production.
Cho mọi service nhận mọi secret.
Tin rằng private Docker Network thay thế database authentication.
Tin rằng Non-root User thay thế patching, scanning hoặc host security.
Checklist production
Final Image có USER non-root hoặc numeric UID/GID.
Runtime artifact có ownership tối thiểu đúng.
App listen port lớn hơn 1024.
Root filesystem read-only khi khả thi.
Writable path chỉ là tmpfs/volume cần thiết.
Drop capability không dùng và bật no-new-privileges.
Không dùng --privileged hay Docker socket.
Build secret dùng BuildKit --mount=type=secret.
Runtime secret được mount chỉ vào service cần dùng.
Secret không nằm trong Git, Image, log hoặc health endpoint.
Source secret local có permission phù hợp.
Production dùng secret manager có audit/rotation.
Credential có quyền tối thiểu và được rotate định kỳ.
CI kiểm tra runtime UID, Image config và secret scan.
Hướng dẫn .dockerignore cho Node.js, Next.js và React: cú pháp pattern, loại node_modules và .env, phân biệt .gitignore, bảo mật secret, monorepo và kiểm tra build context.
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.
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.