Repository navigation
Description
Before reporting an issue
- I have read and understood the above terms for submitting issues, and I understand that my report will be closed without further comment if it does not meet them.
Area
admin/api
Describe the bug
After upgrading from 26.8.1 to 26.8.2, every call to the Admin REST API made with an access token issued by the realm being administered returns 401 {"error":"HTTP 401 Unauthorized"}, even though the token is valid and carries the required realm-management client roles.
The same call made with a token issued by the master realm succeeds (200). Downgrading to 26.8.1 (same database, same configuration) makes the realm-issued token work again.
Our use case is the standard one for backend user management: a confidential client in realm myrealm, service account enabled, service account granted realm-management roles (view-users, query-users, manage-users, view-clients), token obtained with client_credentials, then GET /admin/realms/myrealm/users.
Decoded token (values anonymized), as sent to the Admin API:
{
"iss": "https://keycloak.example.com/realms/myrealm",
"typ": "Bearer",
"azp": "admin-client",
"sid": null,
"iat": 1793955941,
"exp": 1793956541,
"resource_access": {
"realm-management": {
"roles": ["manage-users", "view-users", "view-clients", "query-clients", "query-groups", "query-users"]
}
}
}The token was used 4 seconds after issuance (not expired), and iss matches the configured hostname.
What we ruled out on 26.8.2:
| Variation | Result |
|---|---|
client_credentials token from myrealm, client with Full scope allowed ON (default) |
401 |
Same, Full scope allowed OFF, realm-management roles mapped to the client scope through a composite realm role (token contents shown above, account roles gone) |
401 |
Same, plus Use refresh tokens for client credentials grant ON (token now has a sid, i.e. backed by a real user session) |
401 |
Password-grant token from the master realm (admin-cli, user with admin rights), same request on /admin/realms/myrealm/users |
200 |
Any of the myrealm tokens above against 26.8.1 |
200 |
So the rejection does not depend on scope, roles, or the presence of a user session: it seems to depend only on the token being issued by a realm other than master.
Possibly related: 26.8.2 ships fixes in the realm scoping of the Admin REST API (e.g. CVE-2026-94215). This is only a guess on our side.
Since 26.8.2, the token endpoint also logs this warning for the client. Disabling Full scope allowed (second row of the table) did not change the 401:
WARN [org.keycloak.protocol.oidc.endpoints.TokenEndpoint.full-scope-allowed] Client 'admin-client' in the realm 'myrealm' has 'Full scope allowed' switch enabled. This client switch is deprecated and will be removed in the future. [...]
Version
26.8.2 (last working version: 26.8.1)
Regression
- The issue is a regression
Expected behavior
A token issued by realm myrealm to a service account holding realm-management roles view-users / query-users can call GET /admin/realms/myrealm/users, as it does in 26.8.1 and as documented for realm-level administration with service accounts.
Actual behavior
401 Unauthorized with body {"error":"HTTP 401 Unauthorized"} and no WWW-Authenticate header.
How to Reproduce?
Self-contained script using the official images (quay.io/keycloak/keycloak:<version>, start-dev, nothing else). It creates a realm myrealm with a confidential client admin-client whose service account gets the realm-management roles view-users and query-users, then calls GET /admin/realms/myrealm/users once with a client_credentials token from myrealm and once with a master-realm token.
./repro.sh 26.8.1 18081
./repro.sh 26.8.2 18082repro.sh
#!/usr/bin/env bash
# Reproduces the Keycloak 26.8.2 regression: the Admin REST API rejects (401) a token issued by the
# realm being administered, while a token issued by the master realm is accepted (200).
#
# Usage: ./repro.sh <keycloak-version> <host-port>
# Example: ./repro.sh 26.8.1 18081
# ./repro.sh 26.8.2 18082
#
# Runs the official image quay.io/keycloak/keycloak:<version> in start-dev mode (no Java needed on
# the host). Expected duration: 1 to 2 minutes per version, plus the image pull on first run.
# Requirements: docker, curl, jq.
set -u
VERSION="${1:?missing Keycloak version (e.g. 26.8.2)}"
PORT="${2:?missing host port (e.g. 18082)}"
IMAGE="quay.io/keycloak/keycloak:$VERSION"
NAME="kc-repro-$PORT"
BASE="http://localhost:$PORT"
START_TIMEOUT=180 # seconds
T0=$(date +%s)
log() { printf '[%s +%3ss] %s\n' "$(date +%H:%M:%S)" "$(( $(date +%s) - T0 ))" "$*"; }
STARTED=""
fail() {
log "FAILED: $*"
if [ -n "$STARTED" ]; then
log "Last lines of the Keycloak log (docker logs $NAME):"
docker logs --tail 20 "$NAME" 2>&1 | sed 's/^/ /'
fi
exit 1
}
cleanup() {
if [ -n "$STARTED" ]; then
log "Removing container $NAME..."
docker rm -f "$NAME" >/dev/null 2>&1
log "Container removed."
fi
}
trap cleanup EXIT
for cmd in docker curl jq; do command -v "$cmd" >/dev/null || fail "'$cmd' is not installed"; done
docker info >/dev/null 2>&1 || fail "cannot talk to the Docker daemon (is it running? is this user in the docker group?)"
if docker ps -a --format '{{.Names}}' | grep -qx "$NAME"; then
fail "a container named $NAME already exists (leftover from a previous run?): docker rm -f $NAME"
fi
# ── 1. Image ─────────────────────────────────────────────────────────────────────────────
log "1/6 Pulling $IMAGE (only slow on the first run)..."
docker pull -q "$IMAGE" >/dev/null || fail "cannot pull $IMAGE"
# ── 2. Start ─────────────────────────────────────────────────────────────────────────────
log "2/6 Starting Keycloak $VERSION in start-dev on $BASE (container $NAME)..."
docker run -d --name "$NAME" -p "127.0.0.1:$PORT:8080" \
-e KC_BOOTSTRAP_ADMIN_USERNAME=admin -e KC_BOOTSTRAP_ADMIN_PASSWORD=admin \
"$IMAGE" start-dev >/dev/null || fail "docker run failed (port $PORT already in use?)"
STARTED=1
while :; do
code=$(curl -s --noproxy localhost -o /dev/null -w "%{http_code}" "$BASE/realms/master" 2>/dev/null)
[ "$code" = "200" ] && break
[ "$(docker inspect -f '{{.State.Running}}' "$NAME" 2>/dev/null)" = "true" ] || fail "Keycloak stopped during startup"
elapsed=$(( $(date +%s) - T0 ))
[ "$elapsed" -ge "$START_TIMEOUT" ] && fail "Keycloak still not answering after ${START_TIMEOUT}s"
[ $(( elapsed % 10 )) -eq 0 ] && log " waiting for Keycloak... (last log line: $(docker logs --tail 1 "$NAME" 2>&1 | cut -c1-100))"
sleep 1
done
log " Keycloak is up: $(docker logs "$NAME" 2>&1 | grep -o 'Keycloak [0-9.]* on JVM' | head -1)"
# ── 3. Setup, with kcadm inside the container ────────────────────────────────────────────
kcadm() { docker exec "$NAME" /opt/keycloak/bin/kcadm.sh "$@" --config /tmp/kcadm.config; }
log "3/6 Logging kcadm into the master realm..."
kcadm config credentials --server http://localhost:8080 --realm master --user admin --password admin \
|| fail "kcadm login"
log "4/6 Creating realm myrealm, confidential client admin-client and its realm-management roles..."
kcadm create realms -s realm=myrealm -s enabled=true || fail "realm creation"
kcadm create clients -r myrealm -s clientId=admin-client -s publicClient=false -s serviceAccountsEnabled=true \
-s standardFlowEnabled=false -s secret=s3cret || fail "client creation"
kcadm add-roles -r myrealm --uusername service-account-admin-client --cclientid realm-management \
--rolename view-users --rolename query-users || fail "role assignment"
# ── 4. Token issued by the administered realm ────────────────────────────────â”
8000
€───────────
log "5/6 client_credentials token from myrealm, then GET /admin/realms/myrealm/users..."
TOKEN=$(curl -s --noproxy localhost "$BASE/realms/myrealm/protocol/openid-connect/token" \
-d grant_type=client_credentials -d client_id=admin-client -d client_secret=s3cret | jq -r .access_token)
[ -n "$TOKEN" ] && [ "$TOKEN" != "null" ] || fail "no token for admin-client"
log " realm-management roles in the token: $(echo "$TOKEN" | cut -d. -f2 | tr '_-' '/+' | base64 -d 2>/dev/null | jq -c '.resource_access["realm-management"].roles' 2>/dev/null)"
MYREALM=$(curl -s --noproxy localhost -o /dev/null -w '%{http_code}' "$BASE/admin/realms/myrealm/users?max=1" \
-H "Authorization: Bearer $TOKEN")
# ── 5. Token issued by the master realm ──────────────────────────────────────────────────
log "6/6 Master realm token (admin-cli), then the same request..."
MTOKEN=$(curl -s --noproxy localhost "$BASE/realms/master/protocol/openid-connect/token" \
-d grant_type=password -d client_id=admin-cli -d username=admin -d password=admin | jq -r .access_token)
[ -n "$MTOKEN" ] && [ "$MTOKEN" != "null" ] || fail "no master token"
MASTER=$(curl -s --noproxy localhost -o /dev/null -w '%{http_code}' "$BASE/admin/realms/myrealm/users?max=1" \
-H "Authorization: Bearer $MTOKEN")
# ── Result ───────────────────────────────────────────────────────────────────────────────
log "Result for Keycloak $VERSION:"
echo " myrealm token -> $MYREALM"
echo " master token -> $MASTER"
if [ "$MYREALM" = "401" ] && [ "$MASTER" = "200" ]; then
log "=> Regression reproduced."
elif [ "$MYREALM" = "200" ] && [ "$MASTER" = "200" ]; then
log "=> No regression on this version."
else
log "=> Unexpected result. Last lines of the Keycloak log:"
docker logs --tail 30 "$NAME" 2>&1 | sed 's/^/ /'
fiResult (last lines of each run):
# 26.8.1
myrealm token -> 200
master token -> 200
=> No regression on this version.
# 26.8.2
myrealm token -> 401
master token -> 200
=> Regression reproduced.
Anything else?
Our production setup, in case it matters (the reproduction script above does not use any of it):
- Official image
quay.io/keycloak/keycloak:26.8.2,start(not optimized), PostgreSQL 17. - Behind Traefik v2 (TLS termination):
KC_HOSTNAME=https://keycloak.example.com,KC_PROXY_HEADERS=xforwarded,KC_HTTP_ENABLED=true,KC_HOSTNAME_STRICT=false. The deprecatedKC_PROXY=edgeandKC_PROXY_ADDRESS_FORWARDING=trueare also still set. - Switched in place 26.8.1 → 26.8.2 → 26.8.1 → 26.8.2 → 26.8.1 on the same database: realm-issued admin tokens fail on every 26.8.2 run and work on every 26.8.1 run. We are back on 26.8.1 for now.
- Workaround: stay on 26.8.1, or obtain the admin token from the master realm.
Metadata
Metadata
Assignees
Labels
Type
Fields
Projects
- StatusShow more project fieldsDone
Activity
maxkre96 commentedon Nov 6, 2026
We have verified this issue in our environment. Also rolled back to 26.8.1. However, this prevents us from upgrading our clusters, leaving them vulnerable to CVE-2026-111497.
temp043 commentedon Nov 6, 2026
We can also verify this. In our case, our automated keycloak installer (with a fresh 26.8.2) fails, since it is unable to call
GET /admin/realmswith a freshly generated token. The user behind the token actually has admin role, including all manage-realm permissions.jomu78 commentedon Nov 6, 2026
I can confirm also
angry-botanist commentedon Nov 6, 2026
Confirmed issue with OpenTalk and the [user_search] functionality, performed a rollback to 26.8.1 for now.
Angelos3030 commentedon Nov 6, 2026
Confirmed
0xM58 commentedon Nov 6, 2026
Confirmed
Since this release is fixing 2 critical, could you please prioritize this so we don't rollback
n3vu0r commentedon Nov 6, 2026
A temporary fix is to add
spi-admin--audience-check-enabled=falseto the configuration file.A permanent fix is to add the
kc-admin-apiscope to the service account'sdefaultClientScopes.batorse commentedon Nov 6, 2026
More information on the client-scope "kc-admin-api" can be found in the migration guide for 26.8.2.
selbiselbi commentedon Nov 6, 2026
Should I close this issue then ? Seems like I didn't read carefully enough, my bad.
batorse commentedon Nov 6, 2026
Looks like it is an intended change and not a bug.
On the bight side this issue is a clear description of the problem as well as the solution and will help others with the same problem. It is better understandable then the very close up technical and confusingly formatted release notes. So thanks for writing this up and to n3vu0r for providing the solution.