Direct answer: Keycloak 26.8.2 closes 33 CVEs. CVE-2026-117691 gives login as any SAML federated user, with no account. Today, check the version, check the realms that use SAML, then update to 26.8.2.

Red Hat gives CVE-2026-117691 a CVSS 3.1 score of 9.1. Only 26.8.2 carries the patch. The 26.7, 26.6, and 26.4 branches get no backport.

Main condition: 1 or more realms use a SAML identity provider, or a SAML client on the redirect binding. Limit: this article has no payloads, attack code, or attack sequence.

We read the CVE-2026-117691 record, the GitHub advisory GHSA-c85j-3hq2-q6wj, the Keycloak 26.8.2 release notes, the GitHub release 26.8.2, the Keycloak upgrade guide, the Red Hat CVE page, the Phase Two release analysis, and CISA KEV on 6 November 2026, from 13.35 to 13.55 WIB. We tested 3 check scripts in a podman pod with the official image on quay.io 26.8.1 and 26.8.2, plus an nginx 1.29 rule. We did not test tar.gz, the Operator, the Red Hat build, PostgreSQL, or an attack.

After publication, from 14.09 to 14.15 WIB, we tested the nginx rule again with the path prefix /auth. We also tested the kcadm login through the env KC_CLI_PASSWORD, so that the password does not show in the process list.

The problem: a fake SAML login can get in as any user

Keycloak holds the login for many apps at once, so 1 login flaw opens all the apps behind it.

Keycloak is an open source login server. Businesses use Keycloak for single sign-on to portals, internal apps, and APIs. Many teams connect Keycloak to an office SAML IdP, for example Azure AD or ADFS, through identity brokering.

The CVE record came out on 5 November 2026 at 13.27 UTC. Release 26.8.2 came out on 5 November at 16.00 UTC, and the quay.io image came 4 minutes later.

Timeline: CVE-2026-16443 in August, 26.8.1 closes the semicolon variant on 13 October, report on 26 October, CVE and release 26.8.2 on 5 November, not on KEV.
Red dots: flaws and CVE publication. Teal dots: patch releases. Grey dot: the report to Red Hat. Amber dot: the status today. Source: CVE records, Keycloak GitHub releases, quay.io, the Red Hat CVE page, and CISA KEV.

This is the third SAML flaw in the Keycloak login path in 3 months. The semicolon variant was closed in 26.8.1 on 13 October, so a server on 26.8.0 has 2 variants at once.

How it works: advisory summary

This part uses the official summary from the CVE record. It helps you pick the right steps. It does not explain how to attack.

Summary of CVE-2026-117691

According to the CVE-2026-117691 record, the Keycloak SAML HTTP-Redirect binding endpoint processes parameter names that are written with percent encoding, for example %53AMLResponse. The signature check reads only the plain parameter names. As a result, a SAML message with no valid signature can pass, and an attacker with no account can take over federated user accounts.

Red Hat gives the rating Important and says that no mitigation meets the Red Hat criteria. So the update is the only official fix.

6 other CVEs to read

CVESummaryCVSS 3.1
CVE-2026-117691SAML redirect binding signature check bypassed, broker login as any user9.1
CVE-2026-97175IdP auto-linking with an unverified email gives account takeover8.8
CVE-2026-112208Account takeover through organization identity-first login, 26.8.0 and 26.8.18.1
CVE-2026-92857A default DCR mapper can write the sub and aud claims8.1
CVE-2026-114232UMA permissions can open resources of other users8.1
CVE-2026-19688Admin APIAPIThe official door 2 systems use to exchange data, without anybody copying it by hand.Open the glossary with no audience check. The fix adds the kc-admin-api scope6.8
A red card for CVE-2026-117691 rated 9.1 and an amber card for CVE-2026-112208 rated 8.1. A chart of 33 CVEs in 7 groups. Safe: Keycloak 26.8.2.
(1) Red card: the SAML flaw with no login. (2) Red bars: the account takeover and SAML check groups. (3) Amber bars: other groups. (4) Teal box: the safe version. Source: the Keycloak 26.8.2 release notes, CVE records, and Rama Digital grouping.

The groups in the diagram come from the release note titles, made by Rama Digital. The 5 DoS CVEs are rated 7.5, and 4 of them work with no login. We covered CVE-2026-111497 in the Brotli decoder in the Node2Shell CVE-2026-111497 warning.

Exploitation status on 6 November 2026

We found no attack reports, no public attack code, and no CISA KEV entry for CVE-2026-117691. Rama Digital analysis: the fix is in the public Keycloak repository, so attackers can read the code changes. Treat this release as urgent if you use SAML.

Who is most at risk

The risk depends on how each realm is used, not only on the version. Realms that use only OIDC are not exposed to CVE-2026-117691.

