Webhook icon
Schedule icon
If icon
Fail icon
Request icon
Switch icon
Script icon
Process icon
Set icon
SlackIncomingWebhook icon
Pause icon
OutputValues icon
Loop icon
Sequential icon
Exit icon

Audit Keycloak Realm Security and Fix It with On-Call Approval

Audit a Keycloak realm for admins without OTP, stale users, old client secrets and wildcard redirect URIs; rotate or disable with approval.

Categories
InfrastructureSystem

A Keycloak realm drifts. Someone is made an administrator through a group and never sets up OTP. A contractor leaves and the account stays enabled. A service client keeps the secret it got two years ago. A redirect URI typed as https://app.example.com* also accepts https://app.example.com.attacker.example, which lets an attacker receive authorization codes. None of this shows up until an audit or an incident.

This blueprint reads the realm through the Admin REST API with a dedicated service account, reports each finding once (and again when it is resolved), and turns the two safe fixes into a one-click decision for on-call: rotate an old client secret, or disable an account nobody uses. It was tested on Keycloak 26.4.

How it works

  1. realm_changed (Webhook, event-based) runs an audit right after a realm change, for example at the end of the Terraform or keycloak-config-cli job that manages the realm; daily_audit (Schedule) catches drift made by hand.
  2. validate_realm (If) rejects realm names that are not plain identifiers, since a webhook body can choose the realm.
  3. check_realm (HTTP, retried) calls the public realm endpoint; realm_reachable (If + Switch) tells "realm not found" from "Keycloak unhealthy".
  4. audit (Python, standard library) logs in with the client credentials grant and checks:
    • Administrators without a second factor: effective realm roles and realm-management roles, so roles held through groups and composite roles count; OTP or (optionally) WebAuthn credentials; a pending "Configure OTP" required action is mentioned.
    • Stale accounts: no LOGIN, CODE_TO_TOKEN, REFRESH_TOKEN or identity provider login event for stale_user_days, account older than that, and no active session. The check refuses to run, and says why, when events are off, expire too early, or do not yet reach back far enough.
    • Old client secrets: client.secret.creation.time older than secret_max_age_days, or missing (the secret was never rotated by Keycloak).
    • Redirect URIs: *, a wildcard in the host part, a path wildcard, plain HTTP.
    • The password grant (Direct access grants) on any client. It compares the findings with the previous run's, kept in the KV store.
  5. save_state stores the findings before anyone is asked, so a queued run never reports or asks twice.
  6. report (If) sends new and resolved findings to Slack.
  7. propose (If) asks on-call and waits (Pause, 4 hours) with one checkbox per client or account. The choices are built at run time from the findings.
  8. rotate_secrets and disable_users (Loop, each item in a Sequential with allowFailure) re-check each item just before acting: a secret already changed or an account that signed in since the proposal is skipped.
  9. notify_outcome reports each result; any_failed (If) ends the run WARNING if one item failed.

Error handling

  • Secrets never leave Keycloak. The token exchange and the rotation run inside scripts that output only what is chosen: an HTTP task would keep the token response, and the rotation response with the new secret, in the execution outputs. The client list returned by Keycloak includes secrets; the flow keeps only names and dates. The client's owner copies the new secret from the admin console.
  • Rotation is honest about downtime. Without a secret-rotation client policy the old secret stops working at once, and the report says so. With one (preview feature client-secret-rotation), it reports until when the old secret is still accepted.
  • Actions only touch what was shown. A secret is rotated only if its creation time is still the one in the proposal; an account is disabled only if it has no new login event and no session since the cutoff, and is still enabled.
  • No false "stale" verdicts. If login events are off, expire before stale_user_days, or were enabled too recently to cover the window, the stale account check is skipped and reported as an info finding, instead of flagging everyone.
  • The flow never locks itself out. Its own client is reported but never proposed for rotation.
  • Each item is isolated. A failed rotation (for example a missing manage-clients role) is reported as "NOT rotated", the other items still run, and the run ends WARNING.
  • Clear failures. Unreachable Keycloak (retried), realm not found, rejected credentials and missing roles each fail with the exact fix in the Slack alert, which names the task.
  • No alert storms. Each finding is reported when it appears and when it is resolved; declined or unanswered proposals are asked again only after ask_again_hours. Runs are queued (concurrency), and the state is saved before the approval, so a burst of webhook calls produces one report.

What this blueprint teaches about Kestra

  • A Pause whose MULTISELECT choices come from an expression computed earlier in the run; Kestra rejects any answer that was not offered.
  • The KV store as memory between runs, to report changes instead of the same list every day.
  • Sharing helper code between script tasks with a flow variable and inputFiles, and keeping credentials out of task outputs.
  • A Loop with per-item Sequential + allowFailure and a ?? fallback in the loop output for the failed items.

Prerequisites

  • Keycloak 22 or later (tested on 26.4). In the audited realm, a confidential client (default kestra-auditor) with Service accounts roles on and these realm-management roles: view-users, view-clients, view-events for the audit, manage-users and manage-clients for the actions.
  • Login events saved for at least stale_user_days (Realm settings > Events > User events settings).
  • A Slack incoming webhook.

Secrets

  • KEYCLOAK_URL: base URL, for example https://sso.example.com (add /auth for Keycloak 16 and older).
  • KEYCLOAK_CLIENT_SECRET: secret of the auditor client.
  • SLACK_WEBHOOK_URL: Slack incoming webhook.
  • KEYCLOAK_AUDIT_WEBHOOK_KEY: a long random string, the key in the webhook URL ($KEY in the trigger example).

Inputs

  • realm (STRING, default acme): realm to audit; a webhook body {"realm": "..."} overrides it.
  • client_id (STRING, default kestra-auditor): the auditor client.
  • stale_user_days (INT, 90), secret_max_age_days (INT, 180).
  • admin_realm_roles (ARRAY, admin), admin_client_roles (ARRAY, realm-admin, manage-realm, manage-users, manage-clients, manage-authorization): what makes a user an administrator.
  • accept_webauthn (BOOL, true), ignore_clients (ARRAY, Keycloak's built-in clients), max_users (INT, 5000), ask_again_hours (INT, 72), kestra_url (STRING).

Outputs

  • findings (JSON): every current finding with check, severity, summary and fix.
  • changes (JSON): new and resolved findings. Both are empty when the run fails.

Quick start

  1. Create the auditor client and give its service account the roles above. Turn on login events.
  2. Add the secrets and run the flow. The first run reports every finding; on the test realm it found 12 (5 high, 6 medium, 1 low), for example "client legacy-portal redirect URI https://legacy.example.com* has a wildcard in the host part: it also matches legacy.example.com.attacker.example".
  3. Tick what to fix in the approval form, or nothing. Later runs only report changes: "0 new, 4 resolved, 8 unchanged".
  4. Call the webhook from the pipeline that changes the realm, and enable the daily schedule.

How to extend

  • Push the new secret to Vault or AWS Secrets Manager from the rotation script, instead of leaving it only in Keycloak.
  • Audit several realms: create the auditor client in each, and call the webhook with each realm name. Runs are queued one at a time (concurrency limit 1 with QUEUE), so while one realm waits up to 4 hours for an approval, the other realms' audits wait behind it. For many realms, copy the flow per realm, or lower pauseDuration.
  • Add checks the same way, for example identity providers that trust unverified e-mails, or a brute-force detection setting that is off.

Links

See How

New to Kestra?

Use blueprints to kickstart your first workflows.