使用 Hashicorp Vault 保存 Ansible 的敏感信息

对于敏感信息 (密码/私钥之类), Ansible 提供了 ansible-vault 工具将其加密保存. 运行 playbook 时, 通过输入密码或者读取密码文件来解密, 算是一个简单有效的方案. Hashicorp Vault 则是一个有名的私密信息管理工具. 如果公司已经开始使用 vault, 当然希望能够集中管理各种私密信息, 而不是各个工具各自为战. 这里简单介绍如何使用 vault 的 KV 密文引擎保存敏感信息, 供 ansible-playbook 使用.

准备

通过 ansible 提供的 lookup 类型插件 hashi_vault 可以读取保存在 vault 的 KV 引擎中的数据

1
ansible-doc -t lookup hashi_vault
通过 pip install hvac 来安装 hashi_vault 插件.

本文不会介绍如何启用/配置 vault 密文引擎, 如何配置认证引擎给用户分发 token, 如何为用户设置策略进行权限管理等内容. 假定 vault 在 secret 路径下启用了 KV ver 2 的密文引擎, 用户可以正确获取 token 来访问需要的数据.

使用 KV ver2 引擎的好处是支持多版本, 方便追溯.

示例: rotate password

产生随机新密码保存到 vault 的 secret/dc1/local-password/root 路径下

1
vault kv put secret/dc1/local-password/root value=$(</dev/urandom tr -dc 'A-Za-z0-9+$%#<=>' | head -c12)
这样你的命令行历史记录里面不会留下新密码的内容. 可以通过下面命令查看新密码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
vault kv get secret/dc1/local-password/root

====== Metadata ======
Key Value
--- -----
created_time 2020-06-07T19:30:14.490822153Z
deletion_time n/a
destroyed false
version 1

==== Data ====
Key Value
--- -----
value bqVYx5#VU$0f

ansible 角色定义的 rotate_local_password/tasks/main.yml:

1
2
3
4
5
6
---
# tasks file for rotate_local_password
- name: change target account password
user:
name: "{{ account }}"
password: "{{ new_password | password_hash('sha512', 65534 | random(seed=inventory_hostname) | string) }}"
为了实现幂等, password_hash 的第二个参数使用了基于 inventory_hostname 产生的 salt 值. 即对于同一个 host, 不管执行多少次, 产生的密码哈希值都一样. 如果不提供第二个参数, 则会使用随机产生的 salt, 导致每次运行都会提示发生了变化 (changed).

Playbook:

1
2
3
4
5
6
7
8
9
- name: rotate root password in all hosts
hosts:
- all
roles:
- role: rotate_local_password
vars:
vault_param: "secret=secret/data/dc1/local-password/root:data url=https://vault.svc.vopsdev.com:8200 ca_cert=/etc/pki/ca-trust/source/anchors/private-trusted-bundle.pem"
account: root
new_password: "{{ lookup('hashi_vault', vault_param)['value'] }}"

ansible 需要获取用户的 vault token 来访问 vault 数据. 将登录 vault 的用户名/密码或者 vault token 直接写入 lookup 参数显然不合适. 有几种可行的方法:

  • 用户先执行 vault login ... 通过认证引擎登录 vault, 这样就会在家目录下产生 .vault-token 文件, 用于和 vault 交互
  • 用户将 vault token 通过环境变量 VAULT_TOKEN 导出, 使用此环境变量和 vault 交互

示例: rotate certificate

将更新的 https 证书和私钥保存到 vault 中, 然后通过 ansible 分发到服务器上.

1
2
vault kv put secret/dc1/rproxy/certificate value=@fullchain.pem
vault kv put secret/dc1/rproxy/privatekey value=@privkey.pem

查看内容

1
2
3
vault kv get -field=data -format=json secret/dc1/rproxy/certificate | jq -r .value
vault kv get -field=data -format=json secret/dc1/rproxy/privatekey | jq -r .value
# 输出略

ansible 角色定义的 deploy_certificate/tasks/main.yml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- name: copy certificate content
copy:
content: "{{ certificate_content }}"
dest: /etc/nginx/ssl/server.crt
owner: root
group: root
mode: '0644'
notify: reload nginx

- name: copy key content
copy:
content: "{{ key_content }}"
dest: /etc/nginx/ssl/server.key
owner: root
group: root
mode: '0644'
notify: reload nginx

Playbook:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
---
- name: deploy certificate and private key to all reverse proxies
hosts:
- rproxy
serial: 1
vars:
vault_param: "url=https://vault.svc.vopsdev.com:8200 ca_cert=/etc/pki/ca-trust/source/anchors/private-trusted-bundle.pem"
cert_param: "secret=secret/data/dc1/rproxy/certificate:data {{ vault_param }}"
key_param: "secret=secret/data/dc1/rproxy/privatekey:data {{ vault_param }}"

roles:
- role: deploy_certificate
vars:
certificate_content: "{{ lookup('hashi_vault', cert_param)['value'] }}"
key_content: "{{ lookup('hashi_vault', key_param)['value'] }}"

这里使用 serial: 1 来控制并行度, 确保一台主机上证书更新完成, 服务重新加载配置后再执行下一台 host.