authz-keycloak
The authz-keycloak plugin integrates APISIX and API7 Gateway with Keycloak Authorization Services. It sends the caller's bearer token and requested permissions to Keycloak's User-Managed Access (UMA) token endpoint. Keycloak evaluates its resources, scopes, policies, and permissions before the gateway proxies the request.
Permissions can be selected dynamically or configured explicitly. With dynamic path loading, the gateway uses a Keycloak service account to resolve the request URI through the Protection API. With static permissions, the gateway sends the configured resource and scope names directly to the UMA token endpoint.
Examples
The following setup creates a Keycloak resource server and demonstrates dynamic and static UMA permission checks.
Before proceeding:
- Install Docker.
- Install cURL and jq.
- Follow the Getting Started tutorial to start APISIX with Docker.
- If you plan to use ADC, install and configure ADC before continuing.
- If you plan to use the Ingress Controller examples, set up the Ingress Controller and gateway in the
aicnamespace.
Configure Keycloak
Start Keycloak, then configure a protected resource, authorization policy, and scope-based permission.
The walkthrough uses a Keycloak service account to obtain test access tokens. A client-scope policy permits tokens that include httpbin-access to access the protected resource with the access authorization scope.
Start Keycloak
Choose the environment that matches the gateway deployment.
- Docker
- Kubernetes
If apisix-keycloak is already running from the Keycloak SSO guide, connect it to the APISIX quickstart network:
docker network connect apisix-quickstart-net apisix-keycloak
Then skip the next command. Otherwise, start Keycloak on the APISIX quickstart network in development mode:
docker run -d --name apisix-keycloak \
--network apisix-quickstart-net \
-e 'KC_BOOTSTRAP_ADMIN_USERNAME=quickstart-admin' \
-e 'KC_BOOTSTRAP_ADMIN_PASSWORD=quickstart-admin-pass' \
-p 127.0.0.1:8080:8080 \
quay.io/keycloak/keycloak:26.7.3 start-dev
Save the Keycloak address that containers on the quickstart network can reach:
export KEYCLOAK_URL=http://apisix-keycloak:8080
Create the namespace if it does not already exist:
kubectl create namespace aic --dry-run=client -o yaml | kubectl apply -f -
Create keycloak.yaml with a Keycloak Deployment and Service:
apiVersion: apps/v1
kind: Deployment
metadata:
namespace: aic
name: keycloak
spec:
replicas: 1
selector:
matchLabels:
app: keycloak
template:
metadata:
labels:
app: keycloak
spec:
containers:
- name: keycloak
image: quay.io/keycloak/keycloak:26.7.3
args:
- start-dev
env:
- name: KC_BOOTSTRAP_ADMIN_USERNAME
value: quickstart-admin
- name: KC_BOOTSTRAP_ADMIN_PASSWORD
value: quickstart-admin-pass
ports:
- containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
namespace: aic
name: keycloak
spec:
selector:
app: keycloak
ports:
- port: 8080
targetPort: 8080
Apply the manifest and wait for Keycloak to become available:
kubectl apply -f keycloak.yaml
kubectl rollout status -n aic deployment/keycloak
Save the in-cluster Keycloak address:
export KEYCLOAK_URL=http://keycloak.aic.svc.cluster.local:8080
In a separate terminal, forward the Keycloak port so that the Admin Console is available locally:
kubectl port-forward -n aic service/keycloak 8080:8080
Development mode and the example administrator credentials are intended only for local testing. For a production deployment, use HTTPS, a production database, and a permanent administrator account.
Open http://localhost:8080/admin/ and sign in with the administrator username quickstart-admin and password quickstart-admin-pass.
Create a Realm and Resource Server
Create a realm for the authorization resources:
- Select Manage realms → Create realm.
- Enter
authz-realmas the realm name. - Select Create.
Register a confidential OIDC client as the protected resource server:
- Select Clients → Create client.
- Keep Client type set to OpenID Connect, enter
apisix-authzas the client ID, and select Next. - Turn on Client authentication and Authorization. Leave the interactive authentication flows off and select Save.

