使用 Docker secrets 管理敏感数据

什么是 secret

在 Docker Swarm 服务中,secret 是一段数据,例如密码、SSH 私钥、SSL 证书,或其他不应未经加密就在网络上传输、写入 Dockerfile 或应用源代码的数据。Docker secrets 可以集中管理这些数据,只将它们安全地传给确实需要访问的容器。在 Docker Swarm 中,secret 在传输和存储时都会加密。只有明确获准访问的服务能够访问指定 secret,而且只在该服务的任务运行期间可用。

可以用 secret 管理容器在运行时需要、又不希望写入镜像或版本控制系统的敏感数据,例如:

  • 用户名与密码。
  • TLS 证书与密钥。
  • SSH 密钥。
  • 数据库名称、内部服务器名称等重要信息。
  • 普通字符串或二进制内容,大小最多 500 KB。

注意: Docker secrets 适用于 Swarm 服务,不适用于独立容器。需要使用此功能时,可考虑把容器改为服务运行。有状态容器通常可以用一个副本运行,无须因此修改容器代码。

Secret 还能在容器与凭据之间提供一层抽象。例如,应用有开发、测试和生产环境,各环境使用不同凭据,但在各自 Swarm 中使用同一个 secret 名称。容器只需要知道这个名称,就能在三个环境中工作。

Secret 也可以存放配置文件等非敏感数据。不过 Docker 为非敏感数据提供了 configs。Config 直接挂载到容器文件系统,不使用内存盘。

Windows 支持

Docker 支持在 Windows 容器中使用 secrets。实现上的差异会在下面的示例中说明,主要差异包括:

  • Windows 没有管理内存盘的内置驱动,因此运行中的 Windows 容器会把 secret 明文保存在容器根磁盘上;容器停止时会明确删除这些文件。Windows 也不支持通过 docker commit 或类似命令,把运行中的容器保存为镜像。
  • 建议在宿主机上对 Docker 根目录所在卷启用 BitLocker,使运行中容器的 secret 在磁盘上受到加密保护。
  • 自定义目标路径的 secret 文件不会直接绑定挂载到 Windows 容器,因为 Windows 不支持非目录文件的绑定挂载。容器中的所有 secret 会先挂载到 C:\ProgramData\Docker\internal\secrets,再用符号链接指向指定目标。前一个路径属于实现细节,应用不应依赖它;默认目标路径为 C:\ProgramData\Docker\secrets。
  • Windows 容器服务的 secrets 不支持指定 UID、GID 和 mode。容器中只有管理员以及具有 system 访问权限的用户能够访问它们。

Docker 如何管理 secrets

向 Swarm 添加 secret 时,Docker 通过双向 TLS 连接将它发送到 Swarm 管理节点。Secret 保存在加密的 Raft 日志中;完整日志会复制到其他管理节点,使 secrets 与其他 Swarm 管理数据具有相同的高可用保障。

为新建或运行中的服务授予 secret 访问权限后,解密的数据会在 Linux 容器中挂载到内存文件系统。默认路径是 /run/secrets/<secret_name>;Windows 容器的默认路径是 C:\ProgramData\Docker\secrets,其磁盘实现差异见前节。也可以指定自定义位置。

可以随时更新服务,为它增加 secret 访问权限或撤销某个 secret 的访问权限。

只有 Swarm 管理节点,或正在运行获准访问该 secret 的服务任务的节点,才会接收相应的加密 secret。容器任务停止后,其解密 secret 会从容器内存文件系统卸载,并从节点内存清除。

如果正在运行任务的节点与 Swarm 失去连接,该任务容器仍能访问已获得的 secrets;但在重新连接之前,它无法接收更新。

可以随时添加或检查单个 secret,也可以列出全部 secrets。不能删除正在被运行中的服务使用的 secret;轮换 secret一节展示如何在不中断运行中服务的情况下移除旧 secret。

为了便于更新或回退,可以在 secret 名称中加入版本号或日期。由于容器内的挂载点可以单独指定,这样的命名方式较容易实现。

docker secret 命令参考

以下链接介绍具体命令,也可以直接阅读服务使用 secret 的示例。

在 Compose 文件中定义和使用 secrets

