1. 项目概述
在容器化部署和持续集成的日常工作中,Docker 登录凭证(比如访问 Docker Hub 或私有镜像仓库的用户名和密码)的管理,是一个既基础又容易被忽视的安全环节。很多开发者,包括我自己在早期,都习惯性地在~/.docker/config.json文件里看到一串 base64 编码的密码,就以为万事大吉了。实际上,这种明文存储(尽管是编码)的方式,无异于把钥匙藏在门垫下面——稍微懂点技术的人就能轻松拿到。尤其是在 CentOS 这类常用于生产环境的服务器上,一旦服务器被入侵,这些凭证就成了攻击者进一步渗透的跳板。今天,我就来分享一个在 CentOS 上,利用pass(Password Store)这个遵循 Unix 哲学的命令行密码管理器,来安全存储 Docker 登录凭证的实战方案。这不仅仅是换个地方存密码,而是建立一套基于 GPG 加密、与 Git 集成、且被 Docker 原生支持的凭证管理体系,彻底告别密码“裸奔”的时代。
2. 为什么选择 pass 作为 Docker 的凭证助手?
在深入实操之前,我们有必要厘清为什么是pass,而不是其他方案。Docker 支持多种凭证存储后端,比如 macOS 的osxkeychain、Windows 的wincred,以及在 Linux 上默认会尝试寻找的pass和secretservice。
2.1 核心优势分析
首先,pass的本质是一个基于 GnuPG(GPG)加密的文本文件管理器。它的工作原理非常简洁优雅:
- 基于目录和文件:你的所有密码都被组织成目录树结构,每个密码是一个独立的 GPG 加密文件。例如,
Docker/registry.hub.docker.com这个路径对应一个加密文件,里面存放着 Docker Hub 的密码。 - GPG 非对称加密:这是安全的核心。你用一个 GPG 公钥加密密码文件,只有持有对应私钥的人(或进程)才能解密。这意味着即使整个密码存储目录被拷贝走,没有你的私钥也无法读取内容。
- 与 Git 无缝集成:
pass可以初始化一个 Git 仓库来管理密码文件的版本历史。这对于团队协作、密码变更追踪和异地备份极其有用。 - 命令行友好:完全通过命令行操作,非常适合服务器环境和自动化脚本。
对于 Docker 而言,它需要的只是一个实现了特定协议(store,get,erase)的助手程序。docker-credential-pass就是这个协议的实现者,它作为pass和 Docker 引擎之间的桥梁。
2.2 与其他方案的对比
- 直接存储在
config.json(默认行为):如前所述,base64 编码不是加密,等同于明文存储。这是最不安全的方式,应坚决避免。 secretservice(如 GNOME Keyring, KWallet):这是一个桌面环境下的守护进程服务。在生产服务器上,通常没有图形界面,运行和维护这些服务会增加复杂性。而且,在纯命令行环境下调用有时会遇到 DBus 通信问题。- 第三方商业密码管理器插件:Docker 也支持像
1password这样的插件,但这通常需要额外的订阅、复杂的配置,并且可能不适合内网隔离环境。
因此,pass方案在 Linux 服务器上展现出了独特的优势:轻量、安全、无外部依赖(仅需 GPG)、易于自动化、且被 Docker 官方列为默认的 Linux 凭证存储后端。
3. 在 CentOS 7/8 上部署 pass 与 Docker 凭证助手
下面,我们进入实战环节。我将以 CentOS 7 为例,步骤同样适用于 CentOS 8 或 Rocky Linux、AlmaLinux 等衍生系统。
3.1 系统环境准备与依赖安装
首先,确保你的系统是最新的,并安装必要的工具链。
# 更新系统包 sudo yum update -y # 安装 EPEL 仓库(Extra Packages for Enterprise Linux),提供更多软件包 sudo yum install -y epel-release # 安装核心依赖:GnuPG(用于加密)、Git(用于密码库版本管理)、以及编译工具 sudo yum install -y gnupg2 git make gcc gcc-c++ # 确认 GPG 版本 gpg --version | head -n1注意:CentOS 8 及之后版本可能使用
dnf包管理器,将上述命令中的yum替换为dnf即可。如果系统已经安装了较新版本的 GPG(如gpg而非gpg2),后续命令可能需要相应调整。
3.2 生成与备份 GPG 密钥对
这是整个体系的安全基石。如果你还没有 GPG 密钥,需要生成一对。
# 生成一个新的 GPG 密钥对。以下命令会进入交互式提示。 gpg --full-generate-key在交互过程中,我建议如下选择:
- 密钥类型:
RSA and RSA(默认)。 - 密钥长度:
4096(更高的安全性)。 - 有效期:根据安全策略选择,例如
1y(一年)或0(永不过期)。生产环境建议设置有效期。 - 输入你的姓名和邮箱(这将成为密钥的标识)。
- 设置一个强密码短语(Passphrase)来保护你的私钥。这个密码非常重要,务必牢记。
生成完成后,列出你的密钥以确认:
gpg --list-secret-keys --keyid-format LONG输出类似:
sec rsa4096/3AA5C34371567BD2 2023-10-27 [SC] [expires: 2024-10-26] Key fingerprint = 1234 5678 9ABC DEF0 1234 5678 9ABC DEF0 1234 uid [ultimate] Your Name <your.email@example.com> ssb rsa4096/42B317FD4BA7E9E7 2023-10-27 [E] [expires: 2024-10-26]记下rsa4096/后面的密钥 ID,这里是3AA5C34371567BD2。我们后续会用到。
实操心得:务必立即备份你的私钥和公钥!私钥丢失意味着所有用该公钥加密的密码都无法解密。
# 导出私钥(谨慎保管!) gpg --export-secret-keys --armor YOUR_KEY_ID > private-key-backup.asc # 导出公钥 gpg --export --armor YOUR_KEY_ID > public-key-backup.asc将
private-key-backup.asc文件离线存储在绝对安全的地方(如加密的U盘)。
3.3 安装与初始化 pass
接下来安装pass本身。
# 从 EPEL 仓库安装 pass sudo yum install -y pass # 初始化 pass 存储库,使用上一步生成的 GPG 密钥 ID pass init "YOUR_KEY_ID" # 例如:pass init "3AA5C34371567BD2"这个命令会在~/.password-store/目录下创建一个新的密码存储库,并使用你的公钥进行初始化。
你可以测试一下:
# 插入一个测试密码 pass insert Test/MyFirstPassword # 它会提示你输入两次密码内容,例如输入:`SuperSecret123!` # 查看密码列表 pass ls # 应该显示 `Test/MyFirstPassword` # 获取并显示密码(会要求输入 GPG 私钥的密码短语) pass Test/MyFirstPassword3.4 编译安装 docker-credential-pass
CentOS 的默认仓库里通常没有docker-credential-pass这个二进制文件,我们需要从源码编译。它的源码在 Docker 的docker-credential-helpers项目中。
# 1. 安装 Go 语言环境(编译所需) sudo yum install -y golang # 2. 下载 docker-credential-helpers 源码 git clone https://github.com/docker/docker-credential-helpers.git cd docker-credential-helpers # 3. 编译 pass 版本的 helper make pass # 或者指定编译:go build -o docker-credential-pass ./pass/cmd # 4. 将编译好的二进制文件移动到系统 PATH 目录 sudo cp ./bin/docker-credential-pass /usr/local/bin/ # 5. 验证是否安装成功 docker-credential-pass version # 应该输出类似 `docker-credential-pass (github.com/docker/docker-credential-helpers) v0.8.0` 的信息3.5 配置 Docker 使用 pass 作为凭证存储
现在,我们需要告诉 Docker 使用我们刚刚安装的docker-credential-pass。
编辑 Docker 客户端配置文件~/.docker/config.json。如果文件不存在,就创建它。
vim ~/.docker/config.json输入以下内容:
{ "credsStore": "pass" }保存并退出。
这个配置告诉 Docker 引擎,默认使用pass作为所有仓库的凭证存储后端。
重要提示:如果你之前已经用
docker login登录过,~/.docker/config.json里可能会有旧的、base64 编码的auths字段。务必先执行docker logout退出所有仓库,再添加"credsStore": "pass"配置,否则 Docker 可能仍会读取旧的不安全凭证。
4. 实战:使用 pass 管理 Docker 登录流程
配置完成后,我们来体验一下全新的、安全的登录流程。
4.1 登录 Docker Hub
现在,当你运行docker login时,Docker 会调用docker-credential-pass。
docker login你会看到和以前类似的交互提示,可能会是 Web 登录流程或用户名密码提示。成功登录后,神奇的事情发生了:你的凭证不会以明文形式出现在~/.docker/config.json里。取而代之的是,pass存储库中多了一个条目。
检查一下:
pass ls # 你应该会看到一个类似于 `docker-credential-helpers/docker-pass-initialized-check` 的条目(这是 helper 的初始化标记) # 以及一个真正的凭证条目,例如: # DockerHub/docker.io # 或者根据你登录的仓库地址 pass ls DockerHub/ # 查看具体内容 pass DockerHub/docker.io你会发现,pass里存储的是一个 JSON 结构,包含了服务器地址、用户名和密码(或令牌)。这一切都是被你的 GPG 公钥加密的。
4.2 登录私有镜像仓库
登录私有仓库的流程完全一样,只需指定仓库地址。
docker login registry.mycompany.com:5000输入用户名和密码后,凭证会被安全地存储。在pass中,路径可能会是Docker/registry.mycompany.com:5000。
4.3 自动化与非交互式登录
在 CI/CD 流水线中,我们通常需要非交互式登录。pass方案同样胜任。
方法一:使用--password-stdin(推荐)这是 Docker 官方推荐的方式,可以避免密码出现在命令行历史或进程列表中。
# 假设你的密码已经保存在一个环境变量或保密的 CI 变量中 echo "$MY_DOCKER_PASSWORD" | docker login -u myusername --password-stdinDocker 会从标准输入读取密码,并通过docker-credential-pass安全地存储起来。后续的docker pull或docker push操作,Docker 会自动从pass中获取解密后的密码。
方法二:预先使用pass insert插入密码你也可以提前将密码存入pass,这样docker login时甚至无需交互。
# 首先,手动或通过脚本将密码存入 pass(需要输入一次 GPG 密码短语) pass insert -m DockerHub/docker.io # 然后粘贴你的 JSON,格式为: # { # "ServerURL": "https://index.docker.io/v1", # "Username": "myusername", # "Secret": "my_password_or_token" # } # 按 Ctrl+D 结束输入。 # 之后,docker login 可能会更顺畅,或者在某些配置下直接可用。 # 更常见的做法是,配置好 credsStore 后,直接运行带 --password-stdin 的 login 命令,凭证会被自动存储。关键在于,无论哪种方式,密码的持久化存储环节都已经从明文的config.json转移到了 GPG 加密的pass存储中。
5. 高级配置、管理与故障排查
一套系统要稳健运行,离不开日常的管理和问题的排查。
5.1 配置多用户或团队协作
如果你的服务器需要多个用户使用 Docker,或者你需要与团队共享私有仓库的凭证,pass结合 Git 和 GPG 密钥子密钥(Subkey)是个好方案。
- 导出公钥:将你的 GPG 公钥分发给其他服务器或团队成员。
gpg --export --armor YOUR_KEY_ID > team-public-key.asc - 在其他机器上导入公钥并信任:
gpg --import team-public-key.asc gpg --edit-key YOUR_KEY_ID # 在 gpg> 提示符下,输入 `trust`,然后选择信任级别(例如 5 = 绝对信任),最后 `save`。 - 初始化共享的 pass 存储库:可以将
~/.password-store目录初始化为一个 Git 仓库,并推送到一个安全的内部 Git 服务器。其他成员克隆后,用你的公钥加密的密码文件,他们也能解密(如果你将他们的公钥也加入信任并加密)。# 在初始机器上 cd ~/.password-store git init git add . git commit -m "Initial password store" # 添加远程仓库并推送 git remote add origin your-git-repo-url git push -u origin main # 在其他机器上 git clone your-git-repo-url ~/.password-store pass init "YOUR_KEY_ID" # 使用相同的密钥ID初始化
5.2 日常维护命令
- 列出所有 Docker 相关凭证:
pass ls | grep -i docker - 查看某个凭证的详细信息:
pass DockerHub/docker.io - 更新一个密码:直接使用
pass insert覆盖原有路径,或使用pass edit(需要配置$EDITOR环境变量)。pass insert -m DockerHub/docker.io - 删除一个凭证:
pass rm DockerHub/docker.io # 同时,你可能需要手动清理 Docker 缓存中的旧认证信息 docker logout registry.hub.docker.com
5.3 常见问题与排查技巧实录
即使方案再完美,实践中也难免踩坑。下面是我遇到过的几个典型问题及解决方法。
问题一:执行docker login后,提示Error saving credentials: error storing credentials - ...: pass store is uninitialized
- 原因:
pass存储库没有正确初始化,或者 Docker 找不到docker-credential-pass。 - 排查:
- 确认
pass init YOUR_KEY_ID已成功执行。检查~/.password-store/.gpg-id文件是否存在且内容正确。 - 确认
docker-credential-pass已在PATH中。which docker-credential-pass应返回路径。 - 检查
~/.docker/config.json中"credsStore": "pass"的拼写是否正确。 - 尝试手动测试 helper:
echo "https://index.docker.io/v1" | docker-credential-pass get。如果提示未初始化,可能需要先执行一次pass insert任意内容来激活存储。
- 确认
问题二:在 CI 脚本中非交互式运行时,docker pull失败,提示unauthorized: authentication required
- 原因:CI 环境是全新的,没有 GPG 私钥,或者有私钥但无法非交互式输入密码短语(Passphrase)。
- 解决方案:
- 方案A(推荐,更安全):在 CI 环境中使用特定于仓库的访问令牌(Token)而非账户密码,并结合 Docker 的
--password-stdin登录。令牌可以设置有效期和权限,风险更低。登录后凭证会被缓存一段时间,足够完成本次构建。 - 方案B(适用于受控的私有环境):为 CI 创建一个无密码短语的 GPG 子密钥。
# 在主密钥上创建子密钥(在安全的环境下操作) gpg --edit-key YOUR_KEY_ID # 在 gpg> 提示符下: addkey # 选择密钥类型和大小(例如 RSA,仅用于加密) # 设置有效期(例如 1年) # **关键:当提示输入密码短语时,直接回车留空**。 save # 导出这个无密码的子密钥对 gpg --export-secret-subkeys --armor > ci-subkey-private.asc # 在 CI 服务器上导入这个子私钥 gpg --import ci-subkey-private.asc # 初始化 pass 时使用主密钥ID,但解密时会尝试所有可用的私钥,包括这个无密码的子密钥。 pass init "YOUR_KEY_ID"警告:无密码的私钥极其危险,一旦泄露,其加密的所有内容都将失守。务必仅在高度受控、 ephemeral(临时)的 CI 环境中使用,并严格限制该密钥的访问范围和有效期。
- 方案A(推荐,更安全):在 CI 环境中使用特定于仓库的访问令牌(Token)而非账户密码,并结合 Docker 的
问题三:pass命令执行缓慢
- 原因:GPG 需要生成随机数(熵),在虚拟机或云服务器上,熵可能不足。
- 解决:安装
haveged或rng-tools来增加熵。sudo yum install -y haveged sudo systemctl enable --now haveged
问题四:如何迁移现有的明文凭证到 pass?
如果你已经有很多仓库的凭证以 base64 形式存储在~/.docker/config.json的auths字段里,可以按以下步骤安全迁移:
- 备份旧配置:
cp ~/.docker/config.json ~/.docker/config.json.backup - 逐一退出并重新登录:
在重新登录的过程中,由于已经配置了# 查看当前已登录的仓库 cat ~/.docker/config.json | jq -r '.auths | keys[]' # 如果没有 jq,可以手动查看 # 对每个仓库执行 logout 再 login docker logout registry1.example.com docker login registry1.example.com"credsStore": "pass",新凭证会自动存入pass。 - 验证迁移后:确认新的
~/.docker/config.json中不再有auths字段,或者auths字段为空对象{},同时credsStore设置为pass。 - 删除备份:确认所有操作正常后,安全地删除备份文件
rm ~/.docker/config.json.backup。
通过以上步骤,我们不仅实现了一个安全的 Docker 凭证存储方案,更构建了一套基于开源工具、可审计、可扩展的密码管理基础设施。它让我们的 CentOS 服务器在容器化道路上,安全基础更加牢固。记住,安全往往不在于使用多么高深莫测的技术,而在于是否坚持执行那些已知的、有效的最佳实践。告别密码裸奔,从今天这次配置开始。