单台 VPS 自托管 secrets manager 怎么选?
比较 OpenBao、Infisical、SOPS 配合 age、systemd credentials 与 600 权限环境变量文件,说明单台 VPS 的维护成本、解封风险和凭据泄露边界。
自托管 secrets manager 能做什么,而密码管理器不能做什么
自托管 secrets manager 将凭据提供给进程。密码管理器将凭据提供给人。其他差异都源于这一点。密码管理器由在场且专注的人解锁。secrets manager 则必须在凌晨 03:00、无人醒着时,将数据库密码提供给应用程序。
两者的故障模式不同,而且这种差异很重要。密码管理器处于锁定状态只是不便:重新输入主密码即可。secrets manager 处于密封状态则会导致中断:所有在密封期间重启的服务都无法获取凭据,并会保持停止状态。将 Vaultwarden 作为您自己的密码管理器运行可以很好地解决人的问题。但它无法解决机器的问题,也从未为此设计。如果您同时运行这两类服务,应重点加固其管理员令牌和备份文件,而不是由客户端负责加密的 vault 内容;对 Vaultwarden 进行加固会同时涵盖这两项。
对于一台服务器,实际可行的选项分为两类。OpenBao 和 Infisical 是服务:包含 API、数据库、TLS(传输层安全)、登录步骤,以及一个现在必须持续运行的进程。SOPS 配合 age、systemd credentials 和 Docker secrets 则是文件:静态加密,由已在运行的组件解密,不需要额外监控其他组件。
先直接给出结论。对于只有一台服务器、由一两个人使用的环境,基于文件的选项通常更合适。一个没人能正确解封、也没人轮换凭据的 OpenBao,不如 mode 600 的环境变量文件,因为它增加了一个需要维护的环节,也增加了一个您很可能会错误处理的备份,而且不会带来任何您原本无法手动完成的轮换能力。
mode 600 的环境变量文件是否足够安全?
通常足够。它防范的是同一台主机上的其他用户读取数据库密码。Unix 文件权限可以做到这一点,而且在网络启动前就会生效。
sudo install -d -m 750 -o root -g myapp /etc/myapp
sudo install -m 640 -o root -g myapp /dev/null /etc/myapp/env
sudoedit /etc/myapp/env从两侧检查:
sudo -u myapp cat /etc/myapp/env
sudo -u nobody cat /etc/myapp/env第一条命令会输出文件内容。第二条命令输出 cat: /etc/myapp/env: Permission denied,因为 nobody 不属于 myapp 组,并且文件未授予任何其他用户权限。这就是完整的安全模型,而且确实有效。
泄露发生在后续步骤。包含 EnvironmentFile= 的 systemd 单元会将这些值复制到进程环境中,而进程环境是可读取的。
[Service]
User=myapp
EnvironmentFile=/etc/myapp/env
ExecStart=/usr/local/bin/myappsudo cat /proc/$(pgrep -n myapp)/environ | tr '\0' '\n'该命令会以明文输出您的密钥,因为 root 和运行该进程的用户都可以读取 /proc/<pid>/environ。将环境变量附加到报告中的崩溃报告工具也能看到相同内容。任何以同一账户运行的工具也一样。因此,让 AI 代理无法访问密钥首先要将密钥移出环境变量。将该文件与专用的低权限服务用户配合使用,确保“运行该进程的用户”不是 root。
使用 age 的 SOPS:将加密的密钥提交到 git
SOPS(secrets operations)会加密 YAML 或 JSON 文件中的值,并保留键名明文。age 是一种小型加密工具,只提供一个密钥对,不需要密钥服务器。两者结合后,您可以将 secrets.enc.yaml 与代码一起提交到 git;git diff 仍能显示哪个设置发生了变化,但不会泄露修改后的值。
Ubuntu 24.04 已提供 age 软件包。SOPS 不在其中,因此请从发行页面获取 .deb。截至 2026 年 8 月,当前版本为 3.13.3。
sudo apt update && sudo apt install -y age
curl -LO https://github.com/getsops/sops/releases/download/v3.13.3/sops_3.13.3_amd64.deb
sudo apt install -y ./sops_3.13.3_amd64.deb
sops --version生成密钥对。age-keygen 会将私钥写入文件,并输出公钥,因此您会看到以 Public key: age1... 开头的一行。
mkdir -p ~/.config/sops/age
age-keygen -o ~/.config/sops/age/keys.txt
chmod 600 ~/.config/sops/age/keys.txt
age-keygen -y ~/.config/sops/age/keys.txt将公钥写入仓库根目录中的 .sops.yaml,这样您就不必在命令行中记住接收者。
creation_rules:
- age: age1s3cqcks5genc6ru8chl0hkkd04zmxvczsvdxq99ekffe4gmvjpzsedk23csops encrypt secrets.yaml > secrets.enc.yaml
sops decrypt secrets.enc.yaml不包含 path_regex 的规则会匹配所有内容,这正是初始阶段所需的行为。如果之后添加规则,请确保它匹配传递给 sops 的文件,因为规则会根据输入路径进行匹配,而不是根据您重定向输出到的文件进行匹配。
在运行时,只将这些值传递给一个进程:
sops exec-env secrets.enc.yaml './myapp'sops exec-env 会在内存中解密,并将这些值设置到子进程的环境中,因此不会将明文写入磁盘。上一节提到的环境变量注意事项仍适用于该子进程。
这里有两个常见问题。systemd 下的错误 Failed to get the data key required to decrypt the SOPS file 几乎总是表示 SOPS 查找了错误的主目录,因为单元不会继承您的 HOME。请在单元中使用 Environment=SOPS_AGE_KEY_FILE=/etc/sops/age.txt 显式设置路径。另一方面,编辑 .sops.yaml 不会重新加密已有文件:添加同事的公钥只会影响新文件,因此请对每个已有文件运行 sops updatekeys secrets.enc.yaml。如果您的配置已经通过 Ansible 管理,可以使用 Ansible Vault 加密相同的值,达到相同目的而无需引入第二个工具。
systemd 凭据:机密不会进入环境变量
Ubuntu 24.04 附带 systemd 255,因此无需安装。systemd-creds 会在主机上加密机密,systemd 再将其解密到只有该服务可以读取的私有目录中。
sudo systemd-creds setup
sudo install -d -m 700 /etc/myapp
echo -n 'hunter2' | sudo systemd-creds encrypt --name=db_password - /etc/myapp/db_password.cred[Service]
User=myapp
LoadCredentialEncrypted=db_password:/etc/myapp/db_password.cred
ExecStart=/usr/local/bin/myapp服务会从由 $CREDENTIALS_DIRECTORY 指定的目录中的 db_password 文件读取该值。该值不在环境变量中,因此 /proc/<pid>/environ 不会显示有用内容,明文也不会写入 root 文件系统。
在将单元指向该文件之前,先验证文件可以解密:
sudo systemd-creds decrypt /etc/myapp/db_password.cred -请确认使用了哪个密钥加密,因为这决定了备份是否有用。默认的 --with-key=auto 会在存在且可用时使用 TPM2(可信平台模块版本 2)芯片,否则使用主机密钥。大多数 VPS 实例没有 TPM2。
systemd-analyze has-tpm2no 表示使用了主机密钥,该密钥位于 /var/lib/systemd/credential.secret 中,只有 root 可读取。在没有该文件的全新 VPS 上恢复 db_password.cred 后,将永远无法解密它。请将 credential.secret 一并复制到同一份备份中,或将明文保存在仍可访问的位置。
Docker secrets:/run/secrets 下的文件
Compose 从主机读取文件,并将其挂载到容器中的 /run/secrets/<name>。
services:
app:
image: myapp:latest
environment:
DB_PASSWORD_FILE: /run/secrets/db_password
secrets:
- db_password
secrets:
db_password:
file: ./db_password.txtdocker compose exec app cat /run/secrets/db_password
docker compose exec app env | grep -i password第一个命令会输出 secret。第二个命令只输出 DB_PASSWORD_FILE=/run/secrets/db_password,这正是关键:该值从未写入容器环境,因此不会出现在 docker inspect 的输出中。许多官方镜像已经按这种方式读取 secret,Postgres 镜像也正是通过 POSTGRES_PASSWORD_FILE 读取。
请明确这一机制的边界。在 Swarm mode 之外,任何层都不会加密:./db_password.txt 是主机上的明文文件,唯一的保护措施是文件权限和所有者。请自行设置这两项,因为 Compose 会直接挂载对所有用户可读的文件,且不会报告错误。与直接使用 env_file 这一简便方式相比,完整的权衡说明请参阅Compose 环境文件和 secrets 指南。
运行 OpenBao 和 Vault 的实际成本
OpenBao 是 Linux Foundation 基于 HashiCorp Vault 创建的分支项目。HashiCorp 在 2023 年根据 Business Source License 重新授权 Vault 后,OpenBao 随之启动。OpenBao 继续采用 MPL 2.0(Mozilla Public License)许可。截至 2026 年 8 月,当前版本为 2.6.2。由于该分支保留了相同的命令接口,下面几乎所有内容同样适用于 Vault。
docker pull docker.io/openbao/openbao如果希望使用 apt 管理升级,可以从 OpenBao 下载页面获取 Debian 和 Ubuntu 软件包。服务器需要一个配置文件,其中包含 listener 和 storage backend:
listener "tcp" {
address = "127.0.0.1:8200"
tls_cert_file = "/path/to/full-chain.pem"
tls_key_file = "/path/to/private-key.pem"
}
storage "raft" {
path = "/path/to/raft/data"
node_id = "raft_node_1"
}然后启动一次:
bao operator init默认情况下,系统会将 root key 拆分为 5 个 share,并要求其中 3 个 share 才能解封。这些参数就是 -key-shares 和 -key-threshold。系统只会显示这些 share 和初始 root token 一次,之后不会再次显示。
现在说明大多数对比都会忽略的部分。重启后的服务器处于 sealed 状态。 OpenBao 只将 root key 保存在内存中。因此,重启后必须有人提供达到阈值数量的 share,OpenBao 才能解密自己的存储数据。内核更新或因内存不足被终止,都会导致服务器处于 sealed 状态,应用无法登录。
在单人 VPS 上,Shamir 拆分并不能提供实际保护,因为 5 个 share 最终都存放在同一个人的同一个密码管理器中。Auto unseal 会将密钥交给受信任的设备或服务。在大型云环境中,这通常意味着使用托管密钥服务;而在 VPS 上,通常意味着将 key file 放在与受保护数据相同的磁盘上。这会真实降低安全性,但可以换来服务器重启后自动恢复运行。请明确做出选择,并记录采用了哪种方式。
Infisical:一个 UI、一个数据库,以及必须由您保管的主密钥
Infisical 是一个机密管理平台,提供 Web 界面、项目、环境和按用户划分的访问控制。使用 Compose 自托管的步骤很少:
curl -o docker-compose.prod.yml https://raw.githubusercontent.com/Infisical/infisical/main/docker-compose.prod.yml
curl -o .env https://raw.githubusercontent.com/Infisical/infisical/main/.env.example
docker compose -f docker-compose.prod.yml up -d在执行最后一条命令前编辑 .env。其中两个值必须由您设置,其中一个值之后绝不能更改:
openssl rand -hex 16
openssl rand -base64 32第一个值是 ENCRYPTION_KEY,即一个 16 字节的十六进制字符串。PostgreSQL 使用它加密您的机密。丢失该值后,即使数据库备份完整,也只能得到一堆密文。在运行中的实例上更改该值,会导致现有机密无法解密。第二个值是 AUTH_SECRET,即用于会话的 32 字节 Base64 字符串。SITE_URL 必须是您实际访问的绝对 URL,并且必须包含协议,否则登录重定向会失败。
如果您实际需要的是面向用户的功能,例如为小型团队提供 Web 界面并隔离不同环境,那么 Infisical 比 OpenBao 更合适。OpenBao 更适合管理自行过期的数据库凭据。使用 Infisical 需要维护 PostgreSQL、Redis 和 TLS 证书,并负责为它们打补丁和创建备份。
密钥服务停机且应用重启时会发生什么
这个问题决定了密钥服务是否适合部署在单台服务器上。网络启动前,文件仍可读取。服务则不行。
重启服务器后,应用和 OpenBao 同时启动。应用请求数据库密码时,OpenBao 仍处于密封状态,请求失败。随后,systemd 不断重启应用,直到有人手动输入解封密钥分片。没有任何组件损坏。但也没有任何组件真正运行起来。
有两种合理的处理方式。为单元设置启动顺序,并让应用重试:After= 密钥服务,再设置 Restart=on-failure 和足够长的 RestartSec=,避免持续高频请求 API。或者在部署时获取密钥,而不是在启动时获取:将密钥写入权限模式为 600 的文件或 systemd 凭据,使运行中的系统依赖文件,而不是依赖 API。
令牌过期是在更长时间尺度上的同类问题。OpenBao 令牌和租约都有生存时间,因此长期运行且从不续期的进程会在与部署无关的时间失去访问权限。这种故障尤其难以排查,因为当天并没有任何配置变更。
备份存储本身
这里的每种方案都有一个密钥,没有该密钥的备份毫无价值。请记录密钥的存放位置。
对于 env 文件,文件本身就是机密,因此备份必须加密。使用 SOPS 时,加密文件可以存放在任何公开位置;位于 ~/.config/sops/age/keys.txt 的 age 私钥才是绝对不能丢失的内容。对于 systemd 凭据,请将 /var/lib/systemd/credential.secret 与 .cred 文件一并备份。对于 Infisical,请创建 PostgreSQL 转储,并将 ENCRYPTION_KEY 存放在与转储分开的地方。
使用 raft 存储的 OpenBao 需要创建自身的快照:
bao operator raft snapshot save backup.snap
bao operator raft snapshot restore backup.snap快照包含加密存储,因此将其恢复到新服务器后,仍需要来自 bao operator init 的解封密钥份额。如果每晚将快照复制到对象存储,却没有存放这些密钥份额,那么备份的内容等于没有任何备份。在依赖备份之前,请先在临时 VPS 上测试恢复。
审计日志:谁读取了哪个机密
文件无法提供审计轨迹。权限模式和所有者只能说明谁可能读取机密,不能说明谁实际读取了它。对路径使用 auditd 监控是最接近的替代方案,但它只能报告文件被打开,不能报告使用了哪个值。
OpenBao 会将每个请求记录到显式启用的审计设备:
bao audit enable file file_path=/var/log/openbao_audit.log该日志有两个特性会影响服务器的运行方式。请求和响应中的大多数字符串都会使用 HMAC-SHA256 和盐进行哈希处理。因此,您可以将已知值与日志进行匹配,而日志本身不会包含明文。整数和布尔值会以明文写入,因此数字机密不会受到这种哈希处理的保护。
还要注意一个运行陷阱:如果没有任何已启用的审计设备能够记录请求,OpenBao 将不会响应请求。如果设备以阻塞方式失败,请求会一直挂起,直到有人修复设备。/var/log 上的磁盘已满,会按设计导致机密 API 停止工作。请在第一天就为审计日志分配独立空间并配置 logrotate 规则,不要等到第一次中断后再处理。
应运行哪种自托管密钥管理器?
先统计机器数量和人员数量,再做选择。
- 一台机器、一个人:使用由 root 拥有、服务用户可读取的
mode 600环境文件。如果不希望密钥值出现在进程环境中,可添加 systemd 凭据。 - 一台机器、两到五个人,且配置已存储在 git 中:使用 SOPS 和 age。每个人获得一对密钥,
.sops.yaml列出所有获准解密的公钥。 - 多台机器、一个配置仓库,且不需要会过期的凭据:仍使用 SOPS 和 age。为每台主机配置一个接收方密钥,这样即使某台主机的密钥被窃取,也只能解密该主机的文件。
- 多台机器、多个团队确实需要有生命周期的数据库凭据,并且有人会查看审计记录:使用 OpenBao,并在预算中每月安排一小时运维时间,用于解封和恢复演练。
这4种情况遵循同一条规则。运行能够满足明确需求的最小方案,因为已停止运行的密钥管理器与其中没有任何密钥的密钥管理器无法区分。
FAQ
单个 VPS 使用自托管 secrets manager 是否值得?
如果指的是 OpenBao 或 Infisical 这类服务,通常不值得。在只有一台服务器、只有一两名用户的情况下,权限为 600 的环境变量文件或 systemd 加密凭据可以提供同等的本地用户隔离保护,而且无需执行解封步骤,也无需维护额外服务。拥有多台服务器和多名用户后,或者确实需要凭据自动过期、无需人工轮换时,secrets manager 才开始体现价值。
password manager 和 secrets manager 有什么区别?
password manager 存储用户手动输入的凭据,并由用户在场时解锁。secrets manager 将凭据提供给进程,因此必须在 03:00、无人值守时也能工作。两者的区别就在于此:锁定的 password manager 会要求您重新输入主密码,而已密封的 secrets manager 会阻止所有在密封期间重启的服务启动。
OpenBao 在重启后处于密封状态时,我的应用会怎样?
应用无法获取 secrets,因此会启动失败;systemd 会不断重启这些应用,直到有人提供解封所需的阈值。默认阈值为 5 个共享密钥中的 3 个。OpenBao 只将根密钥保存在内存中,因此每次重启后都会再次密封。您可以启用自动解封,但在单个 VPS 上,这意味着解封密钥最终会与数据存储在同一块磁盘上;也可以在部署时将 secrets 写入文件,使系统启动不依赖 API。
我可以将 SOPS 加密文件提交到公开仓库吗?
文件中的值已加密,因此没有 age 私钥的用户无法获取这些值。密钥本身未加密:读取者可以看到您持有 STRIPE_SECRET_KEY 和 SMTP_PASSWORD,以及它们各自的变更频率。对大多数项目而言,这些元数据可以接受,但少数项目可能无法接受。请勿将 age 私钥存入仓库;每次添加或移除接收者时,都应对所有现有文件运行 sops updatekeys。