← Back to Feed CACHED · 2026-06-02 15:07:51 · CACHE_KEY tenable:111351
tenable:111351 · Disclosed 2018-07-26

Hashicorp Consul Web UI and API access

HashiCorp Consul Web UI/API exposure via non-loopback binding and permissive unauthenticated token handling

ASSESSED — NOISGATE
Vendor
—
—
—
Reassessed
—
—
—
Verdict: —
Do you agree?
01 · The Real Story

This is leaving a side door cracked open, not discovering a new hole in the wall

Tenable plugin 111351 is not a software flaw with a fixed version; it is a configuration exposure finding for HashiCorp Consul. The risky pattern is: the Consul HTTP/UI listener is bound off-loopback via client_addr, the optional Web UI is enabled, and unauthenticated requests inherit permissions from either acl.tokens.default or the built-in anonymous token. HashiCorp documents that client_addr defaults to 127.0.0.1, the UI is not enabled by default for non-dev agents, and ACLs are what secure the API/UI/CLI.

Tenable's HIGH / 8.8 overstates reality for most enterprises because the attack path depends on multiple admin choices that are all non-default and deployment-specific: reachable network exposure, permissive token behavior, and in the worst case additional dangerous settings like enable_script_checks. If those prerequisites are all true on an internet-facing node, the risk climbs fast; but as a fleet-wide patching priority this behaves like a context-heavy hardening issue, not a universally exploitable RCE.

"This is a risky Consul hardening mistake, not a generic 8.8 emergency across the fleet."
02 · The Attack Path

4 steps from start to impact.

STEP 01

Reach the Consul HTTP surface with curl

The attacker first needs TCP reachability to the Consul API/UI, typically port 8500 over HTTP or 8501 over HTTPS. This only exists when operators bind client_addr to a non-loopback address or otherwise publish the service through a proxy, load balancer, or Kubernetes service.
Conditions required:
  • Consul is installed and running
  • client_addr or equivalent exposure is non-loopback
  • Network path exists from attacker to the Consul listener
Where this breaks in practice:
  • HashiCorp documents the default client_addr as 127.0.0.1
  • Many enterprise deployments keep Consul on management or cluster-only networks
  • Firewalls, security groups, or service meshes often block direct access
Detection/coverage: Excellent coverage for listener discovery by Nessus/Nmap and simple HTTP probes; weak at proving the listener is reachable from untrusted zones without path-aware testing.
STEP 02

Inherit permissions through the default or anonymous token

If the attacker sends API requests without a token, Consul will use the configured default ACL token, or the built-in anonymous token if no default token exists. This is the decisive branch: if those effective permissions are restrictive, the finding collapses into mere exposure; if they are permissive, the attacker gets unauthenticated read or write capability.
Conditions required:
  • ACLs are enabled or token behavior is otherwise active
  • Unauthenticated requests map to a token with meaningful permissions
Where this breaks in practice:
  • A correctly configured anonymous token with default_policy = deny blocks useful unauthenticated access
  • The blast radius is constrained by the policy attached to the effective token
  • Tenable does not prove the token policy is actually dangerous on every flagged host
Detection/coverage: Config review and authenticated checks can confirm acl.tokens.default; passive network scanners usually cannot determine the attached policy safely.
STEP 03

Enumerate topology and service data via the API/UI

With readable permissions, the attacker can use curl or the Consul UI to query nodes, services, health checks, and possibly key/value data depending on policy. In a real environment that can leak internal hostnames, addresses, service names, and trust relationships that materially help later movement.
Conditions required:
  • Effective unauthenticated token has read access to catalog, health, or KV endpoints
Where this breaks in practice:
  • Many hardened deployments separate read from write and keep KV secrets elsewhere
  • Read-only exposure is damaging but usually not immediate host takeover
  • Some API endpoints still require stronger privileges than basic catalog discovery
Detection/coverage: Web logs, reverse proxy logs, and Consul audit/logging can show anonymous GETs; most vuln scanners only prove endpoint presence.
STEP 04

Modify service state or weaponize checks with curl/consul

If the inherited permissions include service:write or related agent privileges, an attacker may register or alter services and health checks. That becomes materially worse only if operators also enabled dangerous options such as enable_script_checks or remote exec-related capabilities, which HashiCorp explicitly warns can introduce remote execution risk when the API is exposed.
Conditions required:
  • Effective unauthenticated token includes write permissions
  • The target agent accepts the relevant registration endpoints
  • For code execution scenarios, script checks or remote exec features are enabled
Where this breaks in practice:
  • Service registration write access is not implied by mere UI exposure
  • enable_script_checks is not on by default
  • disable_remote_exec changed to secure-by-default behavior in newer Consul releases
Detection/coverage: Good opportunities for detection in Consul audit/admin logs, config drift tooling, and SIEM alerts on PUT /v1/agent/service/register or new check definitions.
03 · Compensating Control