Enabling Authorization also enables the client service account and assigns its uma_protection role. APISIX uses that service account to query the Protection API when dynamic path loading is enabled.
Create and Assign a Client Scope
Create the client scope required by the authorization policy:
- Select Client scopes → Create client scope.
- Enter
httpbin-accessas the name and keep Protocol set to OpenID Connect. - Turn on Include in token scope and select Save.
- Open Clients → apisix-authz → Client scopes and select Add client scope.
- Select
httpbin-access, select Add, and add it as an optional client scope.

The token request later includes scope=httpbin-access. Keeping the scope optional also makes the denied-request example reproducible without changing the Keycloak configuration.
Create the Authorization Objects
Open Clients → apisix-authz → Authorization and create the scope and protected resource:
-
Open Scopes, select Create authorization scope, enter
access, and select Save. -
Open Resources, select Create resource, and configure these values:
Field Value Name httpbin-anythingDisplay name HTTPBin AnythingURIs /anything/authzAuthorization scopes access -
Select Save.

Create the policy that requires the client scope:
- Open Policies and select Create client policy → Client scope.
- Enter
httpbin-access-policyas the name. - Select
httpbin-accessas the client scope and mark it as required. - Select Save.

Connect the resource and authorization scope to the policy:
- Open Permissions and select Create permission → Scope-based.
- Enter
httpbin-access-permissionas the name. - Select
httpbin-anythingas the resource,accessas the authorization scope, andhttpbin-access-policyas the policy. - Select Save.

