I got tired of managing separate logins for Grafana, ArgoCD, Harbor, and every other internal tool we run. Every new team member meant creating five accounts. Every offboarding meant hoping I remembered to revoke all of them. So I finally sat down and deployed Keycloak on our Kubernetes cluster.
This is what actually happened, not the sanitized version.
Why Keycloak
I looked at Dex, Authelia, and Keycloak. Dex is lightweight but limited if you need more than OIDC proxying. Authelia is great for simple setups but felt thin for our use case. Keycloak is heavier, but it handles OIDC, SAML, user federation, and has a proper admin UI. For a team running 8+ internal services, the weight is justified.
The Helm Chart
I used the Bitnami Keycloak chart. The official Keycloak operator exists too, but I wanted something simpler to start with.
helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo update
helm install keycloak bitnami/keycloak \
--namespace keycloak \
--create-namespace \
--set auth.adminUser=admin \
--set auth.adminPassword=changeme-obviously \
--set postgresql.enabled=true \
--set postgresql.auth.postgresPassword=also-changeme \
--set proxy.enabled=true \
--set proxy.edge=true \
--set ingress.enabled=true \
--set ingress.hostname=sso.internal.example.com \
--set ingress.tls=true \
--set ingress.annotations."cert-manager\.io/cluster-issuer"=letsencrypt-prod
That proxy.edge=true flag is critical if you terminate TLS at the ingress. Without it, Keycloak thinks it’s running on HTTP and generates wrong redirect URIs. I spent an hour on this before reading the docs properly.
The Database Question
The Bitnami chart bundles PostgreSQL by default. For production, I switched to an external PostgreSQL instance. Running a database inside Kubernetes is fine for dev, but for your identity provider, you want something you trust.
# values-production.yaml
postgresql:
enabled: false
externalDatabase:
host: postgres.internal.example.com
port: 5432
user: keycloak
database: keycloak
existingSecret: keycloak-db-secret
existingSecretPasswordKey: password
Realm Setup
I created a single realm called internal for all our tools. One realm, multiple OIDC clients. Each tool gets its own client with its own redirect URIs.
# After logging into the admin console
# Create realm: internal
# Create client: grafana
# Client ID: grafana
# Root URL: https://grafana.internal.example.com
# Valid redirect URIs: https://grafana.internal.example.com/login/generic_oauth
# Client authentication: ON
# Copy the client secret
Wiring Up Grafana
Grafana’s OIDC integration is straightforward. In grafana.ini or via Helm values:
[auth.generic_oauth]
enabled = true
name = Keycloak
allow_sign_up = true
client_id = grafana
client_secret = ${GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET}
scopes = openid profile email
auth_url = https://sso.internal.example.com/realms/internal/protocol/openid-connect/auth
token_url = https://sso.internal.example.com/realms/internal/protocol/openid-connect/token
api_url = https://sso.internal.example.com/realms/internal/protocol/openid-connect/userinfo
role_attribute_path = contains(realm_access.roles[*], 'admin') && 'Admin' || 'Viewer'
That last line maps Keycloak roles to Grafana roles. Users with the admin role in Keycloak get Grafana Admin. Everyone else gets Viewer. Simple, effective.
ArgoCD Was Trickier
ArgoCD uses Dex internally by default for OIDC. You can either configure Dex to talk to Keycloak, or bypass Dex and point ArgoCD directly at Keycloak. I went with the direct approach.
In the ArgoCD ConfigMap:
apiVersion: v1
kind: ConfigMap
metadata:
name: argocd-cm
namespace: argocd
data:
url: https://argocd.internal.example.com
oidc.config: |
name: Keycloak
issuer: https://sso.internal.example.com/realms/internal
clientID: argocd
clientSecret: $oidc.keycloak.clientSecret
requestedScopes:
- openid
- profile
- email
- groups
The groups scope needs a mapper in Keycloak. Go to the argocd client, Mappers tab, create a new mapper:
- Mapper type: Group Membership
- Token claim name: groups
- Full group path: OFF
Then in argocd-rbac-cm:
data:
policy.csv: |
g, /devops-team, role:admin
g, /developers, role:readonly
Groups from Keycloak map directly to ArgoCD roles. When someone joins or leaves a team, you update it in one place.
The Session and Token Gotcha
Default Keycloak token lifetimes are short. Like, 5 minutes for access tokens. This means your users get logged out of Grafana constantly. Bump the access token lifespan in the realm settings to something reasonable:
- Access Token Lifespan: 30 minutes
- SSO Session Idle: 8 hours
- SSO Session Max: 24 hours
This matches a normal workday. People log in once in the morning, and they’re good.
Health Checks and Readiness
Keycloak’s health endpoint changed between versions. In newer versions (22+), it’s:
livenessProbe:
httpGet:
path: /health/live
port: http
readinessProbe:
httpGet:
path: /health/ready
port: http
If you’re using an older version, the paths are different. Check your version’s docs. I wasted 20 minutes watching pods restart because the readiness probe was hitting a 404.
What I’d Do Differently
Start with the Keycloak Operator. The Helm chart works, but the operator handles upgrades and realm configuration as CRDs. For a production setup, that’s cleaner.
Use Terraform for realm config. There’s a Keycloak Terraform provider that lets you define realms, clients, and mappers as code. I set everything up manually through the UI first, which means I now need to reverse-engineer it into Terraform. Don’t repeat my mistake.
resource "keycloak_openid_client" "grafana" {
realm_id = keycloak_realm.internal.id
client_id = "grafana"
name = "Grafana"
enabled = true
access_type = "CONFIDENTIAL"
valid_redirect_uris = [
"https://grafana.internal.example.com/login/generic_oauth"
]
}
Plan your group structure early. I started adding users without thinking about groups and had to restructure later. Decide on your group hierarchy before onboarding anyone.
The Result
One login for everything. New team members get added to Keycloak, assigned to the right groups, and they immediately have access to Grafana, ArgoCD, Harbor, and our internal wikis. Offboarding is one account deletion.
It took about a day to set up properly, including the false starts. If you’re managing more than three internal tools with separate auth, just do it. The time investment pays for itself within a week.
With KubeCon Europe 2026 around the corner and KeycloakCon running as a co-located event, the project is getting serious momentum. The community is active, the docs are improving, and the operator is maturing fast. Good time to get on board.