在 Docker Swarm 中配合 Compose 文件使用 secrets,可以先用 docker secret create 定义 secret:

$ printf "my-secret" | docker secret create some_secret -

然后在 compose.yml 顶层的 secrets 中把该 secret 声明为 external:

services:
    api:
        image: my-web-app
        environment:
            MY_ENV: /run/secrets/some_secret
        secrets:
            - some_secret

secrets:
    some_secret:
        external: true

docker compose 与 docker stack 都支持在 Compose 文件中定义 secrets。语法细节见 Compose 文件参考。它们的分发和存储实现不同;普通 Compose 的文件 secret 按官方说明通过文件绑定挂载交付,不能据此推定它具有 Swarm 的加密 Raft 存储保障。

示例

这一节从简单场景逐步介绍 Docker secrets 的使用方法。示例使用的镜像已增加读取 secret 文件的支持。要给自己的镜像实现类似功能,参阅为镜像添加 Docker secrets 支持。

注意: 为了简化说明,示例使用只有一个 Docker Engine 的 Swarm,服务也不做扩容。大部分示例使用 Linux 容器;Windows 容器同样支持 secrets,差异见 Windows 支持一节。下面的命令及终端输出保留自官方示例,输出用于说明预期形态,不表示本文执行过这些操作。

简单示例:开始使用 secrets

这个例子用少量命令说明 secrets 的基本工作方式。更完整的应用场景见后面的 Nginx 示例。

  1. 向 Docker 添加 secret。docker secret create 的最后一个参数表示读取 secret 的文件;设为 - 时,从标准输入读取。
$ printf "This is a secret" | docker secret create my_secret_data -
  1. 创建 redis 服务,并授予它访问该 secret 的权限。默认情况下,容器通过 /run/secrets/<secret_name> 访问 secret;可以用 target 指定容器中的文件名。
$ docker service  create --name redis --secret my_secret_data redis:alpine
  1. 用 docker service ps 检查任务是否正常。正常运行时,输出形态如下:
$ docker service ps redis

ID            NAME     IMAGE         NODE              DESIRED STATE  CURRENT STATE          ERROR  PORTS
bkna6bpn8r1a  redis.1  redis:alpine  ip-172-31-46-109  Running        Running 8 seconds ago  

如果任务出错、反复失败和重启,输出可能类似下面这样:

$ docker service ps redis

NAME                      IMAGE         NODE  DESIRED STATE  CURRENT STATE          ERROR                      PORTS
redis.1.siftice35gla      redis:alpine  moby  Running        Running 4 seconds ago                             
 \_ redis.1.whum5b7gu13e  redis:alpine  moby  Shutdown       Failed 20 seconds ago      "task: non-zero exit (1)"  
 \_ redis.1.2s6yorvd9zow  redis:alpine  moby  Shutdown       Failed 56 seconds ago      "task: non-zero exit (1)"  
 \_ redis.1.ulfzrcyaf6pg  redis:alpine  moby  Shutdown       Failed about a minute ago  "task: non-zero exit (1)"  
 \_ redis.1.wrny5v4xyps6  redis:alpine  moby  Shutdown       Failed 2 minutes ago       "task: non-zero exit (1)"
  1. 用 docker ps 获取 redis 服务任务容器的 ID,再用 docker container exec 连接容器,读取 secret 数据文件。默认文件名与 secret 名称相同,默认权限允许所有用户读取。下面第一个命令说明如何查找容器 ID,后续命令通过 shell 展开自动完成同样的操作。
$ docker ps --filter name=redis -q

5cb1c2348a59

$ docker container exec $(docker ps --filter name=redis -q) ls -l /run/secrets

total 4
-r--r--r--    1 root     root            17 Dec 13 22:48 my_secret_data

$ docker container exec $(docker ps --filter name=redis -q) cat /run/secrets/my_secret_data

This is a secret
  1. 检查通过提交容器生成的镜像是否包含 secret:
$ docker commit $(docker ps --filter name=redis -q) committed_redis

$ docker run --rm -it committed_redis cat /run/secrets/my_secret_data

cat: can't open '/run/secrets/my_secret_data': No such file or directory
  1. 尝试删除 secret。由于运行中的 redis 服务仍获准访问,删除会失败:
$ docker secret ls

ID                          NAME                CREATED             UPDATED
wwwrxza8sxy025bas86593fqs   my_secret_data      4 hours ago         4 hours ago


$ docker secret rm my_secret_data

Error response from daemon: rpc error: code = 3 desc = secret
'my_secret_data' is in use by the following service: redis
  1. 更新运行中的 redis 服务,撤销其访问权限:
$ docker service update --secret-rm my_secret_data redis
  1. 再次执行第 3、4 步,检查服务是否已经无法访问该 secret。容器 ID 会改变,因为 service update 会重新部署服务。
$ docker container exec -it $(docker ps --filter name=redis -q) cat /run/secrets/my_secret_data

cat: can't open '/run/secrets/my_secret_data': No such file or directory
  1. 停止并移除服务,再删除 Docker 中的 secret:
$ docker service rm redis

$ docker secret rm my_secret_data

简单示例:在 Windows 服务中使用 secrets

这个简单例子在 Windows 10 上运行使用 Windows 容器的 Docker for Windows,为 Microsoft IIS 服务提供 secret。它把网页内容存成一个 secret,只用于演示。示例假设已经安装 PowerShell。

  1. 将以下内容保存为新文件 index.html:
<html lang="en">
  <head><title>Hello Docker</title></head>
  <body>
    <p>Hello Docker! You have deployed a HTML page.</p>
  </body>
</html>
  1. 如果尚未初始化或加入 Swarm,先完成这一操作:
> docker swarm init
  1. 将 index.html 保存为名为 homepage 的 Swarm secret:
> docker secret create homepage index.html
  1. 创建 IIS 服务,并授予它访问 homepage 的权限:
> docker service create `
    --name my-iis `
    --publish published=8000,target=8000 `
    --secret src=homepage,target="\inetpub\wwwroot\index.html" `
    microsoft/iis:nanoserver

注意: 这个例子实际上不需要 secrets,configs 更合适。这里仅用于说明机制。

  1. 访问 http://localhost:8000/,查看 IIS 是否提供第 1 步的 HTML 内容。
  2. 移除服务和 secret:
> docker service rm my-iis
> docker secret rm homepage
> docker image remove secret-test

中级示例:在 Nginx 服务中使用 secrets

这个示例分成两部分。第一部分生成站点证书,不直接涉及 Docker secrets;第二部分把证书、密钥和 Nginx 配置保存为 secrets 并使用它们。

生成站点证书

为站点生成根 CA、TLS 证书和密钥。生产站点可以使用 Let’s Encrypt 等服务生成 TLS 证书和密钥;下面使用命令行工具。步骤稍多,是为了准备可存入 secret 的内容。也可以用 Let’s Encrypt 生成站点密钥与证书,将文件分别命名为 site.key 和 site.crt,直接进入“配置 Nginx 容器”一节。

  1. 生成根密钥:
$ openssl genrsa -out "root-ca.key" 4096
  1. 使用根密钥生成证书签名请求(CSR):
$ openssl req \
          -new -key "root-ca.key" \
          -out "root-ca.csr" -sha256 \
          -subj '/C=US/ST=CA/L=San Francisco/O=Docker/CN=Swarm Secret Example CA'
  1. 配置根 CA。新建 root-ca.cnf,写入以下内容。这个配置限制根 CA 只能签署叶子证书,不能签署中间 CA:
[root_ca]
basicConstraints = critical,CA:TRUE,pathlen:1
keyUsage = critical, nonRepudiation, cRLSign, keyCertSign
subjectKeyIdentifier=hash
  1. 签署证书:
$ openssl x509 -req  -days 3650  -in "root-ca.csr" \
               -signkey "root-ca.key" -sha256 -out "root-ca.crt" \
               -extfile "root-ca.cnf" -extensions \
               root_ca
  1. 生成站点密钥:
$ openssl genrsa -out "site.key" 4096
  1. 用站点密钥生成站点证书请求:
$ openssl req -new -key "site.key" -out "site.csr" -sha256 \
          -subj '/C=US/ST=CA/L=San Francisco/O=Docker/CN=localhost'
  1. 配置站点证书。新建 site.cnf,写入以下内容。该配置限制证书只能用于验证服务器身份,不能用于签署其他证书:
