Single sign-on with Cbox ID
Single sign-on with Cbox ID
[auth.oidc]
issuer = "https://acme.cboxid.com"
That is the whole configuration. telemetryd reads the signing keys from
https://acme.cboxid.com/.well-known/jwks.json, caches them, and validates every
presented token itself. Hosted Cbox ID environments are one subdomain of cboxid.com
per tenant; if you run Cbox ID yourself, use your own host.
Getting a token
Tokens come from Cbox ID, not from telemetryd. Ask for the scope that matches what the holder should be able to do:
| Scope | Opens |
|---|---|
telemetry:write |
/v1/logs, /v1/traces, /v1/metrics, /api/v1/write |
telemetry:read |
the Loki, Tempo and Prometheus read APIs |
telemetry:admin |
/status and /metrics |
A token may carry several. They are not a hierarchy: an admin scope does not grant reads, because running a dashboard is not a reason to read anyone's log lines.
export TELEMETRYD_AUTH_QUERY_TOKEN="$(cbox-id token --scope telemetry:read)"
telemetryd query '{app="checkout"}'
Rename them if they collide with something else on a shared issuer:
[auth.oidc]
issuer = "https://acme.cboxid.com"
scope_read = "acme:telemetry:read"
Turning it on closes open surfaces
An empty query_token means "unguarded" only while nothing else guards that surface.
The moment an issuer is set, the read API demands a valid token — and a
telemetry:admin token does not satisfy it.
If you were deliberately leaving a surface open, that stops being true here. It is the safe direction to fail in, but check it before rolling out, not after.
Bearer tokens, not DPoP
Cbox ID can bind an access token to a key the client holds (DPoP, RFC 9449), putting the
thumbprint in a cnf claim so a stolen token alone is useless.
telemetryd refuses those tokens. It does not validate DPoP proofs, and accepting a bound token as an ordinary bearer would hand back exactly the property the binding was bought for — so it fails closed rather than quietly downgrading.
If DPoP is on for the client you point at telemetryd, every request gets a 401 with an
empty body, because a 401 that explains itself is a hint to whoever is guessing. The
reason goes to the log instead, once, at warn:
refusing a sender-constrained (DPoP) access token: telemetryd cannot validate
the proof, and accepting it as a plain bearer would discard the binding.
Issue plain bearer tokens for telemetryd. Validating the proofs is future work.
It must be an access token
RFC 9068 gives access tokens the media type at+jwt, and Cbox ID sets it on every one
it mints. An id token says JWT, is signed by the same key, and authorises nothing —
so it is refused, with the same empty 401 and a warn line naming what arrived.
A token with no typ at all is accepted; not every issuer sets one, and aud and
scope still have to hold.
Static tokens keep working
Enabling this does not turn them off, and it should not: an app server pushing OTLP wants a token in an environment variable, not an OAuth flow. Static tokens are checked first, so a machine-to-machine deployment pays nothing for this being switched on.
When several services share an issuer
Set the audience, and Cbox ID will mint tokens bound to it:
[auth.oidc]
issuer = "https://acme.cboxid.com"
audience = "https://telemetry.example.com"
Without it, telemetryd accepts the issuer's own value — which is what Cbox ID puts in
aud when no resource was requested. With it, a token minted for another service on
the same issuer cannot be replayed here.
Another provider than Cbox ID
Nothing here is specific to Cbox ID — telemetryd validates a standards-shaped access token, so any OIDC provider that mints one will work. Two things are not universal, so they are settings rather than assumptions.
Where the keys live. The default derives {issuer}/.well-known/jwks.json, which is
a convention, not a rule. A provider's discovery document names the real location in its
jwks_uri, and if it differs, say so:
curl -s https://<issuer>/.well-known/openid-configuration | jq -r .jwks_uri
[auth.oidc]
issuer = "https://accounts.google.com"
jwks_url = "https://www.googleapis.com/oauth2/v3/certs" # nowhere near the issuer
Which claim carries the scopes. OAuth specifies scope, a space-separated string.
Some providers use scp instead, and some send an array rather than a string —
telemetryd accepts either shape, but it has to be told the name:
scope_claim = "scp"
Both are https-only for the same reason the issuer is: whoever answers that request
decides which keys mint valid admin tokens. Loopback is exempt, for testing.
What "works" does and does not mean
A provider working means telemetryd can verify its signatures and trust its issuer. It
does not mean the provider can express these scopes. Google is the useful example:
the key set above fetches fine, but a Google ID token carries no scope claim at all, so
there is nothing to map telemetry:read onto. You would be authenticating a user and
then granting them nothing.
So the question to ask of a provider is not "can telemetryd read its keys" but "can I mint a token that carries a claim naming the access I want". A provider with configurable scopes or custom claims — Cbox ID, Entra, Auth0, Keycloak — can. A consumer sign-in provider generally cannot, and for those, static tokens remain the straightforward answer.
What happens when Cbox ID is down
Nothing, for tokens already issued. telemetryd validates signatures against cached keys and never asks the provider about a token. That is deliberate: this is the thing you open when something is broken, and an identity provider that must be reachable to read your logs is a dependency pointing the wrong way — if Cbox ID is down, you cannot read the logs that would tell you why.
What stops is Cbox ID issuing new tokens, which is its job to worry about.
telemetryd also starts without it. If the key set cannot be fetched at boot it warns and serves anyway; static tokens are unaffected, and the refresh loop picks the keys up when the provider returns.
Key rotation
No restart. Keys refresh on a timer, and a token carrying a key id telemetryd has not seen triggers an immediate refetch — rate-limited to once a minute, so a stream of forged key ids cannot become a stream of outbound requests.
Checking it is working
curl -sS -H "Authorization: Bearer $ADMIN_TOKEN" localhost:4319/status | jq .auth.oidc
{ "issuer": "https://acme.cboxid.com", "keys": 2, "keys_stale": false }
keys: 0 is the thing to alert on. It means every Cbox ID token is being refused
because the key set never loaded, and no other field would tell you.
telemetryd_oidc_keys carries the same number for a dashboard.
If you also run relay mode
Relay mode stamps each record's app label from the credential rather than the payload,
and for a Cbox ID token that comes from the client_id claim — the registered OAuth
client, which the issuer reserves against being overwritten by enrichment hooks. sub is
a user, which is not what an application name means.
So the client each mobile app authenticates as is what its telemetry is labelled with. See the relay guide.
The issuer must be https
telemetryd refuses to start otherwise, loopback aside for testing. It fetches signing keys from that URL, so anyone able to answer the request over plaintext can mint tokens this instance will accept.