Save the Client Credentials
Open Clients → apisix-authz → Credentials and copy the client secret. Save the client ID and secret as environment variables:
export KEYCLOAK_CLIENT_ID=apisix-authz
export KEYCLOAK_CLIENT_SECRET=replace-with-your-client-secret
Keep the client secret confidential. Store production credentials in a secret manager and rotate them according to the organization's credential-rotation policy.
Request an Access Token
Request a service-account token with the optional client scope. Run the command for the environment selected earlier.
- Docker
- Kubernetes
Run the token request from a temporary container on the quickstart network:
export ACCESS_TOKEN="$(
docker run --rm --network apisix-quickstart-net \
curlimages/curl:8.22.0 -sS \
"${KEYCLOAK_URL}/realms/authz-realm/protocol/openid-connect/token" \
--user "${KEYCLOAK_CLIENT_ID}:${KEYCLOAK_CLIENT_SECRET}" \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "grant_type=client_credentials" \
--data-urlencode "scope=httpbin-access" | \
jq -er '.access_token'
)"
Run the token request from a temporary pod in the aic namespace:
export ACCESS_TOKEN="$(
kubectl run authz-token-request --rm -i --restart=Never --quiet \
--namespace aic \
--image curlimages/curl:8.22.0 \
--command -- \
curl -sS \
"${KEYCLOAK_URL}/realms/authz-realm/protocol/openid-connect/token" \
--user "${KEYCLOAK_CLIENT_ID}:${KEYCLOAK_CLIENT_SECRET}" \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "grant_type=client_credentials" \
--data-urlencode "scope=httpbin-access" | \
jq -er '.access_token'
)"
Authorize Requests by Path
Dynamic path loading lets APISIX resolve the incoming request URI to a Keycloak resource. Configure a route that queries the Protection API, then asks the UMA token endpoint whether the caller can access the resolved resource.
Choose the API used to configure the route.
- Admin API
- ADC
- Ingress Controller
Create the route through the Admin API:
curl "http://127.0.0.1:9180/apisix/admin/routes/authz-keycloak" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
--data-binary @- <<EOF
{
"uri": "/anything/authz",
"plugins": {
"authz-keycloak": {
"lazy_load_paths": true,
"discovery": "$KEYCLOAK_URL/realms/authz-realm/.well-known/uma2-configuration",
"client_id": "$KEYCLOAK_CLIENT_ID",
"client_secret": "$KEYCLOAK_CLIENT_SECRET"
},
"serverless-post-function": {
"phase": "access",
"functions": [
"return function(conf, ctx) ngx.req.clear_header('Authorization') end"
]
}
},
"upstream": {
"type": "roundrobin",
"nodes": {
"httpbin.org:80": 1
}
}
}
EOF
Create adc.yaml with the route configuration:
services:
- name: authz-keycloak-httpbin
routes:
- name: authz-keycloak
uris:
- /anything/authz
plugins:
authz-keycloak:
lazy_load_paths: true
discovery: "${KEYCLOAK_URL}/realms/authz-realm/.well-known/uma2-configuration"
client_id: "${KEYCLOAK_CLIENT_ID}"
client_secret: "${KEYCLOAK_CLIENT_SECRET}"
serverless-post-function:
phase: access
functions:
- "return function(conf, ctx) ngx.req.clear_header('Authorization') end"
upstream:
type: roundrobin
nodes:
- host: httpbin.org
port: 80
weight: 1
ADC reconciles services as desired state. The label selector limits this example to its own labeled resources. Preview the scoped changes and confirm that they contain no unintended updates or deletions:
adc diff -f adc.yaml \
--include-resource-type service \
--label-selector docs-example=authz-keycloak
Synchronize the reviewed service configuration:
adc sync -f adc.yaml \
--include-resource-type service \
--label-selector docs-example=authz-keycloak
Configure the plugin with either Gateway API or APISIX custom resources.
- Gateway API
- APISIX CRD
Create authz-keycloak-ic.yaml:
apiVersion: v1
kind: Service
metadata:
namespace: aic
name: httpbin-external-domain
spec:
type: ExternalName
externalName: httpbin.org
---
apiVersion: apisix.apache.org/v1alpha1
kind: PluginConfig
metadata:
namespace: aic
name: authz-keycloak-plugin-config
spec:
plugins:
- name: authz-keycloak
config:
lazy_load_paths: true
discovery: http://keycloak.aic.svc.cluster.local:8080/realms/authz-realm/.well-known/uma2-configuration
client_id: apisix-authz
client_secret: replace-with-your-client-secret
- name: serverless-post-function
config:
phase: access
functions:
- "return function(conf, ctx) ngx.req.clear_header('Authorization') end"
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
namespace: aic
name: authz-keycloak
spec:
parentRefs:
- name: apisix
rules:
- matches:
- path:
type: Exact
value: /anything/authz
filters:
- type: ExtensionRef
extensionRef:
group: apisix.apache.org
kind: PluginConfig
name: authz-keycloak-plugin-config
backendRefs:
- name: httpbin-external-domain
port: 80
Create authz-keycloak-ic.yaml:
apiVersion: apisix.apache.org/v2
kind: ApisixUpstream
metadata:
namespace: aic
name: httpbin-external-domain
spec:
ingressClassName: apisix
externalNodes:
- type: Domain
name: httpbin.org
---
apiVersion: apisix.apache.org/v2
kind: ApisixPluginConfig
metadata:
namespace: aic
name: authz-keycloak-plugin-config
spec:
ingressClassName: apisix
plugins:
- name: authz-keycloak
enable: true
config:
lazy_load_paths: true
discovery: http://keycloak.aic.svc.cluster.local:8080/realms/authz-realm/.well-known/uma2-configuration
client_id: apisix-authz
client_secret: replace-with-your-client-secret
- name: serverless-post-function
enable: true
config:
phase: access
functions:
- "return function(conf, ctx) ngx.req.clear_header('Authorization') end"
---
apiVersion: apisix.apache.org/v2
kind: ApisixRoute
metadata:
namespace: aic
name: authz-keycloak
spec:
ingressClassName: apisix
http:
- name: authz-keycloak
match:
paths:
- /anything/authz
methods:
- GET
upstreams:
- name: httpbin-external-domain
plugin_config_name: authz-keycloak-plugin-config
Apply the configuration:
kubectl apply -f authz-keycloak-ic.yaml
❶ lazy_load_paths: Resolves the request URI to Keycloak resources through the Protection API instead of using a static permission list.
❷ discovery: URI of the Keycloak UMA discovery document. The plugin obtains the token and resource-registration endpoints from this document.
❸ client_id and client_secret: Credentials of the Keycloak resource-server client. APISIX uses them to obtain the service-account token required by the Protection API.
❹ serverless-post-function: Removes the caller's bearer token after authz-keycloak evaluates it, preventing the sample upstream from receiving the credential. Omit this plugin if the upstream application must receive the token.
Verify Dynamic Authorization
Send the access token to the protected route:
curl -i "http://127.0.0.1:9080/anything/authz" \
-H "Authorization: Bearer ${ACCESS_TOKEN}"
An HTTP/1.1 200 OK response verifies that Keycloak permitted the token to access the resource. The response body should contain fields similar to these:
{
"args": {},
"data": "",
"files": {},
"form": {},
"headers": {
"Accept": "*/*",
"Host": "127.0.0.1",
"User-Agent": "curl/8.7.1",
"X-Amzn-Trace-Id": "Root=1-...",
"X-Forwarded-Host": "127.0.0.1:9080"
},
"json": null,
"method": "GET",
"origin": "192.168.155.1, xxx.xxx.xxx.xxx",
"url": "http://127.0.0.1:9080/anything/authz"
}
Header values and the reported origin address vary by environment. The sample upstream should not receive the Authorization header.
Request another access token without the required client scope.
- Docker
- Kubernetes
export TOKEN_WITHOUT_SCOPE="$(
docker run --rm --network apisix-quickstart-net \
curlimages/curl:8.22.0 -sS \
"${KEYCLOAK_URL}/realms/authz-realm/protocol/openid-connect/token" \
--user "${KEYCLOAK_CLIENT_ID}:${KEYCLOAK_CLIENT_SECRET}" \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "grant_type=client_credentials" | \
jq -er '.access_token'
)"
export TOKEN_WITHOUT_SCOPE="$(
kubectl run authz-token-request --rm -i --restart=Never --quiet \
--namespace aic \
--image curlimages/curl:8.22.0 \
--command -- \
curl -sS \
"${KEYCLOAK_URL}/realms/authz-realm/protocol/openid-connect/token" \
--user "${KEYCLOAK_CLIENT_ID}:${KEYCLOAK_CLIENT_SECRET}" \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "grant_type=client_credentials" | \
jq -er '.access_token'
)"
Send the token to the route:
curl -i "http://127.0.0.1:9080/anything/authz" \
-H "Authorization: Bearer ${TOKEN_WITHOUT_SCOPE}"
APISIX returns HTTP/1.1 403 Forbidden because the token does not satisfy httpbin-access-policy.
Send a request without a bearer token:
curl -i "http://127.0.0.1:9080/anything/authz"
APISIX returns HTTP/1.1 401 Unauthorized because the request does not contain a token for Keycloak to evaluate.
Authorize Requests with Static Permissions
Static permissions avoid the Protection API lookup when the required Keycloak resource and scope are known in advance. Configure a route that always asks Keycloak to evaluate httpbin-anything#access.
Choose the API used to configure the route.
- Admin API
- ADC
- Ingress Controller
Create the route through the Admin API:
curl "http://127.0.0.1:9180/apisix/admin/routes/authz-keycloak-static" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
--data-binary @- <<EOF
{
"uri": "/anything/authz-static",
"plugins": {
"authz-keycloak": {
"lazy_load_paths": false,
"permissions": ["httpbin-anything#access"],
"discovery": "$KEYCLOAK_URL/realms/authz-realm/.well-known/uma2-configuration",
"client_id": "$KEYCLOAK_CLIENT_ID"
},
"serverless-post-function": {
"phase": "access",
"functions": [
"return function(conf, ctx) ngx.req.clear_header('Authorization') end"
]
}
},
"upstream": {
"type": "roundrobin",
"nodes": {
"httpbin.org:80": 1
}
}
}
EOF
Create adc-static.yaml with the route configuration:
services:
- name: authz-keycloak-static-httpbin
routes:
- name: authz-keycloak-static
uris:
- /anything/authz-static
plugins:
authz-keycloak:
lazy_load_paths: false
permissions:
- httpbin-anything#access
discovery: "${KEYCLOAK_URL}/realms/authz-realm/.well-known/uma2-configuration"
client_id: "${KEYCLOAK_CLIENT_ID}"
serverless-post-function:
phase: access
functions:
- "return function(conf, ctx) ngx.req.clear_header('Authorization') end"
upstream:
type: roundrobin
nodes:
- host: httpbin.org
port: 80
weight: 1
ADC reconciles services as desired state. The label selector limits this example to its own labeled resources. Preview the scoped changes and confirm that they contain no unintended updates or deletions:
adc diff -f adc-static.yaml \
--include-resource-type service \
--label-selector docs-example=authz-keycloak
Synchronize the reviewed service configuration:
adc sync -f adc-static.yaml \
--include-resource-type service \
--label-selector docs-example=authz-keycloak
Configure the static route with either Gateway API or APISIX custom resources.
- Gateway API
- APISIX CRD
Create authz-keycloak-static-ic.yaml:
apiVersion: v1
kind: Service
metadata:
namespace: aic
name: httpbin-static-external-domain
spec:
type: ExternalName
externalName: httpbin.org
---
apiVersion: apisix.apache.org/v1alpha1
kind: PluginConfig
metadata:
namespace: aic
name: authz-keycloak-static-plugin-config
spec:
plugins:
- name: authz-keycloak
config:
lazy_load_paths: false
permissions:
- httpbin-anything#access
discovery: http://keycloak.aic.svc.cluster.local:8080/realms/authz-realm/.well-known/uma2-configuration
client_id: apisix-authz
- name: serverless-post-function
config:
phase: access
functions:
- "return function(conf, ctx) ngx.req.clear_header('Authorization') end"
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
namespace: aic
name: authz-keycloak-static
spec:
parentRefs:
- name: apisix
rules:
- matches:
- path:
type: Exact
value: /anything/authz-static
filters:
- type: ExtensionRef
extensionRef:
group: apisix.apache.org
kind: PluginConfig
name: authz-keycloak-static-plugin-config
backendRefs:
- name: httpbin-static-external-domain
port: 80
Create authz-keycloak-static-ic.yaml:
apiVersion: apisix.apache.org/v2
kind: ApisixUpstream
metadata:
namespace: aic
name: httpbin-static-external-domain
spec:
ingressClassName: apisix
externalNodes:
- type: Domain
name: httpbin.org
---
apiVersion: apisix.apache.org/v2
kind: ApisixPluginConfig
metadata:
namespace: aic
name: authz-keycloak-static-plugin-config
spec:
ingressClassName: apisix
plugins:
- name: authz-keycloak
enable: true
config:
lazy_load_paths: false
permissions:
- httpbin-anything#access
discovery: http://keycloak.aic.svc.cluster.local:8080/realms/authz-realm/.well-known/uma2-configuration
client_id: apisix-authz
- name: serverless-post-function
enable: true
config:
phase: access
functions:
- "return function(conf, ctx) ngx.req.clear_header('Authorization') end"
---
apiVersion: apisix.apache.org/v2
kind: ApisixRoute
metadata:
namespace: aic
name: authz-keycloak-static
spec:
ingressClassName: apisix
http:
- name: authz-keycloak-static
match:
paths:
- /anything/authz-static
methods:
- GET
upstreams:
- name: httpbin-static-external-domain
plugin_config_name: authz-keycloak-static-plugin-config
Apply the configuration:
kubectl apply -f authz-keycloak-static-ic.yaml
❶ lazy_load_paths: Set to false to use the configured permission list without querying the Protection API.
❷ permissions: Resource and authorization scope that Keycloak evaluates for every request to this route.
❸ discovery and client_id: Identify the Keycloak UMA token endpoint and the resource server. The static workflow does not require the client secret because APISIX does not call the Protection API.
Verify Static Authorization
Send the permitted token to the static route:
curl -i "http://127.0.0.1:9080/anything/authz-static" \
-H "Authorization: Bearer ${ACCESS_TOKEN}"
APISIX returns HTTP/1.1 200 OK. Sending TOKEN_WITHOUT_SCOPE instead returns HTTP/1.1 403 Forbidden.
You have now configured Keycloak Authorization Services to enforce dynamic and static permissions at APISIX. See the authz-keycloak configuration reference for additional options, including HTTP-method scopes and access-denied redirects. See the Keycloak Authorization Services Guide for more policy and permission types.