Connect your identity provider
Connect an identity provider so people and workloads can call the admin API without a shared secret. Pistra verifies tokens. Your provider identifies the caller, and a Cedar policy authorizes its roles.
You will register one OAuth client at your provider, add an issuer
to the top-level issuers list, bind it under admin.issuers, and map
a group claim onto roles. Workloads need less than that. They already
hold a token their own platform minted.
Before you start
Section titled “Before you start”Have the local socket working first. Everything on this page depends on your provider being reachable, so it stops working on the day your provider is the problem. Turning the socket on afterwards is a config change, and somebody locked out of the admin API cannot make one.
admin: local_socket: /run/pistra/admin.sock policy_file: /etc/pistra/admin.cedarpolicy_file becomes required the moment an issuer is configured. An
admin API where every authenticated caller can mint keys and drop raft
members is not a default worth offering, so the gateway refuses to start
without one.
Register one client, with two redirect URIs
Section titled “Register one client, with two redirect URIs”There are two ways a person signs in, and they redirect to different places. Register both on the same public client:
| client | redirect URI |
|---|---|
| the console, in a browser | https://gw.example:8485/ui/callback |
pistra login, on a terminal |
http://127.0.0.1:<any port>/callback |
The console’s is fixed. It is the gateway’s own origin, wherever you
serve the admin listener, with /ui/callback on the end. Add
https://gw.example:8485/ui/ as the post-logout redirect while you are
there.
The CLI’s is a loopback listener on an ephemeral port, as RFC 8252
prescribes for a program on somebody’s laptop. The authorization server
must accept any port on 127.0.0.1. A provider that lets you pin one
should be left unpinned, because another program on the machine can be
sitting on a fixed port first. How you express
“any port” differs by provider, and it is the step most likely to send
you back to the provider’s console.
Everything else about the client is the same for both:
- public client, no secret. A browser tab and a laptop binary can each keep zero secrets. PKCE replaces one. It binds the authorization code to the process that asked for it, so a code intercepted on the way back cannot be redeemed by anybody else.
- PKCE required.
- the device grant enabled, if anyone will ever sign in over SSH.
- a mapper that puts group membership in a claim. Which claim and which scope carries it is your provider’s business. It has to exist, because roles come from it.
A Keycloak realm with the public client, the group mapper and the device
grant is committed as ui/dev/idp/realm.json. Its redirect list covers
the console only, because the repository’s browser tests sign in at it.
Add the loopback URI yourself if you copy the client for CLI use.
Declare the issuer
Section titled “Declare the issuer”Add this configuration:
issuers: - name: corp url: https://login.example.com
admin: listen: 0.0.0.0:8485 policy_file: /etc/pistra/admin.cedar local_socket: /run/pistra/admin.sock issuers: - issuer: corp audience: pistra-admin roles: claim: groups map: "CN=pistra-admins,OU=Groups": [keyadmin] "CN=platform-sre,OU=Groups": [viewer, clusteradmin] login: client_id: pistra-admin scopes: [profile, groups]There are two blocks, because they answer two questions. The entry
under issuers says who signs and how this node reaches them. The
entry under admin.issuers says what the admin API accepts from them.
The same directory can also sign for the data plane, agents presenting
their own tokens instead of a virtual key. That is a second binding of
the same issuer, under auth.issuers, with an audience of its own. See
Authenticate agents with your identity provider.
The issuer is written once either way.
name is what the audit trail, the policy and the bindings call this
issuer, and it is required because a subject is only unique within one.
Two providers both calling somebody admin must not collapse into one
principal.
url is matched against the token’s iss exactly, and is where
discovery looks. Keys are fetched on the first token rather than at
startup, so a provider that is unreachable when the config loads does
not stop the gateway serving traffic.
audience is what the token must carry in aud, and it is required.
Accepting a token minted for a different application is the confused
deputy in its original form. For an ID token, which is the default and
what works everywhere, the audience is the client id, so these two
are normally the same string. Setting them differently without a reason
produces a token the gateway then refuses. That is a confusing way to
find out.
Make roles come from something the issuer assigns
Section titled “Make roles come from something the issuer assigns”This is the one setting worth slowing down for.
Roles come from whatever claim roles.claim names, so a claim the
caller can set is a claim the caller can use to choose their own
roles. With no map it is worse. The claim’s value becomes the role
name directly, so somebody who can edit preferred_username to
keyadmin is a key administrator.
The gateway refuses to start on the claims it knows are usually
self-asserted, email, preferred_username, name, nickname,
upn, unique_name, phone_number, and names the alternative in the
error. That is a list of known-soft claims rather than known-safe ones,
so a provider-specific claim nobody anticipated is allowed. Keep to the
rule it teaches. Read roles from what the issuer assigns: groups,
roles, wids, sub, oid.
map keeps the directory’s naming out of the policy file, so renaming a
group is not a policy change. When a map is present, a value it does not
mention grants nothing. A map is a statement about which groups matter,
and passing the rest through would let a group named keyadmin
somewhere unrelated in the directory grant by accident.
Where your directory owns its mailboxes and forbids profile edits,
email is as good as a group and roles.trust_claim: true says so
deliberately. See
the how-to
for what that does and does not override.
Workloads: no client, no login block, no secret
Section titled “Workloads: no client, no login block, no secret”A pipeline or a controller already holds a token its own platform minted. There is nothing to register and nothing to hand out:
issuers: - name: cluster url: https://kubernetes.default.svc.cluster.local ca_file: /etc/pistra/kube-ca/ca.crt token_file: /etc/pistra/kube-sa/token
admin: issuers: - issuer: cluster audience: pistra-admin roles: claim: sub map: "system:serviceaccount:platform:ci": [keyadmin]There is no login block, because a service account has no browser to
open, and a binding without one is not advertised at
/admin/v1/login, so neither the console nor the CLI offers it.
A Kubernetes API server needs ca_file and token_file specifically.
Its TLS certificate is not in the system pool, and it publishes its
signing keys to system:serviceaccounts rather than to anonymous
callers. Reading them takes a service account token of the gateway’s
own, which cannot be the token being verified, because the API server
refuses one minted for another audience. token_file is re-read on
each fetch, so a projected token that rotates keeps working.
On Kubernetes the chart writes both entries for you. Set
admin.workloadIdentity.enabled and give it the map. The two file
paths are neither optional nor obvious.
The subject carries identity, and it is more precise than a static credential can be. A GitHub Actions token names a repository and a branch, so a pull request from a fork is a different principal and gets nothing:
roles: claim: sub map: "repo:acme/infra:ref:refs/heads/main": [keyadmin] "spiffe://acme.example/ns/platform/sa/ci": [keyadmin]Nothing in the policy file changes for any of this. Role::"keyadmin"
is the same role whether a person or a pod is holding it.
Why a list rather than one issuer
Section titled “Why a list rather than one issuer”The two populations do not share a provider, and each brings its own
keys, its own audience and its own idea of what a subject looks like. A
token is routed to one issuer by its iss claim and that issuer alone
checks the signature, so a forged iss reaches keys that did not sign
it, and finds nothing.
Check the configured login flow
Section titled “Check the configured login flow”List the login flows the gateway advertises:
$ curl -s https://gw.example:8485/admin/v1/login | jq{ "issuers": [ { "name": "corp", "issuer": "https://login.example.com", "client_id": "pistra-admin", "scopes": ["profile","groups"], "token": "id" } ]}Nothing there is a secret. A public client’s id is public by design and
the issuer URL is on the front of every token it mints. An issuer you
expected and do not see has no login block.
Sign in and inspect the resolved principal and roles:
$ pistra login https://gw.example:8485$ pistra admin -url https://gw.example:8485 /admin/v1/whoamiwhoami reports the principal and the roles it resolved. A caller whose
group you forgot to map authenticates fine and is refused everything, so
check this after a rollout rather than waiting for the first complaint.
The console is at /ui/ on the same listener, and the listener’s root
redirects there. It runs the same authorization-code flow in the
browser, keeps the token in that tab’s sessionStorage, and sends it as
a bearer on every request, no cookie, and no session on the server.
When something is refused
Section titled “When something is refused”There are two different events, kept apart on purpose:
admin.auth, we do not know who you are. A bad signature, a wrongaud, an expired token, anissnaming no configured issuer.admin.authz, we know exactly who you are, and you may not. See what a refusal looks like.
Filing them together hides the more urgent one. A burst of admin.authz
records with no rule is a credential missing a role, not a policy
catching somebody.
What changes without a restart, and what does not
Section titled “What changes without a restart, and what does not”Issuers, the policy file and the credentials it decides over are read once at startup, so changing any of them needs a restart. Group membership is not. It comes from the token on every request, so moving somebody between groups in the directory takes effect with the gateway untouched.
That split is the design. The policy is static operator intent and belongs in the review that changes it. Who is in which role is your directory’s business and changes all day.
Related
Section titled “Related”- Restrict what an admin caller can do, the Cedar policy the roles feed
- Sign in from the command line, which flow runs where, and what the CLI caches
- Reach the admin API without your identity provider, the door that does not depend on any of this
- Authorization, how this fits beside the data plane, which authenticates nothing this way
- Policy reference, every action and attribute a policy may name