Docker và Application Ops

Biến môi trường và secret trong Docker: Quản lý thế nào để không bị lộ mật khẩu?

Phân biệt config với secret, dùng Compose secrets và BuildKit secret mount, bảo vệ file .env và xử lý khi thông tin bí mật đã bị commit.

Quản lý secret trong Docker
Mục lục

Trong quá trình phát triển, thói quen phổ biến nhất của lập trình viên là gom tất cả mọi thứ vào một file .env duy nhất: từ cổng kết nối PORT=3000, đường dẫn PUBLIC_URL cho đến mật khẩu database và Stripe API key.

Thói quen này rất tiện khi chạy ở máy cá nhân (localhost), nhưng khi mang nguyên cách làm đó lên máy chủ production, bạn đang đối mặt với những rủi ro bảo mật cực lớn:

  • Vô tình commit file .env lên GitHub: bot quét tự động sẽ vét sạch API key và mật khẩu chỉ trong vòng vài phút.
  • Lệnh COPY . . trong Dockerfile vô tình đóng gói luôn file .env vào bên trong Docker image: bất kỳ ai có quyền pull image về máy đều có thể mở ra và xem sạch mật khẩu.
  • Truyền mật khẩu database qua ARG hoặc ENV trong Dockerfile: giá trị mật khẩu sẽ bị ghi vĩnh viễn vào lịch sử các layer của image (docker history), không có cách nào xóa sạch nếu không build lại từ đầu.

Để vận hành hệ thống an toàn, quy tắc đầu tiên là: Phải tách bạch giữa Cấu hình thông thường (Config) và Dữ liệu bí mật (Secret).

Phân biệt rõ giữa Config và Secret

Không phải biến nào cũng cần bảo mật như nhau. Bạn có thể phân loại nhanh theo bảng sau:

  • Cấu hình thông thường (Config): APP_ENV=production, PORT=8080, LOG_LEVEL=info, PUBLIC_API_URL=https://api.example.com. Những biến này chỉ dùng để điều khiển hành vi ứng dụng hoặc mô tả môi trường. Nếu có bị nhìn thấy thì hệ thống cũng không bị xâm nhập. Bạn có thể quản lý qua file env_file hoặc khai báo trực tiếp trong compose.yaml.
  • Dữ liệu bí mật (Secret runtime): Mật khẩu database, token thanh toán, khóa riêng tư TLS (private key), chuỗi mã hóa phiên (session secret). Đây là những chiếc chìa khóa vạn năng cho phép truy cập trực tiếp vào dữ liệu khách hàng. Chúng tuyệt đối không được xuất hiện trên Git và không nên truyền qua biến môi trường công khai.
  • Bí mật lúc đóng gói (Build secret): Personal Access Token để tải thư viện private từ GitHub, NPM token nội bộ. Những token này chỉ cần dùng trong lúc chạy lệnh docker build, hoàn toàn không cần tồn tại trong container khi chạy thực tế.

Dùng Compose secrets thay vì nhét mật khẩu vào biến môi trường

Khi bạn truyền mật khẩu bằng block environment: trong file Compose, bất kỳ ai gõ lệnh docker inspect <container> trên máy chủ cũng đều nhìn thấy mật khẩu dưới dạng văn bản thuần (plaintext). Hơn nữa, nếu ứng dụng gặp lỗi và dump toàn bộ biến môi trường ra log, mật khẩu cũng sẽ lộ theo.

Docker Compose cung cấp một cơ chế bảo mật tốt hơn nhiều: Compose Secrets.

Thay vì biến môi trường, Docker sẽ mount secret dưới dạng một file chỉ đọc (read-only) nằm trong bộ nhớ tạm /run/secrets/ bên trong container:

yaml
services:
  web:
    image: my-app:1.2.0
    secrets:
      - db_password
    environment:
      # Báo cho ứng dụng biết đường dẫn file đọc mật khẩu
      DB_PASSWORD_FILE: /run/secrets/db_password

  db:
    image: postgres:16
    secrets:
      - db_password
    environment:
      POSTGRES_DB: app_db
      POSTGRES_USER: app_user
      # Các image chính thức (Postgres, MySQL) đều hỗ trợ sẵn đuôi _FILE
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password

secrets:
  db_password:
    file: ./secrets/db_password.txt

