Hashicorp Vault’s Identity Secrets Engine acts as an OIDC provider and can provide any Vault
user an OpenID token (id_token
).
GCP Workload Identity Federation allows you to configure and exchange an external identity sources for one that can be used to access GCP services.
In other words, you should be able to exchange a Vault identity source directly with GCP and access GCP without resorting to using Vault GCP Secrets Engine.
Well, sure, but the implementation details for this makes it impractical and this tutorial pretty academic:
The reason being is that for GCP Workload Federation to use an external source as an OIDC provider, GCP needs a way to access the identity providers oidc .well-known/openid-configuration
endpoint.
In otherwords, https://your_vault_server/v1/identity/oidc/.well-known/openid-configuration
needs to be publicly accessible by GCP.
Thats a non starter almost for everyone but this is my blog and i can write about what i choose.
So i’m going to make this tutorial even more unrealistic: we’re going to run vault
on your laptop and expose its openID discovery endpoint using ngrok
. I’m only doing this for simplicity…if you actually have vault or the well-known
endpoint exposed publicly somehow, you dont’ need to do this silly stuff with ngrok.
For more information, see
This is what you’ll need for this tutorial:
First start ngrok and get the allocated endpoint.
This endpoint is dynamic and you have 1hour to do this tutorial. (pressure..no worries, i did the steps here)
./ngrok http localhost:8200
# note down the https discovery endpoint and export it
export DISCOVERY_URL="https://random_id.ngrok.io"
Start Vault in dev mode
vault server --dev -dev-root-token-id root
## please be aware your vault server is now *visible* to the internet as $DISCOVERY_URL
# in a new window, attach to the root token and configure
export VAULT_ADDR='http://localhost:8200'
export VAULT_TOKEN=root
## Configure the OIDC issuer
vault write identity/oidc/config issuer=$DISCOVERY_URL
vault read identity/oidc/config
## check if the Discovery Endpoint works externally
curl -s $DISCOVERY_URL/v1/identity/oidc/.well-known/openid-configuration | jq '.'
curl -s $DISCOVERY_URL/v1/identity/oidc/.well-known/keys | jq '.'
# enable the username password authentication for vault
# you can use any auth but for this tutorial, w'ere just going to use something basic
vault auth enable userpass
# create a policy to read a token in a role
vault policy write config-policy - <<EOF
path "identity/oidc/token/role-001" {
capabilities = [ "create", "read" ]
}
path "/auth/token/create" {
capabilities = ["create"]
}
path "auth/token/renew" {
capabilities = ["update", "create"]
}
path "auth/token/lookup-self" {
capabilities = ["read"]
}
path "auth/token/renew" {
capabilities = ["update", "create"]
}
path "auth/token/lookup-accessor" {
capabilities = [ "read", "update" ]
}
EOF
## create a user with a given password and allow alice to get
## a token for the "config-policy" we just defined
vault write auth/userpass/users/alice \
password="password" \
token_ttl="1h" \
policies=config-policy
## create an identity entity (this is a vault artifact that allows identity token mappings)
vault write identity/entity \
name="alice" \
metadata="email=alice@domain.com" \
metadata="phone_number=123-456-7890" \
disabled=false
export ENTITY_ID=$(vault read -field=id identity/entity/name/alice)
## also create an "engineering" group
vault write identity/group \
name="engineering" \
member_entity_ids="$ENTITY_ID"
export GROUP_ID=$(vault read -field=id identity/group/name/engineering)
export USERPASS_ACCESSOR=$(vault auth list -detailed -format json | jq -r '.["userpass/"].accessor')
echo $ENTITY_ID
echo $GROUP_ID
echo $USERPASS_ACCESSOR
# now alias the identity (i.,e associate alice with the stuff we just defined)
vault write identity/entity-alias \
name="alice" \
canonical_id="$ENTITY_ID" \
mount_accessor="$USERPASS_ACCESSOR"
# create a key
vault write identity/oidc/key/my-key \
allowed_client_ids="*" \
verification_ttl="2h" \
rotation_period="1h" \
algorithm="RS256"
# define the role alice can use and the template for that role's id_tokens
## the id_tokens will contain the claims below
export USER_TEMPLATE=$(cat << EOF
{
"username": {{identity.entity.name}},
"contact": {
"email": {{identity.entity.metadata.email}},
"phone_number": {{identity.entity.metadata.phone_number}}
},
"groups": {{identity.entity.groups.names}}
}
EOF
)
vault write identity/oidc/role/role-001 \
description="The testrole" \
key=my-key \
ttl=1h \
template="$(echo ${USER_TEMPLATE} | base64 -)"
vault read identity/oidc/role/role-001
export CLIENT_ID=`vault read -field client_id identity/oidc/role/role-001`
The role output will contain something like this:
vault read identity/oidc/role/role-001
Key Value
--- -----
client_id PvnKrIhX5lAZ2gBhjnZhhbf3ez
key my-key
template { "username": {{identity.entity.name}}, "contact": { "email": {{identity.entity.metadata.email}}, "phone_number": {{identity.entity.metadata.phone_number}} }, "groups": {{identity.entity.groups.names}} }
ttl 1h
the id_token that vault will issue will have the audience aud:
value set to PvnKrIhX5lAZ2gBhjnZhhbf3ez
(yours will be different)
Now in a new window, login to vault as alice
export VAULT_ADDR='http://localhost:8200'
vault login -method=userpass \
username=alice \
password=password
# read back the id_token
vault read identity/oidc/token/role-001
The output will return the user’s id_token
Key Value
--- -----
client_id PvnKrIhX5lAZ2gBhjnZhhbf3ez
token eyJhbGciOiJSUzI1NiIsImtpZCI6ImYxMWM1MDJmLWFmYzUtZTc0ZS0wYzAyLWNhNzdmNzA2MTFjMSJ9.eyJhdWQiOiJQdm5LckloWDVsQVoyZ0Joam5aaGhiZjNleiIsImNvbnRhY3QiOnsiZW1haWwiOiJhbGljZUBkb21haW4uY29tIiwicGhvbmVfbnVtYmVyIjoiMTIzLTQ1Ni03ODkwIn0sImV4cCI6MTY0OTcwMzU3NywiZ3JvdXBzIjpbImVuZ2luZWVyaW5nIl0sImlhdCI6MTY0OTY5OTk3NywiaXNzIjoiaHR0cHM6Ly9iMDY0LTcyLTgzLTY3LTE3NC5uZ3Jvay5pby92MS9pZGVudGl0eS9vaWRjIiwibmFtZXNwYWNlIjoicm9vdCIsInN1YiI6IjBjYThlZTFmLTUyNTYtYTQ5OC1jMTdlLTNmOGM2YTI0YTBmNCIsInVzZXJuYW1lIjoiYWxpY2UifQ.HvgHnYaHfsCU7O9nnRxz3V_gUS2MZi2bO_od8dgkZ1r_7Uarx7iiVRLMUyPb-DIFfZxISGHWajDA3uWLXSycsQvy5-amYszxTzZeKNF8h-YOpIidybnTYIgHyYgNQ33NBFvivpXSy90Q1SqHZxW6NhrHk4jKyQJBb-v8CM8j2rTAsimy1PLK5bnNlKJRposYGRXI-K7EIPxvRVP2uwThSs4HOD5MQGHYi3SWlpd7IDhZ54q3uj60ZrF6nWtZvH8WS-l-fjflC1l9PYfnNuJKWX8OIb85HJs476YOG5gt_Zye_CFZOjfNBc-F_xZlIPVElzgWMxCd-KpQIgFgfJdBvw
ttl 1h
The oidc token is in this format. Note the aud
is the client_id and the sub
uniquely represents alice
: 0ca8ee1f-5256-a498-c17e-3f8c6a24a0f4
.
$ vault token lookup
Key Value
--- -----
accessor G7i0sBdvM9fAaPTscg6XJIxk
creation_time 1649699950
creation_ttl 1h
display_name userpass-alice
entity_id 0ca8ee1f-5256-a498-c17e-3f8c6a24a0f4 <<<<<<<<<
expire_time 2022-04-11T14:59:10.264446178-04:00
explicit_max_ttl 0s
id s.rqgInsjuooeCo5j0lSohl3uG
issue_time 2022-04-11T13:59:10.264450729-04:00
meta map[username:alice]
num_uses 0
orphan true
path auth/userpass/login/alice
policies [config-policy default]
renewable true
ttl 55m22s
type service
We’re going to directly bind alice’s sub
filed here (its very specific binding)
{
"alg": "RS256",
"kid": "f11c502f-afc5-e74e-0c02-ca77f70611c1"
}
{
"aud": "PvnKrIhX5lAZ2gBhjnZhhbf3ez",
"contact": {
"email": "alice@domain.com",
"phone_number": "123-456-7890"
},
"exp": 1649703577,
"groups": [
"engineering"
],
"iat": 1649699977,
"iss": "https://b064-72-83-67-174.ngrok.io/v1/identity/oidc",
"namespace": "root",
"sub": "0ca8ee1f-5256-a498-c17e-3f8c6a24a0f4",
"username": "alice"
}
Now w’ere going to configure workload identity.
Back on the window where you are logged in as admin (not alice), configure a federation config just for alice
export ALICE="0ca8ee1f-5256-a498-c17e-3f8c6a24a0f4"
export CLIENT_ID=`vault read -field client_id identity/oidc/role/role-001`
export PROJECT_ID=`gcloud config get-value core/project`
export PROJECT_NUMBER=`gcloud projects describe $PROJECT_ID --format='value(projectNumber)'`
gcloud beta iam workload-identity-pools create pool-vault \
--location="global" \
--description="Vault OIDC Pool" \
--display-name="Vault OIDC Pool" --project $PROJECT_ID
gcloud beta iam workload-identity-pools providers create-oidc oidc-provider-vault-1 \
--workload-identity-pool=pool-vault \
--issuer-uri="$DISCOVERY_URL/v1/identity/oidc" \
--location="global" \
--attribute-mapping="google.subject=assertion.sub,attribute.aud=assertion.aud[0]" \
--allowed-audiences="$CLIENT_ID" \
--project $PROJECT_ID
Now allow the the federated token for alice access to a GCS bucket
# create a test bucket with uniform bucket policy
## i can't get the single command with the -b on working so omit that and use the cloud console..
gsutil mb -b on gs://$PROJECT_ID-vault-federated
echo -n "foobar" > /tmp/foo.txt
gsutil cp /tmp/foo.txt gs://$PROJECT_ID-vault-federated
gsutil iam ch \
principal://iam.googleapis.com/projects/$PROJECT_NUMBER/locations/global/workloadIdentityPools/pool-vault/subject/$ALICE:objectViewer \
gs://$PROJECT_ID-vault-federated
Please note that we are using the federated token without impersonation
export OIDC_TOKEN=`vault read -field token identity/oidc/token/role-001`
# get the token manually
export FEDERATED_TOKEN=`curl -s -X POST -d "grant_type=urn:ietf:params:oauth:grant-type:token-exchange" \
-d "audience=//iam.googleapis.com/projects/$PROJECT_NUMBER/locations/global/workloadIdentityPools/pool-vault/providers/oidc-provider-vault-1" \
-d "subject_token_type=urn:ietf:params:oauth:token-type:jwt" \
-d "requested_token_type=urn:ietf:params:oauth:token-type:access_token" \
-d "scope=https://www.googleapis.com/auth/cloud-platform" \
-d "subject_token=$OIDC_TOKEN" https://sts.googleapis.com/v1beta/token | jq -r '.access_token'`
echo $FEDERATED_TOKEN
Now w’ere finally going to access the gcs object
curl -H "Authorization: Bearer $FEDERATED_TOKEN" https://storage.googleapis.com/storage/v1/b/$PROJECT_ID-vault-federated/o/foo.txt?alt=media
foobar
done…we’ve just used vault’s own provider to access a storage bucket without vault directly issuing a GCP access_token
and…as for ngrok, i had enough time to spare
This site supports webmentions. Send me a mention via this form.