Skip to main content
COSMICBYTEZLABS
NewsSecurityHOWTOsToolsStudyTraining
ProjectsChecklistsAI RankingsNewsletterStatusTagsAbout
Subscribe

Press Enter to search or Esc to close

News
Security
HOWTOs
Tools
Study
Training
Projects
Checklists
AI Rankings
Newsletter
Status
Tags
About
RSS Feed
Reading List
Subscribe

Stay in the Loop

Get the latest security alerts, tutorials, and tech insights delivered to your inbox.

Subscribe NowFree forever. No spam.
COSMICBYTEZLABS

Your trusted source for IT intelligence, cybersecurity insights, and hands-on technical guides.

429+ Articles
114+ Guides

CONTENT

  • Latest News
  • Security Alerts
  • HOWTOs
  • Projects
  • Exam Prep

RESOURCES

  • Search
  • Browse Tags
  • Newsletter Archive
  • Reading List
  • RSS Feed

COMPANY

  • About Us
  • Contact
  • Privacy Policy
  • Terms of Service

© 2026 CosmicBytez Labs. All rights reserved.

System Status: Operational
  1. Home
  2. Projects
  3. Keycloak SSO: Self-Hosted Identity Provider for Your Homelab
Keycloak SSO: Self-Hosted Identity Provider for Your Homelab
PROJECTIntermediate

Keycloak SSO: Self-Hosted Identity Provider for Your Homelab

Deploy Keycloak with Docker Compose and PostgreSQL to build a centralised single sign-on platform for your homelab services, with OIDC integration for...

Dylan H.

Projects

March 26, 2026
11 min read
3-5 hours

Tools & Technologies

DockerDocker ComposeKeycloakPostgreSQLTraefikcurl

Keycloak SSO: Self-Hosted Identity Provider for Your Homelab

Managing separate logins for Grafana, Portainer, Proxmox, Gitea, and every other service in your homelab is a security and usability nightmare. Centralise authentication with Keycloak — a battle-tested, open-source identity and access management platform that supports OpenID Connect (OIDC), SAML 2.0, and social login out of the box.

By the end of this project you will have a production-grade SSO layer running in Docker, backed by PostgreSQL, fronted by Traefik with automatic TLS, and already integrated with Grafana and Portainer as concrete examples you can extend to any OIDC-capable application.


Project Overview

What We're Building

┌──────────────────────────────────────────────────────────────────┐
│                      Homelab SSO Architecture                     │
├──────────────────────────────────────────────────────────────────┤
│                                                                  │
│   Browser ──► Traefik (TLS termination / forward auth)           │
│                    │                                             │
│         ┌──────────▼───────────┐                                 │
│         │       Keycloak        │  ◄─── PostgreSQL (state)       │
│         │   auth.homelab.local  │                                 │
│         │                       │                                 │
│         │  Realm: homelab       │                                 │
│         │  Clients: grafana,    │                                 │
│         │  portainer, traefik   │                                 │
│         └──────────┬───────────┘                                 │
│                    │  OIDC / JWT                                  │
│       ┌────────────┼────────────────┐                            │
│       ▼            ▼                ▼                            │
│   Grafana      Portainer      traefik-forward-auth               │
│ (SSO login)  (SSO login)     (protects any app)                  │
│                                                                  │
└──────────────────────────────────────────────────────────────────┘

Prerequisites

  • Docker 24+ and Docker Compose v2
  • A wildcard or per-service DNS record pointing to your Traefik host (e.g. *.homelab.local → your server IP)
  • Traefik v2/v3 already running as a reverse proxy (or follow the Traefik section below to add it)
  • Ports 80 and 443 reachable from your browser

Resource Requirements

ComponentCPURAMDisk
Keycloak0.5–2 cores512 MB – 2 GB200 MB image
PostgreSQL0.25 cores256 MB1 GB+ data

Part 1: Docker Compose Setup

Step 1: Create the Project Directory

mkdir -p ~/keycloak/{data,postgres-data}
cd ~/keycloak

Step 2: Write the Compose File

# docker-compose.yml
version: "3.9"
 