1
Bind Consul back to loopback where possible — Set client_addr to loopback or remove the override so the documented default applies. This is the cleanest risk kill-switch and, for a MEDIUM verdict, there is no mitigation SLA — go straight to the 365-day remediation window, but confirmed externally reachable cases should be corrected much sooner as operational hardening.
2
Strip permissions from unauthenticated paths — Ensure unauthenticated requests fall to a tightly restricted anonymous token and avoid assigning broad rights to acl.tokens.default. This limits exposure even when the API must remain reachable for operational reasons; complete within the same remediation cycle.
3
Require TLS and trusted network paths — Use TLS for API access and restrict ingress with host firewall rules, security groups, or cluster-only service exposure. This reduces opportunistic discovery and credential leakage while you normalize Consul configs across the fleet.
4
Keep script-based execution paths off — Verify enable_script_checks is disabled unless there is a documented exception, prefer enable_local_script_checks when needed, and keep remote exec disabled. This prevents the finding from turning into a much nastier execution path.
5
Alert on anonymous API use — Log and alert on requests lacking X-Consul-Token or bearer auth, plus PUT operations to service/check registration endpoints. This gives you immediate detection value even if the configuration cannot be changed in one maintenance window.
What doesn't work
  • A generic EDR on the host does not solve the base exposure; it may catch a later script-execution stage, but it won't stop topology leakage or service-catalog tampering via the API.
  • Turning on the Web UI login experience alone does not help if unauthenticated API requests still inherit a permissive default or anonymous token.
  • Relying on 'internal only' network assumptions is weak; if the attacker already has a foothold on a workstation, VPN segment, or peered VPC, this becomes reachable post-initial-access.
04 · Intelligence Metadata

The supporting signals.

In-the-wild statusNo CVE and not listed in CISA KEV; this is a misconfiguration exposure pattern rather than a tracked vulnerability campaign.
PoC availabilityNo exploit is needed for the base condition; curl against the Consul API is sufficient if the listener is exposed and the effective unauthenticated token is permissive. HashiCorp's own docs show the relevant API paths.
EPSSN/A because this finding has no mapped CVE.
KEV statusNot applicable / not listed; there is no CVE record to track in KEV.
Vendor score baselineTenable assigns CVSS 8.8 with CVSS:3.0/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H, but that assumes attacker reachability and high-impact permissions that are not inherent to every flagged Consul node.
Affected scopePotentially any Consul version/configuration where the HTTP/UI listener is exposed beyond loopback and unauthenticated requests inherit meaningful ACL rights. This is configuration-dependent, not version-bound.
Fixed versionNone. There is no patched Consul release for this finding; the remedy is configuration hardening: loopback-only or tightly filtered exposure, restrictive anonymous/default token behavior, ACLs, and TLS.
Default behavior that mattersHashiCorp says client_addr defaults to 127.0.0.1 and the Web UI is not enabled by default for non-dev agents. Those defaults are major downward pressure on severity.
Exposure/scanning realityConsul commonly uses port 8500 for API/UI, and internet-scanning ecosystems index that port. Exposure is easy to find when operators publish it, but enterprise reachability is usually limited to internal management networks.
Disclosure / reportingTenable plugin published 2018-07-26; HashiCorp's support article discussing this scanner finding was updated 2025-01-25.

Sources.

  1. Tenable Nessus Plugin 111351
  2. HashiCorp Help Center: Consul Web UI and API is Accessible Remotely if not Configured Properly
  3. HashiCorp Consul UI documentation
  4. HashiCorp Consul security architecture
  5. HashiCorp HTTP API structure and authentication
  6. HashiCorp ACL token documentation
  7. HashiCorp default token and anonymous token relationship
  8. HashiCorp health checks and script-check security warning
05 · The Call

Final Verdict
↓ DOWNGRADED to MEDIUM (4.8/10)

Why this verdict

  • Downward pressure: requires reachable Consul API/UI. If the listener is left at the documented default 127.0.0.1, the attack chain dies immediately.
  • Downward pressure: requires permissive effective token behavior. Unauthenticated requests only matter if acl.tokens.default or anonymous has useful rights; a deny-by-default anonymous token turns this into noise.
  • Downward pressure: host takeover is not intrinsic. Tenable hints at service registration and remote access, but that escalated outcome generally needs extra risky features like enable_script_checks or remote exec-related capabilities.
  • Upward pressure: if exposed, the intel value is real. Even read-only catalog/KV visibility can materially aid lateral movement by mapping services, nodes, health, and trust edges.
  • Upward pressure: Consul is high-value control-plane tech. Bad permissions here can let an attacker manipulate discovery and traffic decisions, so you should not ignore confirmed unsafe instances.

Why not higher?

This is not a universal unauthenticated RCE and not even a universal unauthorized read. It depends on a stack of non-default conditions: off-loopback binding, network path, permissive token semantics, and possibly additional dangerous features for execution impact.

Why not lower?

A confirmed exposed Consul API with inherited read or write permissions is not harmless. Because Consul is a service-discovery/control-plane component, even partial anonymous access can expose topology or let an attacker poison service state in ways that matter operationally.

