Search code examples
govault

How to make vault Secret ID can be reused multiple times?


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?


Solution

  • 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