强曰为道

与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

第 10 章 · 敏感信息管理:secrets、configs 与 Docker Secrets

第 10 章 · 敏感信息管理

10.1 敏感信息的安全挑战

不安全的做法

# ❌ 密码硬编码在 compose.yaml(会进入版本控制)
services:
  db:
    environment:
      POSTGRES_PASSWORD: MyS3cretP@ss!

# ❌ 密码硬编码在 Dockerfile(会进入镜像层)
# FROM postgres:16
# ENV POSTGRES_PASSWORD=MyS3cretP@ss!

敏感信息泄露风险

风险说明
版本控制泄露compose.yaml 提交到 Git
镜像层泄露docker history 可查看 ENV
进程列表泄露docker inspect 可查看环境变量
共享卷泄露日志文件可能记录敏感信息

安全等级对比

方式安全等级说明
硬编码在 YAML❌ 最低进入版本控制
.env 文件⚠️ 较低.gitignore,仍在磁盘明文
environment + shell 变量⚠️ 中等不在文件中,但在进程环境
Docker Secrets✅ 高文件挂载在 /run/secrets/,不进入镜像层
外部密钥管理✅✅ 最高HashiCorp Vault、AWS Secrets Manager 等

10.2 Docker Secrets(Swarm 模式)

Docker Secrets 原本是 Swarm 模式的功能,但 Compose V2 在独立模式下也支持基于文件的 secrets。

基本用法

services:
  db:
    image: postgres:16-alpine
    secrets:
      - db_password
      - db_user
    environment:
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password
      POSTGRES_USER_FILE: /run/secrets/db_user

secrets:
  db_password:
    file: ./secrets/db_password.txt
  db_user:
    file: ./secrets/db_user.txt
# 创建密钥文件
mkdir -p secrets
echo "MyS3cretP@ss!" > secrets/db_password.txt
echo "admin" > secrets/db_user.txt

