Keycloak and OIDC single sign-on
Sign in to the MAQPNA console and CLI with Keycloak or any OIDC identity provider, map groups to admin roles, and use the same identity provider for approvals, user approval (CIBA) and external MCP clients.
By default the gateway admin API accepts one static admin token. That is fine on a laptop, but in production every person who lists sessions, approves a tool call or revokes an agent should sign in as themselves. MAQPNA does this with OpenID Connect (OIDC): the gateway verifies access tokens from your identity provider and turns the groups in each token into MAQPNA roles. The console and the maqpna CLI then sign in through the identity provider, and the audit ledger records the real approver.
This page uses Keycloak, which the MAQPNA interop suite tests every night. The gateway's verifier is a generic OIDC verifier, so the same settings work for other OIDC providers such as Entra ID, Okta or Google. Only Keycloak is tested end to end.
flowchart LR
U["Person<br/>browser or terminal"] -->|"1 sign in<br/>code + PKCE"| K["Keycloak<br/>realm maqpna"]
K -->|"2 access token<br/>aud, groups"| U
U -->|"3 Bearer token"| C["Console or CLI"]
C -->|"4 /v1 admin API"| G["MAQPNA gateway"]
G -->|"discovery, JWKS"| K
G --> R["roles from groups<br/>viewer, approver, auditor,<br/>admin, killswitch"]
G --> L[("audit ledger<br/>approver = preferred_username")]
When to use it#
- More than one person uses the console or the admin API.
- You need separation of duties: approvers who cannot approve their own calls, and four-eyes approvals.
- You want the person an agent acts for to confirm a sensitive call on their own device (user approval, CIBA).
- IDE or desktop coding agents should call MCP servers through the gateway with tokens from your identity provider.
Prerequisites#
- A Keycloak server that the gateway can reach over HTTPS, or any OIDC provider that publishes
/.well-known/openid-configuration. - MAQPNA installed with the Helm chart (see Production install with Helm).
- The
maqpnaCLI on your machine.
Step 1: Create the realm, groups and client#
In Keycloak, create a realm (here maqpna) with these objects:
| Object | Settings |
|---|---|
| Groups | maqpna-viewers, maqpna-approvers, maqpna-auditors, maqpna-admins, maqpna-sre. These are the chart's default role groups. |
Client maqpna-admin |
Public client, standard flow (authorization code) with PKCE. Valid redirect URIs: the console URL (for example http://127.0.0.1:8088/*) and http://127.0.0.1/* for the CLI loopback login. |
| Mapper: Audience | On client maqpna-admin: Included Client Audience maqpna-admin, added to the access token. |
| Mapper: Group Membership | Claim name groups, Full group path off. |
Two Keycloak defaults break MAQPNA if you leave them:
- Issuer. Keycloak builds
issfrom the host name of the request. Pin it to the URL the gateway uses (--hostname=https://idp.example.eu). Otherwise tokens fetched through another host name fail withtoken issuer mismatch. - Audience and groups. Access tokens carry
aud: accountby default, and group paths start with/. Add the Audience mapper and turn Full group path off. You can instead setadminAuth.oidc.groupsClaimto a claim with plain names, such asrealm_access.roles(dotted paths are supported).
Step 2: Set the Helm values#
adminAuth:
mode: oidc # token | oidc | both
oidc:
issuer: https://idp.example.eu/realms/maqpna
audience: maqpna-admin
clientID: maqpna-admin # published to the console and CLI
scopes: [] # optional extra scopes for the login
groupsClaim: groups
usernameClaim: "" # default: preferred_username, then email, then sub
caSecret: "" # Secret with key ca.crt for a private IdP CA
roles: # role -> groups, or "user:<name>"
viewer: ["maqpna-viewers"]
approver: ["maqpna-approvers"]
auditor: ["maqpna-auditors"]
admin: ["maqpna-admins"]
killswitch: ["maqpna-sre"]
namespaceRoles: # roles in one namespace only
team-a:
approver: ["team-a-leads"]
| Mode | What the admin API accepts |
|---|---|
token (default) |
Only the static admin token (MAQPNA_ADMIN_TOKEN). |
oidc |
Only OIDC tokens. The chart does not inject the static token. |
both |
OIDC tokens, plus the static token as a break-glass credential. Each use of it is logged as break-glass admin token used and recorded with issuer urn:maqpna:break-glass. |
The chart refuses to render oidc or both without adminAuth.oidc.issuer. The gateway fetches discovery and the provider's public keys (JWKS) through its residency-checked dialer. If you set sovereignty.allowedEgressHosts, add the Keycloak host to it.
Apply the values:
helm upgrade maqpna oci://$REGISTRY/charts/maqpna -n maqpna-system --reuse-values -f oidc-values.yaml
REGISTRY is the release registry listed on maqpna.com/download.
Step 3: Sign in with the CLI#
The CLI reads the issuer, client ID and scopes from the gateway (GET /v1/auth/config), so you only pass the gateway URL:
maqpna login --gateway https://gateway.example.eu
It opens the browser for the authorization code flow with PKCE on a 127.0.0.1 loopback redirect. On a host without a browser, use the device flow:
maqpna login --gateway https://gateway.example.eu --device
The token is stored per CLI context in tokens/<context>.json with mode 0600 and refreshed with the refresh token when one was issued. Flags --issuer, --client-id, --scopes and --audience override what the gateway publishes. See maqpna login.
Step 4: Sign in to the console#
maqpna console --open
The console reads the same /v1/auth/config and offers single sign-on. It uses the authorization code flow with PKCE in the browser and keeps the access token in the tab's session storage. Single sign-on needs a secure context: https or localhost.
Verify#
maqpna whoami
The output shows the user name, issuer, groups and roles the gateway derived from your token. Then check one role boundary: a member of maqpna-viewers only can run maqpna approvals list, but maqpna approvals approve answers 403.
Use the same identity provider for approvals#
- Separation of duties. The user a session acts for can never approve its calls (
409 self_approval), and one person cannot vote twice (409 duplicate_approver). - Four-eyes. Add
approval: {minApprovers: 2, approverGroups: [...]}to arequire_approvalrule, or setapprovals.quorumin the chart. See Human approvals. - User approval (CIBA).
approval: {userConfirmation: ciba}on a rule asks the delegating user to confirm on their own device. Configuredelegation.ciba(enabled,issuer,clientID,clientSecretFile,fallback: approvers | deny) and a confidential Keycloak client with the OpenID CIBA grant. Keycloak also needs a CIBA authentication channel that reaches the user. - External MCP clients.
delegation.trustedIssuerslets IDE agents call/mcp/<server>with tokens from your identity provider. Setdelegation.resourceBaseURLto the gateway's public URL; the token audience must be<resourceBaseURL>/mcp/<server>. A tool listed instepUpScopesanswers403 insufficient_scopeuntil the client gets the scope. - Verified session users.
principalBinding.userOIDCmakes the operator verify ID tokens referenced byAgentSession.spec.userAssertionRef.
delegation:
resourceBaseURL: https://maqpna.example.eu
trustedIssuers:
- issuer: https://idp.example.eu/realms/maqpna
namespace: team-a
agent: ide
servers: ["github"]
stepUpScopes: {"github/delete_*": ["tools:github:admin"]}
clientSecretsSecret: maqpna-delegation # key "ciba"
ciba:
enabled: true
issuer: https://idp.example.eu/realms/maqpna
clientID: maqpna-ciba
clientSecretFile: /etc/maqpna/delegation-secrets/ciba
fallback: deny
Troubleshooting#
| Symptom | Cause | Fix |
|---|---|---|
401 token issuer mismatch |
iss differs from adminAuth.oidc.issuer (often a Keycloak host name issue) |
Pin Keycloak's --hostname; copy the exact issuer from the discovery document |
401 token audience mismatch |
The access token has aud: account |
Add the Audience mapper with maqpna-admin |
whoami shows no roles |
Groups arrive as /maqpna-admins, or in another claim |
Turn Full group path off, or set groupsClaim |
401 identity provider keys unavailable |
The gateway cannot fetch discovery or JWKS | Check egress to the IdP, sovereignty.allowedEgressHosts and caSecret for a private CA |
token lacks the <claim> claim |
usernameClaim names a claim the token does not have |
Clear usernameClaim or add a mapper |
| Console says it needs a secure context | The console is served over plain http on a non-local host |
Use maqpna console on localhost, or serve it over https |
503 admin API disabled |
mode: token without an admin token |
Set adminAuth.mode: oidc, or enable adminAuth.breakGlassToken |