SAML with Keycloak
Tested against Keycloak 24+ (new admin console). Goal: login through Keycloak works and your admin user gets org_admin. The keycloak preset reads Keycloak's default mapper output (urn:oid attribute names + Role from the role_list scope) plus a Group list mapper named member — that mapper is the only thing you must add by hand.
1. Create the SAML client
<your realm> → Clients → Create client
- Client type: SAML.
- Client ID:
https://<domain>/saml/metadata— for SAML clients the client ID is the SP entityID and must matchsaml.entityIDexactly. - Next → Valid redirect URIs:
https://<domain>/saml/acs→ Save.
On the client's Settings tab:
- Home URL / Master SAML Processing URL:
https://<domain>/saml/acs(https, no trailing slash — the SP compares exactly). - Name ID format:
email. - Force POST binding: On (default). If this is off, Keycloak answers over the Redirect binding (GET) and the ACS returns HTTP 405.
- Sign documents / Sign assertions: On (defaults are fine).
- Signature keys / "Client signature required": OrbitalReg signs its AuthnRequests, so either
- Keys tab → import/paste the SP certificate (
sp.crt) so Keycloak can verify the request signature, or - set Client signature required: Off while bootstrapping and tighten later. With it On and no/imported-wrong cert, the login fails with
invalid_signaturein the Keycloak server log.
- Keys tab → import/paste the SP certificate (
2. Mappers — Group list as member
Keycloak's builtin mappers already emit email (urn:oid:1.2.840.113549.1.9.1), full name (urn:oid:2.16.840.1.113730.3.1.241) and the role list (Role). What's missing out of the box is a groups attribute:
Client → Client scopes → <client-id>-dedicated → Add mapper → By configuration → Group list
- Name:
groups - Group attribute name:
member← the preset reads exactly this (the conventional name from the Keycloak docs) - Single Group Attribute: On (one multi-valued attribute instead of many single-valued ones)
- Full group path: Off (emit
orbital-admins, not/orbital-admins— the values below match display names)
If the builtin user-property mappers were removed from the dedicated scope, re-add User Property mappers for email and the full name, or the assertion will lack the OID attributes the preset expects.
3. Create the admin group
<realm> → Groups → Create group: orbital-admins; open it → Members → add your user.
Alternatively (or additionally) you can drive roles via Keycloak realm roles: the preset also reads the Role attribute that the default role_list client scope emits, so a realm role named orbital-admins assigned to the user works too.
4. Metadata URL
Keycloak publishes the IdP metadata per realm:
https://<keycloak-host>/realms/<realm>/protocol/saml/descriptorUse the public Keycloak host — if Keycloak runs behind a different internal hostname, set the frontend URL (Realm settings → General → Frontend URL) so the metadata contains public endpoint URLs; otherwise login redirects will point at unreachable internal addresses ("response code null").
5. Values + deploy
Complete example: charts/orbitalreg/values-keycloak.yaml.example. Essentials:
saml:
enabled: true
idpPreset: "keycloak"
rootURL: "https://registry.example.com"
entityID: "https://registry.example.com/saml/metadata" # = Keycloak client ID
idpMetadataURL: "https://<keycloak-host>/realms/<realm>/protocol/saml/descriptor"
existingSecret: "orbitalreg-saml-sp"
auth:
adminGroups:
- "orbital-admins" # group name (no leading slash) or realm-role namehelm upgrade orbitalreg charts/orbitalreg -n <ns> -f values-keycloak.yaml6. Verify
Log out completely (including the Keycloak SSO session), log back in, then check https://<domain>/auth/whoami: groups must contain orbital-admins (or roles-relevant match via the Role attribute), roles must contain org_admin. Email fallback: add your login email to auth.adminGroups, redeploy, re-login.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
Pod crashes, saml init failed | Descriptor URL wrong (realm typo) or unreachable | curl the descriptor URL from the pod network |
| "response code null" on login click | Metadata contains internal Keycloak URLs (no Frontend URL set, metadata fetched internally) | Set the realm Frontend URL to the public host; use the public descriptor URL |
| HTTP 405 after Keycloak login | Force POST binding is off → response arrives as GET, ACS is POST-only | Turn Force POST binding on |
404 on /saml/acs | Master SAML Processing URL / redirect URI has a trailing slash or wrong path | Exactly https://<domain>/saml/acs |
405 opening /saml/acs in a browser | Normal — GET on a POST-only endpoint | Not an error |
Keycloak log: invalid_signature / request rejected | "Client signature required" on, but SP cert not imported (or rotated) | Import the current sp.crt on the client's Keys tab |
Login works but roles: reader | Group list mapper missing, attribute name ≠ member, or Full group path on (/orbital-admins ≠ orbital-admins) | Fix the mapper (name member, full path off); or match the full path string in auth.adminGroups |
groups empty in whoami | Mapper added to the wrong scope, or stale session | Put the mapper in the client's dedicated scope; full re-login |
Reference: what OrbitalReg reads (preset keycloak)
| OrbitalReg expects | SAML attribute name |
|---|---|
| Email (identity) | urn:oid:1.2.840.113549.1.9.1 |
| Groups (→ role) | member |
| Display name | urn:oid:2.16.840.1.113730.3.1.241 |
| Role (secondary) | Role |
See the SSO overview for role mapping and claim-name overrides.