[server]
authorityKeyIdentifier=keyid,issuer
basicConstraints = critical,CA:FALSE
extendedKeyUsage=serverAuth
keyUsage = critical, digitalSignature, keyEncipherment
subjectAltName = DNS:localhost, IP:127.0.0.1
subjectKeyIdentifier=hash
  1. 签署站点证书:
$ openssl x509 -req -days 750 -in "site.csr" -sha256 \
    -CA "root-ca.crt" -CAkey "root-ca.key"  -CAcreateserial \
    -out "site.crt" -extfile "site.cnf" -extensions server
  1. Nginx 服务不需要 site.csr 与 site.cnf,但以后重新生成站点证书时还会用到。妥善保护 root-ca.key。

配置 Nginx 容器

  1. 准备一个通过 HTTPS 提供静态文件的基础 Nginx 配置。将 TLS 证书和密钥存为 Docker secrets,以便轮换。在当前目录创建 site.conf,内容如下:
server {
    listen                443 ssl;
    server_name           localhost;
    ssl_certificate       /run/secrets/site.crt;
    ssl_certificate_key   /run/secrets/site.key;

    location / {
        root   /usr/share/nginx/html;
        index  index.html index.htm;
    }
}
  1. 分别为密钥、证书和 site.conf 创建三个 secrets。小于 500 KB 的文件都可作为 secret,让密钥、证书及配置与使用它们的服务解耦。下面每个命令的最后一个参数,都是宿主机文件系统中待读取文件的路径;示例中 secret 名称与文件名相同。
$ docker secret create site.key site.key

$ docker secret create site.crt site.crt

$ docker secret create site.conf site.conf
$ docker secret ls

ID                          NAME                  CREATED             UPDATED
2hvoi9mnnaof7olr3z5g3g7fp   site.key       58 seconds ago      58 seconds ago
aya1dh363719pkiuoldpter4b   site.crt       24 seconds ago      24 seconds ago
zoa5df26f7vpcoz42qf2csth8   site.conf      11 seconds ago      11 seconds ago
  1. 创建 Nginx 服务,并授予三个 secrets 的访问权限。第一个 docker service create 命令的末尾,将 site.conf 链接到 Nginx 读取额外配置的 /etc/nginx/conf.d/。链接在 Nginx 启动前创建,因此改变配置时不需要重新构建镜像。

    注意: 常见做法是用 Dockerfile 将 site.conf 复制到指定位置,构建自定义镜像,再运行容器。这个例子把放置配置与启动容器合并,不需要自定义镜像。

Secret 默认位于容器的 /run/secrets/,应用要求其他路径时需要额外处理。下面创建指向 secret 实际位置的符号链接,让 Nginx 能读取它:

$ docker service create \
     --name nginx \
     --secret site.key \
     --secret site.crt \
     --secret site.conf \
     --publish published=3000,target=443 \
     nginx:latest \
     sh -c "ln -s /run/secrets/site.conf /etc/nginx/conf.d/site.conf && exec nginx -g 'daemon off;'"

也可以通过 target 自定义挂载位置,直接将 site.conf 放在容器内的 /etc/nginx/conf.d/site.conf,不使用符号链接:

$ docker service create \
     --name nginx \
     --secret site.key \
     --secret site.crt \
     --secret source=site.conf,target=/etc/nginx/conf.d/site.conf \
     --publish published=3000,target=443 \
     nginx:latest \
     sh -c "exec nginx -g 'daemon off;'"

site.key 与 site.crt 使用简写语法,没有自定义 target,所以会以相同名称挂载在 /run/secrets/。运行中的容器内会出现:

  • /run/secrets/site.key
  • /run/secrets/site.crt
  • /etc/nginx/conf.d/site.conf
  1. 检查 Nginx 服务是否运行:
$ docker service ls

ID            NAME   MODE        REPLICAS  IMAGE
zeskcec62q24  nginx  replicated  1/1       nginx:latest

$ docker service ps nginx

