# Does Vault check for the audience when using the Kubernetes auth method?
> **⚠️ WARNING: ⚠️** this document is mostly wrong. I mixed up the paramater `audiences=` with the correct `audience=`. Since Vault doesn't validate parameters (at least not in the Kubernetes plugin), I didn't realize that `audiences=` was ignored entirely. Thanks to Tom Proctor for finding this out ([comment](https://github.com/hashicorp/vault-plugin-auth-kubernetes/issues/175#issuecomment-1370085799)).
**tl;dr:** Vault 1.12 and below have a bug that prevents checking the JWT audience. The issue is documented in [vault-plugin-auth-kubernetes#175](https://github.com/hashicorp/vault-plugin-auth-kubernetes/issues/175). If you configure a Kubernetes auth role with the parameter [`audience`](https://developer.hashicorp.com/vault/api-docs/auth/kubernetes) set to `vault`, that parameter won't do anything. Any audience will be accepted by the role:
```bash
vault write auth/kubernetes/role/cert-manager \
bound_service_account_names=vault \
bound_service_account_namespaces=cert-manager \
audience=vault # <-- ignored 🔥
```
**Context:** as part of [The Vault issuer can now be given a serviceAccountRef (PR 5502)](https://github.com/jetstack/cert-manager/pull/5502), I had a doubt: does Vault verify the audience (`aud` value in the JWT) when using the Kubernetes auth method?
The Vault documentation [says](https://developer.hashicorp.com/vault/api-docs/auth/kubernetes):
> `audience` (string: "") - **Optional** Audience claim to verify in the JWT.
This suggests that Vault checks the audience, probably by passing the `audience` parameter to the [TokenReview](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.26/#tokenreview-v1-authentication-k8s-io) request. When the parameter `audiences` is left empty, we can assume that the TokenReview is performed with `audiences: []`, meaning that the audience is expected to be `https://kubernetes.default.svc.cluster.local` for example.
The TokenReview documentation makes it clear that in case of an empty `audiences` array, the `aud` is matched with the Kubernetes API server's audience:
> `audiences`, string array: Audiences is a list of the identifiers that the resource server presented with the token identifies as. Audience-aware token authenticators will verify that the token was intended for at least one of the audiences in this list. **If no audiences are provided, the audience will default to the audience of the Kubernetes apiserver**.
To check that the TokenReview API works this way, let us create a token with some audience:
```shell
token=$(kubectl create --raw /api/v1/namespaces/default/serviceaccounts/default/token -f- <<EOF | jq -r '.status.token'
{
"apiVersion": "authentication.k8s.io/v1",
"kind": "TokenRequest",
"spec": {
"audiences": ["vault"]
}
}
EOF
)
```
Then, let's review that token with an empty `audiences`, which reproduces what Vault does when `audiences` is left empty:
```shell
kubectl create --raw /apis/authentication.k8s.io/v1/tokenreviews -f- <<EOF | jq
{
"apiVersion": "authentication.k8s.io/v1",
"kind": "TokenReview",
"spec": {
"token": "$token",
"audiences": []
}
}
EOF
```
As suspected, the TokenReview fails:
> invalid bearer token, token audiences ["vault"] is invalid for the target audiences ["https://kubernetes.default.svc.cluster.local"].
Now, let's see how Vault behaves when the parameter `audiences=` isn't used when configuring the role. First, I install Vault:
```shell
kind cluster create
helm upgrade --install vault hashicorp/vault --set server.dev.enabled=true --set server.logLevel=debug --set global.tlsDisable=true --wait
kubectl exec vault-0 -i -- vault auth enable kubernetes
kubectl exec vault-0 -i -- sh -c 'vault write auth/kubernetes/config \
token_reviewer_jwt="$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)" \
kubernetes_host="https://$KUBERNETES_PORT_443_TCP_ADDR:443" \
kubernetes_ca_cert=@/var/run/secrets/kubernetes.io/serviceaccount/ca.crt \
issuer=https://kubernetes.default.svc.cluster.local'
```
Let's create a role without using the `audiences` parameter:
```shell
kubectl exec vault-0 -i -- vault write auth/kubernetes/role/empty-audience \
bound_service_account_names=default \
bound_service_account_namespaces=default
```
Let's get a token with some audience:
```shell
token=$(kubectl create --raw /api/v1/namespaces/default/serviceaccounts/default/token -f- <<EOF | jq -r '.status.token'
{
"apiVersion": "authentication.k8s.io/v1",
"kind": "TokenRequest",
"spec": {"audiences": ["vault"]}
}
EOF
)
```
From the above results, we should get an error since "no audience in token review call = the audience must be the Kubernetes API server's audience".
But the TokenReview works:
```console
$ kubectl exec vault-0 -i -- vault write auth/kubernetes/login role=empty-audience jwt="$token"
Key Value
--- -----
token hvs.CAESICj...kW6jr8rl-G
```
Now, let's see if a mismatching audience is caught:
```shell
kubectl exec vault-0 -i -- vault write auth/kubernetes/role/with-audience \
bound_service_account_names=default \
bound_service_account_namespaces=default \
audiences=mismatch
```
```console
$ kubectl exec vault-0 -i -- vault write auth/kubernetes/login role=with-audience jwt="$token"
Key Value
--- -----
token hvs.CAESIHdhpK5bTrOil
```
It works too... Vault seems to be ignoring when the audience doesn't match.
Now, let's see what is happening behind the scene using mitmproxy. In one shell session, run mitmproxy:
```shell
mitmproxy -p 9090
```
Let us create a service account that Vault will use to do the TokenReview requests:
```shell
kubectl create sa vault
kubectl create clusterrolebinding default:vault --clusterrole=cluster-admin --serviceaccount=default:vault
```
Let us run Vault in another shell session:
```shell
HTTPS_PROXY=:9090 vault server -dev -dev-root-token-id=root --log-level=debug
```
Now, configure Vault:
```shell
export VAULT_ADDR='http://127.0.0.1:8200'
vault auth enable kubernetes
vault write auth/kubernetes/config \
token_reviewer_jwt="$(kubectl create token vault)" \
kubernetes_host="$(kubectl config view --minify --flatten -ojson | jq '.clusters[0].cluster.server' -r | sed 's|127.0.0.1|me|')" \
kubernetes_ca_cert="$(kubectl config view --minify --flatten -ojson | jq '.clusters[0].cluster."certificate-authority-data"' -r | base64 -d)" \
issuer=https://kubernetes.default.svc.cluster.local
```
Finally, let us trigger a token review:
```shell
vault write auth/kubernetes/role/with-audience \
bound_service_account_names=default \
bound_service_account_namespaces=default \
audiences=mismatch
vault write auth/kubernetes/login role=with-audience \
jwt="$(kubectl create token -n default default --audience=foobar)"
```
```http
POST /apis/authentication.k8s.io/v1/tokenreviews HTTP/1.1
Authorization: Bearer abc
Content-Type: application/json
{
"metadata": { "creationTimestamp": null },
"spec": {
"token": "eyJhbGciOiJSUzI1NiIsImtpZCI6Im9SYUp6YTM0LV82dUtBa0ZzYVI5TGIxOHNiRGdSYUhLUG5OSXpPN1gwY1EifQ.eyJhdWQiOlsiZm9vYmFyIl0sImV4cCI6MTY3MDk2Nzk3NiwiaWF0IjoxNjcwOTY0Mzc2LCJpc3MiOiJodHRwczovL2t1YmVybmV0ZXMuZGVmYXVsdC5zdmMuY2x1c3Rlci5sb2NhbCIsImt1YmVybmV0ZXMuaW8iOnsibmFtZXNwYWNlIjoiZGVmYXVsdCIsInNlcnZpY2VhY2NvdW50Ijp7Im5hbWUiOiJkZWZhdWx0IiwidWlkIjoiMWI1YTllMjEtMTE0Yy00YWY1LTg0NTMtYmE4ZWZjODNmY2JmIn19LCJuYmYiOjE2NzA5NjQzNzYsInN1YiI6InN5c3RlbTpzZXJ2aWNlYWNjb3VudDpkZWZhdWx0OmRlZmF1bHQifQ.RMx_1qireu3Ujn3GoSFktqPMBAUIFEXaCJLl6E9fO0CA8qCXYJoDLsDGg09NZ8Lpf01fETqI6arX6xiT5ElaAv4d0kdla1hx1MZcRGGFsjC1nUldyDaGr43NqZfaKNfva8a9edhdomc7IjGa8efiORcy1jCS6RDeGA3Bxkf81smc-IudjHCUvcco7iNXuIUj7Kugjv6ZVj5kKTJd3VUqyR846XzqHAuXV4L6cFKqbct03O3h4eSjA7yPpUmpAYrDXPjAsVqkbPnrczAylrG7MI7S7iPD5AXv8NZAmKLgBFw10xRErUU1ihvMVcSxPQrlMSoQPqIO7w8FcQlp7LE9Xg",
"audiences": ["foobar"]
},
"status": { "user": {} }
}
```
The response is positive:
```
HTTP/1.1 201 Created
```
As you can see, Vault does not pass the `mismatch` audience to the TokenReview request. Instead, it copies whatever `aud` is in the JWT into the TokenReview request.
It seems there is a bug. The code in [path_login.go](https://github.com/hashicorp/vault-plugin-auth-kubernetes/blob/eabe60240605c4cc3c2d73038931c2fbf47ff6aa/path_login.go#L408) goes something like this:
```go
sa := &serviceAccount{}
_ = mapstructure.Decode(jwtClaims, sa)
r, _ := tr.Review(ctx, client, jwtStr, sa.Audience)
```
Vault is checking that the audience in the JWT matches the audience in the JWT, instead of checking that the audience in the JWT matches the role's audience.