Keycloak + Kong API Gateway: End-to-End Setup
Last updated: March 2026
Introduction
API gateways sit at the front door of your microservices architecture, handling cross-cutting concerns like authentication, rate limiting, and request routing. When you pair Kong Gateway with Keycloak, you get a powerful combination: Kong handles traffic management while Keycloak manages identity, tokens, and single sign-on.
This guide walks through a complete integration. You will set up both services with Docker Compose, configure Kong’s OIDC plugin to validate Keycloak-issued JWTs, map Kong consumers to Keycloak clients, and apply per-client rate limiting. By the end, every request reaching your upstream services will be authenticated and authorized at the gateway level.
If you are new to Keycloak deployment, our Docker Compose Generator can help you scaffold a local Keycloak instance quickly.
Architecture Overview
The request flow looks like this:
- A client application authenticates with Keycloak and receives an access token (JWT).
- The client sends API requests to Kong with the token in the
Authorizationheader. - Kong’s OIDC plugin validates the JWT against Keycloak’s public keys.
- If valid, Kong maps the token to a consumer and applies rate-limiting and other policies.
- Kong forwards the request to the upstream service with identity context in headers.
This architecture offloads all authentication logic from your microservices. Your backend code only needs to trust headers set by Kong.
Prerequisites
- Docker and Docker Compose installed
- Basic familiarity with OAuth 2.0 and OpenID Connect
- A terminal with
curlavailable
Docker Compose Setup
Below is a complete docker-compose.yml that spins up Keycloak, Kong (with its database), and a simple upstream echo service:
version: "3.9"
services:
# --- Keycloak ---
keycloak-db:
image: postgres:16-alpine
environment:
POSTGRES_DB: keycloak
POSTGRES_USER: keycloak
POSTGRES_PASSWORD: keycloak_pass
volumes:
- keycloak_data:/var/lib/postgresql/data
keycloak:
image: quay.io/keycloak/keycloak:26.1.0
command: start-dev
environment:
KC_DB: postgres
KC_DB_URL: jdbc:postgresql://keycloak-db:5432/keycloak
KC_DB_USERNAME: keycloak
KC_DB_PASSWORD: keycloak_pass
KC_HOSTNAME_STRICT: "false"
KEYCLOAK_ADMIN: admin
KEYCLOAK_ADMIN_PASSWORD: admin
ports:
- "8080:8080"
depends_on:
- keycloak-db
# --- Kong ---
kong-db:
image: postgres:16-alpine
environment:
POSTGRES_DB: kong
POSTGRES_USER: kong
POSTGRES_PASSWORD: kong_pass
volumes:
- kong_data:/var/lib/postgresql/data
kong-migration:
image: kong:3.9
command: kong migrations bootstrap
environment:
KONG_DATABASE: postgres
KONG_PG_HOST: kong-db
KONG_PG_USER: kong
KONG_PG_PASSWORD: kong_pass
depends_on:
- kong-db
kong:
image: kong:3.9
environment:
KONG_DATABASE: postgres
KONG_PG_HOST: kong-db
KONG_PG_USER: kong
KONG_PG_PASSWORD: kong_pass
KONG_PROXY_ACCESS_LOG: /dev/stdout
KONG_ADMIN_ACCESS_LOG: /dev/stdout
KONG_PROXY_ERROR_LOG: /dev/stderr
KONG_ADMIN_ERROR_LOG: /dev/stderr
KONG_ADMIN_LISTEN: "0.0.0.0:8001"
ports:
- "8000:8000" # Proxy
- "8001:8001" # Admin API
depends_on:
kong-migration:
condition: service_completed_successfully
# --- Upstream echo service ---
echo-service:
image: hashicorp/http-echo
command: ["-text", '{"message":"Hello from upstream"}']
ports:
- "5678:5678"
volumes:
keycloak_data:
kong_data:
Start everything:
docker compose up -d
Wait for Keycloak to be ready at http://localhost:8080 and Kong Admin at http://localhost:8001.
Configuring Keycloak
Create a Realm and Client
- Log in to the Keycloak admin console at
http://localhost:8080/adminwithadmin/admin. - Create a new realm called
api-gateway. - Under Clients, create a new client:
| Setting | Value |
|---|---|
| Client ID | kong-api |
| Client Protocol | openid-connect |
| Client Authentication | ON |
| Authorization | OFF |
| Valid Redirect URIs | * (for development only) |
| Web Origins | * |
- On the Credentials tab, note the client secret.
Create Roles and a Test User
Create two realm roles: api-read and api-write.
Then create a test user:
- Username:
testuser - Email:
[email protected] - Set a password (disable “Temporary”)
- Under Role Mappings, assign
api-read
For production deployments, you would configure RBAC policies to control which roles grant access to which API endpoints.
Verify Token Issuance
Obtain a token using the Resource Owner Password Grant (suitable for testing only):
KC_TOKEN=$(curl -s -X POST
"http://localhost:8080/realms/api-gateway/protocol/openid-connect/token"
-H "Content-Type: application/x-www-form-urlencoded"
-d "grant_type=password"
-d "client_id=kong-api"
-d "client_secret=YOUR_CLIENT_SECRET"
-d "username=testuser"
-d "password=testpassword"
-d "scope=openid" | jq -r '.access_token')
echo $KC_TOKEN
Paste the token into the JWT Token Analyzer to inspect its claims, roles, and expiration.
Configuring Kong
Register the Upstream Service and Route
# Create a service pointing to the echo backend
curl -s -X POST http://localhost:8001/services
-d name=echo-service
-d url=http://echo-service:5678
# Create a route
curl -s -X POST http://localhost:8001/services/echo-service/routes
-d "name=echo-route"
-d "paths[]=/api"
Test that the route works without authentication:
curl -s http://localhost:8000/api
# Should return: {"message":"Hello from upstream"}
Enable JWT Plugin
Kong’s built-in JWT plugin validates tokens without needing the full OIDC plugin (which is an Enterprise feature). For open-source Kong, the JWT plugin works well with Keycloak:
# Enable the JWT plugin on the service
curl -s -X POST http://localhost:8001/services/echo-service/plugins
-d "name=jwt"
-d "config.uri_param_names=jwt"
-d "config.claims_to_verify=exp"
-d "config.key_claim_name=iss"
-d "config.secret_is_base64=false"
Create a Kong Consumer and Map to Keycloak
Kong consumers represent authenticated entities. We create a consumer and associate Keycloak’s JWT signing key:
# Create a consumer
curl -s -X POST http://localhost:8001/consumers
-d "username=keycloak-issuer"
# Fetch the Keycloak realm's public key
KC_PUBLIC_KEY=$(curl -s
"http://localhost:8080/realms/api-gateway/protocol/openid-connect/certs"
| jq -r '.keys[] | select(.use=="sig" and .alg=="RS256") | .x5c[0]')
# Register the JWT credential
# The 'key' must match the 'iss' claim in the token
curl -s -X POST http://localhost:8001/consumers/keycloak-issuer/jwt
-d "key=http://localhost:8080/realms/api-gateway"
-d "algorithm=RS256"
-d "rsa_public_key=-----BEGIN PUBLIC KEY-----
${KC_PUBLIC_KEY}
-----END PUBLIC KEY-----"
Now test with a valid Keycloak token:
# This should succeed
curl -s -H "Authorization: Bearer ${KC_TOKEN}"
http://localhost:8000/api
# This should return 401
curl -s http://localhost:8000/api
Alternative: OIDC Plugin (Kong Enterprise / Kong Gateway)
If you have Kong Enterprise or Kong Gateway with the OIDC plugin, the setup is simpler because it handles discovery and key rotation automatically:
curl -s -X POST http://localhost:8001/services/echo-service/plugins
-d "name=openid-connect"
-d "config.issuer=http://keycloak:8080/realms/api-gateway"
-d "config.client_id=kong-api"
-d "config.client_secret=YOUR_CLIENT_SECRET"
-d "config.auth_methods[]=bearer"
-d "config.bearer_token_param_type[]=header"
-d "config.scopes_required[]=openid"
-d "config.consumer_claim[]=sub"
-d "config.consumer_by[]=username"
The OIDC plugin automatically fetches Keycloak’s JWKS endpoint and handles key rotation, which is a significant operational advantage.
Rate Limiting Per Client
One of the biggest benefits of the gateway pattern is applying per-client policies. With the JWT validated and a consumer identified, you can apply rate limiting:
# Global rate limit: 100 requests/minute
curl -s -X POST http://localhost:8001/services/echo-service/plugins
-d "name=rate-limiting"
-d "config.minute=100"
-d "config.policy=local"
-d "config.limit_by=consumer"
# Override for a specific high-volume consumer
curl -s -X POST http://localhost:8001/consumers/keycloak-issuer/plugins
-d "name=rate-limiting"
-d "config.minute=1000"
-d "config.policy=local"
The limit_by=consumer setting ensures each Keycloak client gets its own rate limit bucket.
Forwarding Identity Context to Upstream
By default, Kong’s JWT plugin sets the X-Consumer-Username and X-Consumer-ID headers on the upstream request. To pass additional token claims (like roles or email), add the post-function plugin or use request-transformer:
curl -s -X POST http://localhost:8001/services/echo-service/plugins
-d "name=request-transformer"
-d "config.add.headers[]=X-Auth-Token-Sub:$(kong.ctx.shared.authenticated_jwt_token_claims.sub)"
-d "config.add.headers[]=X-Auth-Roles:$(kong.ctx.shared.authenticated_jwt_token_claims.realm_access.roles)"
Alternatively, use the pre-function plugin for custom Lua logic:
-- Custom header injection plugin (serverless function)
local jwt_claims = kong.ctx.shared.authenticated_jwt_token_claims
if jwt_claims then
kong.service.request.set_header("X-User-Sub", jwt_claims.sub)
kong.service.request.set_header("X-User-Email", jwt_claims.email or "")
-- Forward realm roles as comma-separated list
local roles = jwt_claims.realm_access and jwt_claims.realm_access.roles
if roles then
kong.service.request.set_header("X-User-Roles", table.concat(roles, ","))
end
end
Your upstream microservices can then use these headers for authorization decisions without decoding the JWT themselves.
Testing the Full Flow
Here is a complete test script:
#!/bin/bash
set -e
KEYCLOAK_URL="http://localhost:8080"
KONG_URL="http://localhost:8000"
REALM="api-gateway"
CLIENT_ID="kong-api"
CLIENT_SECRET="YOUR_CLIENT_SECRET"
echo "=== Step 1: Get access token from Keycloak ==="
TOKEN_RESPONSE=$(curl -s -X POST
"${KEYCLOAK_URL}/realms/${REALM}/protocol/openid-connect/token"
-H "Content-Type: application/x-www-form-urlencoded"
-d "grant_type=password"
-d "client_id=${CLIENT_ID}"
-d "client_secret=${CLIENT_SECRET}"
-d "username=testuser"
-d "password=testpassword"
-d "scope=openid")
ACCESS_TOKEN=$(echo $TOKEN_RESPONSE | jq -r '.access_token')
echo "Token obtained (first 50 chars): ${ACCESS_TOKEN:0:50}..."
echo ""
echo "=== Step 2: Call API without token (expect 401) ==="
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" ${KONG_URL}/api)
echo "Response code: ${HTTP_CODE}"
echo ""
echo "=== Step 3: Call API with valid token (expect 200) ==="
RESPONSE=$(curl -s -w "n%{http_code}"
-H "Authorization: Bearer ${ACCESS_TOKEN}"
${KONG_URL}/api)
echo "Response: ${RESPONSE}"
echo ""
echo "=== Step 4: Call API with invalid token (expect 401) ==="
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}"
-H "Authorization: Bearer invalid.token.here"
${KONG_URL}/api)
echo "Response code: ${HTTP_CODE}"
echo ""
echo "=== Step 5: Check rate limit headers ==="
curl -s -I -H "Authorization: Bearer ${ACCESS_TOKEN}"
${KONG_URL}/api 2>&1 | grep -i "ratelimit"
Production Considerations
Key Rotation
Keycloak rotates signing keys periodically. If you use the open-source JWT plugin, you need to update the consumer credential when keys rotate. The Enterprise OIDC plugin handles this automatically via the JWKS endpoint.
For a managed Keycloak deployment on Skycloak, key rotation is handled for you, and you can monitor signing key changes through audit logs.
High Availability
In production, both Kong and Keycloak should run in clustered mode:
- Kong supports database-backed clustering and DB-less declarative mode
- Keycloak supports clustering via Infinispan caches (see our cluster configuration best practices)
Security Hardening
- Always use HTTPS between Kong and Keycloak in production
- Restrict the Kong Admin API to internal networks
- Enable MFA on the Keycloak admin console
- Set short token expiration times and use refresh tokens
- Monitor API access patterns with Skycloak Insights
Declarative Configuration
For GitOps workflows, export your Kong configuration as a declarative YAML file:
# kong.yml
_format_version: "3.0"
services:
- name: echo-service
url: http://echo-service:5678
routes:
- name: echo-route
paths:
- /api
plugins:
- name: jwt
config:
claims_to_verify:
- exp
key_claim_name: iss
- name: rate-limiting
config:
minute: 100
policy: local
limit_by: consumer
consumers:
- username: keycloak-issuer
jwt_secrets:
- key: "http://localhost:8080/realms/api-gateway"
algorithm: RS256
rsa_public_key: |
-----BEGIN PUBLIC KEY-----
YOUR_PUBLIC_KEY_HERE
-----END PUBLIC KEY-----
Load it with:
kong config db_import kong.yml
Conclusion
Kong and Keycloak together provide a clean separation of concerns: Keycloak handles identity, tokens, and user management, while Kong enforces authentication, rate limiting, and routing at the edge. This keeps your microservices focused on business logic.
The key steps covered in this guide:
- Setting up both services with Docker Compose
- Configuring Keycloak as the OIDC provider
- Validating JWTs at the Kong gateway layer
- Mapping Kong consumers to Keycloak issuers
- Applying per-client rate limits
- Forwarding identity context to upstream services
For further reading, consult the Kong JWT Plugin documentation and the Keycloak Server Administration Guide.
Ready to run Keycloak in production without managing infrastructure? Try Skycloak free and get a fully managed Keycloak cluster with built-in API gateway support.
Ready to simplify your authentication?
Deploy production-ready Keycloak in minutes. Unlimited users, flat pricing, no SSO tax.