Keycloak conditionRiskReason
Realm with a SAML identity provider (broker)HighestCVE-2026-117691: broker login as any user, with no account
SAML client on the redirect bindingHighPhase Two says this path is also exposed
26.8.0 or 26.8.1 with organizations onHighCVE-2026-112208, CVSS 8.1
Open Dynamic Client RegistrationCheck5 CVEs of forged token claims through mappers
Realm endpoints open to the internetCheck5 DoS CVEs, 4 with no login
OIDC only and already on 26.8.2LowStill look for signs from before the update
Six risk cards: SAML IdP, SAML client, and organizations on 26.8.x are red; open DCR and endpoints on the internet are amber; OIDC only on 26.8.2 is grey.
Red cards: the highest risk. Amber cards: check. Grey card: low risk. Bottom box: the exploitation status on 6 November 2026. Source: CVE records, Phase Two, CISA KEV, and Rama Digital analysis.

This order follows the Phase Two release analysis: update today if you use a SAML broker or organizations on 26.8.x. Update this week if realm endpoints are open to the internet or DCR is on.

Vulnerable and safe versions

What counts is the Keycloak version that runs, not the Java or operating system version.

VersionStatus on 6 November 2026Action
26.8.0 and 26.8.1Vulnerable, 33 CVEs. 26.8.0 also has the semicolon variantUpdate to 26.8.2 today
26.7.xVulnerable. The branch stopped at 26.7.5Move to 26.8.2
26.6.x, 26.4.x, and olderVulnerable. No backportMove to 26.8.2
Red Hat build of KeycloakAffected, no patch yetStep 4, watch for an RHSA
Red Hat SSO 7Not affected by CVE-2026-117691Check the other CVEs
26.8.2Safe for the 33 CVEsCheck the kc-admin-api scope, then step 5
A table of 6 rows: 26.8.0 and 26.8.1, 26.7, and 26.6 and older are vulnerable, the Red Hat build has no patch, Red Hat SSO 7 is not affected, 26.8.2 is safe.
Red: vulnerable. Amber: no patch yet. Grey: not affected by the main CVE. Teal: safe. Bottom box: 3 ways to run the version script. Source: Keycloak release notes, GitHub and quay.io tags, the Red Hat CVE page, and Rama Digital test.

The advisory has no list of affected versions. Phase Two advises you to treat all versions before 26.8.2 as vulnerable. According to the Red Hat CVE page, the Red Hat build of Keycloak is Affected with no patch on 6 November 2026, and Red Hat SSO 7 is not affected.

Rama Digital analysis: a move from 26.4 or 26.6 to 26.8.2 crosses several migration notes. Read the Keycloak upgrade guide for each version that you skip, then test on staging first.

Prerequisites

  • SSH access to the Keycloak server, or access to the Docker host or the Kubernetes cluster.
  • A master realm admin account and kcadm.sh from the Keycloak bin/ folder.
  • jq and curl on the machine that runs the scripts.
  • A backup of the Keycloak database, for example a PostgreSQL dump, before the update.
  • A list of the clients and service accounts that call the Admin API.
  • An update window of 30 to 60 minutes. A restart stops logins for a short time.

Step 1: Check the Keycloak version

Save this script as check-version-keycloak.sh. The script reads a Keycloak folder, a Docker container, or a version that you type, then rates that version.

#!/usr/bin/env bash
# Check the Keycloak version for CVE-2026-117691 (SAML HTTP-Redirect, signature check bypass) and 32 other CVEs in release 26.8.2.
# The script only reads. Needs bash. For a container name it needs docker or podman (DOCKER=podman).
# Use: bash check-version-keycloak.sh /opt/keycloak
#   or a container name: bash check-version-keycloak.sh keycloak
#   or type the version: bash check-version-keycloak.sh 26.8.1
set -u
SAFE_V=26.8.2

# less_than A B: true when A < B
less_than() { [ "$1" != "$2" ] && [ "$(printf '%s\n%s\n' "$1" "$2" | sort -V | head -n 1)" = "$1" ]; }

rate() {
  local raw=$1 v
  case "$raw" in *[Rr]ed[Hh]at*|*RHBK*) echo "RED HAT BUILD: Red Hat lists Affected with no patch on 6 November 2026. Use step 4 and watch for an RHSA."; return ;; esac
  v=$(printf '%s' "$raw" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -n 1)
  [ -z "$v" ] && { echo "CHECK: unknown version ${raw:-empty}"; return; }
  if ! less_than "$v" $SAFE_V; then echo "SAFE for the 33 CVEs of release $SAFE_V"; return; fi
  case "$v" in
    26.8.*) if less_than "$v" 26.8.1; then echo "VULNERABLE: update to $SAFE_V (also the ';' variant closed in 26.8.1)"; else echo "VULNERABLE: update to $SAFE_V"; fi ;;
    26.7.*) echo "VULNERABLE: the 26.7 branch stopped at 26.7.5, move to $SAFE_V" ;;
    *) if less_than "$v" 26.7.0; then echo "VULNERABLE: no patch on this branch, move to $SAFE_V"; else echo "CHECK: unknown version $v"; fi ;;
  esac
}