services:
  postgres:
    image: postgres:16-alpine
    container_name: keycloak-db
    restart: unless-stopped
    environment:
      POSTGRES_DB: keycloak
      POSTGRES_USER: keycloak
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    volumes:
      - ./postgres-data:/var/lib/postgresql/data
    networks:
      - keycloak-internal
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U keycloak"]
      interval: 10s
      timeout: 5s
      retries: 5
 
  keycloak:
    image: quay.io/keycloak/keycloak:26.0
    container_name: keycloak
    restart: unless-stopped
    command: start
    depends_on:
      postgres:
        condition: service_healthy
    environment:
      # Database
      KC_DB: postgres
      KC_DB_URL_HOST: keycloak-db
      KC_DB_URL_DATABASE: keycloak
      KC_DB_USERNAME: keycloak
      KC_DB_PASSWORD: ${POSTGRES_PASSWORD}
      # Hostname (must match what Traefik exposes)
      KC_HOSTNAME: auth.homelab.local
      KC_HOSTNAME_STRICT: "true"
      KC_HOSTNAME_ADMIN: auth.homelab.local
      # HTTP (TLS terminated by Traefik upstream)
      KC_HTTP_ENABLED: "true"
      KC_HTTP_PORT: "8080"
      KC_HTTPS_REQUIRED: none
      KC_PROXY_HEADERS: xforwarded
      # Admin bootstrap (26.x naming — only used on first start)
      KC_BOOTSTRAP_ADMIN_USERNAME: ${KEYCLOAK_ADMIN}
      KC_BOOTSTRAP_ADMIN_PASSWORD: ${KEYCLOAK_ADMIN_PASSWORD}
      # Performance & observability
      KC_HEALTH_ENABLED: "true"
      KC_METRICS_ENABLED: "true"
      JAVA_OPTS_APPEND: "-Xms256m -Xmx512m"
    volumes:
      - ./data:/opt/keycloak/data
    networks:
      - keycloak-internal
      - proxy          # shared Traefik network
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.keycloak.rule=Host(`auth.homelab.local`)"
      - "traefik.http.routers.keycloak.entrypoints=websecure"
      - "traefik.http.routers.keycloak.tls=true"
      - "traefik.http.routers.keycloak.tls.certresolver=letsencrypt"
      - "traefik.http.services.keycloak.loadbalancer.server.port=8080"
    healthcheck:
      test: ["CMD-SHELL", "curl -f http://localhost:9000/health/ready || exit 1"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 90s
 
networks:
  keycloak-internal:
    internal: true
  proxy:
    external: true

Step 3: Create the Environment File

# .env  (never commit this file)
POSTGRES_PASSWORD=change_me_strong_db_pass
KEYCLOAK_ADMIN=admin
KEYCLOAK_ADMIN_PASSWORD=change_me_strong_admin_pass
# Generate a random cookie secret: openssl rand -base64 32
chmod 600 .env

Step 4: Start the Stack

docker compose up -d
 
# Watch Keycloak initialise (takes 60-90 seconds on first boot)
docker compose logs -f keycloak

Look for: Keycloak 24.0.x on JVM ... started in 65.123s.


Part 2: Initial Keycloak Configuration

Step 5: Log In to the Admin Console

Open https://auth.homelab.local in your browser. Use the admin credentials from your .env file.

Security note: After completing setup, disable the master realm's admin console from public access and use only the homelab realm for service accounts.

Step 6: Create a Dedicated Realm

A realm is an isolated namespace. Never use master for application clients.

  1. Click the realm dropdown (top-left) → Create realm
  2. Realm name: homelab
  3. Display name: Homelab SSO
  4. Enabled: On → Create

Alternatively, import a realm JSON via the Keycloak Admin CLI:

# Export realm config later for reproducibility
docker exec -it keycloak /opt/keycloak/bin/kc.sh export \
  --dir /opt/keycloak/data/export \
  --realm homelab \
  --users realm_file

Step 7: Configure Realm Settings

In the homelab realm, navigate to Realm settings:

TabSettingValue
GeneralDisplay nameHomelab SSO
LoginUser registrationOff (homelab only)
LoginForgot passwordOn
LoginRemember meOn
EmailFrom addresssso@homelab.local
TokensAccess token lifespan5 minutes
TokensSSO session idle30 minutes
SessionsSSO session max10 hours

Step 8: Create Users

Realm menu → Users → Add user

Username:     dylan
Email:        dylan@homelab.local
First name:   Dylan
Last name:    H.
Email verified: On

After saving, go to the Credentials tab → Set password → enter a strong password → toggle Temporary off.

Step 9: Create Groups and Roles

Create groups that map to application roles:

Realm menu → Groups → Create group

GroupPurpose
homelab-adminsFull admin access to all services
homelab-viewersRead-only access to dashboards
homelab-devsDeveloper access to Gitea, Portainer

Assign your user to homelab-admins:

Users → dylan → Groups → Join group → homelab-admins


Part 3: OIDC Client — Grafana

Step 10: Create the Grafana Client

Realm menu → Clients → Create client

Client type:   OpenID Connect
Client ID:     grafana
Name:          Grafana Monitoring

Next → Client authentication: On → Next

Set the following URLs:

Root URL:                 https://grafana.homelab.local
Home URL:                 https://grafana.homelab.local
Valid redirect URIs:      https://grafana.homelab.local/login/generic_oauth
Valid post logout URIs:   https://grafana.homelab.local/login/generic_oauth
Web origins:              https://grafana.homelab.local

Save → go to Credentials tab → copy the Client secret.

Step 11: Add a Mapper for Groups

In the Grafana client, go to Client scopes → grafana-dedicated → Add mapper → By configuration → Group Membership:

FieldValue
Namegroups
Token claim namegroups
Full group pathOff
Add to ID tokenOn
Add to access tokenOn
Add to userinfoOn

Step 12: Configure Grafana

Add these environment variables to Grafana's container (or grafana.ini):

# grafana.ini  [auth.generic_oauth] section
[auth.generic_oauth]
enabled = true
name = Homelab SSO
allow_sign_up = true
client_id = grafana
client_secret = <paste-secret-from-step-10>
scopes = openid email profile groups
auth_url = https://auth.homelab.local/realms/homelab/protocol/openid-connect/auth
token_url = https://auth.homelab.local/realms/homelab/protocol/openid-connect/token
api_url = https://auth.homelab.local/realms/homelab/protocol/openid-connect/userinfo
role_attribute_path = contains(groups[*], 'homelab-admins') && 'Admin' || contains(groups[*], 'homelab-viewers') && 'Viewer' || 'Editor'

Or via environment variables in docker-compose.yml:

environment:
  GF_AUTH_GENERIC_OAUTH_ENABLED: "true"
  GF_AUTH_GENERIC_OAUTH_NAME: "Homelab SSO"
  GF_AUTH_GENERIC_OAUTH_CLIENT_ID: "grafana"
  GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET: "<secret>"
  GF_AUTH_GENERIC_OAUTH_SCOPES: "openid email profile groups"
  GF_AUTH_GENERIC_OAUTH_AUTH_URL: "https://auth.homelab.local/realms/homelab/protocol/openid-connect/auth"
  GF_AUTH_GENERIC_OAUTH_TOKEN_URL: "https://auth.homelab.local/realms/homelab/protocol/openid-connect/token"
  GF_AUTH_GENERIC_OAUTH_API_URL: "https://auth.homelab.local/realms/homelab/protocol/openid-connect/userinfo"
  GF_AUTH_GENERIC_OAUTH_ROLE_ATTRIBUTE_PATH: "contains(groups[*], 'homelab-admins') && 'Admin' || 'Viewer'"

Restart Grafana — the login page now shows a Sign in with Homelab SSO button.


Part 4: OIDC Client — Portainer

Step 13: Create the Portainer Client

Create a new client in the homelab realm:

Client ID:   portainer
Name:        Portainer CE

Set these redirect URIs:

Valid redirect URIs:   https://portainer.homelab.local/
Web origins:           https://portainer.homelab.local

Client authentication: On → save, then copy the client secret.

Step 14: Configure Portainer OAuth

In Portainer → Settings → Authentication → OAuth:

FieldValue
ProviderCustom
Client IDportainer
Client secret<secret>
Authorization URLhttps://auth.homelab.local/realms/homelab/protocol/openid-connect/auth
Access token URLhttps://auth.homelab.local/realms/homelab/protocol/openid-connect/token
Resource URLhttps://auth.homelab.local/realms/homelab/protocol/openid-connect/userinfo
Redirect URLhttps://portainer.homelab.local/
User ID claimpreferred_username
Scopesopenid profile email

Enable Automatic user provisioning if you want Portainer to create accounts on first login.


Part 5: Traefik Forward Auth (Protect Any App)

Forward auth lets you put SSO in front of apps that have no native OIDC support (e.g., Prometheus, raw web UIs).

Step 15: Create the Forward-Auth Client in Keycloak

Client ID:  traefik-forward-auth
Name:       Traefik Forward Auth
Valid redirect URIs:  https://*.homelab.local/_oauth
Web origins:          https://*.homelab.local

Client authentication: On → save, copy secret.

Step 16: Deploy thomseddon/traefik-forward-auth

# Add to your main docker-compose or Traefik stack
  forward-auth:
    image: thomseddon/traefik-forward-auth:2
    container_name: traefik-forward-auth
    restart: unless-stopped
    environment:
      PROVIDERS_OIDC_ISSUER_URL: https://auth.homelab.local/realms/homelab
      PROVIDERS_OIDC_CLIENT_ID: traefik-forward-auth
      PROVIDERS_OIDC_CLIENT_SECRET: <secret>
      SECRET: <random-32-char-string>
      AUTH_HOST: auth.homelab.local
      COOKIE_DOMAIN: homelab.local
      DEFAULT_PROVIDER: oidc
      LOG_LEVEL: info
    labels:
      - "traefik.enable=true"
      - "traefik.http.middlewares.sso-auth.forwardauth.address=http://forward-auth:4181"
      - "traefik.http.middlewares.sso-auth.forwardauth.authResponseHeaders=X-Forwarded-User"
      - "traefik.http.middlewares.sso-auth.forwardauth.trustForwardHeader=true"
      - "traefik.http.routers.forward-auth.rule=Host(`auth.homelab.local`) && PathPrefix(`/_oauth`)"
      - "traefik.http.routers.forward-auth.middlewares=sso-auth"
    networks:
      - proxy

Step 17: Protect an Application with SSO

Add the sso-auth middleware label to any service you want to gate:

  prometheus:
    image: prom/prometheus:latest
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.prometheus.rule=Host(`prometheus.homelab.local`)"
      - "traefik.http.routers.prometheus.entrypoints=websecure"
      - "traefik.http.routers.prometheus.tls=true"
      - "traefik.http.routers.prometheus.middlewares=sso-auth"   # <-- add this line

Any unauthenticated request now redirects to Keycloak login and back.


Part 6: Multi-Factor Authentication

Step 18: Enforce TOTP for Admin Accounts

In the homelab realm:

  1. Authentication → Policies → OTP Policy
    • Algorithm: SHA1, 6 digits, 30s period
  2. Authentication → Required actions → Configure OTP → Default action: On

Or enforce MFA only for the homelab-admins group:

Authentication → Flows → Duplicate "browser" flow

Name it browser-mfa. In the copied flow, set the OTP Form authenticator to Required and add a Condition — user attribute check that limits it to users with the requires_mfa attribute, then assign this flow to the homelab-admins group via:

Groups → homelab-admins → Authentication flow overrides → Browser flow: browser-mfa


Testing

Verify Keycloak Health

# Health endpoint (served on internal port 9000 — exec into container or use internal network)
curl -s http://localhost:9000/health/ready | python3 -m json.tool
 
# Or via the management path exposed through Traefik (requires internal access)
curl -s https://auth.homelab.local/health | python3 -m json.tool
 
# OIDC discovery document
curl -s https://auth.homelab.local/realms/homelab/.well-known/openid-configuration \
  | python3 -m json.tool | grep -E '"issuer|authorization_endpoint|token_endpoint"'

Expected output snippet:

{
  "issuer": "https://auth.homelab.local/realms/homelab",
  "authorization_endpoint": "https://auth.homelab.local/realms/homelab/protocol/openid-connect/auth",
  "token_endpoint": "https://auth.homelab.local/realms/homelab/protocol/openid-connect/token"
}

Test the Token Flow Manually

# Get an access token via Resource Owner Password Credentials (testing only)
curl -s -X POST \
  "https://auth.homelab.local/realms/homelab/protocol/openid-connect/token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=password" \
  -d "client_id=grafana" \
  -d "client_secret=<secret>" \
  -d "username=dylan" \
  -d "password=<password>" \
  -d "scope=openid profile groups" \
  | python3 -m json.tool | grep -E '"access_token|expires_in"'

Decode the JWT at jwt.io (or with jwt decode):

pip install PyJWT
python3 -c "
import jwt, base64, json
token = '<paste access_token here>'
header = json.loads(base64.b64decode(token.split('.')[0] + '=='))
payload = json.loads(base64.b64decode(token.split('.')[1] + '=='))
print('Header:', json.dumps(header, indent=2))
print('Claims:', json.dumps(payload, indent=2))
"

Confirm the groups claim contains homelab-admins.

Grafana SSO Test

  1. Open https://grafana.homelab.local in an incognito window
  2. Click Sign in with Homelab SSO — you are redirected to Keycloak
  3. Log in as dylan — you are redirected back as Grafana Admin
  4. Check Server Admin → Users — Dylan's account shows role Admin

Verification Checklist

Infrastructure:

  • Keycloak health endpoint returns {"status": "UP"}
  • OIDC discovery document reachable at /.well-known/openid-configuration
  • TLS certificate valid (Let's Encrypt or internal CA)
  • PostgreSQL data persisting across container restarts

Realm Config:

  • homelab realm created, master realm access restricted
  • Users cannot self-register
  • Groups homelab-admins and homelab-viewers exist
  • Group membership claim present in access tokens

Integrations:

  • Grafana shows Sign in with Homelab SSO
  • Admin user gets Admin role in Grafana via group claim
  • Portainer OAuth login works
  • Traefik forward auth redirects unauthenticated users to Keycloak

Troubleshooting

SymptomLikely CauseFix
Invalid parameter: redirect_uriRedirect URI not registeredAdd exact URI in Keycloak client settings
Client not foundWrong realm in URLCheck KC_HOSTNAME and realm name in token URL
Keycloak OOM-killedNot enough RAMSet JAVA_OPTS_APPEND=-Xms256m -Xmx512m in environment
PostgreSQL connection refusedDB not ready when Keycloak startsAdd depends_on healthcheck (already in the Compose file above)
Redirect loop in forward authAUTH_HOST misconfiguredSet AUTH_HOST to your Keycloak host, not forward-auth host
Groups claim missingMapper not added to client scopeRe-add group membership mapper to the client's dedicated scope

Backup and Recovery

# Backup PostgreSQL
docker exec keycloak-db pg_dump -U keycloak keycloak \
  | gzip > ~/backups/keycloak-$(date +%Y%m%d).sql.gz
 
# Restore
gunzip -c ~/backups/keycloak-20260326.sql.gz \
  | docker exec -i keycloak-db psql -U keycloak keycloak
 
# Export realm config (portable, version-controllable)
docker exec -it keycloak /opt/keycloak/bin/kc.sh export \
  --dir /opt/keycloak/data/export \
  --realm homelab

Commit the exported realm JSON to a private git repository. On disaster recovery, docker compose up then import the realm JSON via the admin UI or CLI.


Extensions and Next Steps

  1. LDAP / Active Directory sync — bind Keycloak to your AD for unified user management under User Federation → Add LDAP provider
  2. Social login — add GitHub or Google as identity providers so users can link external accounts
  3. Webhook events — configure the Keycloak Event Listener SPI to push login/logout events to your SIEM or n8n workflow for auditing
  4. Account linking — let existing users link their Keycloak account to a GitHub OAuth identity
  5. High availability — run two Keycloak nodes with --cache=ispn and a shared Infinispan/JDBC session store for zero-downtime restarts
  6. Passkey / WebAuthn — enable the built-in WebAuthn authenticator under Authentication → Policies → WebAuthn Policy for passwordless login

Resources

  • Keycloak Documentation
  • Keycloak Docker Images
  • thomseddon/traefik-forward-auth
  • Grafana Generic OAuth

Questions? Join the CosmicBytez community Discord.

Related Reading

  • HashiCorp Vault: Secrets Management for Your Homelab
  • Kubernetes Homelab Cluster with K3s
  • Network Traffic Analysis with Zeek and Suricata
#Keycloak#SSO#Identity#OIDC#Docker#Security#Homelab#authentication

Related Articles

Homelab Media Server with Full ARR Stack

Deploy a complete self-hosted media automation system with Plex, Sonarr, Radarr, Prowlarr, and more. Includes Traefik reverse proxy, Authentik SSO, and...

7 min read

Kubernetes Homelab Cluster with K3s

Build a production-grade K3s cluster on Proxmox/bare metal with Longhorn storage, Traefik ingress, cert-manager, and ArgoCD for GitOps.

5 min read

HashiCorp Vault: Secrets Management for Your Homelab and

Deploy HashiCorp Vault to centrally manage secrets, certificates, and dynamic credentials — eliminating hardcoded passwords from your infrastructure with...

12 min read
Back to all Projects