NAME                  IMAGE         NODE  DESIRED STATE  CURRENT STATE          ERROR  PORTS
nginx.1.9ls3yo9ugcls  nginx:latest  moby  Running        Running 3 minutes ago
  1. 检查能否访问 Nginx,以及使用的 TLS 证书是否正确。以下为原文的命令与历史示例输出;其中协议、密码套件和会话值不能作为当前生产环境配置建议:
$ curl --cacert root-ca.crt https://localhost:3000

<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
    body {
        width: 35em;
        margin: 0 auto;
        font-family: Tahoma, Verdana, Arial, sans-serif;
    }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>

<p>For online documentation and support. refer to
<a href="https://nginx.org">nginx.org</a>.<br/>
Commercial support is available at
<a href="https://www.nginx.com">nginx.com</a>.</p>

<p><em>Thank you for using nginx.</em></p>
</body>
</html>
$ openssl s_client -connect localhost:3000 -CAfile root-ca.crt

CONNECTED(00000003)
depth=1 /C=US/ST=CA/L=San Francisco/O=Docker/CN=Swarm Secret Example CA
verify return:1
depth=0 /C=US/ST=CA/L=San Francisco/O=Docker/CN=localhost
verify return:1
---
Certificate chain
 0 s:/C=US/ST=CA/L=San Francisco/O=Docker/CN=localhost
   i:/C=US/ST=CA/L=San Francisco/O=Docker/CN=Swarm Secret Example CA
---
Server certificate
-----BEGIN CERTIFICATE-----
…
-----END CERTIFICATE-----
subject=/C=US/ST=CA/L=San Francisco/O=Docker/CN=localhost
issuer=/C=US/ST=CA/L=San Francisco/O=Docker/CN=Swarm Secret Example CA
---
No client certificate CA names sent
---
SSL handshake has read 1663 bytes and written 712 bytes
---
New, TLSv1/SSLv3, Cipher is AES256-SHA
Server public key is 4096 bit
Secure Renegotiation IS supported
Compression: NONE
Expansion: NONE
SSL-Session:
    Protocol  : TLSv1
    Cipher    : AES256-SHA
    Session-ID: A1A8BF35549C5715648A12FD7B7E3D861539316B03440187D9DA6C2E48822853
    Session-ID-ctx:
    Master-Key: F39D1B12274BA16D3A906F390A61438221E381952E9E1E05D3DD784F0135FB81353DA38C6D5C021CB926E844DFC49FC4
    Key-Arg   : None
    Start Time: 1481685096
    Timeout   : 300 (sec)
    Verify return code: 0 (ok)
  1. 完成演练后,可以移除 nginx 服务及保存的 secrets:
$ docker service rm nginx

$ docker secret rm site.crt site.key site.conf

高级示例:在 WordPress 服务中使用 secrets

这个例子创建一个单节点 MySQL 服务,设置自定义 root 密码,把凭据保存为 secrets,再创建一个用这些凭据连接 MySQL 的单节点 WordPress 服务。下一例在此基础上轮换 WordPress 数据库用户的密码并更新服务,使 WordPress 仍能连接 MySQL。

此例演示如何避免把敏感凭据保存在镜像中,或直接写进创建服务的命令参数。

注意: 为了简化说明,例子使用单 Engine Swarm 和单节点 MySQL。不能通过简单增加服务副本来扩容单个 MySQL 实例;配置 MySQL 集群超出此例范围。更改 MySQL root 密码也需要执行查询或 mysqladmin 命令,不能只替换磁盘上的文件。

  1. 生成随机 MySQL 密码,并用 docker secret create 保存为 mysql_password。调整 openssl 的最后一个参数可以改变生成长度。下面使用 Base64 表示随机数据,字符不局限于字母和数字;也可以采用其他密码生成方式。

    注意: Secret 创建后不能原地更新,只能删除再重新创建,而且不能删除正被服务使用的 secret。可以通过 docker service update 授予或撤销访问权限。需要轮换时,可以在名称中加入版本,先创建新版本,再让服务使用它,最后删除旧版本。

最后的 - 表示从标准输入读取:

$ openssl rand -base64 20 | docker secret create mysql_password -

l1vinzevzhj4goakjap5ya409

返回值是 secret 的 ID,不是密码。后续例子省略 ID 输出。

