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
Section titled “Configuring a source”Each entry in jwt_auth_sources:
| Field | Meaning |
|---|---|
name |
Identifies this source in error logs; unique per cluster. |
issuer |
The trusted iss claim. |
jwks_url or oidc_discovery_url |
Where to fetch signing keys - exactly one of the two. |
audiences |
Allowed aud values; empty means any. |
ca_cert |
Base64 PEM CA bundle, if the source’s TLS isn’t publicly trusted. |
grants |
List 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 form: Launch Cluster for a new cluster, or Edit on an existing one.
- Under JWT Authentication, click Add JWT Source.
- Fill in the issuer,
jwks_urloroidc_discovery_url, and optional audiences, matching the fields above. - Click Add Grant and set the role and claims. Add one grant per role.
- Save the cluster.
resource "clusternest_prometheus" "metrics" {
name = "metrics"
tier = "standard"
organization_id = 123
jwt_auth_sources = [
{
name = "idp"
issuer = "https://idp.example.com"
jwks_url = "https://idp.example.com/jwks"
grants = [
{ role = "write", claims = { sub = "remote-write-agent" } },
]
},
]
}See the resource reference for the full schema.
curl "https://api.clusternest.com/cluster/prometheus/$CLUSTER_ID" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
| jq '.jwt_auth_sources += [{
"name": "idp",
"issuer": "https://idp.example.com",
"jwks_url": "https://idp.example.com/jwks",
"grants": [{"role": "write", "claims": {"sub": "remote-write-agent"}}]
}]' > cluster.json
curl -X PUT "https://api.clusternest.com/cluster/prometheus/$CLUSTER_ID" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
--data-binary @cluster.json(+= appends to the existing array rather than replacing it, so any JWT sources already
configured stay in place.)
Takes effect within seconds, no cluster restart.
Worked example: Kubernetes ServiceAccount tokens (EKS)
Section titled “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
Section titled “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
Section titled “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
Section titled “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
Section titled “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
protobuf_message: io.prometheus.write.v2.Request
protobuf_message selects remote write 2.0, which uses far less
egress; omit it to send 1.0.
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
Section titled “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.