Keycloak is the right answer for self-hosted SSO. It implements the full OIDC/OAuth2 spec, has production-grade HA clustering, supports every client type you'll encounter, and has been hardened by the Red Hat security team for years. It is also, unfortunately, documented primarily for enterprise teams with a dedicated identity engineer and a week to read the docs.
This guide cuts through that. It covers what you actually need to know to get Keycloak running for a team of 5โ100 people, connected to your real applications, with MFA enforced and production hardening in place โ in a single focused session. We run 47ID on Keycloak internally and deploy it for Studio clients; this is the setup we use.
What Keycloak actually does
Before touching configuration: Keycloak is an identity provider (IdP). It is responsible for authenticating users and issuing tokens. Your applications โ the OIDC clients โ never handle usernames and passwords directly. Instead, they redirect unauthenticated users to Keycloak, which authenticates them and returns a signed JWT that the application can verify and trust.
The key concepts you need to understand:
- Realm: A namespace that contains users, clients, roles, and configuration. Think of it as a tenant. You'll have one realm for your organisation. Multiple realms can run on one Keycloak instance and are completely isolated from each other.
- Client: An application that trusts Keycloak to authenticate users. Each application you want to protect is a separate client in Keycloak. Clients are configured with a redirect URI (where Keycloak sends users after login) and a client secret (for confidential clients).
- User: An identity stored in Keycloak's database or federated from an external directory (LDAP, Active Directory, Google Workspace). Users can have roles, groups, and attributes.
- Flow: The sequence of steps Keycloak runs when a user authenticates. The default browser flow handles username/password. You'll modify it to require TOTP MFA.
Deployment
Run Keycloak in a container. For a team under 100 with modest login volume, a single instance with PostgreSQL backend is sufficient. Keycloak's clustering story is good but adds complexity that isn't warranted until you have either HA requirements or very high login rates.
version: "3.9"
services:
postgres:
image: postgres:16-alpine
environment:
POSTGRES_DB: keycloak
POSTGRES_USER: keycloak
POSTGRES_PASSWORD: ${KC_DB_PASSWORD}
volumes:
- kc_postgres:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U keycloak"]
interval: 10s
keycloak:
image: quay.io/keycloak/keycloak:26.0
command: start
environment:
KC_DB: postgres
KC_DB_URL: jdbc:postgresql://postgres/keycloak
KC_DB_USERNAME: keycloak
KC_DB_PASSWORD: ${KC_DB_PASSWORD}
KC_HOSTNAME: id.yourorg.com
KC_HTTPS_CERTIFICATE_FILE: /opt/keycloak/conf/server.crt
KC_HTTPS_CERTIFICATE_KEY_FILE: /opt/keycloak/conf/server.key
KEYCLOAK_ADMIN: admin
KEYCLOAK_ADMIN_PASSWORD: ${KC_ADMIN_PASSWORD}
volumes:
- ./certs:/opt/keycloak/conf:ro
depends_on:
postgres:
condition: service_healthy
ports:
- "8443:8443"
volumes:
kc_postgres:
Put TLS termination at a reverse proxy (Caddy, Nginx, Traefik) in front. Keep the internal Keycloak port off the public internet โ your proxy should be the only entry point. The KC_HOSTNAME must match the hostname your users and clients will reach; getting this wrong causes redirect mismatches that are annoying to debug.
Realm setup
Log into the admin console (https://id.yourorg.com/admin) with your bootstrap credentials. Create a new realm โ call it something like org or your company name. Never use the master realm for production; it's the admin realm and should be used only for Keycloak administration.
In your new realm, configure these settings first:
- Realm Settings โ Login: Enable "Require SSL" for all requests. Disable user self-registration unless you want users to create their own accounts. Enable "Login with email" if your users think in email addresses rather than usernames.
- Realm Settings โ Email: Configure SMTP so Keycloak can send password reset and MFA setup emails. Without this, the self-service flows don't work.
- Realm Settings โ Sessions: Set SSO session idle timeout to something sensible (8 hours for an office environment, 1 hour for a security-sensitive context). Access token lifespan should be short โ 5 minutes is fine; refresh tokens handle the renewal.
- Realm Settings โ Tokens: Ensure access tokens are signed with RS256. This is the default but worth verifying โ clients will use the realm's JWKS endpoint to verify signatures, and the algorithm must match.
Configuring OIDC clients
Each application you want to protect gets its own Keycloak client. Go to Clients โ Create client. The client ID should be meaningful โ grafana, vault, your-app. Set Client type to OpenID Connect.
The key settings for a standard web application (confidential client):
- Client authentication: On (this is a confidential client โ it has a server-side backend that can keep a secret)
- Valid redirect URIs:
https://grafana.yourorg.com/login/generic_oauthโ the exact callback URL your application expects. Wildcards work but are a security risk; be specific. - Web origins: The application's origin for CORS โ needed if the application makes OIDC requests from the browser directly
After saving, go to the Credentials tab and copy the client secret. Store it in Vault or your secrets manager โ never in a config file or environment variable in plaintext.
Public clients vs confidential clients: Single-page applications (React, Vue) that run entirely in the browser cannot keep a secret. Configure these as public clients (Client authentication: Off) and use PKCE (Proof Key for Code Exchange) instead of a client secret. Any OIDC library worth using supports PKCE.
Enforcing MFA with TOTP
The default authentication flow doesn't require MFA. You need to modify the browser flow or create a custom one. In Keycloak 26, go to Authentication โ Flows. Find "browser" and click the three-dot menu โ Duplicate. Name it something like "browser-mfa".
In your duplicated flow, find the "Browser - Conditional OTP" sub-flow. It's set to Conditional by default, which means MFA is only required if the user has OTP configured. Change the condition to "Required" to enforce MFA for all users. Then bind this flow to your realm: Authentication โ Bindings โ Browser Flow โ select "browser-mfa".
The first time a user logs in, they'll be prompted to configure TOTP. Keycloak shows a QR code compatible with any TOTP app (Aegis, Authy, Google Authenticator, 1Password). Backup codes are generated automatically โ make sure users save them.
Recovery planning: Decide in advance what happens when a user loses their TOTP device. Keycloak doesn't provide admin-initiated TOTP reset via email by default โ you'll need to manually remove the credential from the admin console or set up a custom reset flow. Most small teams use the admin console reset; document the procedure so it doesn't become a crisis at 11pm when someone locks themselves out.
User management and groups
Create users manually for small teams or import via the Admin REST API for bulk setup. Set users' required actions to "Verify Email" and "Configure OTP" on creation โ this forces them to complete setup on first login without requiring admin intervention.
Groups are the right way to manage access at scale. Create groups that map to functional roles โ engineering, finance, support โ and assign realm roles or client-specific roles to groups rather than individual users. When someone joins or leaves a team, you change their group membership once rather than updating roles across multiple clients.
Client roles allow per-application access control. A client can define roles like admin and viewer, and users or groups are assigned those roles. Your application reads the resource_access claim in the JWT to determine what the user can do. This is cleaner than managing access in each application separately.
Production hardening
Common gotchas
Redirect URI mismatch. The most common error when setting up a new client. The URI in the Keycloak client settings must exactly match what the application sends in the OIDC request โ including trailing slashes and protocol. Use the Keycloak admin console's "Evaluate" feature (under Clients โ your client โ Client Scopes โ Evaluate) to debug what claims are being issued.
Clock skew. OIDC tokens have iat (issued at) and exp (expiry) claims that are validated against the server's clock. If your application server's clock drifts more than a few seconds from Keycloak's, tokens will be rejected as expired or not-yet-valid. Run NTP on all hosts and verify sync status.
Session vs token expiry. Keycloak has two separate expiry settings: the SSO session (how long the user is considered logged in to Keycloak) and the access token lifetime (how long the JWT is valid). These interact. A user can have a valid SSO session but an expired access token โ applications should refresh the token using the refresh token, not consider the user logged out.
The master realm. New Keycloak installations default to putting everything in the master realm. The master realm's admin is a superadmin who can manage all other realms. Create your application realm and work there. Reserve master realm access for infrastructure maintenance.
Where 47ID fits
47ID is our Keycloak distribution โ pre-configured with hardened defaults, integrated with HashiCorp Vault for secrets, and connected to the 47Network product ecosystem so your users get SSO across all 47Network products out of the box. If you're running multiple 47Network products or want a managed Keycloak deployment without the setup overhead, it's the faster path. If you want to run vanilla Keycloak on your own terms, this guide covers everything you need.