再为 MySQL root 用户生成另一个 secret。它只用于初始化 mysql 服务,不会共享给后面创建的 WordPress 服务。

$ openssl rand -base64 20 | docker secret create mysql_root_password -

用 docker secret ls 列出 Docker 管理的 secrets:

$ docker secret ls

ID                          NAME                  CREATED             UPDATED
l1vinzevzhj4goakjap5ya409   mysql_password        41 seconds ago      41 seconds ago
yvsczlx9votfw3l0nz5rlidig   mysql_root_password   12 seconds ago      12 seconds ago

它们保存在 Swarm 的加密 Raft 日志中。

  1. 创建用户自定义 overlay 网络,供 MySQL 与 WordPress 服务通信。无需把 MySQL 服务暴露给外部宿主机或容器。
$ docker network create -d overlay mysql_private
  1. 创建 MySQL 服务。它具有以下设置:

    • 副本数为 1,只运行一个 MySQL 任务。MySQL 负载均衡留作后续课题,需要的配置不止增加副本。
    • 只有 mysql_private 网络中的其他容器能访问它。
    • 使用 mydata 卷保存 MySQL 数据,使服务重启后数据仍保留。
    • 两个 secrets 分别挂载在 tmpfs 文件系统的 /run/secrets/mysql_password 与 /run/secrets/mysql_root_password,不会变成环境变量,也不会通过 docker commit 写入镜像。非特权的 WordPress 容器用 mysql_password 连接数据库。
    • 环境变量 MYSQL_PASSWORD_FILE 和 MYSQL_ROOT_PASSWORD_FILE 分别指向对应 secret 文件。MySQL 镜像第一次初始化系统数据库时读取文件中的密码,之后密码保存在 MySQL 系统数据库里。
    • 设置 MYSQL_USER 与 MYSQL_DATABASE。容器启动时创建名为 wordpress 的数据库;wordpress 用户只拥有该数据库的完整权限,不能创建或删除其他数据库,也不能修改 MySQL 配置。
$ docker service create \
     --name mysql \
     --replicas 1 \
     --network mysql_private \
     --mount type=volume,source=mydata,destination=/var/lib/mysql \
     --secret source=mysql_root_password,target=mysql_root_password \
     --secret source=mysql_password,target=mysql_password \
     -e MYSQL_ROOT_PASSWORD_FILE="/run/secrets/mysql_root_password" \
     -e MYSQL_PASSWORD_FILE="/run/secrets/mysql_password" \
     -e MYSQL_USER="wordpress" \
     -e MYSQL_DATABASE="wordpress" \
     mysql:latest
  1. 用 docker service ls 检查 MySQL 容器是否运行:
$ docker service ls

ID            NAME   MODE        REPLICAS  IMAGE
wvnh0siktqr3  mysql  replicated  1/1       mysql:latest
  1. 创建连接到 MySQL 的 WordPress 服务。它具有以下设置:

    • 副本数为 1,只运行一个 WordPress 任务。由于 WordPress 会话数据保存在容器文件系统等限制,负载均衡需要另行设计。
    • 将宿主机的 30000 端口对外开放。如果宿主机的 80 端口没有 Web 服务器占用,也可以改为暴露 80 端口。
    • 加入 mysql_private 网络,与 MySQL 容器通信,并将容器 80 端口发布到全部 Swarm 节点的 30000 端口。
    • 访问 mysql_password secret,但把目标文件名设为 /run/secrets/wp_db_password。
    • 环境变量 WORDPRESS_DB_PASSWORD_FILE 指向 secret 文件。WordPress 服务从中读取 MySQL 密码,并写入 wp-config.php。
    • 使用 wordpress 用户和 /run/secrets/wp_db_password 中的密码连接 MySQL;如果 wordpress 数据库尚不存在,镜像会尝试创建它。
    • 将主题、插件等数据保存在 wpdata 卷,使服务重启后这些文件仍在。
