Cubbyhole
我们之前介绍了 Vault 的响应封装。要正确使用响应封装,我们需要使用一种名为 Cubbyhole 的特殊存储引擎。
Cubbyhole 是美语,意为一个舒适的小房间,或是一个储物柜。每一个 Token 都会拥有一个独立的 Cubbyhole 存储空间,彼此相互隔离;每当一个 Token 被吊销或是过期时,Vault 会清空相应的 Cubbyhole 存储。
Cubbyhole 对外也是提供了一个 key-value 格式的存储形式,但与 Key-Value 机密引擎不同的是,任何 Token 无权访问其他 Token 的 Cubbyhole(即使是 Root 令牌),而 Key-Value 机密引擎提供的则是一个全局的 Key-Value 存储,凡是配置了相应路径读写 Policy 的用户都可以读写同一个路径下的机密数据。
创建实验环境
我们假想的场景是,管理员为自动化工具创建一个对应的 Vault 策略,然后创建一个拥有该策略权限的 Token,并且利用 Cubbyhole Response Wrap 机制,安全地将 Token 分发给自动化工具,自动化工具拿到封装着 Token 的信息后,通过 Vault 解封得自己要使用的 Token,并利用该 Token 完成与 Vault 的交互。
那为什么不直接传递 Token ,而是传递 Token 的封装呢?这是因为一个 Token 封装只能被解封一次,重复申请解封会被 Vault 拒绝。也就是说,假设传递给工具的 Token 封装被攻击者截获了,如果工具率先解封得到了 Token,那么攻击者随后试图解封将会失败;反过来如果攻击者率先成功解封得到了 Token,那么会造成工具解封失败,无法得到 Token,而这种类型的错误足以拉响运维系统的警报,督促管理员在第一时间吊销泄漏的 Token,并开始追查泄漏途径。
我们还是使用 Vault 的测试服务器:
$ vault server -dev
==> Vault server configuration:
Api Address: http://127.0.0.1:8200
Cgo: disabled
Cluster Address: https://127.0.0.1:8201
Go Version: go1.17.5
Listener 1: tcp (addr: "127.0.0.1:8200", cluster address: "127.0.0.1:8201", max_request_duration: "1m30s", max_request_size: "33554432", tls: "disabled")
Log Level: info
Mlock: supported: false, enabled: false
Recovery Mode: false
Storage: inmem
Version: Vault v1.9.2
Version Sha: f4c6d873e2767c0d6853b5d9ffc77b0d297bfbdf+CHANGES
==> Vault server started! Log data will stream in below:
...
WARNING! dev mode is enabled! In this mode, Vault runs entirely in-memory
and starts unsealed with a single unseal key. The root token is already
authenticated to the CLI, so you can immediately begin using Vault.
You may need to set the following environment variable:
$ export VAULT_ADDR='http://127.0.0.1:8200'
The unseal key and root token are displayed below in case you want to
seal/unseal the Vault or re-authenticate.
Unseal Key: uKMxO//ttcdRqQDQuesylq2vAmquIwo9SBbdf9IreI8=
Root Token: s.iydnufld9coAyr1CMcAQsvyl
Development mode should NOT be used in production installations!
使用 Root 令牌登录:
$ export VAULT_ADDR='http://127.0.0.1:8200'
$ vault login s.iydnufld9coAyr1CMcAQsvyl
Success! You are now authenticated. The token information displayed below
is already stored in the token helper. You do NOT need to run "vault login"
again. Future Vault requests will automatically use this token.
Key Value
--- -----
token s.iydnufld9coAyr1CMcAQsvyl
token_accessor dFDhtueTVZ4L3jU4TnGf8jsl
token_duration ∞
token_renewable false
token_policies ["root"]
identity_policies []
policies ["root"]
首先,我们使用 Root 用户添加一个新策略,允许应用读取 kv
引擎的特定路径:
$ vault policy write apps -<<EOF
path "secret/data/dev" {
capabilities = [ "read" ]
}
EOF
Success! Uploaded policy: apps
拥有 apps
策略的用户,可以读取 secret/data/dev
下的数据。
这里要特别说明一下,一般情况下,包括 secret
在内的机密引擎在一个新的 Vault 集群上,都不是默认启用的,需要操作员显式启用,而我们使用的 -dev
模式的测试服务则会为我们自动启用 secret
路径;另外,secret
路径默认挂载的是 Vault 的 K-V Version 2
机密引擎,该引擎我们会在后续的文章中单独介绍,现在我们只需要了解到,我们可以在 secret/dev
路径下读写 Key-Value 格式的数据,例如,利用 Root 令牌在 secret/dev
路径下写入两条 Key-Value 数据:
$ vault kv put secret/dev username="webapp" password="my-long-password"
Key Value
--- -----
created_time 2021-12-28T03:16:05.350108Z
custom_metadata <nil>
deletion_time n/a
destroyed false
version 1
下面,我们要创建一个包含 apps
策略的 Token,并利用 Cubbyhole Response Wrap 机制将其封装,传递给工具:
$ vault token create -policy=apps -wrap-ttl=120
Key Value
--- -----
wrapping_token: s.7zHZJEXCbN4mbgq0ODudaEzm
wrapping_accessor: uzc6SiSLXZM8zh3mQskPeuQk
wrapping_token_ttl: 2m
wrapping_token_creation_time: 2021-12-28 11:26:41.152364 +0800 CST
wrapping_token_creation_path: auth/token/create
wrapped_accessor: YkhPxMxs73MXZVkqIQfUgqcD
我们在创建给工具使用的 Token 时,指定了 -wrap-ttl
参数,值为 120
,Vault 创建了一个包含 apps
策略的 Token,但没有直接返回给我们,而是又创建了另一个 Token:s.rsqdLYcPWzPCRxKFGbwmrFKt
,这个 Token 被称为"Wrapping Token";我们为工具创建的 Token 是被存储在这个 Wrapping Token 的 Cubbyhole 中的,而这个 Wrapping Token 的有效期为 120 秒。
下面,我们假设我们位于运行着工具的环境中。假设我们得到了 Wrapping Token,为了能够顺利解封,我们首先要一个最低权限的 Token:
$ vault token create -policy=default
Key Value
--- -----
token s.AXjSluO2ui19eB769fc1Lv5b
token_accessor fQ1NxDuGFrXEduMNnt8IBwvz
token_duration 768h
token_renewable true
token_policies ["default"]
identity_policies []
policies ["default"]
该 Token 只拥有 default
策略,默认情况下没有任何权限,但足以让我们进行后续的交互。我们先用该 Token 登录,然后解封 Wrapping Token:
$ vault login s.AXjSluO2ui19eB769fc1Lv5b
Success! You are now authenticated. The token information displayed below
is already stored in the token helper. You do NOT need to run "vault login"
again. Future Vault requests will automatically use this token.
Key Value
--- -----
token s.AXjSluO2ui19eB769fc1Lv5b
token_accessor fQ1NxDuGFrXEduMNnt8IBwvz
token_duration 767h59m50s
token_renewable true
token_policies ["default"]
identity_policies []
policies ["default"]
$ $ VAULT_TOKEN=s.7zHZJEXCbN4mbgq0ODudaEzm vault unwrap
Key Value
--- -----
token s.fK43de9uwneJXz0jYPWwcoya
token_accessor YkhPxMxs73MXZVkqIQfUgqcD
token_duration 768h
token_renewable true
token_policies ["apps" "default"]
identity_policies []
policies ["apps" "default"]
我们首先使用含有 default
策略的 Token 执行了登录,然后将 VAULT_TOKEN
环境变量设置为 Wrapping Token 的值,并执行 vault unwrap
操作,结果得到了值为 s.fK43de9uwneJXz0jYPWwcoya
的 Token,这就是刚才 Root 用户为工具创建的,包含 apps
策略的 Token。我们试试看该 Token 是否能够顺利读取 secret/dev
下的数据:
$ VAULT_TOKEN=s.fK43de9uwneJXz0jYPWwcoya vault kv get secret/dev
======= Metadata =======
Key Value
--- -----
created_time 2021-12-28T03:16:05.350108Z
custom_metadata <nil>
deletion_time n/a
destroyed false
version 1
====== Data ======
Key Value
--- -----
password my-long-password
username webapp
该 Token 如我们预期一般可用。
我们试试如果再执行一次解封操作会发生什么:
$ VAULT_TOKEN=s.7zHZJEXCbN4mbgq0ODudaEzm vault unwrap
Error unwrapping: Error making API request.
URL: PUT http://localhost:8200/v1/sys/wrapping/unwrap
Code: 400. Errors:
* wrapping token is not valid or does not exist
Vault 拒绝了解封,给出的原因是 Wrapping Token 已经不存在了,这是因为 Cubbyhole Response Wrap 机制只允许我们解封一次。如果我们在正常执行流程中看到这样的错误,就说明传递的 Wrapping Token 已经泄漏了,同时为工具创建的 Token 也已经泄漏了,这时该怎么办?让我们重新看一下创建的 Wrapping Token:
$ vault token create -policy=apps -wrap-ttl=120
Key Value
--- -----
wrapping_token: s.7zHZJEXCbN4mbgq0ODudaEzm
wrapping_accessor: uzc6SiSLXZM8zh3mQskPeuQk
wrapping_token_ttl: 2m
wrapping_token_creation_time: 2021-12-28 11:26:41.152364 +0800 CST
wrapping_token_creation_path: auth/token/create
wrapped_accessor: YkhPxMxs73MXZVkqIQfUgqcD
返回结果中除了 wrapping_token
外,还有一个 wrapped_accessor
,值为 YkhPxMxs73MXZVkqIQfUgqcD
,这个 wrapped_accessor
可以理解为一个指向被封装的 Token 的指针或者引用。我们可以通过该引用吊销这个 Token:
$ VAULT_TOKEN=s.iydnufld9coAyr1CMcAQsvyl vault token revoke -accessor YkhPxMxs73MXZVkqIQfUgqcD
Success! Revoked token (if it existed)
这样一来,原先创建的包含 apps
策略的 Token 就被我们吊销了,攻击者即使抢先解封得到了该 Token,也无法继续使用了:
$ VAULT_TOKEN=s.fK43de9uwneJXz0jYPWwcoya vault kv get secret/dev
Error making API request.
URL: GET http://localhost:8200/v1/sys/internal/ui/mounts/secret/dev
Code: 403. Errors:
* permission denied