Envoy Proxy Security Testing: Admin Interface Exposure, xDS API Abuse, and Sidecar Exploitation

Envoy is the data plane of every major service mesh — Istio, Consul Connect, AWS App Mesh, and others. As the proxy between every service-to-service call, a compromised or misconfigured Envoy instance can intercept all traffic, bypass authorization, and pivot to any service in the mesh. This guide covers Envoy-specific security testing: admin interface exploitation, xDS control plane API abuse, JWT filter bypass, external authorization service bypass, RBAC misconfiguration, and mTLS validation gaps.

Table of Contents

  1. Envoy Attack Surface
  2. Admin Interface Exploitation
  3. xDS Management API Abuse
  4. JWT Authentication Filter Bypass
  5. External Authorization Bypass
  6. RBAC Policy Misconfiguration
  7. mTLS Validation Gaps
  8. Envoy Security Hardening

Envoy Attack Surface

ComponentDefault PortAttack Surface
Admin interface9901Full config access, traffic dump, hot restart
xDS management (ADS)15010 (Istio)Inject malicious listener/cluster/route configs
Ingress listenerVaries (80/443)HTTP filter bypass, route manipulation
Sidecar proxy (Istio)15001/15006Bypass iptables redirect, access without proxy
Health check endpoint15021 (Istio)Information disclosure, availability check
Stats endpoint15090/9901Traffic metadata, upstream counts, connection state

Admin Interface Exploitation

# Envoy admin interface defaults to 0.0.0.0:9901 — should be restricted
# In Kubernetes, accessible via kubectl port-forward or if misbound to pod IP

# Test if admin is accessible from outside the pod
kubectl port-forward pod/envoy-pod -n default 9901:9901 &
curl -s "http://localhost:9901/help"

# Key admin endpoints:
# /config_dump           — full Envoy configuration (listeners, clusters, routes, certs)
# /stats                 — all counters and gauges
# /clusters              — upstream cluster health and connection pool state
# /listeners             — active listeners
# /runtime               — runtime configuration
# /reset_counters        — reset all stats (destructive for monitoring)
# /drain_listeners       — drain connections (causes outage)
# /quitquitquit          — shut down Envoy (causes outage!)
# /healthcheck/fail      — force health check to fail (removes from LB)

# Dump full configuration — reveals all certs, routes, upstream endpoints
curl -s "http://localhost:9901/config_dump" | python3 -m json.tool > envoy_config.json
cat envoy_config.json | python3 -c "
import json,sys

config = json.load(sys.stdin)
# Extract TLS certificates
for item in config.get('configs',[]):
    if item.get('@type','').endswith('SecretsConfigDump'):
        for secret in item.get('dynamic_active_secrets',[]):
            tls = secret.get('secret',{}).get('tlsCertificate',{})
            if tls:
                print(f'[TLS] Name: {secret[\"name\"]}')
                print(f'  Private key: {\"PRESENT\" if tls.get(\"privateKey\") else \"absent\"}')
"

# Extract all upstream endpoints (reveals internal service topology)
curl -s "http://localhost:9901/clusters" | grep -oP 'upstream_.*?::\d+\.\d+\.\d+\.\d+:\d+' | sort | uniq

xDS Management API Abuse

The xDS (discovery service) API is how control planes (Istio Pilot, Consul, etc.) push configuration to Envoy. A compromised or exposed xDS server can push malicious configs that redirect traffic or disable mTLS.

# In Istio, the Pilot/Istiod management server listens on port 15010 (plaintext) or 15012 (TLS)
# Check if plaintext xDS is exposed
kubectl get service -n istio-system istiod -o json | \
  python3 -c "