$ docker service create \
     --name wordpress \
     --replicas 1 \
     --network mysql_private \
     --publish published=30000,target=80 \
     --mount type=volume,source=wpdata,destination=/var/www/html \
     --secret source=mysql_password,target=wp_db_password \
     -e WORDPRESS_DB_USER="wordpress" \
     -e WORDPRESS_DB_PASSWORD_FILE="/run/secrets/wp_db_password" \
     -e WORDPRESS_DB_HOST="mysql:3306" \
     -e WORDPRESS_DB_NAME="wordpress" \
     wordpress:latest
  1. 用 docker service ls 与 docker service ps 检查服务是否运行:
$ docker service ls

ID            NAME       MODE        REPLICAS  IMAGE
wvnh0siktqr3  mysql      replicated  1/1       mysql:latest
nzt5xzae4n62  wordpress  replicated  1/1       wordpress:latest
$ docker service ps wordpress

ID            NAME         IMAGE             NODE  DESIRED STATE  CURRENT STATE           ERROR  PORTS
aukx6hgs9gwc  wordpress.1  wordpress:latest  moby  Running        Running 52 seconds ago   

此时 WordPress 已把 secret 写入 wp-config.php,理论上可以撤销它对 mysql_password 的访问。先保留访问权限,后面的轮换示例会用到它。

  1. 从任意 Swarm 节点访问 http://localhost:30000/,通过网页向导完成 WordPress 设置。这些设置保存在 MySQL 的 wordpress 数据库中。WordPress 会为站点用户生成一个密码,它与 WordPress 访问 MySQL 的密码不同。请安全保存,例如存入密码管理器;轮换数据库密码后,仍需要它登录 WordPress。

可以创建一两篇测试文章,安装插件或主题,检查 WordPress 是否完整可用,以及服务重启后状态是否保留。

  1. 如果要继续下面的数据库用户密码轮换示例,先保留所有服务和 secrets。

示例:轮换 secret

本例接续前例:创建包含新 MySQL 密码的 secret,让 mysql 和 wordpress 服务使用它,再删除旧 secret。

注意: MySQL 数据库已经存在时,镜像不会重新用环境变量或文件初始化密码。密码保存在数据库中,因此必须另外执行查询或命令来修改。轮换密码或其他 secrets,可能需要 Docker 之外的步骤。

  1. 创建新密码,保存为 mysql_password_v2:
$ openssl rand -base64 20 | docker secret create mysql_password_v2 -
  1. 更新 MySQL 服务,让它同时访问旧、新两个 secrets。Secret 不能更新或重命名;可以撤销旧挂载,再以新的目标文件名授予访问权限。
$ docker service update \
     --secret-rm mysql_password mysql

$ docker service update \
     --secret-add source=mysql_password,target=old_mysql_password \
     --secret-add source=mysql_password_v2,target=mysql_password \
     mysql

更新会重启服务。重启后,旧 secret 位于 /run/secrets/old_mysql_password,新 secret 位于 /run/secrets/mysql_password。

此时服务能访问两个 secrets,但数据库中 wordpress 用户的实际密码还没有改变。

注意:这个例子不轮换 MySQL root 密码。

  1. 用 mysqladmin 修改 wordpress 用户的数据库密码。示例命令从 /run/secrets 文件读取旧、新密码,避免把明文直接写进 shell 历史。但 shell 展开后仍会把密码作为进程参数传给程序,不能将此方法视为没有参数泄露风险;MySQL 的密码安全指南也说明了命令行密码参数的风险。

密码修改后,WordPress 暂时不能连接 MySQL,因此应紧接着更新 WordPress 服务。

先查找 MySQL 任务容器的 ID:

$ docker ps --filter name=mysql -q

c7705cf6176f

将 ID 替换到下面命令中;也可以用后面的 shell 展开版本一步完成。

$ docker container exec <CONTAINER_ID> \
    bash -c 'mysqladmin --user=wordpress --password="$(< /run/secrets/old_mysql_password)" password "$(< /run/secrets/mysql_password)"'

或者:

$ docker container exec $(docker ps --filter name=mysql -q) \
    bash -c 'mysqladmin --user=wordpress --password="$(< /run/secrets/old_mysql_password)" password "$(< /run/secrets/mysql_password)"'
  1. 更新 wordpress 服务,改用新密码,同时保留目标路径 /run/secrets/wp_db_password。这会触发滚动重启,重启后的服务使用新 secret。
