Vault
在企业级应用开发过程中,团队每时每刻都需要管理各种各样的私密信息,从个人的登陆密码、到生产环境的 SSH Key 以及数据库登录信息、API 认证信息等。如何添加机密信息、应用程序能够获取私密信息、采用不同策略更新私密信息、及时回收私密信息等变得越来越关键。所以企业需要一套统一的接口来处理私密信息的方方面面,而 HashiCorp Vault 就是这样的一款工具
机密蔓生(Secret Sprawl):
生产环境的数据库用户名密码被以明文形式编码在配置文件中,或是保存在生产环境中某个不设防的配置管理服务中;某个应用调用云服务接口所用的令牌被硬编码在代码里,随后不小心被程序员上传到一个公共的 Github 仓库中等等。
机密蔓生可能造成的问题:
- 安全性:难以保证分布在各个存储位置的所有机密信息都是安全的,无法做到同意管理;
- 访问:特定的机密信息不便于查找;
- 依赖某些员工:如果大量机密信息分布在不同的位置,可能只有几个人知道文件存储位置。掌握机密的员工离职后可能泄密或是恶意报复。
首要解决的问题:机密信息集中化管理(账号密码、数据库凭证、证书和API密钥等);应用程序无需保存机密信息,在需要时询问Vault并在使用后丢弃即可;设置一些机密的有效时间。
下载Vault
创建Vault配置文件
vault.hlc(需要手动添加)
ui = true
storage "consul" {
address = "127.0.0.1:8500"
token = "a28ab319-a93d-d8e0-243e-a18ac9ca4671"
path = "vault"
}
listener "tcp" {
address = "127.0.0.1:8200"
tls_disable = 1
}
密钥的封印与解封
当 Vault 服务启动时,默认处于封印状态。在该状态下,Vault 无法解密任何存储的数据(不解封啥也干不了)。
Vault保存再后台的数据都是加密的,数据由加密密钥加密,而加密密钥和密文一同存储,加密密钥本身由主密钥(master key)加密。因此,若想解密数据,首先要使用主密钥解密加密密钥。而主密钥在创建时被分割为一些份额,解封就是获取主密钥的过程
在生产环境中使用时可将各个份额存储到不同的计算机中,以提升整个系统的安全性,服务器重启需要重新解封。
系统初始化->获得密钥份额和root token
$ vault operator init
Unseal Key 1: 4g8FTfPiyE5f/6vzURVm+ljTEfOCRQPXSZp1WteK1rJu
Unseal Key 2: si53pSe8AulsCZelBPFypf1dyuNeLgUVGKdVfbNWc3+3
Unseal Key 3: qhONs6vqG7eO7DeRgatuCbVPMZ2X06g/U6gnWVliO+de
Unseal Key 4: H8GNnfQdkeACSuHo4dyj+yNrRTs/UEVZbYWHNn6drRR3
Unseal Key 5: uSIbqitWpwcfzP4Tg3y4GJO7NIxRQ+qiy0ZbJBvmHSpY
Initial Root Token: s.n3YQoQMDILejIuWPcQBIGW3Q
Vault initialized with 5 key shares and a key threshold of 3. Please securely
distribute the key shares printed above. When the Vault is re-sealed,
restarted, or stopped, you must supply at least 3 of these keys to unseal it
before it can start servicing requests.
Vault does not store the generated master key. Without at least 3 keys to
reconstruct the master key, Vault will remain permanently sealed!
It is possible to generate new unseal keys, provided you have a quorum of
existing unseal keys shares. See "vault operator rekey" for more information.
封印状态
解封三次
还可以重新封印
关于密钥的总结:利用主密钥(master key)保护所有数据,再利用秘密分享算法确保主密钥的安全性
Shamir门限秘密共享方案
核心思想:将秘密点扩充为秘密线
门限秘密分享方案:
在2维空间坐标系内给定个点,要求各不相同, 对于任意,有且仅有一个使得阶多项式成立。不失一般性,令,随机选取个整数构造阶多项式,其中.
可利用Lagrange插值函数:
已知中的个,可以恢复多项式,得到,任意个份额无法恢复多项式。
-
随机构造阶多项式:;
-
计算,将分发给用户;
-
个用户恢复多项式
插值基函数,:第个份额
展开
一个实例:
-
;
-
;
-
3个份额:;
-
身份认证
token
根(root)令牌权限最大,是vault中唯一可设置永不过期而无需任何租约的令牌类型
可用子命令
Usage: vault token <subcommand> [options] [args]
# ...
Subcommands:
capabilities Print capabilities of a token on a path
create Create a new token
lookup Display information about a token
renew Renew a token lease
revoke Revoke a token and its children
初始化建立根令牌
vault operator init
设置令牌有效时间
设置令牌可使用次数
vault token create -ttl=1h -use-limit=1
用户名密码
启用用户名密码身份验证方法
vault write sys/auth/my-auth type=userpass
配置
vault write auth/userpass/users/mitchellh \
password=foo \
policies=admins
登录
vault login -method=userpass \
username=mitchellh \
password=foo
静态机密-Key/Value机密引擎
启动引擎
vault secrets enable -version=1 kv
使用
- 写一个秘密
vault kv put kv/my-secret my-value=s3cr3t
- 读一个秘密
vault kv get kv/my-secret
- 列出键
vault kv list kv/
- 删除键
vault kv delete kv/my-secret
存储证书
- 生成证书文件
openssl req -x509 -sha256 -nodes -newkey rsa:2048 -keyout selfsigned.key -out cert.pem
- 存储证书
vault kv put kv-v1/prod/cert/mysql cert=@cert.pem
policy
- 定义策略,只允许对kv / fakebank路径进行读访问;
- 将策略文件写入;
- 利用该策略创建令牌
-
利用生成的token登录
-
只能读取,不能写入
更多策略:
# This section grants all access on "secret/*". Further restrictions can be
# applied to this broad policy, as shown below.
path "secret/*" {
capabilities = ["create", "read", "update", "delete", "list"]
}
# Even though we allowed secret/*, this line explicitly denies
# secret/super-secret. This takes precedence.
path "secret/super-secret" {
capabilities = ["deny"]
}
# Permit reading everything prefixed with "zip-". An attached token could read
# "secret/zip-zap" or "secret/zip-zap/zong", but not "secret/zip/zap
path "secret/zip-*" {
capabilities = ["read"]
}
create(POST/PUT)——允许在指定路径创建数据。只有很少的Vault部件会区分create和update,所以大多数操作同时需要create以及update能力。需要区分二者的部分会在相关文档中说明。read(GET)——允许读取指定路径的数据update(POST/PUT)——允许修改指定路径的数据。对多数Vault部件来说,这隐含了在指定位置创建初始值的能力delete(DELETE)——允许删除指定路径的数据list(LIST)——允许罗列指定路径的所有值。要注意的是,经由list操作返回的键是未经策略过滤的。请勿在键名中编码敏感信息。不是所有后端都支持list操作
Transit引擎
在存储敏感信息时,通常使用对称加密算法加密,而对称密钥算法的关键是密钥,如果在应用程序代码中调用加密库加解密数据,不可避免就要使用密钥,密钥的管理,访问密钥的权限,以及密钥的轮替和紧急状况下密钥的撤销较为复杂。 Vault 提供了名为 transit 的机密引擎,对外提供了“加密即服务”功能(Encryption as a Service)
利用Transit加解密数据
密钥版本
查看当前密钥环信息
vault read transit/keys/orders
Key Value
--- -----
allow_plaintext_backup false
deletion_allowed false
derived false
exportable false
keys map[1:1641036346]
latest_version 1
min_available_version 0
min_decryption_version 1
min_encryption_version 0
name orders
supports_decryption true
supports_derivation true
supports_encryption true
supports_signing false
type aes256-gcm96
transit/keys/orders 密钥环采用的是 aes256-gcm96 算法,当前最新密钥版本是 1,保存的最久远的密钥版本 min_decryption_version 也是 1。
可以看到,最新密钥版本变成了 2,而 min_decryption_version 还是 1,这代表轮替后将默认使用版本号为 2 的密钥加密数据,但使用版本号为 1 的密钥加密的数据仍然可以正常解密。让我们验证一下现在我们是否还可以解密刚才的密文:
$ vault write -format=json transit/decrypt/orders ciphertext="vault:v1:lnvn6fiOjBgpTUlYw1Oqx2uBT8dq2LfrAn2r/fFf+W8Hp12b5WFj/EDRstRBX5LO" | jq -r .data.plaintext | base64 -d
4111 1111 1111 1111
使用版本 1 的密钥加密的密文目前仍然可以解密。让我们再次加密同一段明文试试看:
$ vault write transit/encrypt/orders plaintext=$(base64 <<< "4111 1111 1111 1111")
Key Value
--- -----
ciphertext vault:v2:pLwQJs39PWFGHvnvX/5qrBoHBi7Ly5l4bC6ouYX9SYWgOMLlOxm+HTGhTcvpW3o8
key_version 2
可以看到,同样的明文,现在得到的密文发生了变化:
vault:v1:lnvn6fiOjBgpTUlYw1Oqx2uBT8dq2LfrAn2r/fFf+W8Hp12b5WFj/EDRstRBX5LO
vault:v2:pLwQJs39PWFGHvnvX/5qrBoHBi7Ly5l4bC6ouYX9SYWgOMLlOxm+HTGhTcvpW3o8
新的密文是使用版本 2 的密钥加密的。目前这两个版本的密文都可以被正常解密:
$ vault write -format=json transit/decrypt/orders ciphertext="vault:v2:pLwQJs39PWFGHvnvX/5qrBoHBi7Ly5l4bC6ouYX9SYWgOMLlOxm+HTGhTcvpW3o8" | jq -r .data.plaintext | base64 -d
4111 1111 1111 1111
更新密文版本
如果我们数据库中目前存储的是 v1 版本的密文,在轮替密钥后,我们希望把旧版本密文全部更新成新版本,可以使用 Vault 的 rewrap 来完成:
$ vault write transit/rewrap/orders ciphertext="vault:v1:lnvn6fiOjBgpTUlYw1Oqx2uBT8dq2LfrAn2r/fFf+W8Hp12b5WFj/EDRstRBX5LO"
Key Value
--- -----
ciphertext vault:v2:o8XaRyDyyj2+ai46DVW2SYssGBMdvYxPPxrC7y+UaEWA1zAf+iaIfWaIEqtU3hL5
key_version 2
通过 rewrap,我们在不知晓明文的前提下,将密文的密钥版本从 v1 更新到了 v2。
假设我们在数据库中存放了大量密文数据,一种比较好的实践是定期用轮替生成新的密钥,并且编写定时任务,定时将密文全部更新到最新密钥版本,这样即使密文意外泄漏,存放在 Vault 中的密钥可能也已经被抛弃了。
抛弃旧密钥
在生产环境中我们定期轮替生成新密钥,但老密钥还是被保存在 Vault 中,长此以往会在 Vault 中留下大量的无用密钥(旧密文已被定期更新到新密钥版本)。我们可以设置密钥环的最低解密密钥版本:
$ vault write transit/keys/orders/config min_decryption_version=2
Success! Data written to: transit/keys/orders/config
$ vault read transit/keys/orders
Key Value
--- -----
allow_plaintext_backup false
deletion_allowed false
derived false
exportable false
keys map[2:1641036686]
latest_version 2
min_available_version 0
min_decryption_version 2
min_encryption_version 0
name orders
supports_decryption true
supports_derivation true
supports_encryption true
supports_signing false
type aes256-gcm96
如此,当前的 min_decryption_version 就被设置为 2。我们现在试试解密 v1 版本的密文:
$ vault write -format=json transit/decrypt/orders ciphertext="vault:v1:lnvn6fiOjBgpTUlYw1Oqx2uBT8dq2LfrAn2r/fFf+W8Hp12b5WFj/EDRstRBX5LO"
Error writing data to transit/decrypt/orders: Error making API request.
URL: PUT http://127.0.0.1:8200/v1/transit/decrypt/orders
Code: 400. Errors:
* ciphertext or signature version is disallowed by policy (too old)
Vault 拒绝解密 v1 版本的密文,原因是 too old。v1 版本的密钥已经被 Vault 删除。需要注意的是,抛弃密钥前必须确保所有旧版本密文都已被更新到新密钥版本,否则抛弃旧密钥等同于删除旧版本密文。
SSH OPT 密码
使用 Vault 生成一次性的 Linux 登陆密码(One Time Password, OTP)。
管理用户在生产服务器上的用户名、密码是一件很枯燥而又关键的事,Vault 提供了一种创建一次性登陆密码的方式,它的工作原理如图:
首先在被登陆的服务器上配置 vault-ssh-helper 程序,它可以取代 Linux 默认的登陆验证程序,在用户传递了登陆用户名密码后,转而向 Vault 服务器请求验证用户名密码的正确性。用户首先登陆 Vault,通过 Vault 创建一个属于目标服务器的 otp,随后远程连接目标服务器,给出这组 otp,在 vault-ssh-helper 验证通过后成功登陆,同时 Vault 服务器会在成功验证后删除这个 otp,确保密码的确是一次性的。
- 启动ssh引擎
vault secrets enable ssh
Success! Enabled the ssh secrets engine at: ssh/
- 创建角色,将key_type设置为otp
vault write ssh/roles/otp_key_role \
key_type=otp \
default_user=ubuntu \
cidr_list=0.0.0.0/0
- 建个策略
tee test.hcl <<EOF
# To list SSH secrets paths
path "ssh/*" {
capabilities = [ "list" ]
}
# To use the configured SSH secrets engine otp_key_role role
path "ssh/creds/otp_key_role" {
capabilities = ["create", "read", "update"]
}
EOF
- 将本地策略写入,登录
vault policy write test ./test.hcl
vault auth enable userpass
vault write auth/userpass/users/ubuntu password="training" policies="test"
- 在远程主机端安装
vault-ssh-helper
wget https://releases.hashicorp.com/vault-ssh-helper/0.2.1/vault-ssh-helper_0.2.1_linux_amd64.zip
sudo unzip -q vault-ssh-helper_0.2.1_linux_amd64.zip -d /usr/local/bin
sudo chmod 0755 /usr/local/bin/vault-ssh-helper
sudo chown root:root /usr/local/bin/vault-ssh-helper
- 创建shh-helper配置文件
sudo mkdir /etc/vault-ssh-helper.d/
nvim /etc/vault-ssh-helper.d/config.hcl
- vault-ssh-helper读取该配置文件
vault_addr = "<VAULT_EXTERNAL_ADDR>"
tls_skip_verify = false
ca_cert = "<PEM_ENCODED_CA_CERT>"
ssh_mount_point = "ssh"
namespace = "my_namespace"
allowed_roles = "*"
- vault_addr:生成otp的vault服务器地址
- tls_skip_verify:跳过tls验证
生产环境必须配置 tls 验证,使用 https 地址,因为 vault-ssh-helper 会将用户提供的 OTP 通过网络传递给 Vault,非加密链路存在被中间人拦截的可能性。
- 创建一个vault服务端地址和端口的环境变量并写入配置文件
VAULT_EXTERNAL_ADDR=<VAULT_EXTERNAL_ADDR>
sudo tee /etc/vault-ssh-helper.d/config.hcl <<EOF
vault_addr = "$VAULT_EXTERNAL_ADDR"
tls_skip_verify = false
ssh_mount_point = "ssh"
allowed_roles = "*"
EOF
- 修改
/etc/pam.d/sshd:
# PAM configuration for the Secure Shell service
# Standard Un*x authentication.
#@include common-auth
auth requisite pam_exec.so quiet expose_authtok log=/tmp/vaultssh.log /usr/local/bin/vault-ssh-helper -dev -config=/etc/vault-ssh-helper.d/config.hcl
auth optional pam_unix.so not_set_pass use_first_pass nodelay
...
在这段配置里,把原有的 include common-auth 这一行注释了,common-auth 是 linux 提供标准的身份验证模块,必须将它注释,才能使用后面定义的 vault-ssh-helper 模块。生产环境的配置中不要开启 -dev 模式
修改 /etc/ssh/sshd_config,确保其中的三项配置设置如下:
ChallengeResponseAuthentication yes
PasswordAuthentication no
UsePAM yes
- 重启 sshd 服务:
$ sudo systemctl restart sshd
待登陆服务器配置完成。
- 通过用户名密码的方式登录
vault login -method=userpass username=ubuntu password=training
- 生成otp
vault write ssh/creds/otp_key_role ip=$REMOTE_HOST_IP
ssh ubuntu@10.11.38.16
动态数据库凭证
应用程序向 Vault 请求数据库凭据,而不是将它们设置为环境变量。管理员为数据库凭据设置 TTL 以限制其有效期,以便在不再使用时自动撤销它们。
定义密码策略
假如我们要生成的密码必须要符合以下要求:
- 至少 20 位字符长度
- 至少一个大写字母
- 至少一个小写字母
- 至少一个数字
- 至少一个符号
通过名为 example_policy.hcl 的文件定义一个密码策略:
$ tee example_policy.hcl <<EOF
length=20
rule "charset" {
charset = "abcdefghijklmnopqrstuvwxyz"
min-chars = 1
}
rule "charset" {
charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
min-chars = 1
}
rule "charset" {
charset = "0123456789"
min-chars = 1
}
rule "charset" {
charset = "!@#$%^&*"
min-chars = 1
}
EOF
策略使用 HCL 编写。length 字段设置了密码长度为 20 位字符。每个 rule 配置节定义一个字符集以及这些字符需要出现在生成的密码中的最少出现次数。这些规则是累积的,因此每个规则都对生成的密码增加了更多要求。
使用 example_policy.hcl 创建名为 example 的密码策略:
$ vault write sys/policies/password/example policy=@example_policy.hcl
Success! Data written to sys/polices/password/example_policy
现在可以直接访问此策略以生成密码或在配置支持的机密引擎时通过它的名字 example 引用该策略。
使用 example 密码策略生成一个密码:
$ vault read sys/policies/password/example/generate
Key Value
--- -----
password #v!RQDHxHunJ1TUmCyys
轮换数据库根凭证
Vault的database机密引擎为管理各种数据库提供了一个集中化的工作流程。通过该方式,每个服务实例都能得到唯一一个数据库凭证,而不是共享同一个凭证,并且只在服务的生命周期有效,对于任何异常的访问模式,管理员可以立即撤销其凭证,而不需要更改全局的凭证。
使用以下命令轮换根凭据:
$ vault write -force database/rotate-root/postgresql
Success! Data written to: database/rotate-root/postgresql
一旦轮换了根凭据,那么就只有 Vault 知道新的根账号密码了。这一点对该 Vault 机密引擎中配置的所有的数据库根凭据都是一样的。所以,应当配置一个 Vault 专用的根账号。
使用Vault和MongoDB实现动态数据库凭证:
- 启动MongoDB,用户名为
mdbadmin
docker run -d \
-p 0.0.0.0:27017:27017 -p 0.0.0.0:28017:28017 \
--name=mongodb \
-e MONGO_INITDB_ROOT_USERNAME="mdbadmin" \
-e MONGO_INITDB_ROOT_PASSWORD="hQ97T9JJKZoqnFn2NXE" \
mongo
- 启动VAULT
vault server -dev -dev-root-token-id=root
- 设置环境变量
export VAULT_ADDR=http://127.0.0.1:8200
- 登录
- 使用MongoDB数据库连接凭证配置数据库机密引擎
vault write mongodb/config/mongo-test \
plugin_name=mongodb-database-plugin \
allowed_roles="tester" \
connection_url="mongodb://{{username}}:{{password}}@127.0.0.1:27017/admin?tls=false" \
username="mdbadmin" \
password="hQ97T9JJKZoqnFn2NXE"
- 新建角色 遵循最小特权原则,为每个数据库客户适当地创建一个角色(一些应用程序可能需要只读权限,而另一些可能需要读写权限)。
创建tester角色,生存周期为1小时,最大TTL为24小时。
vault write mongodb/roles/tester \
db_name=mongo-test \
creation_statements='{ "db": "admin", "roles": [{ "role": "readWrite" }, {"role": "read", "db": "foo"}] }' \
default_ttl="1h" \
max_ttl="24h"
- 测试是否创建成功
vault list mongodb/roles
Keys
----
tester
- 请求数据库凭证
从tester数据库角色中读取一个凭证
vault read mongodb/creds/tester
Key Value
--- -----
lease_id mongodb/creds/tester/fyF5xDomnKeCHNZNQgStwBKD
lease_duration 1h
lease_renewable true
password A1ackirtymYaXACpIHn
username v-token-tester-6iRIcGv8tLpu816oblPY-1556567086
- 连接
docker exec -it mongodb mongo \
--username v-token-tester-fO7smPyTgCrySIgQNrst-1615451034 \
--password IePqQ26-EyIk6zEA637w
MongoDB shell version v4.4.4
connecting to:mongodb://127.0.0.1:27017/?compressors=disabled&gssapiServiceName=mongodb
Implicit session: session { "id" : UUID("aabf31e2-27f9-4ac5-9145-710dbd84ef8d") }
MongoDB server version: 4.4.4
> show dbs;
admin 0.000GB
ui
启动数据库机密引擎
支持如下数据库:
配置
添加角色,该界面也可以查看连接情况
生成凭证
查看账号密码