如何使保险库(vault)的秘密ID可以多次重复使用?

huangapple go评论72阅读模式
英文:

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 [ &quot;/init_vault.sh&quot; ]

HEALTHCHECK \
    --start-period=5s \
    --interval=1s \
    --timeout=1s \
    --retries=30 \
        CMD [ &quot;/bin/sh&quot;, &quot;-c&quot;, &quot;[ -f /tmp/healthy ]&quot; ]

init_vault.sh contains:

#!/bin/sh

set -e

export VAULT_ADDR=&#39;http://127.0.0.1:8200&#39;
export VAULT_FORMAT=&#39;json&#39;

# 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=&quot;0.0.0.0:8200&quot; &amp;
sleep 5s

# authenticate container&#39;s local Vault CLI
# ref: https://www.vaultproject.io/docs/commands/login
vault login -no-print &quot;${VAULT_DEV_ROOT_TOKEN_ID}&quot;

# 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=&quot;32d&quot; \
    token_ttl=&quot;32d&quot; \
    token_max_ttl=&quot;32d&quot;

# overwrite our role id
vault write auth/approle/role/dummy_role/role-id role_id=&quot;${APPROLE_ROLE_ID}&quot;

# for terraform
# ref: https://www.vaultproject.io/docs/commands/token/create
vault token create \
    -id=&quot;${TERRAFORM_TOKEN}&quot; \
    -policy=terraform-policy \
    -ttl=&quot;32d&quot;

# keep container alive
tail -f /dev/null &amp; trap &#39;kill %1&#39; TERM ; wait

with reseller1-policy.hcl:

# This section grants access for the app
path &quot;secret/data/dummy_config_yaml/reseller1/*&quot; {
  capabilities = [&quot;read&quot;]
}

path &quot;secret/dummy_config_yaml/reseller1/*&quot; { # v1
  capabilities = [&quot;read&quot;]
}

and terraform-policy.hcl:

# Grant &#39;update&#39; permission on the &#39;auth/approle/role/&lt;role_name&gt;/secret-id&#39; path for generating a secret id
path &quot;auth/approle/role/dummy_role/secret-id&quot; {
  capabilities = [&quot;update&quot;]
}

path &quot;secret/data/dummy_config_yaml/*&quot; {
  capabilities = [&quot;create&quot;,&quot;update&quot;,&quot;read&quot;,&quot;patch&quot;,&quot;delete&quot;]
}

path &quot;secret/dummy_config_yaml/*&quot; { # v1
  capabilities = [&quot;create&quot;,&quot;update&quot;,&quot;read&quot;,&quot;patch&quot;,&quot;delete&quot;]
}

path &quot;secret/metadata/dummy_config_yaml/*&quot; {
  capabilities = [&quot;list&quot;]
}

This was started with docker-compose.yml:


version: &#39;3.3&#39;
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:
      - &quot;8200:8200&quot;

then run some script copy_config2vault_secret2tmp.sh on shell:

TERRAFORM_TOKEN=`cat docker-compose.yml | grep TERRAFORM_TOKEN | cut -d&#39;:&#39; -f2 | xargs echo -n`
VAULT_ADDRESS=&quot;127.0.0.1:8200&quot;

# retrieve secret for appsecret so dummy app can load the /tmp/secret
curl \
   --request POST \
   --header &quot;X-Vault-Token: ${TERRAFORM_TOKEN}&quot; \
   --header &quot;X-Vault-Wrap-TTL: 32d&quot; \
      &quot;${VAULT_ADDRESS}/v1/auth/approle/role/dummy_role/secret-id&quot; &gt; /tmp/debug

cat /tmp/debug | jq -r &#39;.wrap_info.token&#39; &gt; /tmp/secret

# check appsecret exists
cat /tmp/debug
cat /tmp/secret

VAULT_DOCKER=`docker ps| grep vault | cut -d&#39; &#39; -f 1`

echo &#39;put secret&#39;
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 &#39;check secret length&#39;
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 (
	&quot;context&quot;
	&quot;fmt&quot;
	&quot;log&quot;
	&quot;time&quot;

	vault &quot;github.com/hashicorp/vault/api&quot;
	&quot;github.com/hashicorp/vault/api/auth/approle&quot;
)

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 := &amp;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(&quot;connecting to vault: success!&quot;)

	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 
{&quot;request_id&quot;:&quot;&quot;,&quot;lease_id&quot;:&quot;&quot;,&quot;renewable&quot;:false,&quot;lease_duration&quot;:0,&quot;data&quot;:null,&quot;wrap_info&quot;:{&quot;token&quot;:&quot;hvs.CAESIDSE_hR3-CW1CLLotQoVAhes55vI1MCDemmbWbsAvDS6Gh4KHGh2cy5QdzQ0bzlxRTJ6MUZJUFRoeGpSWFRzV0E&quot;,&quot;accessor&quot;:&quot;7jLABMbzGVHKPCKAd7qkPx5J&quot;,&quot;ttl&quot;:2764800,&quot;creation_time&quot;:&quot;2023-07-18T19:34:48.619332723Z&quot;,&quot;creation_path&quot;:&quot;auth/approle/role/dummy_role/secret-id&quot;,&quot;wrapped_accessor&quot;:&quot;2493fc83-aaf6-7553-dd04-2ccedc39a4b1&quot;},&quot;warnings&quot;:null,&quot;auth&quot;: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    &lt;nil&gt;
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)*&amp;BN)(*&amp;%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

huangapple
  • 本文由 发表于 2023年7月19日 03:41:48
  • 转载请务必保留本文链接:https://go.coder-hub.com/76716052.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定