External Secrets Operator (ESO) synchronizes secrets from external backends (AWS Secrets Manager, HashiCorp Vault, GCP Secret Manager, Azure Key Vault) into Kubernetes Secrets. The operator holds provider credentials with access to all synchronized secrets — a misconfigured SecretStore grants any namespace access to secrets intended for another team, a ClusterSecretStore without namespace restriction becomes a cluster-wide secret vending machine, and ExternalSecret field path expressions can be abused to extract secrets beyond their intended scope. This guide covers systematic ESO security assessment.
# Enumerate all SecretStores and ClusterSecretStores
kubectl get secretstores --all-namespaces -o json | python3 -c "
import json,sys
stores = json.load(sys.stdin)['items']
for s in stores:
meta = s['metadata']
spec = s.get('spec',{})
provider = spec.get('provider',{})
# Identify provider type
provider_name = list(provider.keys())[0] if provider else 'unknown'
secret_ref = provider.get(provider_name,{}).get('auth',{}).get('secretRef',{}) \
or provider.get(provider_name,{}).get('auth',{}).get('jwt',{}).get('serviceAccountRef',{})
print(f\"SecretStore: {meta['namespace']}/{meta['name']} provider={provider_name}\")
"
kubectl get clustersecretstores -o json | python3 -c "
import json,sys
stores = json.load(sys.stdin)['items']
for s in stores:
meta = s['metadata']
spec = s.get('spec',{})
conditions = spec.get('conditions',[])
print(f\"ClusterSecretStore: {meta['name']}\")
# ClusterSecretStore: accessible from ANY namespace by default
# Check if namespace conditions are configured
for c in conditions:
print(f\" Namespace condition: {c}\")
if not conditions:
print(' [!] NO namespace conditions — accessible from ALL namespaces')
"
# ESO SecretStore spec references a Kubernetes secret containing provider credentials
# These secrets contain AWS access keys, Vault tokens, or service account JSON
# Find credential secrets referenced by SecretStores
kubectl get secretstores --all-namespaces -o json | python3 -c "
import json,sys
stores = json.load(sys.stdin)['items']
for s in stores:
meta = s['metadata']
spec = s.get('spec',{}).get('provider',{})
# Dig through provider-specific auth patterns
def find_secret_refs(obj, path=''):
if isinstance(obj, dict):
if 'secretRef' in obj:
sr = obj['secretRef']
print(f\" Credential secret: {sr.get('namespace',meta['namespace'])}/{sr.get('name','?')} key: {sr.get('key','?')}\")
for k,v in obj.items():
find_secret_refs(v, path+'.'+k)
elif isinstance(obj, list):
for item in obj:
find_secret_refs(item, path)
print(f\"SecretStore: {meta['namespace']}/{meta['name']}\")
find_secret_refs(spec)
"
# Extract provider credentials (example: AWS Secrets Manager)
kubectl get secret aws-credentials -n external-secrets -o json | python3 -c "
import json,sys,base64
secret = json.load(sys.stdin)
for k,v in secret.get('data',{}).items():
print(f'{k}: {base64.b64decode(v).decode()}')
"
# access-key-id: AKIA... (AWS access key ID)
# secret-access-key: (AWS secret access key)
# With these: aws secretsmanager list-secrets → enumerate ALL secrets the key can access
# ExternalSecret defines WHICH secrets to sync and what Kubernetes secret to create
# The remoteRef.key and remoteRef.property fields are user-controllable
# Test: can a user in namespace A request a secret scoped to namespace B?
# List ExternalSecrets and check what they're pulling
kubectl get externalsecrets --all-namespaces -o json | python3 -c "
import json,sys
es_list = json.load(sys.stdin)['items']
for es in es_list:
meta = es['metadata']
spec = es.get('spec',{})
store_ref = spec.get('secretStoreRef',{})
data = spec.get('data',[]) + spec.get('dataFrom',[])
print(f\"ExternalSecret: {meta['namespace']}/{meta['name']}\")
print(f\" Store: {store_ref.get('kind','SecretStore')}/{store_ref.get('name','?')}\")
for d in data:
remote = d.get('remoteRef',{}) or d.get('extract',{}) or {}
print(f\" Remote key: {remote.get('key','?')} property: {remote.get('property','ALL')}\")
"
# Test: cross-namespace secret access via ClusterSecretStore
# If developer namespace can reference a ClusterSecretStore:
kubectl apply -f - <
# ClusterSecretStore is cluster-scoped, accessible from any namespace by default
# ESO v0.8+ added namespace conditions to restrict access — test whether they're configured
# Check ClusterSecretStore conditions
kubectl get clustersecretstores -o json | python3 -c "
import json,sys
stores = json.load(sys.stdin)['items']
for s in stores:
meta = s['metadata']
spec = s.get('spec',{})
conditions = spec.get('conditions',[])
if not conditions:
print(f'[CRITICAL] ClusterSecretStore {meta[\"name\"]}: NO namespace isolation')
print(' Any namespace can create ExternalSecrets referencing this store')
else:
for c in conditions:
ns_regex = c.get('namespaces',{}).get('matchRegex','*')
print(f'{meta[\"name\"]}: accessible from namespaces matching: {ns_regex}')
"
# Test: from a developer namespace, can you use a ClusterSecretStore?
kubectl auth can-i create externalsecrets \
--as=system:serviceaccount:developer:default \
-n developer
# If yes and ClusterSecretStore has no namespace conditions:
# → Developer can pull any secret from the backend that the ESO credential can access
# Audit: which ExternalSecrets are using ClusterSecretStore cross-namespace
kubectl get externalsecrets --all-namespaces -o json | python3 -c "
import json,sys
es_list = json.load(sys.stdin)['items']
for es in es_list:
meta = es['metadata']
store = es.get('spec',{}).get('secretStoreRef',{})
if store.get('kind','') == 'ClusterSecretStore':
print(f\"{meta['namespace']}/{meta['name']} uses ClusterSecretStore: {store.get('name','?')}\")
"
| Security Test | Method | Risk |
|---|---|---|
| ClusterSecretStore without namespace conditions | kubectl get clustersecretstores — check conditions | Critical |
| Developer can create ExternalSecrets against prod store | Create ExternalSecret with ClusterSecretStore ref | Critical |
| Provider credential secret accessible by unauthorized SA | kubectl auth can-i get secrets -n external-secrets | High |
| dataFrom extract with broad path prefix | ExternalSecret with extract.key=prod/ | High |
| Backend IAM credential has list-all-secrets permission | AWS: secretsmanager:ListSecrets policy check | High |
| PushSecret enabled (Kubernetes → backend write) | kubectl get pushsecrets --all-namespaces | Medium |
Ironimo tests ESO deployments for ClusterSecretStore namespace isolation gaps, SecretStore credential exposure, ExternalSecret field path abuse, backend IAM over-privilege, and PushSecret exfiltration paths — ensuring your Kubernetes secret synchronization doesn't become a cluster-wide credential vending machine.
Start free scan