Skip to main content

Keycloak Administration

STOA uses Keycloak as its identity provider. This guide covers realm configuration, OIDC client setup, RBAC role mapping, and production hardening.

Architecture Overview​

Realm Configuration​

Create the Realm​

  1. Log in to Keycloak Admin Console (https://auth.<YOUR_DOMAIN>/admin)
  2. Click Create Realm
  3. Set realm name: stoa

Realm Settings​

SettingValuePurpose
Display nameSTOA IdentityShown on login page
SSL requiredexternal (prod) / none (dev)TLS enforcement
Registration allowedfalseInvite-only access
Login with emailtrueEmail as username
Brute force protectiontrueSecurity hardening
Reset passwordtrueSelf-service recovery

Token Lifespans​

SettingDevProductionPurpose
Access token lifespan3600s (1h)300s (5min)Short-lived for security
SSO session idle1800s (30min)900s (15min)Idle timeout
SSO session max36000s (10h)28800s (8h)Maximum session

RBAC Roles​

Create these 4 realm roles in Realm Settings > Roles:

RoleDescriptionScope
cpi-adminFull platform administrator, can manage all tenants and system settingsPlatform-wide
tenant-adminTenant administrator, can manage their own tenant's APIs and usersTenant-scoped
devopsDevOps engineer, can deploy and promote APIsTenant-scoped
viewerRead-only access to tenant resourcesTenant-scoped

Role Hierarchy​

cpi-admin (platform-wide)
└── tenant-admin (tenant-scoped)
└── devops (deploy-scoped)
└── viewer (read-only)

Higher roles inherit all permissions of lower roles. See RBAC Permissions for the full permission matrix.

User-Tenant Mapping​

Each user needs a tenant attribute to scope their access:

  1. Go to Users > select user > Attributes
  2. Add attribute: tenant = acme (the tenant slug)

The tenant claim is included in tokens via a protocol mapper (configured per client).

OIDC Clients​

1. Control Plane API (Backend)​

Purpose: Backend service-to-service authentication.

SettingValue
Client IDcontrol-plane-api
Client typeConfidential
Standard flowEnabled
Direct access grantsEnabled
Service accountsEnabled
Client authenticationOn (client secret)

Redirect URIs:

  • Dev: http://localhost:8000/*
  • Prod: https://api.<YOUR_DOMAIN>/*

2. Console UI (Admin Console)​

Purpose: Admin console for API providers. Public client using PKCE.

SettingValue
Client IDcontrol-plane-ui
Client typePublic
Standard flowEnabled
Direct access grantsEnabled

Redirect URIs:

  • Dev: http://localhost:3000/*, http://localhost:5173/*
  • Prod: https://console.<YOUR_DOMAIN>/*

Protocol mapper (required): Add an Audience mapper:

  • Name: control-plane-api-audience
  • Mapper type: Audience
  • Included Client Audience: control-plane-api
  • Add to ID token: Yes
  • Add to access token: Yes

This ensures the aud claim includes control-plane-api, allowing the backend to validate tokens.

3. Developer Portal​

Purpose: Public-facing developer portal for API consumers.

SettingValue
Client IDstoa-portal
Client typePublic
Standard flowEnabled
Direct access grantsEnabled

Redirect URIs:

  • Dev: http://localhost:3001/*
  • Prod: https://portal.<YOUR_DOMAIN>/*

Protocol mapper: Same audience mapper as Console (control-plane-api-audience).

4. MCP Gateway​

Purpose: Gateway JWT validation and service account for tool discovery.

SettingValue
Client IDstoa-mcp-gateway
Client typeConfidential
Standard flowEnabled
Direct access grantsEnabled
Service accountsEnabled

Redirect URIs:

  • Dev: http://localhost:8081/*
  • Prod: https://mcp.<YOUR_DOMAIN>/*

5. OpenSearch Dashboards (Optional)​

Purpose: OIDC SSO for the logs/search UI.

SettingValue
Client IDopensearch-dashboards
Client typeConfidential
Standard flowEnabled

Protocol mappers:

  • tenant-mapper: User Attribute mapper, claims tenant_id from user attribute tenant
  • realm-roles-mapper: Realm Role mapper, claims roles with user's realm roles

Redirect URIs:

  • Prod: https://opensearch.<YOUR_DOMAIN>/*

6. Observability (Optional)​

Purpose: Grafana SSO and Prometheus OAuth2 proxy.

SettingValue
Client IDstoa-observability
Client typeConfidential
Standard flowEnabled

Protocol mapper: realm-roles mapper adding roles claim.

Redirect URIs:

  • Grafana: https://grafana.<YOUR_DOMAIN>/login/generic_oauth
  • Prometheus: https://prometheus.<YOUR_DOMAIN>/oauth2/callback

Client Summary​

Client IDTypeService AccountAudience MapperUsers
control-plane-apiConfidentialYes--Backend services
control-plane-uiPublicNocontrol-plane-apiAdmins (Console)
stoa-portalPublicNocontrol-plane-apiDevelopers (Portal)
stoa-mcp-gatewayConfidentialYes--Gateway service
opensearch-dashboardsConfidentialNo--Observability users
stoa-observabilityConfidentialNo--Grafana/Prometheus

Security Hardening (Production)​

Browser Security Headers​

Configure in Realm Settings > Security Defenses > Headers:

HeaderValue
Content-Security-Policyframe-src 'self'; frame-ancestors 'self' https://console.<YOUR_DOMAIN>; object-src 'none';
X-Content-Type-Optionsnosniff
X-XSS-Protection1; mode=block
Strict-Transport-Securitymax-age=31536000; includeSubDomains
Referrer-Policyno-referrer

Brute Force Protection​

Enable in Realm Settings > Security Defenses > Brute Force Detection:

SettingRecommended Value
EnabledYes
Max login failures5
Wait increment (seconds)60
Max wait (seconds)900
Failure reset time (seconds)43200 (12h)

TOTP/2FA (Optional)​

STOA supports step-up authentication with TOTP for sensitive operations:

SettingValue
OTP policy typeTOTP
AlgorithmHmacSHA256
Digits6
Period30 seconds
Supported appsGoogle Authenticator, Microsoft Authenticator, Authy, 1Password, FreeOTP

Enable via a custom authentication flow (step-up-totp) that requires OTP for admin operations.

Realm Export/Import​

Export (Backup)​

Configure your environment

The examples below use environment variables. Set them for your STOA instance:

export STOA_API_URL="https://api.gostoa.dev"       # Replace with your domain
export STOA_AUTH_URL="https://auth.gostoa.dev" # Keycloak OIDC provider
export STOA_GATEWAY_URL="https://mcp.gostoa.dev" # MCP Gateway endpoint

Self-hosted? Replace gostoa.dev with your domain.

# Export realm configuration (no secrets)
curl -s "${STOA_AUTH_URL}/admin/realms/stoa" \
-H "Authorization: Bearer ${ADMIN_TOKEN}" | jq > stoa-realm-backup.json

Import (Bootstrap)​

For initial deployment, import the reference realm:

# Using Keycloak CLI
/opt/keycloak/bin/kcadm.sh config credentials \
--server "${STOA_AUTH_URL}" \
--realm master \
--user admin \
--password "${KEYCLOAK_ADMIN_PASSWORD}"

/opt/keycloak/bin/kcadm.sh create realms \
-f stoa-realm.json

Or via Docker Compose with volume mount:

keycloak:
image: quay.io/keycloak/keycloak:24.0
command: start-dev --import-realm
volumes:
- ./init/keycloak-realm.json:/opt/keycloak/data/import/stoa-realm.json

Grafana OIDC Integration​

Configure Grafana to use Keycloak SSO:

# Helm values (kube-prometheus-stack)
grafana:
grafana.ini:
auth.generic_oauth:
enabled: true
name: Keycloak
client_id: stoa-observability
client_secret: ${GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET}
auth_url: https://auth.<YOUR_DOMAIN>/realms/stoa/protocol/openid-connect/auth
token_url: https://auth.<YOUR_DOMAIN>/realms/stoa/protocol/openid-connect/token
api_url: https://auth.<YOUR_DOMAIN>/realms/stoa/protocol/openid-connect/userinfo
scopes: openid profile email roles
role_attribute_path: "contains(roles[*], 'stoa:admin') && 'Admin' || 'Viewer'"

This maps Keycloak roles to Grafana roles:

  • Users with stoa:admin scope get Grafana Admin role
  • All others get Grafana Viewer role

Troubleshooting​

ProblemCauseFix
invalid_grant errorToken expired or wrong client secretRegenerate client secret in Keycloak
audience_mismatchMissing audience mapper on clientAdd control-plane-api-audience protocol mapper
unauthorized_clientWrong grant type enabledEnable Standard Flow and/or Direct Access Grants
Login page shows stoa realmCorrect behaviorUsers authenticate against the stoa realm
CORS errors from ConsoleWrong Web OriginsAdd Console URL to client's Web Origins
Token missing tenant claimUser has no tenant attributeAdd tenant user attribute in Keycloak
Grafana SSO shows 403Missing roles claimAdd realm-roles protocol mapper to stoa-observability client