$ docker service update \
     --secret-rm mysql_password \
     --secret-add source=mysql_password_v2,target=wp_db_password \
     wordpress    
  1. 再次从任意 Swarm 节点访问 http://localhost:30000/,检查 WordPress 是否可用。使用前面向导生成的站点用户名和密码登录,检查测试文章仍存在,修改过的配置也仍保留。

  2. 撤销 MySQL 服务对旧 secret 的访问,再从 Docker 删除旧 secret:

$ docker service update \
     --secret-rm mysql_password \
     mysql

$ docker secret rm mysql_password
  1. 演练结束后,下面的命令会删除 WordPress 与 MySQL 服务、mydata 与 wpdata 数据卷,以及 Docker secrets。数据卷删除会移除本例保存的数据,只有确定不再需要这些演练数据时才执行。
$ docker service rm wordpress mysql

$ docker volume rm mydata wpdata

$ docker secret rm mysql_password_v2 mysql_root_password

为镜像添加 Docker secrets 支持

如果开发的容器要以服务运行,而且需要通过环境变量接收凭据等敏感数据,可以考虑让镜像支持 Docker secrets。一种方法是让创建容器时传入的参数,也能从文件中读取。

Docker library 中的许多官方镜像,包括前例使用的 WordPress 镜像,已经支持这种方式。

启动 WordPress 容器时,通常用环境变量传递参数。包含重要信息的变量,例如 WORDPRESS_DB_PASSWORD,还提供从文件读取值的变体 WORDPRESS_DB_PASSWORD_FILE。这样既保持兼容,也允许容器从 Docker 管理的 secret 文件读取数据。

注意: Docker secrets 不会直接设置环境变量。这是有意的设计,因为环境变量可能意外泄露给其他容器,例如使用 --link 时。

在 Compose 中使用 secrets


services:
   db:
     image: mysql:latest
     volumes:
       - db_data:/var/lib/mysql
     environment:
       MYSQL_ROOT_PASSWORD_FILE: /run/secrets/db_root_password
       MYSQL_DATABASE: wordpress
       MYSQL_USER: wordpress
       MYSQL_PASSWORD_FILE: /run/secrets/db_password
     secrets:
       - db_root_password
       - db_password

   wordpress:
     depends_on:
       - db
     image: wordpress:latest
     ports:
       - "8000:80"
     environment:
       WORDPRESS_DB_HOST: db:3306
       WORDPRESS_DB_USER: wordpress
       WORDPRESS_DB_PASSWORD_FILE: /run/secrets/db_password
     secrets:
       - db_password


secrets:
   db_password:
     file: db_password.txt
   db_root_password:
     file: db_root_password.txt

volumes:
    db_data:

这个 Compose 文件通过两个 secrets 创建一个简单的 WordPress 站点。顶层 secrets 定义 db_password 和 db_root_password,各自的 file 指定其内容来源。db 服务使用两个 secrets,wordpress 只使用其中一个。服务通过环境变量告诉镜像到哪个文件路径读取 secret。

文件在容器内的路径为 /run/secrets/<secret_name>。此处要区分部署方式:普通 Docker Compose 会将宿主机文件绑定挂载到容器;不能保证源文件只保存在内存或从不落盘。 原文这一小节把“只在内存管理”笼统应用到 Compose,本文依据Compose 官方 secrets 说明明确了其文件挂载实现。Swarm 的内存挂载与加密分发规则见前文。

短语法、长语法等更多细节见 Compose Specification。

原样示例的进一步核验: 上面保留的 Compose 代码将 WORDPRESS_DB_HOST 设为 db:3306;应按实际数据库监听端口配置。本稿保留下载当时源文件的全部代码,未执行或保证这些示例与任意 latest 镜像兼容。


来源:Docker 文档团队及贡献者,Manage sensitive data with Docker secrets。Copyright 2016 Docker, Inc.;网站标注 Copyright © 2013–2026 Docker Inc.。正文与代码来源仓库按 Apache License 2.0 使用,许可副本随离线源资料提供。本稿将正文译为中文,保留全部代码和原文样例输出,补充普通 Compose 文件挂载及命令行口令风险说明,并修正了原文关于轮换对象和配置路径的文字不一致。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容