概述
在自动化运维中,Ansible Playbook 经常需要处理数据库密码、API 密钥、SSH 私钥等敏感信息。如果这些数据以明文形式存储在代码仓库中,一旦仓库泄露,所有凭据都将暴露。Ansible Vault 作为 Ansible 内置的加密工具,通过 AES-256 对称加密算法保护敏感数据,确保只有授权用户才能访问。
我将从基础概念到生产实践,详细梳理 Ansible Vault 的使用方法、密码管理策略,以及在 CI/CD 流水线中的集成方案。
为什么需要 Ansible Vault
明文存储的风险
在实际运维中,敏感信息散落在多个位置:
| 位置 | 常见敏感数据 | 风险等级 |
|---|---|---|
group_vars/all.yml | 数据库密码、Redis 密码 | 高 |
host_vars/web01.yml | SSH 连接密码、BECOME 密码 | 高 |
| Inventory 文件 | ansible_password、ansible_ssh_pass | 高 |
| Playbook 变量 | API Token、第三方密钥 | 中 |
| Jinja2 模板 | 证书私钥、JWT Secret | 高 |
明文存储的风险包括:
- 代码仓库泄露:Git 历史记录中永远保留明文密码,即使后续删除也能从历史中恢复
- 合规审计失败:PCI-DSS、ISO 27001 等安全标准要求敏感数据加密存储
- 团队协作风险:任何有仓库访问权限的人都能看到所有密码
- 日志泄露:Ansible 执行日志可能输出变量值,导致密码出现在日志文件中
Ansible Vault 的定位
Ansible Vault 不是唯一的密钥管理方案,但它是 Ansible 生态中最直接的选择:
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| Ansible Vault | 内置、零依赖、YAML 原生 | 单密码加密、无细粒度权限 | 中小规模团队、快速上手 |
| HashiCorp Vault | 动态密钥、租约管理、审计日志 | 需要额外部署和维护 | 大型企业、高安全要求 |
| AWS Secrets Manager | 云原生、自动轮转 | 厂商锁定、按量计费 | AWS 云环境 |
| SOPS + age | 支持多密钥加密、Git 友好 | 需要额外工具 | 多人协作、GitOps 场景 |
参考 Ansible Vault 官方文档 了解完整功能列表。
核心概念与加密机制
加密原理
Ansible Vault 使用 AES-256 对称加密算法(在早期版本中可能是 AES-128)。加密流程:
明文 YAML → AES-256 加密(Vault 密码作为密钥)→ 密文 YAML($ANSIBLE_VAULT 头部标识)
加密后的文件以 $ANSIBLE_VAULT;1.1;AES256 开头,后面跟着 base64 编码的密文。例如:
$ANSIBLE_VAULT;1.1;AES256
66386439653236336462626566653033393664633164636136383765393730353066386230336230
363366323737366336663737333635303462653066333637356436653066333763350a6366373134
37633733336236393030303237373332643761306635316631393963363033663638366363313061
...
可加密的文件类型
Ansible Vault 可以加密任何 Ansible 使用的数据文件:
| 文件类型 | 示例 | 是否可加密 |
|---|---|---|
| 变量文件 | group_vars/all.yml | 是 |
| Inventory 文件 | inventory.ini | 是 |
| Playbook 文件 | site.yml | 是(但不推荐) |
| Jinja2 模板 | nginx.conf.j2 | 是 |
| 独立密钥文件 | db_creds.yml | 是 |
| 证书/私钥文件 | server.key | 是 |
ansible.cfg | ansible.cfg | 否 |
注意:
ansible.cfg不能被加密,因为 Ansible 启动时必须读取该文件。如果配置中有敏感信息,应将其移到加密的变量文件中,通过ansible.cfg引用。
基础操作
创建加密文件
# 交互式创建(会提示输入密码)
ansible-vault create secrets.yml
# 使用密码文件创建(推荐用于自动化)
ansible-vault create --vault-password-file=~/.vault_pass secrets.yml
创建后,文件内容示例:
# secrets.yml (加密后)
db_password: "prod-db-2026!"
admin_api_key: "sk_live_abc123def456"
ssh_private_key: |
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEA...
-----END RSA PRIVATE KEY-----
加密已有文件
# 加密单个文件
ansible-vault encrypt existing_vars.yml
# 加密多个文件
ansible-vault encrypt group_vars/all.yml host_vars/web01.yml
# 指定输出文件(不覆盖原文件)
ansible-vault encrypt --output=encrypted.yml plaintext.yml
查看、编辑和重新加密
# 查看加密文件内容(不会在磁盘留下明文)
ansible-vault view secrets.yml
# 编辑加密文件(自动解密 → 编辑 → 保存时重新加密)
ansible-vault edit secrets.yml
# 修改加密密码(rekey)
ansible-vault rekey secrets.yml
# 永久解密文件(移除加密,慎用!)
ansible-vault decrypt secrets.yml
常用命令速查
| 命令 | 功能 | 安全等级 |
|---|---|---|
ansible-vault create | 创建并加密新文件 | 安全 |
ansible-vault encrypt | 加密已有文件 | 安全 |
ansible-vault edit | 编辑加密文件 | 安全(临时解密在内存) |
ansible-vault view | 查看加密内容 | 安全(不分页到磁盘) |
ansible-vault rekey | 修改加密密码 | 安全 |
ansible-vault decrypt | 永久解密 | 危险(明文落盘) |
密码管理策略
密码文件方式
创建一个只包含密码字符串的文件:
# 创建密码文件
echo "MyVaultPassword2026!" > ~/.vault_pass
# 设置严格权限(必须!)
chmod 600 ~/.vault_pass
在 ansible.cfg 中配置默认密码文件路径:
[defaults]
vault_password_file = ~/.vault_pass
之后执行 Ansible 命令时不需要手动指定密码:
# 自动使用 ansible.cfg 中配置的密码文件
ansible-playbook site.yml
多密码(Vault ID)方式
在生产环境中,不同环境(开发、测试、生产)应使用不同的 Vault 密码。Ansible 2.4+ 引入了 Vault ID 概念:
# 创建带 Vault ID 的加密文件
ansible-vault create --vault-id dev@~/.vault_dev_pass dev_secrets.yml
ansible-vault create --vault-id prod@~/.vault_prod_pass prod_secrets.yml
执行 Playbook 时指定多个 Vault ID:
ansible-playbook site.yml \
--vault-id dev@~/.vault_dev_pass \
--vault-id prod@~/.vault_prod_pass
Ansible 会根据文件加密时使用的 Vault ID 自动选择正确的密码解密。
在 ansible.cfg 中配置多个 Vault ID:
[defaults]
vault_identity_list = dev@~/.vault_dev_pass, prod@~/.vault_prod_pass, staging@~/.vault_staging_pass
Vault ID 命名规范
建议使用环境名作为 Vault ID 标识符:
| Vault ID | 用途 | 密码文件 |
|---|---|---|
dev | 开发环境 | ~/.vault_dev_pass |
staging | 预发环境 | ~/.vault_staging_pass |
prod | 生产环境 | ~/.vault_prod_pass |
db | 数据库专用密码 | ~/.vault_db_pass |
network | 网络设备密码 | ~/.vault_network_pass |
好的实践:生产环境密码文件应存储在受限位置(如密码管理器、HSM),而不是用户 home 目录。
在 Playbook 中使用加密变量
方式一:vars_files 引用
# site.yml
---
- name: Deploy Application
hosts: prod_servers
vars_files:
- secrets.yml # 自动解密并加载变量
tasks:
- name: Configure database connection
template:
src: db.conf.j2
dest: /etc/app/db.conf
# {{ db_password }} 在运行时自动解密并填充
方式二:include_vars 动态加载
---
- name: Load secrets based on environment
hosts: all
tasks:
- name: Include encrypted vars for current environment
include_vars:
file: "{{ env }}_secrets.yml"
no_log: true # 防止变量内容出现在日志中
方式三:命令行传入
# 使用 --ask-vault-pass 交互式输入
ansible-playbook site.yml --ask-vault-pass
# 使用 --vault-password-file 指定密码文件
ansible-playbook site.yml --vault-password-file=~/.vault_pass
# 使用 --vault-id 指定带标识的密码
ansible-playbook site.yml --vault-id prod@~/.vault_prod_pass
防止日志泄露
即使使用了 Vault 加密,Ansible 在执行过程中也可能将变量值输出到日志。使用 no_log 属性防止泄露:
---
- name: Create database user
community.mysql.mysql_user:
name: "{{ db_user }}"
password: "{{ db_password }}"
priv: "*.*:ALL"
state: present
no_log: true # 关键!防止密码出现在日志中
也可以在全局级别设置 no_log:
---
- name: Sensitive operations
hosts: all
no_log: true # 该 Play 中所有 task 都不记录日志
tasks:
- name: Deploy with secrets
...
项目结构设计
推荐目录结构
project/
├── ansible.cfg
├── inventory/
│ ├── production/
│ │ ├── hosts.ini
│ │ ├── group_vars/
│ │ │ ├── all/
│ │ │ │ ├── common.yml # 公共变量(明文)
│ │ │ │ └── vault.yml # 加密变量
│ │ │ └── webservers/
│ │ │ ├── vars.yml # 明文变量
│ │ │ └── vault.yml # 加密变量
│ │ └── host_vars/
│ │ └── web01/
│ │ ├── vars.yml
│ │ └── vault.yml
│ └── staging/
│ ├── hosts.ini
│ └── group_vars/
│ └── all/
│ └── vault.yml
├── playbooks/
│ ├── site.yml
│ ├── deploy.yml
│ └── db_setup.yml
├── roles/
│ ├── nginx/
│ ├── mysql/
│ └── app/
├── files/
│ └── ssl/
│ └── server.key # 加密的 SSL 私钥
└── templates/
└── db.conf.j2
明密分离原则
将明文变量和加密变量放在不同文件中,便于管理和审计:
# group_vars/all/common.yml (明文,可提交到 Git)
---
app_name: "myapp"
app_port: 8080
db_host: "db.internal"
db_port: 3306
# group_vars/all/vault.yml (加密,可提交到 Git 但内容不可读)
$ANSIBLE_VAULT;1.1;AES256
6638643965323633646262656665303339366463316463613638376539373035...
# ansible.cfg
[defaults]
inventory = inventory/
vault_identity_list = dev@~/.vault_dev_pass, staging@~/.vault_staging_pass, prod@~/.vault_prod_pass
host_key_checking = False
CI/CD 集成
GitLab CI 集成
在 CI/CD 环境中,Vault 密码不能交互式输入。推荐通过 CI 变量注入:
# .gitlab-ci.yml
stages:
- deploy
deploy_production:
stage: deploy
image: ansible/ansible-runner:latest
variables:
# Vault 密码通过 CI 变量注入(在 GitLab UI 中设置为 Masked 变量)
VAULT_PASS_PROD: $VAULT_PASS_PROD
before_script:
# 将 CI 变量写入临时密码文件
- echo "$VAULT_PASS_PROD" > /tmp/vault_pass
- chmod 600 /tmp/vault_pass
script:
- ansible-playbook -i inventory/production playbooks/deploy.yml
--vault-id prod@/tmp/vault_pass
after_script:
# 清理密码文件
- rm -f /tmp/vault_pass
only:
- main
GitHub Actions 集成
# .github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Ansible
run: pip install ansible-core
- name: Create vault password file
run: |
echo "${{ secrets.VAULT_PASS_PROD }}" > ~/.vault_pass
chmod 600 ~/.vault_pass
- name: Run playbook
run: |
ansible-playbook -i inventory/production playbooks/deploy.yml \
--vault-id prod@~/.vault_pass
- name: Cleanup
if: always()
run: rm -f ~/.vault_pass
Jenkins Pipeline 集成
pipeline {
agent any
environment {
VAULT_PASS = credentials('ansible-vault-prod')
}
stages {
stage('Deploy') {
steps {
sh '''
echo "${VAULT_PASS}" > /tmp/vault_pass
chmod 600 /tmp/vault_pass
ansible-playbook -i inventory/production playbooks/deploy.yml \
--vault-id prod@/tmp/vault_pass
rm -f /tmp/vault_pass
'''
}
}
}
}
安全提示:CI/CD 中的密码文件应在
after_script或post步骤中清理,确保即使任务失败也不会残留。
密码轮转
定期轮转策略
安全合规通常要求定期更换密码。Ansible Vault 的 rekey 命令支持密码轮转:
# 交互式轮转(输入旧密码 → 设置新密码)
ansible-vault rekey secrets.yml
# 使用密码文件轮转
ansible-vault rekey \
--vault-password-file=~/.vault_old_pass \
--new-vault-password-file=~/.vault_new_pass \
secrets.yml
# 使用 Vault ID 轮转
ansible-vault rekey \
--vault-id prod@~/.vault_prod_old \
--new-vault-id prod@~/.vault_prod_new \
prod_secrets.yml
批量轮转脚本
#!/bin/bash
# rotate-vault-passwords.sh
# 批量轮转所有加密文件的 Vault 密码
set -euo pipefail
OLD_PASS_FILE="$1"
NEW_PASS_FILE="$2"
VAULT_DIR="${3:-group_vars}"
if [ ! -f "$OLD_PASS_FILE" ] || [ ! -f "$NEW_PASS_FILE" ]; then
echo "Usage: $0 <old_pass_file> <new_pass_file> [vault_dir]"
exit 1
fi
# 查找所有加密文件
find "$VAULT_DIR" -type f -name "*.yml" -exec grep -l '^\$ANSIBLE_VAULT' {} \; | while read -r file; do
echo "Rekeying: $file"
ansible-vault rekey \
--vault-password-file="$OLD_PASS_FILE" \
--new-vault-password-file="$NEW_PASS_FILE" \
"$file"
done
echo "All vault files rekeyed successfully."
轮转自动化
结合 cron 或 CI/CD 实现定期自动轮转:
# /etc/cron.d/vault-rotate
# 每季度第一天凌晨2点执行密码轮转
0 2 1 */3 * /opt/scripts/rotate-vault-passwords.sh \
/etc/ansible/.vault_pass \
/etc/ansible/.vault_pass_new \
/etc/ansible/group_vars \
&& mv /etc/ansible/.vault_pass_new /etc/ansible/.vault_pass
安全审计
加密文件扫描
定期扫描仓库中的明文敏感数据:
#!/bin/bash
# scan-plaintext-secrets.sh
# 扫描可能包含明文密码的文件
PATTERNS=(
"password.*=.*['\"].\{8,\}['\"]"
"secret.*=.*['\"].\{8,\}['\"]"
"api_key.*=.*['\"].\{20,\}['\"]"
"token.*=.*['\"].\{20,\}['\"]"
"private_key"
)
SCAN_DIR="${1:-.}"
ISSUES=0
for pattern in "${PATTERNS[@]}"; do
while IFS= read -r file; do
# 跳过已加密的文件
if head -1 "$file" | grep -q '^\$ANSIBLE_VAULT'; then
continue
fi
# 跳过 .git 目录
if echo "$file" | grep -q '\.git/'; then
continue
fi
echo "[WARNING] Potential plaintext secret in: $file"
grep -n "$pattern" "$file" | head -5
ISSUES=$((ISSUES + 1))
done < <(grep -rl "$pattern" "$SCAN_DIR" 2>/dev/null)
done
if [ "$ISSUES" -gt 0 ]; then
echo ""
echo "Found $ISSUES files with potential plaintext secrets."
echo "Consider encrypting them with: ansible-vault encrypt <file>"
exit 1
else
echo "No plaintext secrets found."
exit 0
fi
审计日志
在团队协作中,记录谁在何时访问了加密数据:
# 记录 vault 操作的审计日志
vault_audit() {
local action="$1"
local file="$2"
local user="$(whoami)"
local timestamp="$(date '+%Y-%m-%d %H:%M:%S')"
local log_file="/var/log/ansible-vault-audit.log"
echo "[$timestamp] user=$user action=$action file=$file" >> "$log_file"
}
# 用法
vault_audit "view" "group_vars/all/vault.yml"
ansible-vault view group_vars/all/vault.yml
高级技巧
加密部分变量
有时只需要加密少量敏感字段,而非整个文件。可以使用 !vault 标签加密单个变量:
# group_vars/all.yml
---
db_host: "db.internal"
db_port: 3306
db_password: !vault |
$ANSIBLE_VAULT;1.1;AES256
66386439653236336462626566653033393664633164636136383765393730353066386230336230
363366323737366336663737333635303462653066333637356436653066333763350a6366373134
...
api_key: !vault |
$ANSIBLE_VAULT;1.1;AES256
37323438366638343536373633336630333134356238393630353436393866636330303037343030
...
# 非敏感变量保持明文
app_name: "myapp"
app_port: 8080
生成加密变量值:
# 使用 encrypt_string 命令
ansible-vault encrypt_string --vault-id prod@~/.vault_prod_pass 'my_secret_password'
输出可以直接粘贴到 YAML 文件中:
# 指定变量名
ansible-vault encrypt_string --vault-id prod@~/.vault_prod_pass \
--name 'db_password' \
'prod-db-2026!'
与 HashiCorp Vault 集成
对于需要更高安全级别的场景,可以结合 HashiCorp Vault 使用:
# playbook.yml
---
- name: Get secrets from HashiCorp Vault
hosts: localhost
vars:
vault_url: "http://vault:8200"
tasks:
- name: Read database credentials from Vault
hashivault_read:
url: "{{ vault_url }}"
token: "{{ lookup('env', 'VAULT_TOKEN') }}"
secret: "secret/data/database"
key: "password"
register: db_secret
no_log: true
- name: Use the secret
debug:
msg: "DB password retrieved from Vault (masked)"
no_log: true
加密整个目录
#!/bin/bash
# encrypt-directory.sh
# 加密目录下所有 YAML 文件
DIR="${1:-group_vars}"
find "$DIR" -type f \( -name "*.yml" -o -name "*.yaml" \) | while read -r file; do
# 跳过已加密的文件
if head -1 "$file" | grep -q '^\$ANSIBLE_VAULT'; then
echo "[SKIP] Already encrypted: $file"
continue
fi
echo "[ENCRYPT] $file"
ansible-vault encrypt --vault-id prod@~/.vault_prod_pass "$file"
done
常见问题与排错
忘记 Vault 密码
Ansible Vault 使用对称加密,没有后门。 如果忘记密码,数据将无法恢复。
预防措施:
- 将密码存储在企业密码管理器中(如 1Password、Bitwarden)
- 多人持有密码副本
- 定期备份加密文件
解密失败
# 错误信息示例
ERROR! Attempting to decrypt but no valid secrets found
# 排查步骤
# 1. 检查密码文件是否存在且可读
ls -la ~/.vault_pass
# 2. 检查密码文件内容(确保没有多余的换行或空格)
cat -A ~/.vault_pass
# 3. 检查 ansible.cfg 中的 vault_password_file 路径
grep vault ansible.cfg
# 4. 检查 Vault ID 是否匹配
ansible-vault view --vault-id prod@~/.vault_prod_pass secrets.yml
Git 中的加密文件差异
加密文件每次修改后,即使只改了一个字符,整个文件的密文都会变化。这导致 Git diff 无法显示实际变更。
解决方案——使用 ansible-vault decrypt 临时解密后查看 diff:
# 在 .gitattributes 中配置
echo "*.yml diff=ansible_vault" >> .gitattributes
# 在 .git/config 中配置 diff 工具
git config diff.ansible_vault.textconv "ansible-vault view"
配置后,git diff 会自动解密并显示明文差异。
常见错误速查
| 错误信息 | 原因 | 解决方案 |
|---|---|---|
Attempting to decrypt but no valid secrets found | 未提供正确的 Vault 密码 | 检查密码文件或 --vault-id 参数 |
Vault format unhandled | 文件格式不兼容 | 检查 Ansible 版本,升级到最新版 |
Decryption failed | 密码错误或文件损坏 | 尝试使用正确密码,或从备份恢复 |
value must be valid YAML | 加密文件内容损坏 | 从 Git 历史恢复,或重新创建 |
ansible.cfg not found | 配置文件被意外加密 | ansible.cfg 不能加密,从备份恢复 |
总结
Ansible Vault 为 Ansible 自动化运维提供了轻量级的敏感数据保护方案。在使用过程中,需要关注以下几点:
- 明密分离:将敏感变量和普通变量放在不同文件中,只加密敏感文件,便于管理和审计
- 多密码管理:使用 Vault ID 为不同环境设置不同密码,降低单点泄露风险
- CI/CD 安全:在流水线中通过 Secret 变量注入密码,使用后立即清理临时文件
- 定期轮转:配合 cron 或 CI/CD 实现密码定期轮转,满足安全合规要求
- 日志防护:在处理敏感变量的 task 上设置
no_log: true,防止密码泄露到日志 - 备份与恢复:加密文件同样需要备份,密码也需要安全存储在企业密码管理器中
Ansible Vault 的设计哲学是"够用就好"——它不追求 HashiCorp Vault 那样的企业级密钥管理能力,而是以最小的学习成本和零额外依赖,解决 Ansible 场景中最常见的敏感数据保护问题。对于中小团队和快速迭代的项目,它是性价比最高的选择。
参考资源
- Ansible Vault 官方文档
- Ansible Vault 命令参考
- Ansible 安全好的实践
- HashiCorp Vault 与 Ansible 集成
- Google SRE Book — Security
参考资料与致谢
本文在撰写过程中参考了以下资料,感谢原作者的贡献:
- Ansible Vault 官方文档 — Ansible 社区,参考了Ansible Vault 官方文档相关内容
- Ansible Vault 命令参考 — Ansible 社区,参考了Ansible Vault 命令参考相关内容
- Ansible 安全好的实践 — Ansible 社区,参考了Ansible 安全好的实践相关内容
- HashiCorp Vault 与 Ansible 集成 — HashiCorp,参考了HashiCorp Vault 与 Ansible 集成相关内容
- Google SRE Book — Security — Google SRE 团队,参考了Google SRE Book — Security相关内容