英文:
How to make vault Secret ID can be reused multiple times?
问题
所以我有一个带有类似以下内容的Dockerfile
的 PoC Vault(完整的存储库在这里):
FROM hashicorp/vault
RUN apk add --no-cache bash jq
COPY reseller1-policy.hcl /vault/config/reseller1-policy.hcl
COPY terraform-policy.hcl /vault/config/terraform-policy.hcl
COPY init_vault.sh /init_vault.sh
EXPOSE 8200
ENTRYPOINT ["/init_vault.sh"]
HEALTHCHECK \
--start-period=5s \
--interval=1s \
--timeout=1s \
--retries=30 \
CMD ["/bin/sh", "-c", "[ -f /tmp/healthy ]"]
init_vault.sh
包含:
#!/bin/sh
set -e
export VAULT_ADDR='http://127.0.0.1:8200'
export VAULT_FORMAT='json'
# 为开发 Vault 服务器生成一个新进程,并等待其上线
# 参考:https://www.vaultproject.io/docs/concepts/dev-server
vault server -dev -dev-listen-address="0.0.0.0:8200" &
sleep 5s
# 验证容器的本地 Vault CLI
# 参考:https://www.vaultproject.io/docs/commands/login
vault login -no-print "${VAULT_DEV_ROOT_TOKEN_ID}"
# 添加策略
# 参考:https://www.vaultproject.io/docs/concepts/policies
vault policy write terraform-policy /vault/config/terraform-policy.hcl
vault policy write reseller1-policy /vault/config/reseller1-policy.hcl
# 启用 AppRole 认证方法
# 参考:https://www.vaultproject.io/docs/auth/approle
vault auth enable approle
# 配置 AppRole
# 参考:https://www.vaultproject.io/api/auth/approle#parameters
vault write auth/approle/role/dummy_role \
token_policies=reseller1-policy \
token_num_uses=9000 \
secret_id_ttl="32d" \
token_ttl="32d" \
token_max_ttl="32d"
# 覆盖我们的角色 ID
vault write auth/approle/role/dummy_role/role-id role_id="${APPROLE_ROLE_ID}"
# 为 Terraform
# 参考:https://www.vaultproject.io/docs/commands/token/create
vault token create \
-id="${TERRAFORM_TOKEN}" \
-policy=terraform-policy \
-ttl="32d"
# 保持容器运行
tail -f /dev/null & trap 'kill %1' TERM ; wait
使用 reseller1-policy.hcl
:
# 此部分授予应用程序访问权限
path "secret/data/dummy_config_yaml/reseller1/*" {
capabilities = ["read"]
}
path "secret/dummy_config_yaml/reseller1/*" { # v1
capabilities = ["read"]
}
和 terraform-policy.hcl
:
# 授予在“auth/approle/role/<role_name>/secret-id”路径上生成密钥 ID 的“update”权限
path "auth/approle/role/dummy_role/secret-id" {
capabilities = ["update"]
}
path "secret/data/dummy_config_yaml/*" {
capabilities = ["create","update","read","patch","delete"]
}
path "secret/dummy_config_yaml/*" { # v1
capabilities = ["create","update","read","patch","delete"]
}
path "secret/metadata/dummy_config_yaml/*" {
capabilities = ["list"]
}
这是使用 docker-compose.yml
启动的:
version: '3.3'
services:
testvaultserver1:
build: ./vault-server/
cap_add:
- IPC_LOCK
environment:
VAULT_DEV_ROOT_TOKEN_ID: root
APPROLE_ROLE_ID: dummy_app
TERRAFORM_TOKEN: dummyTerraformToken
ports:
- "8200:8200"
然后在 shell 上运行一些脚本 copy_config2vault_secret2tmp.sh
:
TERRAFORM_TOKEN=`cat docker-compose.yml | grep TERRAFORM_TOKEN | cut -d':' -f2 | xargs echo -n`
VAULT_ADDRESS="127.0.0.1:8200"
# 检索 appsecret 的密钥,以便虚拟应用程序可以加载 /tmp/secret
curl \
--request POST \
--header "X-Vault-Token: ${TERRAFORM_TOKEN}" \
--header "X-Vault-Wrap-TTL: 32d" \
"${VAULT_ADDRESS}/v1/auth/approle/role/dummy_role/secret-id" > /tmp/debug
cat /tmp/debug | jq -r '.wrap_info.token' > /tmp/secret
# 检查 appsecret 是否存在
cat /tmp/debug
cat /tmp/secret
VAULT_DOCKER=`docker ps| grep vault | cut -d' ' -f 1`
echo 'put secret'
cat config.yaml | docker exec -i $VAULT_DOCKER vault -v kv put -address=http://127.0.0.1:8200 -mount=secret dummy_config_yaml/reseller1/region99 raw=-
echo 'check secret length'
docker exec -i $VAULT_DOCKER vault -v kv get -address=http://127.0.0.1:8200 -mount=secret dummy_config_yaml/reseller1/region99 | wc -l
然后创建一个程序来读取密钥并从 Vault 中检索 config.yaml
:
package main
import (
"context"
"fmt"
"log"
"time"
vault "github.com/hashicorp/vault/api"
"github.com/hashicorp/vault/api/auth/approle"
)
const AppRoleID = `dummy_app`
func main() {
conf, err := TryUseVault(`http://127.0.0.1:8200`, `secret/data/dummy_config_yaml/reseller1/region99`)
if err != nil {
log.Println(err)
return
}
log.Println(conf)
}
func TryUseVault(address, configPath string) (string, error) {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
const secretFile = `/tmp/secret`
config := vault.DefaultConfig() // modify for more granular configuration
config.Address = address
client, err := vault.NewClient(config)
if err != nil {
return ``, fmt.Errorf(`failed to create vault client: %w`, err)
}
approleSecretID := &approle.SecretID{
FromFile: secretFile,
}
appRoleAuth, err := approle.NewAppRoleAuth(
AppRoleID,
approleSecretID,
approle.WithWrappingToken(), // only required if the SecretID is response-wrapped
)
if err != nil {
return ``, fmt.Errorf(`failed to create approle auth: %w`, err)
}
authInfo, err := client.Auth().Login(ctx, appRoleAuth)
if err != nil {
return ``, fmt.Errorf(`failed to login to vault: %w`, err)
}
if authInfo == nil {
return ``, fmt.Errorf(`failed to login to vault: authInfo is nil`)
}
log.Println("connecting to vault: success!")
secret, err := client.Logical().Read(configPath)
if err != nil {
return ``, fmt.Errorf(`failed to read secret from vault: %w`, err)
}
if secret == nil {
return ``, fmt.Errorf(`failed to read secret from vault: secret is nil`)
}
if len(secret.Data) == 0 {
return ``, fmt.Errorf(`failed to read secret from vault: secret.Data is empty`)
}
data := secret.Data[`data`]
if data == nil {
return ``, fmt.Errorf(`failed to read secret from vault: secret.Data.data is nil`)
}
m, ok := data.(map[string]interface{})
if !ok {
return ``, fmt.Errorf(`failed to read secret from vault: secret.Data.data is not a map[string]interface{}`)
}
raw, ok := m[`raw`]
if !ok {
return ``, fmt.Errorf(`failed to read secret from vault: secret.Data.data.raw is nil`)
}
rawStr, ok := raw.(string)
if !ok {
return ``, fmt.Errorf(`failed to read secret from vault: secret.Data.data.raw is not a string`)
}
// set viper from string
return rawStr, nil
}
它工作得很好,但问题是,密钥只能使用一次
$ ./copy_config2vault_secret2tmp.sh
{"request_id":"","lease_id":"","renewable":false,"lease_duration":0,"data":null,"wrap_info":{"token":"hvs.CAESIDSE_hR3-CW1CLLotQoVAhes55vI1MCDemmbWbsAvDS6Gh4KHGh2cy5QdzQ0bzlxRTJ6MUZJUFRoeGpSWFRzV0E","accessor":"7jLABMbzGVHKPCKAd7qkPx5J","ttl":2764800,"creation_time":"2023-07-18T19:34:48.619332723Z","creation_path":"auth/approle/role/dummy_role/secret-id","wrapped_accessor":"2493fc83-aaf6-7553-dd04-2ccedc39a4b1"},"warnings":null,"auth":null}
hvs.CAESIDSE_hR3-CW1CLLotQoVAhes55vI1MCDemmbWbsAvDS6Gh4KHGh2cy5QdzQ0bzlxRTJ6MUZJUFRoeGpSWFRzV0E
put secret
================== Secret Path ==================
secret/data/dummy_config_yaml/reseller1/region99
======= Metadata =======
Key Value
--- -----
created_time 2023-07-18T19:34:48.827508755Z
custom_metadata <nil>
deletion_time n/a
destroyed false
version 9
check secret length
19
检索一次工作正常:
$ go run main.go
2023/07/19 02:35:52 connecting to vault: success!
2023/07/19 02:35:52
this:
is:
some:
secret: a35)*&BN)(*&%TN_@#
但是当我第二次运行它时,它总是报错(除非我再次运行获取密钥的 copy_config2vault_secret2tmp.sh
脚本):
$ go run main.go
2023/07/19 02:36:06 failed to login to vault: unable to log in to auth method: unable to unwrap response wrapping token: Error making API request.
URL: PUT http://127.0.0.1:8200/v1/sys/wrapping/unwrap
Code: 400. Errors:
* wrapping token is not valid or does not exist
密钥 ID 是否只能使用一次?如果不是,可能的原因是什么?
英文:
So I have a PoC Vault with Dockerfile
something like this (full repo here):
FROM hashicorp/vault
RUN apk add --no-cache bash jq
COPY reseller1-policy.hcl /vault/config/reseller1-policy.hcl
COPY terraform-policy.hcl /vault/config/terraform-policy.hcl
COPY init_vault.sh /init_vault.sh
EXPOSE 8200
ENTRYPOINT [ "/init_vault.sh" ]
HEALTHCHECK \
--start-period=5s \
--interval=1s \
--timeout=1s \
--retries=30 \
CMD [ "/bin/sh", "-c", "[ -f /tmp/healthy ]" ]
init_vault.sh
contains:
#!/bin/sh
set -e
export VAULT_ADDR='http://127.0.0.1:8200'
export VAULT_FORMAT='json'
# Spawn a new process for the development Vault server and wait for it to come online
# ref: https://www.vaultproject.io/docs/concepts/dev-server
vault server -dev -dev-listen-address="0.0.0.0:8200" &
sleep 5s
# authenticate container's local Vault CLI
# ref: https://www.vaultproject.io/docs/commands/login
vault login -no-print "${VAULT_DEV_ROOT_TOKEN_ID}"
# add policy
# ref: https://www.vaultproject.io/docs/concepts/policies
vault policy write terraform-policy /vault/config/terraform-policy.hcl
vault policy write reseller1-policy /vault/config/reseller1-policy.hcl
# enable AppRole auth method
# ref: https://www.vaultproject.io/docs/auth/approle
vault auth enable approle
# configure AppRole
# ref: https://www.vaultproject.io/api/auth/approle#parameters
vault write auth/approle/role/dummy_role \
token_policies=reseller1-policy \
token_num_uses=9000 \
secret_id_ttl="32d" \
token_ttl="32d" \
token_max_ttl="32d"
# overwrite our role id
vault write auth/approle/role/dummy_role/role-id role_id="${APPROLE_ROLE_ID}"
# for terraform
# ref: https://www.vaultproject.io/docs/commands/token/create
vault token create \
-id="${TERRAFORM_TOKEN}" \
-policy=terraform-policy \
-ttl="32d"
# keep container alive
tail -f /dev/null & trap 'kill %1' TERM ; wait
with reseller1-policy.hcl
:
# This section grants access for the app
path "secret/data/dummy_config_yaml/reseller1/*" {
capabilities = ["read"]
}
path "secret/dummy_config_yaml/reseller1/*" { # v1
capabilities = ["read"]
}
and terraform-policy.hcl
:
# Grant 'update' permission on the 'auth/approle/role/<role_name>/secret-id' path for generating a secret id
path "auth/approle/role/dummy_role/secret-id" {
capabilities = ["update"]
}
path "secret/data/dummy_config_yaml/*" {
capabilities = ["create","update","read","patch","delete"]
}
path "secret/dummy_config_yaml/*" { # v1
capabilities = ["create","update","read","patch","delete"]
}
path "secret/metadata/dummy_config_yaml/*" {
capabilities = ["list"]
}
This was started with docker-compose.yml
:
version: '3.3'
services:
testvaultserver1:
build: ./vault-server/
cap_add:
- IPC_LOCK
environment:
VAULT_DEV_ROOT_TOKEN_ID: root
APPROLE_ROLE_ID: dummy_app
TERRAFORM_TOKEN: dummyTerraformToken
ports:
- "8200:8200"
then run some script copy_config2vault_secret2tmp.sh
on shell:
TERRAFORM_TOKEN=`cat docker-compose.yml | grep TERRAFORM_TOKEN | cut -d':' -f2 | xargs echo -n`
VAULT_ADDRESS="127.0.0.1:8200"
# retrieve secret for appsecret so dummy app can load the /tmp/secret
curl \
--request POST \
--header "X-Vault-Token: ${TERRAFORM_TOKEN}" \
--header "X-Vault-Wrap-TTL: 32d" \
"${VAULT_ADDRESS}/v1/auth/approle/role/dummy_role/secret-id" > /tmp/debug
cat /tmp/debug | jq -r '.wrap_info.token' > /tmp/secret
# check appsecret exists
cat /tmp/debug
cat /tmp/secret
VAULT_DOCKER=`docker ps| grep vault | cut -d' ' -f 1`
echo 'put secret'
cat config.yaml | docker exec -i $VAULT_DOCKER vault -v kv put -address=http://127.0.0.1:8200 -mount=secret dummy_config_yaml/reseller1/region99 raw=-
echo 'check secret length'
docker exec -i $VAULT_DOCKER vault -v kv get -address=http://127.0.0.1:8200 -mount=secret dummy_config_yaml/reseller1/region99 | wc -l
Then create a program to read the secret and retrieve the config.yaml
from vault:
package main
import (
"context"
"fmt"
"log"
"time"
vault "github.com/hashicorp/vault/api"
"github.com/hashicorp/vault/api/auth/approle"
)
const AppRoleID = `dummy_app`
func main() {
conf, err := TryUseVault(`http://127.0.0.1:8200`, `secret/data/dummy_config_yaml/reseller1/region99`)
if err != nil {
log.Println(err)
return
}
log.Println(conf)
}
func TryUseVault(address, configPath string) (string, error) {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
const secretFile = `/tmp/secret`
config := vault.DefaultConfig() // modify for more granular configuration
config.Address = address
client, err := vault.NewClient(config)
if err != nil {
return ``, fmt.Errorf(`failed to create vault client: %w`, err)
}
approleSecretID := &approle.SecretID{
FromFile: secretFile,
}
appRoleAuth, err := approle.NewAppRoleAuth(
AppRoleID,
approleSecretID,
approle.WithWrappingToken(), // only required if the SecretID is response-wrapped
)
if err != nil {
return ``, fmt.Errorf(`failed to create approle auth: %w`, err)
}
authInfo, err := client.Auth().Login(ctx, appRoleAuth)
if err != nil {
return ``, fmt.Errorf(`failed to login to vault: %w`, err)
}
if authInfo == nil {
return ``, fmt.Errorf(`failed to login to vault: authInfo is nil`)
}
log.Println("connecting to vault: success!")
secret, err := client.Logical().Read(configPath)
if err != nil {
return ``, fmt.Errorf(`failed to read secret from vault: %w`, err)
}
if secret == nil {
return ``, fmt.Errorf(`failed to read secret from vault: secret is nil`)
}
if len(secret.Data) == 0 {
return ``, fmt.Errorf(`failed to read secret from vault: secret.Data is empty`)
}
data := secret.Data[`data`]
if data == nil {
return ``, fmt.Errorf(`failed to read secret from vault: secret.Data.data is nil`)
}
m, ok := data.(map[string]interface{})
if !ok {
return ``, fmt.Errorf(`failed to read secret from vault: secret.Data.data is not a map[string]interface{}`)
}
raw, ok := m[`raw`]
if !ok {
return ``, fmt.Errorf(`failed to read secret from vault: secret.Data.data.raw is nil`)
}
rawStr, ok := raw.(string)
if !ok {
return ``, fmt.Errorf(`failed to read secret from vault: secret.Data.data.raw is not a string`)
}
// set viper from string
return rawStr, nil
}
it works fine, but the problem is, the secret can only be used once
$ ./copy_config2vault_secret2tmp.sh
{"request_id":"","lease_id":"","renewable":false,"lease_duration":0,"data":null,"wrap_info":{"token":"hvs.CAESIDSE_hR3-CW1CLLotQoVAhes55vI1MCDemmbWbsAvDS6Gh4KHGh2cy5QdzQ0bzlxRTJ6MUZJUFRoeGpSWFRzV0E","accessor":"7jLABMbzGVHKPCKAd7qkPx5J","ttl":2764800,"creation_time":"2023-07-18T19:34:48.619332723Z","creation_path":"auth/approle/role/dummy_role/secret-id","wrapped_accessor":"2493fc83-aaf6-7553-dd04-2ccedc39a4b1"},"warnings":null,"auth":null}
hvs.CAESIDSE_hR3-CW1CLLotQoVAhes55vI1MCDemmbWbsAvDS6Gh4KHGh2cy5QdzQ0bzlxRTJ6MUZJUFRoeGpSWFRzV0E
put secret
================== Secret Path ==================
secret/data/dummy_config_yaml/reseller1/region99
======= Metadata =======
Key Value
--- -----
created_time 2023-07-18T19:34:48.827508755Z
custom_metadata <nil>
deletion_time n/a
destroyed false
version 9
check secret length
19
retrieve it once works fine:
$ go run main.go
2023/07/19 02:35:52 connecting to vault: success!
2023/07/19 02:35:52
this:
is:
some:
secret: a35)*&BN)(*&%TN_@#
But when I run it second time, it always error (unless I run the get secret copy_config2vault_secret2tmp.sh
script again):
$ go run main.go
2023/07/19 02:36:06 failed to login to vault: unable to log in to auth method: unable to unwrap response wrapping token: Error making API request.
URL: PUT http://127.0.0.1:8200/v1/sys/wrapping/unwrap
Code: 400. Errors:
* wrapping token is not valid or does not exist
Is the secret ID can only be used only once by design? or if it's not, what's the possible cause of this?
答案1
得分: 2
包装令牌仅限单次使用。这就是为什么只有在运行Go应用程序之前执行copy_config2vault_secret2tmp.sh
时才起作用。
根据Vault文档的参考:
当新创建的令牌被包装时,Vault将生成的令牌插入到单次使用令牌的小隔间中,并返回该单次使用的包装令牌。检索密钥需要对该包装令牌进行解包操作。
这个特定部分解释了包装令牌的使用,可能有助于理解细节:
https://developer.hashicorp.com/vault/tutorials/secrets-management/cubbyhole-response-wrapping#step-2-unwrap-the-secret
英文:
Wrapping tokens are limited to single use only. That's why it works only when You execute copy_config2vault_secret2tmp.sh
before running Go app.
As a reference from Vault's documentation:
> When a newly created token is wrapped, Vault inserts the generated
> token into the cubbyhole of a single-use token, returning that
> single-use wrapping token. Retrieving the secret requires an unwrap
> operation against this wrapping token.
This specific part explaining use of wrapping token might help to understand the details:
https://developer.hashicorp.com/vault/tutorials/secrets-management/cubbyhole-response-wrapping#step-2-unwrap-the-secret
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论