import json,sys
svc = json.load(sys.stdin)
for port in svc['spec']['ports']:
    print(f\"Port: {port['port']} Name: {port['name']}\")
"

# Check if any pod can reach the control plane without authentication
kubectl exec -it test-pod -- curl -s "http://istiod.istio-system.svc.cluster.local:15010/debug/registryz" | \
  python3 -m json.tool 2>/dev/null | head -50

# Debug endpoints on Istiod — often accessible within the mesh
kubectl port-forward -n istio-system svc/istiod 15014:15014 &
curl -s "http://localhost:15014/debug/configz" | head -100   # Full mesh config
curl -s "http://localhost:15014/debug/connections" | head -50  # Connected proxies
curl -s "http://localhost:15014/debug/syncz" | head -50        # xDS sync state

# Attempt to get proxy config for all pods (information disclosure)
curl -s "http://localhost:15014/debug/syncz" | \
  python3 -c "import json,sys; [print(p.get('proxy','?')) for p in json.load(sys.stdin)]"

JWT Authentication Filter Bypass

# Envoy's JWT filter verifies JWTs on incoming requests
# Common misconfigurations allow bypass

# Test 1: Missing route-level JWT requirement
# Some routes may not have JWT requirements configured
curl -s "http://service.example.com/api/admin" \
  # No Authorization header
  -H "Content-Type: application/json" | head -5
# If 200 returned without auth → bypass

# Test 2: forward_payload_header trust exploitation
# Envoy extracts JWT claims and adds them as a header to upstream
# If you can inject that header directly (bypassing Envoy), the upstream trusts it
curl -s "http://service.example.com/api/user/profile" \
  -H "X-Forwarded-User-Claims: {\"sub\":\"admin\",\"role\":\"superadmin\"}"
# If upstream trusts this header, auth is bypassed

# Test 3: JWT algorithm confusion via Envoy config
# Check which algorithms Envoy's JWT filter accepts
kubectl get envoyfilter -n default -o yaml | grep -A5 "jwt\|algorithm"

# Test 4: JWKS caching — use an expired JWT with a rotated key
# If Envoy caches JWKS and hasn't refreshed, old tokens may still be accepted
# Create a JWT signed with a previously-valid key, test if it's accepted

# Test 5: Missing JWT validation on gRPC methods
# JWT filter may be configured for HTTP/1.1 but gRPC (HTTP/2) routes may differ
curl -s --http2-prior-knowledge "http://service.example.com" \
  -H "Content-Type: application/grpc" \
  -d "" | head -5

External Authorization Bypass

# Envoy's ext_authz filter calls an external authorization service for every request
# If the external auth service is slow, returns errors, or is misconfigured:
# - failure_mode_allow: true → auth failure defaults to ALLOW (critical misconfiguration)
# - On ext_authz service timeout → behavior depends on configuration

# Check Envoy's ext_authz configuration
curl -s "http://localhost:9901/config_dump" | \
  python3 -c "
import json,sys
config = json.load(sys.stdin)
for item in config.get('configs',[]):
    if 'http_filters' in str(item):
        print(json.dumps(item, indent=2)[:2000])
" | grep -A20 "ext_authz\|envoy.filters.http.ext_authz"
# Look for: failure_mode_allow: true

# Test ext_authz timeout behavior
# Create many concurrent requests to overwhelm the authz service
for i in $(seq 1 100); do
  curl -s -o /dev/null "http://service.example.com/api/data" &
done
# If ext_authz service times out under load, does Envoy allow or deny?

# Direct access bypass — skip Envoy sidecar entirely
# In Kubernetes, pods can communicate directly if iptables rules don't capture traffic
# Test by connecting to the pod IP directly (not through the service)
POD_IP=$(kubectl get pod app-pod -o jsonpath='{.status.podIP}')
kubectl exec -it test-pod -- curl -s "http://$POD_IP:8080/api/admin"
# If this works, the app doesn't enforce auth independent of Envoy

# Check if app_protocol != http causes ext_authz to be skipped
# gRPC with non-standard content types may bypass HTTP-focused authz configs

RBAC Policy Misconfiguration

# Envoy RBAC filter controls which principals can reach which services
# In Istio: AuthorizationPolicy resources configure RBAC

# Enumerate Istio authorization policies
kubectl get authorizationpolicies --all-namespaces -o yaml | \
  python3 -c "
import yaml, sys
docs = list(yaml.safe_load_all(sys.stdin))
for ap in docs:
    if not ap: continue
    meta = ap.get('metadata', {})
    spec = ap.get('spec', {})
    print(f\"Policy: {meta.get('namespace','?')}/{meta.get('name','?')}\")
    print(f\"  Action: {spec.get('action', 'ALLOW')}\")
    rules = spec.get('rules', [])
    for rule in rules:
        principals = [p.get('requestPrincipal','?') for p in rule.get('from',[{}])[0].get('source',{}).get('requestPrincipals',[])]
        print(f\"  Principals: {principals}\")
        ops = rule.get('to',[{}])[0].get('operation',{})
        print(f\"  Paths: {ops.get('paths', ['*'])}\")
"

# Test for ALLOW-by-default (no policy = allow in some mesh configs)
kubectl exec -it test-pod -- curl -s "http://target-service.default.svc.cluster.local/admin"

# Test for path traversal in RBAC rules
# Rule allows: paths: ["/api/public/*"]
# Test: /api/public/../admin → may match wildcard but reach admin path
kubectl exec -it test-pod -- curl -s \
  "http://target-service.default.svc.cluster.local/api/public/../admin"

# Test for missing method constraint (GET allowed but POST not restricted)
kubectl exec -it test-pod -- curl -s -X POST \
  "http://target-service.default.svc.cluster.local/api/read-only-endpoint" \
  -d '{"action":"delete_all"}'

mTLS Validation Gaps

# Istio mTLS modes: STRICT, PERMISSIVE, DISABLE
# PERMISSIVE = accepts both mTLS and plaintext — allows non-mesh clients to connect

# Check PeerAuthentication policies
kubectl get peerauthentication --all-namespaces -o yaml | grep -A5 "mtls:\|mode:"

# Check for PERMISSIVE mode (security gap)
kubectl get peerauthentication -A -o json | \
  python3 -c "
import json,sys
for pa in json.load(sys.stdin).get('items',[]):
    meta = pa['metadata']
    spec = pa.get('spec',{})
    mode = spec.get('mtls',{}).get('mode','UNSET')
    if mode in ['PERMISSIVE','DISABLE']:
        print(f\"[!] {meta['namespace']}/{meta['name']}: mTLS mode = {mode}\")
"

# Test plaintext connection to a service in a mesh with PERMISSIVE mTLS
kubectl exec -it external-pod -- curl -s "http://internal-service.default.svc.cluster.local:8080/api/data"
# If data returned → PERMISSIVE mode confirmed (attacker in cluster can bypass mTLS)

# Test certificate validation — does Envoy verify the peer cert's SPIFFE identity?
# In STRICT mode with valid cert but wrong workload identity
# Requires custom cert generation — only in authorized test environments

# Check if service has destination rule for client-side mTLS
kubectl get destinationrule --all-namespaces -o json | \
  python3 -c "
import json,sys
for dr in json.load(sys.stdin).get('items',[]):
    tls = dr.get('spec',{}).get('trafficPolicy',{}).get('tls',{})
    mode = tls.get('mode','UNSET')
    if mode == 'DISABLE':
        meta = dr['metadata']
        print(f\"[!] {meta['namespace']}/{meta['name']}: TLS DISABLED for client\")
"

Envoy Security Hardening

# 1. Restrict admin interface to localhost only
admin:
  address:
    socket_address:
      address: 127.0.0.1  # NOT 0.0.0.0
      port_value: 9901

# 2. Set failure_mode_allow: false for ext_authz
http_filters:
  - name: envoy.filters.http.ext_authz
    typed_config:
      "@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthz
      failure_mode_allow: false  # Deny on authz service failure
      grpc_service:
        envoy_grpc:
          cluster_name: ext_authz_cluster
        timeout: 250ms  # Short timeout, fail closed

# 3. Enforce STRICT mTLS in Istio
apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
  name: default
  namespace: istio-system
spec:
  mtls:
    mode: STRICT

# 4. Add explicit AuthorizationPolicy deny-all, then allow-list
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
  name: deny-all
  namespace: default
spec: {}  # Empty spec = DENY all traffic

---
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
  name: allow-specific
  namespace: default
spec:
  action: ALLOW
  rules:
  - from:
    - source:
        principals: ["cluster.local/ns/frontend/sa/frontend-service"]
    to:
    - operation:
        methods: ["GET"]
        paths: ["/api/products"]

# 5. Disable debug endpoints in production
# Istio: set PILOT_ENABLE_ANALYSIS=false and restrict /debug/ endpoints
# Apply NetworkPolicy to block access to 15014 from non-admin pods

# 6. Regularly rotate mesh certificates
# Istio: default cert rotation is 24h
# Set shorter rotation for sensitive workloads:
# CITADEL_WORKLOAD_CERT_TTL=3600 (1 hour)

# 7. Monitor Envoy stats for anomalies
# Watch for:
# - ext_authz.denied vs ext_authz.ok ratio spikes
# - upstream_cx_destroy spikes (connection failures to upstreams)
# - jwt_authn.denied spikes (JWT bypass attempts)
Envoy Security Checklist: Restrict admin interface to 127.0.0.1; set failure_mode_allow=false on ext_authz filter; enforce STRICT mTLS via PeerAuthentication; audit all AuthorizationPolicies for wildcard principals or paths; test JWT filter coverage across all routes; check xDS management API access from within the mesh; verify admin endpoints are not accessible from user traffic; monitor ext_authz denial rates; validate mTLS is actually enforced (not PERMISSIVE).
Security TestMethodRisk
Admin interface accessiblecurl pod-ip:9901/config_dumpCritical
failure_mode_allow: trueInspect config_dump for ext_authzCritical
PERMISSIVE mTLS modekubectl get peerauthentication -AHigh
Wildcard AuthorizationPolicykubectl get authorizationpolicies -AHigh
JWT filter bypass (missing routes)Request without Auth header to all routesHigh
xDS debug endpoints accessiblecurl istiod:15014/debug/configzMedium
Direct pod IP bypass (no iptables)curl pod-ip directly from test podMedium

Automate Service Mesh Security Testing

Ironimo tests Envoy-based service mesh deployments for admin interface exposure, mTLS enforcement gaps, authorization policy misconfigurations, and JWT filter bypass paths — ensuring your service mesh actually provides the security guarantees it promises.

Start free scan