06 · Verification

Crowdsourced verification payload.

Run this on the target Consul host as root or with enough privileges to read Consul config files and inspect listening sockets. Example: sudo bash ./check_consul_exposure.sh /etc/consul.d; it checks whether Consul is bound off-loopback and whether unauthenticated reads succeed against the local API.

noisgate-verify.sh
BASHREAD-ONLYSAFE
#!/usr/bin/env bash
# check_consul_exposure.sh
# Exit codes:
#   0 = PATCHED (not vulnerable based on current checks)
#   1 = VULNERABLE
#   2 = UNKNOWN

set -u
CONFIG_DIR="${1:-/etc/consul.d}"
API_PORT="8500"
HTTPS_PORT="8501"
status="UNKNOWN"
reason=""

have_cmd() { command -v "$1" >/dev/null 2>&1; }

json_get_http_code() {
  local url="$1"
  curl -ksS -o /tmp/consul_check_body.$$ -w "%{http_code}" "$url" 2>/dev/null || echo "000"
}

cleanup() {
  rm -f /tmp/consul_check_body.$$ 2>/dev/null || true
}
trap cleanup EXIT

if ! have_cmd curl; then
  echo "UNKNOWN - curl not found"
  exit 2
fi

# 1) Check listening addresses for Consul on 8500/8501.
listen_non_loopback=0
if have_cmd ss; then
  if ss -lntp 2>/dev/null | grep -E ':(8500|8501)\b' | grep -vqE '127\.0\.0\.1:|\[::1\]:'; then
    listen_non_loopback=1
  fi
elif have_cmd netstat; then
  if netstat -lntp 2>/dev/null | grep -E ':(8500|8501)\b' | grep -vqE '127\.0\.0\.1:|::1:'; then
    listen_non_loopback=1
  fi
else
  echo "UNKNOWN - neither ss nor netstat found"
  exit 2
fi

# 2) Query unauthenticated local endpoints.
# If HTTP is disabled locally, try HTTPS.
code_catalog="$(json_get_http_code "http://127.0.0.1:${API_PORT}/v1/catalog/services")"
if [[ "$code_catalog" == "000" ]]; then
  code_catalog="$(json_get_http_code "https://127.0.0.1:${HTTPS_PORT}/v1/catalog/services")"
fi

code_self="$(json_get_http_code "http://127.0.0.1:${API_PORT}/v1/agent/self")"
if [[ "$code_self" == "000" ]]; then
  code_self="$(json_get_http_code "https://127.0.0.1:${HTTPS_PORT}/v1/agent/self")"
fi

# 3) Optional config hints.
ui_enabled="unknown"
client_addr_external="unknown"
default_token_present="unknown"
script_checks_enabled="unknown"

if [[ -d "$CONFIG_DIR" ]]; then
  if grep -RqsE 'ui_config\s*\{[^}]*enabled\s*=\s*true|^\s*-ui\b|^\s*ui\s*=\s*true' "$CONFIG_DIR" 2>/dev/null; then
    ui_enabled="true"
  else
    ui_enabled="false"
  fi

  if grep -RqsE 'client_addr\s*=\s*"(0\.0\.0\.0|[^1][^"]*|{{.*}})"' "$CONFIG_DIR" 2>/dev/null; then
    client_addr_external="true"
  else
    client_addr_external="false"
  fi

  if grep -RqsE 'tokens\s*\{[^}]*default\s*=' "$CONFIG_DIR" 2>/dev/null; then
    default_token_present="true"
  else
    default_token_present="false"
  fi

  if grep -RqsE 'enable_script_checks\s*=\s*true' "$CONFIG_DIR" 2>/dev/null; then
    script_checks_enabled="true"
  else
    script_checks_enabled="false"
  fi
fi

# 4) Decision logic.
if [[ "$listen_non_loopback" -eq 1 && ( "$code_catalog" == "200" || "$code_self" == "200" ) ]]; then
  status="VULNERABLE"
  reason="Consul listens off-loopback and unauthenticated API reads succeeded"
elif [[ "$listen_non_loopback" -eq 0 ]]; then
  status="PATCHED"
  reason="Consul listener appears loopback-only"
elif [[ "$listen_non_loopback" -eq 1 && ( "$code_catalog" == "403" || "$code_self" == "403" ) ]]; then
  status="PATCHED"
  reason="Consul is reachable locally but unauthenticated requests are denied; exposure remains a hardening concern if network-published"
else
  status="UNKNOWN"
  reason="Could not conclusively verify listener exposure and unauthenticated access"
fi

echo "${status} - ${reason} | ui_enabled=${ui_enabled} client_addr_external=${client_addr_external} default_token_present=${default_token_present} script_checks_enabled=${script_checks_enabled} http_catalog=${code_catalog} http_self=${code_self}"

case "$status" in
  VULNERABLE) exit 1 ;;
  PATCHED) exit 0 ;;
  *) exit 2 ;;
esac