# 设置权限
chmod 600 secrets/*.txt

Secrets 在容器中的位置

# 容器内默认挂载在 /run/secrets/<secret_name>
docker compose exec db ls /run/secrets/
# db_password
# db_user

# 读取密钥
docker compose exec db cat /run/secrets/db_password
# MyS3cretP@ss!

密钥的生命周期

┌─────────────────────────────────────────────┐
│             Secret 生命周期                   │
│                                             │
│  secrets/                                   │
│  ├── db_password.txt    ← 宿主机文件         │
│  └── db_user.txt                             │
│          │                                  │
│     docker compose up                       │
│          │                                  │
│          ▼                                  │
│  ┌───────────────────┐                      │
│  │ Docker 管理的 tmpfs │  ← 内存中,不落盘    │
│  │ /run/secrets/      │                      │
│  ├── db_password      │                      │
│  └── db_user          │                      │
│  └───────────────────┘                      │
│          │                                  │
│     docker compose down                     │
│          │                                  │
│          ▼                                  │
│  密钥从容器中移除(内存释放)                  │
└─────────────────────────────────────────────┘

10.3 Secrets 高级配置

自定义挂载路径

services:
  app:
    image: myapp:latest
    secrets:
      - source: api_key
        target: /app/config/api_key.txt
        mode: 0400           # 只读权限
        uid: "1000"
        gid: "1000"
      - source: tls_cert
        target: /app/certs/server.crt
        mode: 0444

secrets:
  api_key:
    file: ./secrets/api_key.txt
  tls_cert:
    file: ./secrets/tls.crt

环境变量方式引用密钥文件

services:
  app:
    image: myapp:latest
    secrets:
      - source: db_password
        target: db_password
    environment:
      # 许多官方镜像支持 _FILE 后缀
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password

  # 支持 _FILE 后缀的常见官方镜像
  # - PostgreSQL: POSTGRES_PASSWORD_FILE
  # - MySQL: MYSQL_ROOT_PASSWORD_FILE, MYSQL_PASSWORD_FILE
  # - Redis: REDIS_PASSWORD_FILE (需确认版本)
  # - WordPress: WORDPRESS_DB_PASSWORD_FILE

在应用代码中读取密钥

# Python — 读取文件中的密钥
def get_secret(secret_name, default=None):
    """从 Docker Secrets 或环境变量读取密钥"""
    secret_path = f"/run/secrets/{secret_name}"
    try:
        with open(secret_path, 'r') as f:
            return f.read().strip()
    except FileNotFoundError:
        return os.environ.get(secret_name.upper(), default)

db_password = get_secret('db_password')
api_key = get_secret('api_key')
// Node.js
const fs = require('fs');

function getSecret(name) {
  try {
    return fs.readFileSync(`/run/secrets/${name}`, 'utf8').trim();
  } catch {
    return process.env[name.toUpperCase()];
  }
}

const dbPassword = getSecret('db_password');
// Go
import "os"

func getSecret(name string) string {
    data, err := os.ReadFile(fmt.Sprintf("/run/secrets/%s", name))
    if err == nil {
        return strings.TrimSpace(string(data))
    }
    return os.Getenv(strings.ToUpper(name))
}

10.4 Docker Configs(配置对象)

Configs 类似 Secrets,但用于非敏感配置数据。

基本用法

services:
  nginx:
    image: nginx:alpine
    configs:
      - source: nginx_config
        target: /etc/nginx/nginx.conf
        mode: 0444

  app:
    image: myapp:latest
    configs:
      - source: app_config
        target: /app/config.yaml

configs:
  nginx_config:
    file: ./config/nginx.conf
  app_config:
    file: ./config/app.yaml

内联配置

configs:
  nginx_config:
    content: |
      server {
        listen 80;
        server_name localhost;

        location / {
          proxy_pass http://app:3000;
          proxy_set_header Host $host;
          proxy_set_header X-Real-IP $remote_addr;
        }

        location /health {
          return 200 'OK';
          add_header Content-Type text/plain;
        }
      }

Secrets vs Configs

维度SecretsConfigs
用途敏感信息(密码、密钥)非敏感配置
存储方式tmpfs(内存)挂载文件
文件大小限制500KB无限制
Swarm 加密✅ Raft 日志加密❌ 明文
容器中路径/run/secrets/自定义
更新行为需要重启容器可自动更新(Swarm)

10.5 外部密钥管理

HashiCorp Vault 集成

services:
  vault:
    image: hashicorp/vault:latest
    cap_add:
      - IPC_LOCK
    environment:
      VAULT_DEV_ROOT_TOKEN_ID: "root-token"
      VAULT_DEV_LISTEN_ADDRESS: "0.0.0.0:8200"
    ports:
      - "8200:8200"

  app:
    image: myapp:latest
    environment:
      VAULT_ADDR: "http://vault:8200"
      VAULT_TOKEN_FILE: /run/secrets/vault_token
    secrets:
      - vault_token
    depends_on:
      - vault

secrets:
  vault_token:
    file: ./secrets/vault_token.txt

使用 AWS Secrets Manager

services:
  app:
    image: myapp:latest
    environment:
      AWS_REGION: us-east-1
      AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID}
      AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY}
      # 应用代码中通过 AWS SDK 获取密钥

使用 SOPS 加密

# 安装 sops
# 使用 age 或 GPG 加密 .env 文件
sops -e .env.production > .env.production.enc
# 在 CI/CD 中解密
# sops -d .env.production.enc > .env.production
# docker compose --env-file .env.production up -d

10.6 TLS 证书管理

使用 Secrets 挂载证书

services:
  nginx:
    image: nginx:alpine
    ports:
      - "443:443"
    secrets:
      - source: tls_cert
        target: /etc/nginx/certs/server.crt
        mode: 0444
      - source: tls_key
        target: /etc/nginx/certs/server.key
        mode: 0400
    configs:
      - source: nginx_ssl_config
        target: /etc/nginx/conf.d/default.conf

secrets:
  tls_cert:
    file: ./certs/server.crt
  tls_key:
    file: ./certs/server.key

configs:
  nginx_ssl_config:
    content: |
      server {
        listen 443 ssl;
        ssl_certificate /etc/nginx/certs/server.crt;
        ssl_certificate_key /etc/nginx/certs/server.key;
        location / {
          proxy_pass http://app:3000;
        }
      }

10.7 完整的安全配置示例

项目结构

secure-app/
├── compose.yaml
├── .env                    # 仅非敏感配置
├── .env.example            # 模板
├── secrets/
│   ├── db_password.txt     # 数据库密码
│   ├── api_key.txt         # API 密钥
│   ├── jwt_secret.txt      # JWT 签名密钥
│   └── tls/
│       ├── server.crt      # TLS 证书
│       └── server.key      # TLS 私钥
├── config/
│   ├── nginx.conf
│   └── app.yaml
└── app/
    └── Dockerfile

compose.yaml

services:
  app:
    build: ./app
    environment:
      NODE_ENV: ${NODE_ENV:-production}
      LOG_LEVEL: ${LOG_LEVEL:-info}
      DB_HOST: db
      DB_NAME: myapp
      DB_USER: postgres
      # 敏感值通过文件传递
      DB_PASSWORD_FILE: /run/secrets/db_password
      API_KEY_FILE: /run/secrets/api_key
      JWT_SECRET_FILE: /run/secrets/jwt_secret
    secrets:
      - db_password
      - api_key
      - jwt_secret
    configs:
      - source: app_config
        target: /app/config.yaml
    depends_on:
      db:
        condition: service_healthy
    restart: unless-stopped

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: myapp
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password
    secrets:
      - db_password
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 10s
      retries: 5
    restart: unless-stopped

  nginx:
    image: nginx:alpine
    ports:
      - "443:443"
    secrets:
      - source: tls_cert
        target: /etc/nginx/certs/server.crt
        mode: 0444
      - source: tls_key
        target: /etc/nginx/certs/server.key
        mode: 0400
    configs:
      - source: nginx_config
        target: /etc/nginx/conf.d/default.conf
    depends_on:
      - app
    restart: unless-stopped

secrets:
  db_password:
    file: ./secrets/db_password.txt
  api_key:
    file: ./secrets/api_key.txt
  jwt_secret:
    file: ./secrets/jwt_secret.txt
  tls_cert:
    file: ./secrets/tls/server.crt
  tls_key:
    file: ./secrets/tls/server.key

configs:
  app_config:
    file: ./config/app.yaml
  nginx_config:
    file: ./config/nginx.conf

volumes:
  pgdata:

.gitignore

# 敏感文件
secrets/
.env.production
.env.staging

# 保留模板
!secrets/.gitkeep
!.env.example

10.8 安全最佳实践

实践说明
使用 Secrets 而非 environment密码不进入进程环境变量
使用 _FILE 后缀配合官方镜像的文件读取机制
限制文件权限chmod 600 secrets/*.txt
.gitignore 排除密钥文件不进入版本控制
提供 .env.example模板文件供团队参考
轮换密钥定期更新密码和密钥
外部密钥管理生产环境使用 Vault/AWS SM
加密备份密钥文件的备份需要加密
最小权限密钥文件的 mode 设置合理权限

10.9 常见问题

问题原因解决方案
Secret 文件找不到路径错误确认 file: 路径相对于 compose.yaml
权限拒绝UID/GID 不匹配使用 uid/gid/mode 配置
Secret 值包含换行文件末尾有换行符应用代码 strip() 去除
环境变量优先级覆盖 Secret_FILE 不是标准机制需要在应用代码中实现读取逻辑
Secret 文件大小超限超过 500KB使用 Configs 或挂载卷

10.10 小结

概念说明
Secrets敏感信息管理,挂载在 /run/secrets/,tmpfs 存储
Configs非敏感配置,支持内联和文件两种方式
_FILE 模式官方镜像的文件读取约定
外部密钥管理Vault、AWS Secrets Manager、SOPS
安全原则不硬编码、不进版本控制、最小权限、定期轮换

扩展阅读


上一章:第 9 章 · 多环境管理 ← | 下一章:第 11 章 · Swarm 部署 →