Skip to main content

JWT/OIDC Federation for ClusterNest Managed Prometheus

Alongside the static admin/readonly/write credentials (which always exist and can't be turned off), a cluster can also trust one or more external JWT issuers directly - your own OIDC identity provider, or a Kubernetes cluster's own apiserver authenticating its workloads via their projected ServiceAccount tokens. A token presented as a Bearer credential is checked against every configured source; if it verifies, it receives whichever access level(s) its claims match.

One precedence rule worth knowing: if a request's Authorization header carries a Basic credential (as curl -u sends), it's judged on that alone - a misconfigured Basic credential rejects the request even if a valid JWT is also on offer somewhere else. Don't mix the two on the same request when testing.

Configuring a source​

Each entry in prometheus_jwt_sources:

FieldMeaning
nameIdentifies this source in error logs; unique per cluster.
issuerThe trusted iss claim.
jwks_url or oidc_discovery_urlWhere to fetch signing keys - exactly one of the two.
audiencesAllowed aud values; empty means any.
ca_certBase64 PEM CA bundle, if the source's TLS isn't publicly trusted.
grantsList of {role, claims} - see below.

A grant's role is admin/readonly/write, same meaning as the static credentials. Its claims is an exact-match map (e.g. {"sub": "..."}) a token must satisfy to receive that grant; an empty map matches any token the source verifies. A token can match more than one grant - its effective access is the union (a token matching both a write grant and a broader, claims-free admin grant gets admin access).

Open the cluster's settings and add an entry under JWT Authentication - issuer, jwks_url or oidc_discovery_url, optional audiences, and one or more role/claims grants, matching the fields above.

Takes effect within seconds, no cluster restart.

Worked example: Kubernetes ServiceAccount tokens (EKS)​

A workload running inside a Kubernetes cluster can push metrics using its own ServiceAccount token as a Bearer credential instead of a static password - no secret to distribute or rotate, since kubelet mints and refreshes the token automatically. This is the same mechanism ClusterNest's own internal metrics pipeline uses to let one cluster push into another.

Any cluster with ServiceAccountIssuerDiscovery enabled works this way; EKS has it on by default, so this example uses EKS - the same steps apply to any conformant Kubernetes cluster, just with a different issuer URL.

1. Find your EKS cluster's OIDC issuer​

The same issuer URL IRSA already uses:

aws eks describe-cluster --name my-cluster --query "cluster.identity.oidc.issuer" --output text
# https://oidc.eks.<region>.amazonaws.com/id/<cluster-id>

2. Configure the JWT source​

{
"name": "eks",
"issuer": "https://oidc.eks.<region>.amazonaws.com/id/<cluster-id>",
"oidc_discovery_url": "https://oidc.eks.<region>.amazonaws.com/id/<cluster-id>/.well-known/openid-configuration",
"audiences": ["cortex"],
"grants": [
{
"role": "write",
"claims": { "sub": "system:serviceaccount:monitoring:prometheus-agent" }
}
]
}

The sub claim on a Kubernetes-issued ServiceAccount token always has the form system:serviceaccount:<namespace>:<service-account-name> - match on it directly instead of trying to scope by anything else.

3. Project the token into the pod​

Give the pod a token bound to the cortex audience above (matching audiences), refreshed automatically by kubelet:

spec:
serviceAccountName: prometheus-agent
containers:
- name: prometheus-agent
volumeMounts:
- name: cortex-token
mountPath: /var/run/secrets/tokens
volumes:
- name: cortex-token
projected:
sources:
- serviceAccountToken:
path: cortex-token
audience: cortex
expirationSeconds: 3600

4. Point remote_write at the token file​

remote_write:
- url: https://$PROMETHEUS_HOST/api/v1/push
authorization:
credentials_file: /var/run/secrets/tokens/cortex-token

Prometheus/Grafana Agent re-reads credentials_file on every request, so kubelet's periodic token refresh (every expirationSeconds, well before actual expiry) is picked up without a pod restart.

Testing a source​

Decode whatever token you're using (jwt.io, or kubectl create token <sa> --audience=cortex for a quick manual one) to confirm its iss/aud/sub match what's configured, then push a zero-timeseries request (see Pushing metrics in for why empty.snappy has to be a file, not an inline argument):

printf '\x00' > empty.snappy

curl -i -X POST "https://$PROMETHEUS_HOST/api/v1/push" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/x-protobuf" \
-H "Content-Encoding: snappy" \
--data-binary @empty.snappy

A 401 means the token didn't verify (wrong issuer/audience/signature); a 403 means it verified but no grant's claims matched; 200 means it worked.