In this tutorial we show how to use SPIRE and OIDC to authenticate workloads to retrieve Vault secrets.
This tutorial is based on the SPIFFE documentation ("Using SPIRE JWT-SVIDs to Authenticate to Hashicorp Vault")
This part of the tutorial assumes that OIDC is already enabled on SPIRE
For the purpose of this tutorial you can start your own instance of Vault as described here or you can start the simple Vault instance as described in TSI documentation Setup Vault
Obtain the VAULT_ADDR and ROOT_TOKEN as documented. (ROOT_TOKEN is displayed in the log file)
export VAULT_ADDR=http://<Ingress_or_external_route>
export ROOT_TOKEN=$(kubectl -n tsi-vault logs $(kubectl -n tsi-vault get po | grep tsi-vault-| awk '{print $1}') | grep Root | cut -d' ' -f3); echo "export ROOT_TOKEN=$ROOT_TOKEN"
Now test the connection to Vault:
vault login -no-print "${ROOT_TOKEN}"
We have a script examples/spire/vault-oidc.sh that configures the Vault instance with the required demo configuration, but before we run it, let's first explain what happens.
All the commands listed here are in the script, so don't run them!
First few commands enable the Secret Engine and setup Vault OIDC Federation with our instance of SPIRE.
# Enable the kv (key-value) secrets engine on the secret/ path:
vault secrets enable -path=secret kv
# Enable the JWT authentication method:
vault auth enable jwt
Set up our OIDC Discovery URL, using the values created in OIDC tutorial setup and using defalt role dev:
vault write auth/jwt/config oidc_discovery_url=$OIDC_URL default_role=“dev”
Define a policy my-dev-policy
that gives read
access to my-super-secret
:
cat > vault-policy.hcl <<EOF
path "secret/data/my-super-secret" {
capabilities = ["read"]
}
EOF
# create this policy:
vault policy write my-dev-policy ./vault-policy.hcl
Create eurole
role that allows 1h access to the above policy only to applications
running in EU region (eu-*), in any cluster, in any namespace, under elon-musk
service account and mars-mission-main
container.
bound_subject
does not allow using wildcards, so we use bound_claims
instead:
cat > role.json <<EOF
{
"role_type":"jwt",
"user_claim": "sub",
"bound_audiences": "vault",
"bound_claims_type": "glob",
"bound_claims": {
"sub":"spiffe://openshift.space-x.com/region/*/cluster_name/*/ns/*/sa/elon-musk/pod_name/mars-mission-*"
},
"token_ttl": "1h",
"token_policies": "my-dev-policy"
}
EOF
vault write auth/jwt/role/marsrole -<role.json
We are ready to run the setup script examples/spire/vault-oidc.sh
Please make sure the following env. variables are set:
- OIDC_URL
- ROOT_TOKEN
- VAULT_ADDR
or pass them as script parameters:
examples/spire/vault-oidc.sh
# or
examples/spire/vault-oidc.sh <OIDC_URL> <ROOT_TOKEN> <VAULT_ADDR>
Here is our example:
examples/spire/vault-oidc.sh https://oidc-tornjak.space-x01-9d995c4a8c7c5f281ce13d546a94-0000.us-east.containers.appdomain.cloud $ROOT_TOKEN $VAULT_ADDR
Once the script successfully completes, create a test secret value:
vault kv put secret/my-super-secret test=123
Put a sample file into Vault:
vault kv put secret/config.json @config.json
For testing this setup we are going to use the test deployment file examples/spire/mars-demo.yaml
This container already has AWS S3 cli, Vault client and the SPIRE agent binaries for running this experiment.
This example has a following label:
template:
metadata:
labels:
identity_template: "true"
This label indicates that pod gets its identity in format defined in the K8s workload registrar configuration file k8s-workload-registrar-configmap.tpl
The default format is:
identity_template = "{{ "region/{{.Context.Region}}/cluster_name/{{.Context.ClusterName}}/ns/{{.Pod.Namespace}}/sa/{{.Pod.ServiceAccount}}/pod_name/{{.Pod.Name}}" }}"
Update the mars-demo
deployment file with the following attributes:
- VAULT_ADDR - Vault address as obtained earlier during the Vault setup
- VAULT_ROLE - Vault role used during setup
- VAULT_SECRET - Secret name and location
Example:
- name: VAULT_ADDR
value: "http://tsi-kube01-9d995c4a8c7c5f281ce13d5467ff6a94-0000.us-south.containers.appdomain.cloud"
- name: VAULT_ROLE
value: "marsrole"
- name: VAULT_SECRET
value: "/v1/secret/data/my-super-secret"
Let's create a pod and get inside the container:
kubectl -n default create -f examples/spire/mars-demo.yaml
kubectl -n default get po
NAME READY STATUS RESTARTS AGE
mars-mission-97745ff46-mmzpb 1/1 Running 0 6h8m
kubectl -n default exec -it mars-mission-97745ff46-mmzpb -- sh
Once inside, let's run the demo-vault.sh script that contains demoscript to execute the demo commands. (Use the space bar to drive the script steps.)
root@ip-192-168-62-164:/usr/local/bin# ./demo-vault.sh
$ /opt/spire/bin/spire-agent api fetch jwt -audience vault -socketPath /run/spire/sockets/agent.sock
This operation retrieves the SPIFFE id for this pod with its JWT representation. e.g:
token(spiffe://openshift.space-x.com/region/us-east-1/cluster_name/aws-tsi-test-03/ns/default/sa/elon-musk/pod_name/mars-mission-f5844b797-br5w9)
. . . .
Then we capture the JWT into token.jwt
file. We use vault
as audience field.
$ /opt/spire/bin/spire-agent api fetch jwt -audience vault -socketPath /run/spire/sockets/agent.sock | sed -n '2p' | xargs > token.jwt
JWT=$(cat token.jwt)
Using the captured JWT, we try to login to Vault and get the authentication token for this identity:
$ curl --max-time 10 -s -o vout --request POST --data '{"jwt": "${JWT}", "role": "${VAULT_ROLE}" }' http://tsi-kube01-9d995c4a8c7c5f281ce13d5467ff6a94-0000.us-south.containers.appdomain.cloud/v1/auth/jwt/login
$ TOKEN=$(cat vout | jq -r ".auth.client_token")
Using this authentication token we request the secret:
$ curl -s -H "X-Vault-Token: $TOKEN" http://tsi-kube01-9d995c4a8c7c5f281ce13d5467ff6a94-0000.us-south.containers.appdomain.cloud/v1/secret/data/my-super-secret | jq -r '.data.data'
{
"test": "123"
}