Ưu điểm vượt trội của mô hình này:

  1. Bảo mật tuyệt đối trong container: Mật khẩu không nằm trong biến môi trường, không bị lộ khi chạy docker inspect.
  2. Cấp quyền đúng người đúng việc: Service nào cần mật khẩu (như web, db) mới được khai báo trong mục secrets. Các service khác như Nginx proxy hay Redis cache hoàn toàn không nhìn thấy file mật khẩu này.
  3. Thư mục ./secrets trên máy chủ: Được đặt ngoài Git (đã thêm vào .gitignore) và phân quyền nghiêm ngặt trên Linux (chỉ user chạy docker mới có quyền đọc: chmod 600 ./secrets/*).

Build secret: Đừng bao giờ dùng ARG cho token riêng tư

Nhiều lập trình viên có thói quen truyền token tải thư viện bằng lệnh:

dockerfile
# SAI: Token sẽ bị lưu vĩnh viễn vào image layer history!
ARG NPM_TOKEN
RUN npm config set //registry.npmjs.org/:_authToken=$NPM_TOKEN && npm install

Dù bạn có thêm lệnh xóa file token ở dòng sau, bất kỳ ai gõ docker history <image> cũng vẫn đọc được nguyên vẹn giá trị của ARG NPM_TOKEN.

Cách làm chuẩn là sử dụng BuildKit secret mount:

dockerfile
# syntax=docker/dockerfile:1
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./

# Đọc token tạm thời trong bộ nhớ lúc chạy lệnh, không lưu vào image layer
RUN --mount=type=secret,id=npm_token \
    NPM_TOKEN=$(cat /run/secrets/npm_token) npm install

COPY . .
RUN npm run build

Khi build, bạn truyền secret vào bằng lệnh:

bash
docker build --secret id=npm_token,src=./secrets/npm_token.txt -t my-app .

Token chỉ tồn tại đúng trong vài chục giây chạy lệnh npm install, sau đó biến mất hoàn toàn và không để lại bất kỳ dấu vết nào trong artifact cuối cùng.

Luôn có file .dockerignore chuẩn chỉ

Trước khi Docker tiến hành build image, toàn bộ thư mục dự án (build context) sẽ được đóng gói và gửi sang Docker daemon. Nếu bạn không có file .dockerignore, file .env chứa mật khẩu máy cá nhân của bạn sẽ bị gửi sang và rất dễ bị copy nhầm vào container qua lệnh COPY . ..

Một file .dockerignore tối thiểu cho dự án production luôn phải có:

text
.git
.env
.env.*
secrets/
*.pem
*.key
backups/
node_modules

Phải làm gì nếu lỡ commit file .env chứa secret lên Git?

Nếu một ngày bạn giật mình phát hiện mình hoặc đồng nghiệp vừa lỡ tay push file .env chứa mật khẩu thật lên repository (kể cả repo private của công ty):

Đừng chỉ đơn giản là xóa file đó rồi commit một lần nữa. Toàn bộ lịch sử Git vẫn lưu lại commit cũ, và ai xem qua git log cũng đều thấy được mật khẩu.

Hành động đúng duy nhất lúc này là:

  1. Lập tức thu hồi (Revoke) hoặc đổi mật khẩu (Rotate) tại nguồn cấp: Vào database đổi mật khẩu mới, vào cổng thanh toán hoặc AWS thu hồi API token cũ và tạo token mới ngay lập tức. Hãy coi như chìa khóa cũ đã rơi vào tay kẻ xấu.
  2. Cập nhật chìa khóa mới vào máy chủ: Đưa secret mới vào file bí mật trên VPS và tạo lại container (docker compose up -d).
  3. Dọn dẹp Git sau cùng: Sau khi chìa khóa cũ đã bị vô hiệu hóa, bạn mới dùng các công cụ như git-filter-repo hoặc BFG Repo-Cleaner để làm sạch lịch sử Git nếu cần thiết.

Tóm lại

Bảo mật biến môi trường không đòi hỏi hạ tầng đắt đỏ, nó bắt đầu từ thói quen tách bạch:

  • Config đưa vào compose.yaml hoặc env_file.
  • Secret runtime đưa vào Docker Compose secrets.
  • Secret build đưa vào BuildKit --mount=type=secret.
  • Khóa chặt thư mục nhạy cảm bằng .dockerignore.

Khi các lớp này đã chuẩn chỉ, bạn có thể tự tin triển khai hệ thống theo Checklist đưa ứng dụng Docker Compose lên production mà không sợ vô tình để lộ dữ liệu kinh doanh của doanh nghiệp.

Nguồn tham khảo

Chia sẻ bài viết hữu ích

Gửi bài viết này cho đồng nghiệp