kind create cluster
kubectl apply -f cli.yaml
kubectl apply -f discovery.yaml
If you don't opt out of it, every pod by default gets a token in a projected volume. You can check this out in the pod definition:
kubectl get pods --selector "app=cli" -o json | jq '.items[].spec.volumes[]'
Output
{
"name": "kube-api-access-kdlt5",
"projected": {
"defaultMode": 420,
"sources": [
{
"serviceAccountToken": {
"expirationSeconds": 3607,
"path": "token"
}
},
{
"configMap": {
"items": [
{
"key": "ca.crt",
"path": "ca.crt"
}
],
"name": "kube-root-ca.crt"
}
},
{
"downwardAPI": {
"items": [
{
"fieldRef": {
"apiVersion": "v1",
"fieldPath": "metadata.namespace"
},
"path": "namespace"
}
]
}
}
]
}
}
The token is mounted at /var/run/secrets/kubernetes.io/serviceaccount/token
:
kubectl get pods --selector "app=cli" -o json | jq '.items[].spec.containers[].volumeMounts[]'
Output
{
"mountPath": "/var/run/secrets/kubernetes.io/serviceaccount",
"name": "kube-api-access-kdlt5",
"readOnly": true
}
We can then inspect the contents of the mounted token.
kubectl exec -it $(kubectl get pods --selector "app=cli" --no-headers) -- cat /var/run/secrets/kubernetes.io/serviceaccount/token > token
You can decode the token easily with jq
:
jq -R '.' token | jq 'split(".")|{header: .[0]|@base64d|fromjson, payload: .[1]|@base64d|fromjson}'
Output
{
"header": {
"alg": "RS256",
"kid": "OAjVVaejWFc0Yt9ykr0_8lMMuRNs67OXTWHsN02Pkyw"
},
"payload": {
"aud": [
"https://localhost:6443"
],
"exp": 1660853296,
"iat": 1629317296,
"iss": "https://localhost:6443",
"kubernetes.io": {
"namespace": "default",
"pod": {
"name": "cli-6fdfcfd5c8-8f7f7",
"uid": "262a6af2-a6a8-4e0a-bdf9-a47f8f845c46"
},
"serviceaccount": {
"name": "jwt-test",
"uid": "272e2776-0648-448a-a166-848d0742abf2"
},
"warnafter": 1629320903
},
"nbf": 1629317296,
"sub": "system:serviceaccount:default:jwt-test"
}
}
You can see that the token has a sub
claim that contains information on the service
account (namespace and name).
It also contains an iss
claim as well, this is an URL where you would expose
the cluster issuer discovery endpoint.
It also has a kid
in the header that tells us what key to look up in the discovery
endpoint to use to verify the signature of the JWT.
Let's try and read the discovery URL of the cluster. We need to look at a well-known path of the issuer URL:
curl -s -k https://localhost:6443/.well-known/openid-configuration | jq '.'
Output
{
"issuer": "https://localhost:6443",
"jwks_uri": "https://localhost:6443/openid/v1/jwks",
"response_types_supported": [
"id_token"
],
"subject_types_supported": [
"public"
],
"id_token_signing_alg_values_supported": [
"RS256"
]
}
The discovery document contains a jwks_uri
field.
If we query that URI we get back a JSON Web Key Set (JWKS).
curl -s -k $(curl -s -k https://localhost:6443/.well-known/openid-configuration | jq -r '.jwks_uri') | jq '.'
Output
{
"keys": [
{
"use": "sig",
"kty": "RSA",
"kid": "OAjVVaejWFc0Yt9ykr0_8lMMuRNs67OXTWHsN02Pkyw",
"alg": "RS256",
"n": "mAZAXERdLFYDNMywmze9xeInj4BWleUhMRVM1Dx4k8nCmgvsPVbLQiz013TfTEb00EEifILZ4Ji-hyqAkd555QaaX_LuhHYUdSVTKzIVIm8sDSglBMROkmVhFiYUF9uDK4Kl8khzLcJOchT2o9UJSnfp56Ms_q7ZIuL0P6WeCOcma-4jU-NPQtt5AhWcibnvnAg-3cemzG7BawNtAzYSVHhPUgWYVdsGJy2PqN7QUA6aIsAgmgBCv1eAcw2b3bb1kQLg_5eFxPMauwDf7CfjUcC-5NnAv9PDpJ7H10B5d1qpdtfUNJcSSZ8KnJQzLmcJeVDicOUGKx0-paK9s6zkvQ",
"e": "AQAB"
}
]
}
Notice that in the keys
array you should be able to find the same the kid
we had in the JWT. That key must be used to verify the signature of the
JWT token.