Intro
There are times where solutions that consume Terraform require API access. This may be through a higher order system like a CI/CD tool or pipeline. It could also be something such as system that will pull statistics or metrics from an API. How can we generate role specific API keys? Vault’s new Terraform Secret Engine can help us here.
Before we get into that lets understand a couple of different token types
Token
User Tokens - each User can have a number of API tokens on their behalf. They will inherit all role based access based upon their identity.
Team Tokens - each team can an a single API token at a given time. This is designed for applies by a higher order system.
Organization Tokens - whilst only one at a time can be granted it is designed for automating team management, membership, and workspaces!
If you want to know more click here
Demo time
An example that will demonstrate how to setup the secrets engine in Vault then explore how something would retrieve a secret from Vault. We will do this via API and CLI.
The high level workflow is the same for either method and is performed in two parts
Administrative Workflow
- Preparation
- Create Teams
- Create initial token
- Authenticate to Vault
- Create Terraform Secret Engine
- Define a role for Pipelines
- Define a role for Consumption
- Define a user role for testing
Consumption Workflow
- Authenticate to Vault
- Retrieve Token
- Test against API
- Refresh lease
- Validate revocation
Administration Workflow
What we are doing here is one time configuration for the setup of this particular pattern.
Preparation - Terraform
There are a few steps we need to perform to start this off. Using the same token or an individual user token we need to setup the connection for Vault. I would recommend making a team and then using a specific team token as these are lazy rotated. This would mean that the team that binds Vault to Terraform won’t break or expire but can be rotated as needed.
Creating Teams
Perform the following in Terraform
- Create a team called
vault-terraform-token
- Create a
gitlab-pipeline
team - Ensure that both teams visibility is set to secret
To retrieve the team-id
you can select your newly created team and find the team-id
in the URL or use the following on the CLI
curl \
--header "Authorization: Bearer $TOKEN" \
--header "Content-Type: application/vnd.api+json" \
--request GET \
https://app.terraform.io/api/v2/organizations/$ORG/teams \
| jq '.data[] | .id + " " + .attributes.name'
Note Please use the envvar $TOKEN
with a token that can read all teams. Please define an $ORG
. In this case mine is burkey
.
The results should list all teams and jq
will concatenate two trings. The first is the team id
and then the team name
.
"team-bUPL36Yf9yJ9cbsx burkey-read-only"
"team-L2oUtQkF2gH4rBt2 gitlab-pipeline"
"team-Aw9YmqQN8sow7JmU owners"
"team-un4EYK9U1h5k2gtV burkey-outputs-only"
"team-J7eAmiNTAWTysLb4 relevent-team-name"
"team-cPuDYa4B5VRrmDsD vault-terraform-token"
The value I am after here is team-L2oUtQkF2gH4rBt2
. This will be used in our role
creation.
Generate Terraform/Vault token
There is a need for an authorartive token that allows Vault to programmatically drive Terraform. This should not be a user token give they’re bound to a user and subject to a TTL. Generate this token by selecting the team vault-terraform-token
and then under Team API Token
. Create a token.
Store this token as an envvar with export TF_TOKEN=<TOKEN>
.
Shopping list
You now should have the following created:
- Group called
gitlab-pipeline
- Group called
vault-terraform-token
- A team token created for the group
vault-terraform-token
.
Now - onto the creation of the Secrets Engine.
Create the secrets engine
Here we will create the secrets engine
vault secrets enable -path tfc terraform
I have decided to use the tfc
path for this Secrets Engine due to the fact I have a self-hosted terraform which I want to separate with a different Secret Engine name.
Now that we have a team token we generated against vault-terraform-token
we will use the envvar $TF_TOKEN
we set earlier.
vault write tfc/config token=$TF_TOKEN
Now that the engine is created we can create a number of roles depending on token type.
Create the Roles
Now I am creating the role for the gitlab-pipeline
team. I retrieved previously retrieved via the API team-L2oUtQkF2gH4rBt2
and stored it in $GHP_TOKEN
. I am also giving it a TTL of 2 minutes.
vault write tfc/role/gitlab-pipeline team_id=$GHP_TOKEN ttl=2m
Success! Data written to: tfc/role/gitlab-pipeline
Ace! Let’s confirm this.
vault read tfc/role/gitlab-pipeline
Key Value
--- -----
max_ttl 0s
name gitlab-pipeline
team_id team-L2oUtQkF2gH4rBt2
ttl 2m
CLI Secret Retrieval
The CLI method will be how humans and operators potentially interact with the terraform secrets engine.
vault read tfc/creds/burkey-org
Key Value
--- -----
organization burkey
role burkey-org
team_id n/a
token WAjxRdizm084qA.atlasv1.vdSGXwGn9dl2Hyea2DJyVaub9EhMw3SUisD702QxJ3WsCpTdR4R0h1G6tnnwYBwTFok
token_id at-Lv43GzevZK5mZycr
vault read tfc/creds/gitlab-pipeline
Key Value
--- -----
organization n/a
role gitlab-pipeline
team_id team-L2oUtQkF2gH4rBt2
token DQ9Fx0W3EEV1Ow.atlasv1.aeAvKQu6elAeVm1allHbsryVYod9gpjSvZyZwX9QVGAm59yt13x0YnGRpq7lNLXwJSA
token_id at-a8RCaYBVVtdDNaqJ
Now I need to rotate these credentials as they are valid! Cheeky people reading this may try to access my organization or team otherwise.
vault write -f tfc/rotate-role/burkey-org && vault write -f tfc/rotate-role/gitlab-pipeline
API Secret Retrieval
The API approach will be how other machines will retrieve their tokens for Terraform from Vault.
Here we retrieve the current token for the team role for gitlab-pipeline
. I then pipe it to jq
for easier reading and manipulation.
curl \
--header "X-Vault-Token: $VAULT_TOKEN" \
--header "X-Vault-Namespace: $VAULT_NAMESPACE" \
--request GET \
$VAULT_ADDR/v1/tfc/creds/gitlab-pipeline | jq
<SNIP>
{
"request_id": "2f45ecf0-adeb-068d-0ad0-62a734f6d173",
"lease_id": "",
"renewable": false,
"lease_duration": 0,
"data": {
"organization": "",
"role": "gitlab-pipeline",
"team_id": "team-L2oUtQkF2gH4rBt2",
"token": "DQ9Fx0W3EEV1Ow.atlasv1.aeAvKQu6elAeVm1allHbsryVYod9gpjSvZyZwX9QVGAm59yt13x0YnGRpq7lNLXwJSA",
"token_id": "at-a8RCaYBVVtdDNaqJ"
},
"wrap_info": null,
"warnings": null,
"auth": null
}
Now I want to place this into an environment variable so that it can be consumed without being written to disk
export RETRIEVED_TOKEN=$(curl \
--header "X-Vault-Token: $VAULT_TOKEN" \
--header "X-Vault-Namespace: $VAULT_NAMESPACE" \
--request GET \
$VAULT_ADDR/v1/tfc/creds/gitlab-pipeline \
| jq -r .data.token )
echo $RETRIEVED_TOKEN
DQ9Fx0W3EEV1Ow.atlasv1.aeAvKQu6elAeVm1allHbsryVYod9gpjSvZyZwX9QVGAm59yt13x0YnGRpq7lNLXwJSA
Given that I just showed you the value of a working API token, I best rotate this as I dont want any of you scallywag readers to access my org!
Rotate Team Token
Rotating is a Team or Organization token is the method of refreshing. This lazy rotation
curl \
--header "X-Vault-Token: $VAULT_TOKEN" \
--header "X-Vault-Namespace: $VAULT_NAMESPACE" \
--request POST \
$VAULT_ADDR/v1/tfc/rotate-role/gitlab-pipeline
Now if we are to retrieve it again it will not be the same as the previous value in $RETRIEVED_TOKEN
Access Control
What’s super about this is that based on my authentication method I can control who accesses these engines. I would want to ensure whatever role I had for my authentication method included the following for the consumer.
path "tfc/creds/burkey-user" {
capabilities = [ "read" ]
}
path "tfc/creds/gitlab-pipeline" {
capabilities = [ "read", "update" ]
}
path "tfc/creds/burkey-org" {
capabilities = [ "read", "update" ]
}
What this role does is that for this explicit path allow the following actions to be performed. I chose not to do a tfc/creds/*
wildcard as I wanted a specific set of child paths to be accessible. This was burkey-user
, gitlab-pipeline
, and burkey-org
. If there was a grant-org
then I would need a path configured I would need something to explicitly grant me permission! In this case it is read and write to allow read to retrieve a token and write for the rotation.
An Administrative policy would need additions that pertained to the following:
# Base permissions to actually mount the engine
path "sys/mounts/*" {
capabilities = [ "create", "read", "update", "delete", "list" ]
}
# permissions to create the tfc engine
path "tfc/*" {
capabilities = [ "create", "read", "update", "delete", "list" ]
}
These should be used in conjunction with other permissions that allow lease lifecycling.
Summary
This Vault secrets engine allows creation of specific user, team, or organization API tokens. This means that a higher order system such as a pipeline that wants to interact with a commercial offering of Terraform can generate an on demand API token. The role specific within Vault ensures it is managed accordingly to a specific TTL if a user token, and lazily rotated if an organization or team token.