ARG=${1:-/opt/keycloak}
if printf '%s' "$ARG" | grep -qE '^[0-9]+\.[0-9]+'; then
  echo "Keycloak $ARG: $(rate "$ARG")"; exit 0
fi

if [ -d "$ARG" ]; then
  DIR=$ARG
  echo "== Folder: $(cd "$DIR" && pwd)"
  if [ -f "$DIR/version.txt" ]; then
    RAW=$(head -n 1 "$DIR/version.txt"); echo "version.txt: $RAW"
  else
    RAW=$(ls "$DIR"/lib/lib/main/ 2>/dev/null | grep -E 'keycloak-services-' | head -n 1)
    [ -n "$RAW" ] && echo "jar: $RAW"
  fi
  [ -z "${RAW:-}" ] && { echo "Folder $DIR is not a Keycloak folder (no version.txt or keycloak-services-*.jar)"; exit 1; }
  RAW=${RAW#Keycloak - Version }
  echo "Keycloak ${RAW#*keycloak-services-}: $(rate "${RAW#*keycloak-services-}")"
  exit 0
fi

# Not a version and not a folder: treat it as a container name.
D=${DOCKER:-docker}
RAW=$($D exec "$ARG" cat /opt/keycloak/version.txt 2>/dev/null | head -n 1)
[ -z "$RAW" ] && RAW=$($D exec "$ARG" ls /opt/keycloak/lib/lib/main 2>/dev/null | grep -E 'keycloak-services-' | head -n 1)
[ -z "$RAW" ] && { echo "No folder, version, or container named $ARG"; exit 1; }
echo "Container $ARG: $RAW"
RAW=${RAW#Keycloak - Version }
echo "Keycloak ${RAW#*keycloak-services-}: $(rate "${RAW#*keycloak-services-}")"
# Install from zip or tar.gz
bash check-version-keycloak.sh /opt/keycloak
# Docker container named keycloak (Podman: put DOCKER=podman before the command)
bash check-version-keycloak.sh keycloak
# A version that you copied from another server
bash check-version-keycloak.sh 26.7.5
Terminal: the version script rates 26.8.1, 26.7.5, and 26.4.16 as VULNERABLE, and 26.8.2 as SAFE for the 33 CVEs.
The script reads version.txt in a Keycloak 26.8.1 folder, then rates 3 typed versions. Only 26.8.2 gets SAFE. Source: Rama Digital test in a podman pod with the official quay.io/keycloak/keycloak 26.8.1 and 26.8.2 images, 6 November 2026.

Check the result: "VULNERABLE" means go to steps 2 and 3 today. "RED HAT BUILD" means use step 4 and watch for an RHSA. "SAFE" still means run step 5, because the flaws existed before you updated.

Step 2: Check the realms that use SAML

This step answers 3 questions: which realms have an active SAML identity provider, which realms have a SAML client, and which realms keep login events.

Save this script as check-exposure-keycloak.sh. The script only reads the configuration through kcadm.sh and does not keep the login token.

#!/usr/bin/env bash
# Check the Keycloak realms that use SAML (CVE-2026-117691). The script only reads. Needs bash, jq, and curl.
# Use: bash check-exposure-keycloak.sh [public URL]
# Env: KCADM (default /opt/keycloak/bin/kcadm.sh), KC_URL (default http://localhost:8080),
#      KC_USER (default admin), KC_PASSWORD (if empty, the script asks on the terminal and hides the password).
# Optional public URL argument: the script sends one GET without parameters to the broker endpoint of each SAML IdP and prints the HTTP code.
set -u
KCADM=${KCADM:-/opt/keycloak/bin/kcadm.sh}
KC_URL=${KC_URL:-http://localhost:8080}
KC_USER=${KC_USER:-admin}
PUB=${1:-}
if [ -z "${KC_PASSWORD:-}" ]; then read -rs -p "Password $KC_USER: " KC_PASSWORD < /dev/tty; echo; fi
CFG=$(mktemp); trap 'rm -f "$CFG"' EXIT
kc() { "$KCADM" "$@" --config "$CFG"; }
# Send the password in the env KC_CLI_PASSWORD, not in the --password argument. Arguments show in the process list (ps).
KC_CLI_PASSWORD="$KC_PASSWORD" "$KCADM" config credentials --server "$KC_URL" --realm master --user "$KC_USER" --config "$CFG" >/dev/null 2>&1 \
  || { echo "Login to $KC_URL failed"; exit 1; }
unset KC_PASSWORD

NREALM=0; NIDP=0; NCLI=0
echo "== 1. Realms at $KC_URL"
for R in $(kc get realms --fields realm 2>/dev/null | jq -r '.[].realm'); do
  INFO=$(kc get "realms/$R" --fields eventsEnabled,organizationsEnabled 2>/dev/null)
  IDPS=$(kc get identity-provider/instances -r "$R" 2>/dev/null | jq -c '[.[] | select(.providerId=="saml")]')
  CLI=$(kc get clients -r "$R" --fields clientId,protocol 2>/dev/null | jq -r '.[] | select(.protocol=="saml") | .clientId')
  NI=$(echo "$IDPS" | jq 'length'); NC=$(printf '%s' "$CLI" | grep -c .)
  echo "Realm $R: SAML IdP $NI, SAML client $NC, organizations $(echo "$INFO" | jq -r '.organizationsEnabled'), events $(echo "$INFO" | jq -r '.eventsEnabled')"
  [ "$NI" -gt 0 ] || [ "$NC" -gt 0 ] && NREALM=$((NREALM + 1))
  while IFS=$'\t' read -r A EN VS PB; do
    [ -z "$A" ] && continue
    echo "  IdP $A: enabled=$EN validateSignature=$VS postBindingResponse=$PB"
    if [ "$EN" = true ]; then echo "  AFFECTED: active SAML IdP $A"; NIDP=$((NIDP + 1)); fi
    if [ -n "$PUB" ]; then
      K=$(curl -s -o /dev/null -w '%{http_code}' --max-time 15 "${PUB%/}/realms/$R/broker/$A/endpoint")
      case "$K" in 000|403|404) echo "  Broker endpoint from this machine: HTTP $K" ;; *) echo "  OPEN from this machine: HTTP $K ${PUB%/}/realms/$R/broker/$A/endpoint" ;; esac
    fi
  done < <(echo "$IDPS" | jq -r '.[] | [.alias, .enabled, (.config.validateSignature // "-"), (.config.postBindingResponse // "-")] | @tsv')
  for C in $CLI; do echo "  CHECK: SAML client $C"; NCLI=$((NCLI + 1)); done
  [ "$(echo "$INFO" | jq -r '.organizationsEnabled')" = true ] && echo "  CHECK: organizations enabled (CVE-2026-112208 in 26.8.0 and 26.8.1)"
  [ "$(echo "$INFO" | jq -r '.eventsEnabled')" != true ] && echo "  CHECK: login events are not stored"
done
echo "== 2. Summary"
echo "Realms with SAML: $NREALM. Active SAML IdPs: $NIDP. SAML clients: $NCLI."
exit 0
# Run on the Keycloak server. The script asks for the master realm admin password.
KC_URL=http://localhost:8080 bash check-exposure-keycloak.sh
# Add the public URL to check the SAML broker endpoints from this machine
KC_URL=http://localhost:8080 bash check-exposure-keycloak.sh https://sso.example.com
Terminal: the exposure script finds 1 active SAML IdP and 1 SAML client in realm toko. Login events are off in the master and internal realms.
Realm toko has the active SAML IdP saml-kantor, so the script prints AFFECTED. The broker endpoint answers HTTP 400, so that endpoint is open from this machine. Source: Rama Digital test in a podman pod with the official quay.io/keycloak/keycloak 26.8.1 and 26.8.2 images, 6 November 2026.

Check the result: write down every "AFFECTED" and "CHECK" line. A realm with "AFFECTED: active SAML IdP" needs step 3 today. If the update cannot run yet, apply step 4 now.

Step 3: Update to 26.8.2

The update is the only step that closes these 33 CVEs. This release also changes some behavior, so read the Keycloak upgrade guide before the update.

3a. Official image

# 3a. Official image (tested with podman, the docker commands are the same)
docker pull quay.io/keycloak/keycloak:26.8.2
docker stop keycloak && docker rm keycloak
# Use the same volumes, env, ports, and start command as the old container
docker run -d --name keycloak -p 127.0.0.1:8080:8080 \
  --env-file keycloak.env -v keycloak-data:/opt/keycloak/data \
  quay.io/keycloak/keycloak:26.8.2 start
# Your own image: change the FROM line to quay.io/keycloak/keycloak:26.8.2, then build again
Terminal: podman pull of the quay.io/keycloak/keycloak:26.8.2 image, stop and remove the old container, then run a new container with the same kc-data volume.
The new container uses the same data volume as the old container. Source: Rama Digital test in a podman pod with the official quay.io/keycloak/keycloak 26.8.1 and 26.8.2 images, 6 November 2026.

In our test, Keycloak 26.8.2 was ready in 15 seconds, and the master, toko, and internal realms stayed. Our test used start-dev with the H2 database on a volume. In production, use the start command and the same database as the old container.

3b. tar.gz install or Keycloak Operator

# 3b. Install from the GitHub release tar.gz: download and check SHA-1
curl -LO https://github.com/keycloak/keycloak/releases/download/26.8.2/keycloak-26.8.2.tar.gz
curl -LO https://github.com/keycloak/keycloak/releases/download/26.8.2/keycloak-26.8.2.tar.gz.sha1
[ "$(sha1sum keycloak-26.8.2.tar.gz | cut -d ' ' -f 1)" = "$(cat keycloak-26.8.2.tar.gz.sha1)" ] && echo SHA1 MATCH
# Copy your conf/, providers/, and themes/ to the new folder, then build and start (not tested)
tar xzf keycloak-26.8.2.tar.gz && cd keycloak-26.8.2
bin/kc.sh build && bin/kc.sh start --optimized
# Keycloak Operator: apply the 26.8.2 operator manifests (not tested)
kubectl -n keycloak apply -f https://raw.githubusercontent.com/keycloak/keycloak-k8s-resources/26.8.2/kubernetes/kubernetes.yml

The .sha1 file in the GitHub release 26.8.2 holds only the hash, so the command above compares the file content directly. We did not test the build from tar.gz or the Operator upgrade, because those steps depend on your providers and themes.

3c. Add the kc-admin-api scope to your service accounts

26.8.2 closes CVE-2026-19688 with an audience check in the Admin API. The built-in clients admin-cli and security-admin-console get the kc-admin-api scope automatically. Clients and service accounts that you made yourself do not.

# 3c. Log kcadm.sh in to the master realm. kcadm.sh asks for the admin password.
kcadm.sh config credentials --server http://localhost:8080 --realm master --user admin
# Add the kc-admin-api scope to a client that you made (example: ci-bot in realm toko)
CID=$(kcadm.sh get clients -r toko -q clientId=ci-bot --fields id --format csv --noquotes)
SID=$(kcadm.sh get client-scopes -r toko --fields id,name --format csv --noquotes | grep kc-admin-api | cut -d, -f1)
kcadm.sh update clients/$CID/default-client-scopes/$SID -r toko -n
# Check: a ci-bot token to the Admin API. The correct result: 200. CI_SECRET holds the ci-bot client secret.
T=$(curl -s -d grant_type=client_credentials -d client_id=ci-bot -d client_secret="$CI_SECRET" \
  http://localhost:8080/realms/toko/protocol/openid-connect/token | jq -r .access_token)
curl -s -o /dev/null -w '%{http_code}\n' -H "Authorization: Bearer $T" 'http://localhost:8080/admin/realms/toko/users?max=1'
Terminal: the version script prints SAFE on 26.8.2. The ci-bot service account gets 401 from the Admin API, then 200 after we add the kc-admin-api scope.
After the update, ci-bot gets 401 until we add the kc-admin-api scope. Realm toko and IdP saml-kantor stay enabled. Source: Rama Digital test in a podman pod with the official quay.io/keycloak/keycloak 26.8.1 and 26.8.2 images, 6 November 2026.

In our test, the ci-bot service account got 200 from the Admin API on 26.8.1, then 401 on 26.8.2. After we added the kc-admin-api scope, the code was 200 again. Do this command again for each client that calls the Admin API.

3d. Read the other behavior changes

  • Only realm admins can manage protocol mappers.
  • The allowed DCR mappers now follow a new realm option. That option is empty by default.
  • The SAML broker requires a valid bearer SubjectConfirmation. SAML 1.1 parsing is off by default.
  • IdP auto-linking requires a verified email. A token from token exchange cannot live longer than the original token.
  • The SOAP endpoints deny requests above 120000 bytes.
An 8-row ladder: check the version, check SAML realms, change the image, zip or operator, the kc-admin-api scope, 2 temporary steps with red labels, then signs.
(1) to (3c): the fix order. (4a) and (4b): temporary steps. The red labels remind you that step 4 does not make the server safe. Source: the Keycloak 26.8.2 release notes and upgrade guide, Rama Digital test, and Rama Digital analysis.

Check the result: the version script prints "SAFE for the 33 CVEs", your realms and IdPs are still there, and every service account gets 200 from the Admin API.

Step 4: Reduce the impact until the update is done

This step reduces the impact, but it does not make the server safe. The 32 other CVEs stay open until you update.

Rama Digital analysis: Red Hat says that no mitigation meets the Red Hat criteria. We chose 2 temporary steps. Condition: the update cannot run today, or you use the Red Hat build. Limit: request forms that we did not test can get past the proxy rule.

4a. Deny percent-encoded parameter names at the reverse proxy

# Step 4a: deny SAML parameters with percent-encoded names or a ';' separator in nginx, before the request reaches Keycloak.
# This is a temporary step if you cannot update to 26.8.2 yet. The rule does not replace the update.
# Test: nginx 1.29 in front of Keycloak (port 8080) on port 8081. On your server, put the location block in your existing 443 server.
server {
    listen 8081;
    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Forwarded-Host $host;
    }
    # SAML broker and SAML client endpoints. Answer 403 if a parameter name has %XX or the query has ';'.
    # The regex has no ^, so it also matches a path prefix, for example /auth from KC_HTTP_RELATIVE_PATH.
    # Put this block above other regex locations. A ^~ or = location for the Keycloak path makes nginx skip this rule.
    location ~ /realms/[^/]+/(broker/[^/]+/endpoint|protocol/saml) {
        if ($args ~ "(^|&)[^=&]*%[0-9A-Fa-f]{2}[^=&]*=") { return 403; }
        if ($args ~ ";") { return 403; }
        proxy_pass http://127.0.0.1:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Forwarded-Host $host;
    }
}
Terminal: nginx -t succeeds. Requests with percent-encoded parameter names or a semicolon get 403. A plain SAML request gets 400, OIDC discovery gets 200.
3 test requests with percent-encoded names or a semicolon stop at nginx with 403. Plain requests still reach Keycloak. Source: Rama Digital test with the official nginx 1.29 image in front of Keycloak 26.8.1, 6 November 2026.

In our test, nginx answered 403 to 3 test requests with percent-encoded names or a semicolon. A plain SAML request still reached Keycloak and got 400 from Keycloak. OIDC discovery got 200.

We also tested this rule with Keycloak on the path prefix /auth. The result was the same: 403 for the test requests, 400 and 200 for plain requests.

Put this block above other regex locations on your server. A ^~ or = location for the Keycloak path makes nginx skip this rule.

After you apply the rule, test it from outside through your public URL. The correct result is 403.

# 4a. Test the rule from outside through the public URL. Replace realm toko and alias saml-kantor.
# If Keycloak uses a path prefix (example /auth), write that prefix before /realms.
# The correct result: 403 from the proxy.
curl -s -o /dev/null -w '%{http_code}\n' 'https://sso.example.com/realms/toko/broker/saml-kantor/endpoint?%53AMLResponse=x'

Also test 1 real SAML login from your IdP. We did not test this rule with a real IdP.

Also close ports 8080 and 8443 at the firewall, so that Keycloak can only be reached through the proxy.

4b. Disable SAML IdPs that you do not use

# 4b. Log kcadm.sh in first, as in step 3c
# Disable a SAML IdP that you do not use (alias from step 2)
kcadm.sh update identity-provider/instances/saml-kantor -r toko -s enabled=false
# Enable it again after the update to 26.8.2
kcadm.sh update identity-provider/instances/saml-kantor -r toko -s enabled=true
Terminal: kcadm.sh disables the IdP saml-kantor in realm toko. The exposure script prints enabled=false, and the AFFECTED line is gone.
The AFFECTED line is gone. The SAML client in realm toko still shows as CHECK, because step 4b does not touch SAML clients. Source: Rama Digital test in a podman pod with the official quay.io/keycloak/keycloak 26.8.1 and 26.8.2 images, 6 November 2026.

Users who log in through that IdP cannot log in until you enable it again. Use this step only for IdPs that you do not use, or when the business team accepts a login pause.

Check the result: the exposure script shows that IdP as disabled, and the public URL test command from step 4a gets 403 from the proxy.

Step 5: Look for signs of use

These flaws existed before you updated, so also look for signs from before the update.

Save this script as check-signs-keycloak.sh. The script looks for unusual SAML requests in the access logs, login events through an IdP, and new users since the date that you choose.

#!/usr/bin/env bash
# Look for signs that CVE-2026-117691 was used, in access logs and in Keycloak. The script only reads. Needs bash, jq, and zgrep.
# Use: bash check-signs-keycloak.sh <date YYYY-MM-DD> [access log folder]
# Access logs: access.log* files (including .gz) from nginx or another proxy in front of Keycloak.
# Env: KCADM (default /opt/keycloak/bin/kcadm.sh), KC_URL (default http://localhost:8080),
#      KC_USER (default admin), KC_PASSWORD (if empty, the script asks on the terminal and hides the password).
set -u
SINCE=${1:?start date, for example 2026-10-01}
LOGDIR=${2:-}
KCADM=${KCADM:-/opt/keycloak/bin/kcadm.sh}
KC_URL=${KC_URL:-http://localhost:8080}
KC_USER=${KC_USER:-admin}
SIGNS=0; CHECKS=0
sign() { echo "  SIGN: $1"; SIGNS=$((SIGNS + 1)); }
check() { echo "  CHECK: $1"; CHECKS=$((CHECKS + 1)); }
SAML='"GET /realms/[^/ ]+/(broker/[^/ ]+/endpoint|protocol/saml)'
access() { find "$LOGDIR" -type f -name 'access.log*' 2>/dev/null | sort; }
search() { access | xargs -r zgrep -hE "$1" 2>/dev/null; }

echo "== 1. Access logs (${LOGDIR:-no folder given}, since $SINCE)"
if [ -n "$LOGDIR" ]; then
  echo "  $(access | wc -l) access.log files"
  while IFS= read -r l; do sign "percent-encoded parameter name or ';': ${l:0:150}"; done \
    < <(search "$SAML"'\?([^ ]*&)?[^=& ]*%[0-9A-Fa-f]{2}[^=& ]*=|'"$SAML"'\?[^ ]*;' | head -n 5)
  echo "  SAML GETs per IP (top 5):"
  search "$SAML" | awk '{print $1}' | sort | uniq -c | sort -rn | head -n 5 | sed 's/^ */  INFO: /'
fi

if [ -z "${KC_PASSWORD:-}" ]; then read -rs -p "Password $KC_USER: " KC_PASSWORD < /dev/tty; echo; fi
CFG=$(mktemp); trap 'rm -f "$CFG"' EXIT
kc() { "$KCADM" "$@" --config "$CFG"; }
# Send the password in the env KC_CLI_PASSWORD, not in the --password argument. Arguments show in the process list (ps).
KC_CLI_PASSWORD="$KC_PASSWORD" "$KCADM" config credentials --server "$KC_URL" --realm master --user "$KC_USER" --config "$CFG" >/dev/null 2>&1 \
  || { echo "Login to $KC_URL failed"; exit 1; }
unset KC_PASSWORD

echo "== 2. Realm events since $SINCE"
for R in $(kc get realms --fields realm 2>/dev/null | jq -r '.[].realm'); do
  if [ "$(kc get "realms/$R" --fields eventsEnabled 2>/dev/null | jq -r '.eventsEnabled')" != true ]; then
    check "realm $R: events are off"; continue
  fi
  while IFS= read -r l; do check "realm $R: $l"; done < <(kc get events -r "$R" -q "dateFrom=$SINCE" -q max=1000 2>/dev/null \
    | jq -r '.[] | select(.type=="IDENTITY_PROVIDER_LOGIN" or .type=="IDENTITY_PROVIDER_FIRST_LOGIN" or .type=="FEDERATED_IDENTITY_LINK") | "\((.time/1000)|todate) \(.type) user \(.userId // "-")"' | head -n 20)
done

echo "== 3. Users created since $SINCE"
LIMIT=$(date -d "$SINCE" +%s)000
for R in $(kc get realms --fields realm 2>/dev/null | jq -r '.[].realm'); do
  while IFS= read -r u; do check "realm $R: new user $u"; done < <(kc get users -r "$R" --fields username,createdTimestamp -q max=1000 2>/dev/null \
    | jq -r --argjson t "$LIMIT" '.[] | select(.createdTimestamp >= $t) | .username')
done

echo "== Result: SIGNS $SIGNS, CHECKS $CHECKS"
[ "$SIGNS" -gt 0 ] && echo "Keep the logs as evidence, cut access from the internet, then follow step 5 in the article."
exit 0
# nginx access logs in front of Keycloak, from 1 October 2026
bash check-signs-keycloak.sh 2026-10-01 /var/log/nginx
Terminal: the signs script finds 3 SIGNS from 1 dummy IP in 2 access.log files, events off in 2 realms, and 2 new users. Result: SIGNS 3, CHECKS 4.
The access logs in this test are dummy data with documentation example IPs. The script also reads .gz files, so it checks old logs too. Source: Rama Digital test in a podman pod with the official quay.io/keycloak/keycloak 26.8.1 and 26.8.2 images, 6 November 2026.

In this test, the script found 3 SIGNS from 1 dummy IP in 2 log files, including 1 .gz file. The script also printed 4 CHECKS: events off in the master and internal realms, and 2 new users. The user admin is the bootstrap admin of the test, so that line has an explanation.

Two columns of signs: in the access logs (percent-encoded parameter names, semicolons, unknown IPs) and in Keycloak (IdP login events, new identity links, new admin roles).
Amber column: signs in the access logs. Red column: signs in Keycloak. Bottom box: the steps when you find even 1 sign. Source: the CVE-2026-117691 record, Keycloak event documentation, Rama Digital test, and Rama Digital analysis.

Keycloak keeps login events only when "Save events" is on in the realm. If events are off, you only have the proxy access logs. Turn on events today, so that the next incident leaves a trace.

Check the result: the script prints "SIGNS 0", and every CHECK has an explanation from your team. Attackers can delete logs, so 0 signs does not prove that the server is safe.

If you find even 1 sign, keep the logs as evidence. Then sign out all sessions of that realm, change the admin passwords and client secrets, check the admin roles, and update to 26.8.2.

If Keycloak holds customer or staff accounts, Indonesian Law 27/2022 on Personal Data Protection requires a written notice within 3 times 24 hours after a personal data breach. Also read the LXD and Incus CVE-2026-112816 warning.

Example: 1 afternoon of response for 3 Keycloak servers

This simulation uses dummy data. The server names, times, and results are not customer data.

Time (WIB)Dummy serverWhat was checkedResultAction
13.00sso-portal (Docker 26.8.1)Steps 1 and 2VULNERABLE, 1 active SAML IdPStep 4a now, update this afternoon
13.30sso-partner (zip 26.6.7, OIDC only)Steps 1 and 2VULNERABLE, 0 SAML realmsPlan the move to 26.8.2 this week
14.00sso-internal (Red Hat build)Steps 1 and 2RED HAT BUILD, 1 SAML clientStep 4a, watch for an RHSA
16.00sso-portalSteps 3 and 5SAFE, 1 service account gets 401Add the kc-admin-api scope

The lesson from the simulation: a server with OIDC only still needs the update, because the 32 other CVEs do not depend on SAML.

Who installs the patch

Keycloak often runs on your own server, so your hosting provider does not install this patch for you.

Service typePatch ownerWhat you do
Keycloak on a VPS, Docker, or KubernetesYour teamSteps 1 to 5
Red Hat build of KeycloakRed Hat, through an RHSASteps 2, 4, and 5 until the RHSA comes out
Managed Keycloak from a vendorThe vendor, as the contract saysAsk for the update date and the step 5 result
An app that ships Keycloak insideThe app vendorAsk for the Keycloak version in writing and the patch date

Rama Digital recommendation: watch the security releases of all your login systems and web apps on a fixed schedule. This week we also wrote the Apache OFBiz CVE-2026-83542 warning and the Rails Active Storage CVE-2026-122006 warning.

Checklist for today

  1. List all Keycloak servers, including staging. Owner: IT team. Evidence: a list of names and versions.
  2. Check the version with step 1. Owner: IT team. Evidence: script output.
  3. Check SAML realms with step 2. Owner: IT team. Evidence: script output.
  4. Apply step 4 on every SAML server that cannot update today. Owner: IT team. Evidence: a 403 code from the proxy.
  5. Update to 26.8.2 and add the kc-admin-api scope. Owner: IT team. Evidence: script output after the update and a 200 code from the Admin API.
  6. Turn on login events in every realm. Owner: IT team. Evidence: exposure script output.
  7. Look for signs with step 5. Owner: IT team and security team. Evidence: script output.

Questions and answers

Has this flaw been used in attacks?

On 6 November 2026, we found no attack reports, and CVE-2026-117691 is not on CISA KEV. But the fix is in public code, so update today if you use SAML.

My realms use only OIDC. Am I safe?

According to Phase Two, realms with OIDC only are not exposed to CVE-2026-117691. Still update this week, because the 32 other CVEs, including 5 DoS, do not depend on SAML.

I use the Red Hat build of Keycloak. What do I do?

On 6 November 2026, Red Hat lists the status Affected with no patch. Apply step 4, run step 5, and watch for an RHSA on the Red Hat CVE page.

I am still on 26.4 or 26.6. Why is there no patch?

Keycloak released this fix only in 26.8.2. Move to 26.8.2, and read the migration notes for each version that you skip.

Why does my service account get 401 after the update?

26.8.2 checks the audience in the Admin API. Add the kc-admin-api client scope to that client as in step 3c. In our test, the code changed from 401 to 200.

Does the image update delete my realms?

No, if you use the same database or volume. In our test, the realm and the SAML IdP stayed after the image change. Still make a backup before the update.

Do the check scripts change anything on my server?

No. The 3 scripts only read. The commands that change something are only in steps 3 and 4, and each command is written separately.

Next steps

Limits that still apply: step 4 does not replace the update, and the update does not undo access that already happened. If your Keycloak shows signs in step 5, or you need help with the update and an audit, use Vibe Code Rescue. For 1 urgent problem today, book a 30-minute session.

Sources