生成动态数据库凭证
数据保护是重中之重,数据库凭证轮换是任何数据保护计划的关键部分。每个角色都有一组不同的权限来访问数据库。为抵御黑客攻击,自动化的连续凭证轮换是非常必要的。
解决方案
应用程序向 Vault 请求数据库凭据,而不是将它们设置为环境变量。管理员为数据库凭据设置 TTL 以限制其有效期,以便在不再使用时自动撤销它们。
每个应用程序实例都可以获得他们独享的凭据。通过使这些凭据的生命周期限制在一个较短的时间内,我们可以减少它们可能被泄露的机会。如果应用程序遭到入侵,可以撤销应用程序使用的凭据,而不是更改更多的全局凭据集合。
参与实验的角色
- admin:拥有 Vault 以及数据库管理员特权的管理员
- app:从 Vault 读取数据库动态凭据的应用程序
前置条件
- 一个已经初始化并解封的 Vault 服务器
- 一个 PostgreSQL 数据库(本实验使用 Docker 运行 Postgres,所以请确保已安装 Docker)
admin
需要拥有如下 Vault 权限的 Vault 令牌:
# Mount secrets engines
path "sys/mounts/*" {
capabilities = [ "create", "read", "update", "delete", "list" ]
}
# Configure the database secrets engine and create roles
path "database/*" {
capabilities = [ "create", "read", "update", "delete", "list" ]
}
# Manage the leases
path "sys/leases/+/database/creds/readonly/*" {
capabilities = [ "create", "read", "update", "delete", "list", "sudo" ]
}
path "sys/leases/+/database/creds/readonly" {
capabilities = [ "create", "read", "update", "delete", "list", "sudo" ]
}
# Write ACL policies
path "sys/policies/acl/*" {
capabilities = [ "create", "read", "update", "delete", "list" ]
}
# Manage tokens for verification
path "auth/token/create" {
capabilities = [ "create", "read", "update", "delete", "list", "sudo" ]
}
app
需要拥有如下 Vault 权限的 Vault 令牌:
# Get credentials from the database secrets engine 'readonly' role.
path "database/creds/readonly" {
capabilities = [ "read" ]
}
启动 Postgres
我们使用 Docker 启动一个用户名为 root
,密码为 rootpassword
的 Postgres 数据库实例:
$ docker run \
--name learn-postgres \
-e POSTGRES_USER=root \
-e POSTGRES_PASSWORD=rootpassword \
-p 5432:5432 \
--rm \
postgres
确认数据库容器正常运行:
$ docker ps -f name=learn-postgres --format "table {{.Names}}\t{{.Status}}"
NAMES STATUS
learn-postgres Up 5 seconds
在确认容器正常运行后,创建相关数据库角色。该数据库角色会在后续的设置中被 Vault 用来签发数据库动态凭据。
创建一个名为 ro
的数据库角色:
$ docker exec -i \
learn-postgres \
psql -U root -c "CREATE ROLE \"ro\" NOINHERIT;"
CREATE ROLE
赋予角色 ro
读取所有表的权限:
$ docker exec -i \
learn-postgres \
psql -U root -c "GRANT SELECT ON ALL TABLES IN SCHEMA public TO \"ro\";"
GRANT
至此,该角色已经搭配所需的权限被正确创建完了。
启动 Vault
我们在新的命令行终端中启动一个 -dev
模式的 Vault 服务,使用 root
作为根令牌:
$ vault server -dev -dev-root-token-id root
该 Vault 实例目前运行于 127.0.0.1:8200
上,服务已被初始化并解封。
注意,我们在这里是为了教学目的所以使用了 -dev
模式。请不要在生产环境这样做。
通过环境变量设置命令行使用的 Vault 服务地址:
$ export VAULT_ADDR=http://127.0.0.1:8200
通过环境变量设置命令行使用的 Vault 令牌:
$ export VAULT_TOKEN=root
注意,我们在这里是为了教学目的所以使用了 root
令牌。请不要在生产环境这样做,而是使用配置了合适权限的令牌。
至此,Vault 服务已经正常运行。
启用数据库机密引擎
(操作者:admin
)
通过命令行在 database/
路径上启用数据库机密引擎:
$ vault secrets enable database
配置 PostgreSQL 机密引擎
(操作者:admin
)
通过命令行配置使用的 Postgres 连接凭据:
$ vault write database/config/postgresql \
plugin_name=postgresql-database-plugin \
connection_url="postgresql://{{username}}:{{password}}@localhost:5432/postgres?sslmode=disable" \
allowed_roles=readonly \
username="root" \
password="rootpassword"
要注意的是,由于本实验中没有为数据库启用 ssl
,所以在 connection_url
连接字符串的末尾特意加上了 ?sslmode=disable
。请勿在生产环境这样做。
创建角色
(操作者:admin
)
在前面的步骤中,我们配置 Postgres 机密引擎允许名为 readonly
的角色调用。Vault 中的角色是一个映射到数据库凭据的逻辑名称。这些附加到 Vault 角色的凭据以 SQL 语句的形式表达。
定义用来创建凭据的 SQL 语句:
$ tee readonly.sql <<EOF
CREATE ROLE "{{name}}" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}' INHERIT;
GRANT ro TO "{{name}}";
EOF
SQL 语句包含了模板化的字段 {{name}}
、{{password}}
以及 {{expiration}}
。这些字段会在 Vault 创建凭据时被填充。这将创建一个新角色,并将先前在 Postgres 中创建的名为 ro
的角色拥有的权限赋予这个新角色。
我们用以下命令来创建使用 readonly.sql
文件的内容创建凭据的名为 readonly
的角色:
$ vault write database/roles/readonly \
db_name=postgresql \
creation_statements=@readonly.sql \
default_ttl=1h \
max_ttl=24h
该角色创建出来的数据库凭据拥有默认的长度为 1 小时的 TTL 以及长度为 24 小时的最大 TTL。
请求 Postgres 凭据
(操作者:app
)
应用程序通过以下命令,从 readonly
数据库角色读取数据库凭据:
$ vault read database/creds/readonly
Key Value
--- -----
lease_id database/creds/readonly/fyF5xDomnKeCHNZNQgStwBKD
lease_duration 1h
lease_renewable true
password A1a-ckirtymYaXACpIHn
username v-token-readonly-6iRIcGv8tLpu816oblPY-1556567086
Postgres 凭据通过 username
和 password
字段展示出来。凭据在 Vault 中的租约就是 lease_id
。
验证
连接到 Postgres 数据库,列出所有数据库用户:
$ docker exec -i \
learn-postgres \
psql -U root -c "SELECT usename, valuntil FROM pg_user;"
输出展示了所有生成的数据库凭据的表。之前 app
生成的凭据也在其中:
usename | valuntil
--------------------------------------------------+------------------------
root |
v-token-readonly-ExP3fop3xpzCoZkzdiT7-1635943853 | 2021-11-04 12:50:58+00
(2 rows)
管理租约
(操作者:admin
)
Vault 创建的所有凭据都关联了对应的租约 ID,在 TTL 到期前或是被吊销前凭据有效。一旦租约被吊销,则凭据也将不再有效。
列出存在的租约:
$ vault list sys/leases/lookup/database/creds/readonly
Keys
----
IQKUMCTg3M5QTRZ0abmLKjTX
输出列出了所有有效的数据库凭据的租约。
将第一个租约 ID 保存在一个环境变量中:
$ LEASE_ID=$(vault list -format=json sys/leases/lookup/database/creds/readonly | jq -r ".[0]")
传递租约 ID 续约租约及相应的数据库凭据:
$ vault lease renew database/creds/readonly/$LEASE_ID
Key Value
--- -----
lease_id database/creds/readonly/IQKUMCTg3M5QTRZ0abmLKjTX
lease_duration 1h
lease_renewable true
续约后的租约的 TTL 为 1h
。
在租约过期前吊销租约:
$ vault lease revoke database/creds/readonly/$LEASE_ID
All revocation operations queued successfully!
尝试再列出存在的租约:
$ vault list sys/leases/lookup/database/creds/readonly
No value found at sys/leases/lookup/database/creds/readonly/
无效的租约不会被显示出来。重新读取一个新的数据库凭据:
$ vault read database/creds/readonly
Key Value
--- -----
lease_id database/creds/readonly/P6tTTiWsR1fVCp0btLktU0Dm
lease_duration 1m
lease_renewable true
password A1a-pfgGk7Ptb0TxGBJI
username v-token-readonly-9blxDY3dIKXsFMkv8kvH-1600278284
下面我们吊销一个路径上所有的租约。让我们吊销所有前缀为 database/creds/readonly
的租约:
$ vault lease revoke -prefix database/creds/readonly
prefix
标志匹配了所有路径前缀为 database/creds/readonly
的有效租约。
尝试列出存在的租约:
$ vault list sys/leases/lookup/database/creds/readonly
No value found at sys/leases/lookup/database/creds/readonly/
所有该路径下的租约都已经被吊销了。
定义密码策略
要执行以下步骤需要 Vault 1.6 或是更新的版本。
数据库机密引擎生成的密码遵循默认模式,可以用新密码策略覆盖。策略定义了密码必须遵守的规则和要求,并且可以直接通过新端点或在机密引擎中生成该密码。
假如我们要生成的密码必须要符合以下要求:
- 至少 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
生成的密码符合上面的规定。
应用密码策略
配置数据库机密引擎,使用 example
密码策略:
$ vault write database/config/postgresql \
password_policy="example"
与数据库建立连接的其他信息没有变化,唯一的变化是 password_policy
被设置为使用 example
策略。
使用 readonly
数据库角色读取凭据:
$ vault read database/creds/readonly
Key Value
--- -----
lease_id database/creds/readonly/ZxoKlbklsliYA4hZs7umoPIz
lease_duration 1h
lease_renewable true
password 9MSegMz7N1Fr69ZTyb#D
username v-token-readonly-wGLPkpDyc6AgqBfMZTD3-1604195404
定义用户名模板
要执行以下步骤需要 Vault 1.7 或是更新的版本。
数据库机密引擎生成符合默认模式的用户名。为满足组织和团队的合规要求,我们可以使用自定义的用户名模板。
请确保自定义用户名模板包含足够的随机性,以防止多次生成相同的用户名。
使用 readonly
数据库角色读取凭据:
$ vault read database/creds/readonly
Key Value
--- -----
lease_id database/creds/readonly/ZxoKlbklsliYA4hZs7umoPIz
lease_duration 1h
lease_renewable true
password 9MSegMz7N1Fr69ZTyb#D
username v-token-readonly-wGLPkpDyc6AgqBfMZTD3-1604195404
生成的用户名,v-token-readonly-wGLPkpDyc6AgqBfMZTD3-1604195404
,使用的默认模式可以用如下 Go 模板表达:{{ printf "v-%s-%s-%s-%s" (.DisplayName | truncate 8) (.RoleName | truncate 8) (random 20) (unix_time) | truncate 63 }}
。
使用用户名模板配置数据库机密引擎:
$ vault write database/config/postgresql \
username_template="myorg-{{.RoleName}}-{{unix_time}}-{{random 8}}"
用户名以 myorg-
为前缀,使用角色名 readonly
,然后是以秒计的 unix 时间戳,最后是一个 8 位字符长的随机序列。
从 readonly
数据库角色读取凭据:
$ vault read database/creds/readonly
Key Value
--- -----
lease_id database/creds/readonly/NOCGtSbz7g4FFjcztX6Bqh3S
lease_duration 1h
lease_renewable true
password -h3B-JteYjgOPYIC6dGQ
username myorg-readonly-1616447348-af9eHMWD
生成的用户名满足我们先前通过模板定义的规则。