Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Assess the security posture of Kubernetes etcd clusters by evaluating encryption at rest, TLS configuration, access controls, backup encryption, and network isolation.
.claude/skills/performing-kubernetes-etcd-security-assessment/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-03 | ✗→✓ | ▲ Improved | — | — |
| case-16 | ✓→✓ | = Same ✓ | — | — |
| case-04 | ✗→✗ | = Same ✗ | — | — |
| case-09 | ✗→✗ | = Same ✗ | — | — |
| case-11 | ✗→✗ | = Same ✗ | — | — |
etcd is the distributed key-value store that serves as Kubernetes' backing store for all cluster data, including Secrets, RBAC policies, ConfigMaps, and workload configurations. Without proper hardening, etcd exposes all cluster secrets in plaintext, making it the highest-value target for attackers who gain control plane access. A comprehensive security assessment covers encryption at rest, TLS for transport, access control, backup security, and network isolation.
Verify that Kubernetes encrypts Secret data stored in etcd:
bash# Check if EncryptionConfiguration is configured on API server ps aux | grep kube-apiserver | grep encryption-provider-config # View the encryption configuration cat /etc/kubernetes/enc/encryption-config.yaml
Expected secure configuration:
yamlapiVersion: apiserver.config.k8s.io/v1 kind: EncryptionConfiguration resources: - resources: - secrets - configmaps providers: - aescbc: keys: - name: key1 secret: <base64-encoded-32-byte-key> - identity: {} # Fallback for reading unencrypted data
Verify secrets are actually encrypted in etcd:
bash# Read a secret directly from etcd ETCDCTL_API=3 etcdctl \ --endpoints=https://127.0.0.1:2379 \ --cacert=/etc/kubernetes/pki/etcd/ca.crt \ --cert=/etc/kubernetes/pki/etcd/server.crt \ --key=/etc/kubernetes/pki/etcd/server.key \ get /registry/secrets/default/my-secret | hexdump -C | head -20 # If encrypted, output starts with "k8s:enc:aescbc:v1:key1" # If NOT encrypted, you'll see plaintext key-value pairs
bash# Verify etcd uses TLS for client connections ETCDCTL_API=3 etcdctl endpoint health \ --endpoints=https://127.0.0.1:2379 \ --cacert=/etc/kubernetes/pki/etcd/ca.crt \ --cert=/etc/kubernetes/pki/etcd/server.crt \ --key=/etc/kubernetes/pki/etcd/server.key # Check peer TLS configuration ps aux | grep etcd | tr ' ' '\n' | grep -E "peer-cert|peer-key|peer-trusted-ca" # Verify certificate expiration openssl x509 -in /etc/kubernetes/pki/etcd/server.crt -noout -enddate openssl x509 -in /etc/kubernetes/pki/etcd/peer.crt -noout -enddate
Expected flags:
| Flag | Required Value | Purpose | |------|---------------|---------| | --cert-file | Path to server cert | Client-to-server TLS | | --key-file | Path to server key | Client-to-server TLS | | --trusted-ca-file | Path to CA cert | Client certificate validation | | --peer-cert-file | Path to peer cert | Peer-to-peer TLS | | --peer-key-file | Path to peer key | Peer-to-peer TLS | | --peer-trusted-ca-file | Path to peer CA | Peer certificate validation | | --client-cert-auth | true | Require client certificates | | --peer-client-cert-auth | true | Require peer certificates |
bash# Verify etcd is not exposed on all interfaces ps aux | grep etcd | tr ' ' '\n' | grep listen-client-urls # Should be: https://127.0.0.1:2379 (not 0.0.0.0) # Check who can access etcd certificates ls -la /etc/kubernetes/pki/etcd/ # Should be readable only by root/etcd user # Verify API server is the only etcd client ss -tlnp | grep 2379 # Only kube-apiserver should have connections
bash# Create an encrypted etcd backup ETCDCTL_API=3 etcdctl snapshot save /backup/etcd-snapshot.db \ --endpoints=https://127.0.0.1:2379 \ --cacert=/etc/kubernetes/pki/etcd/ca.crt \ --cert=/etc/kubernetes/pki/etcd/server.crt \ --key=/etc/kubernetes/pki/etcd/server.key # Encrypt the backup file gpg --symmetric --cipher-algo AES256 /backup/etcd-snapshot.db # Verify backup integrity ETCDCTL_API=3 etcdctl snapshot status /backup/etcd-snapshot.db --write-out=table
bash# Verify etcd ports are firewalled iptables -L -n | grep -E "2379|2380" # Check if etcd is accessible from worker nodes (should NOT be) # Run from a worker node: curl -k https://<control-plane-ip>:2379/health # Should be rejected/timeout
| CIS Control | Check | Expected Result | |-------------|-------|----------------| | 2.1 | etcd cert-file set | TLS certificate configured | | 2.2 | etcd client-cert-auth | Client certificate authentication enabled | | 2.3 | etcd auto-tls disabled | auto-tls=false | | 2.4 | etcd peer cert-file set | Peer TLS configured | | 2.5 | etcd peer client-cert-auth | Peer authentication enabled | | 2.6 | etcd peer auto-tls disabled | peer-auto-tls=false | | 2.7 | etcd unique CA | Separate CA for etcd (not shared with cluster) |
bash# 1. Generate new encryption key NEW_KEY=$(head -c 32 /dev/urandom | base64) # 2. Update EncryptionConfiguration with new key first cat > /etc/kubernetes/enc/encryption-config.yaml <<EOF apiVersion: apiserver.config.k8s.io/v1 kind: EncryptionConfiguration resources: - resources: - secrets providers: - aescbc: keys: - name: key2 secret: ${NEW_KEY} - name: key1 secret: <old-key> - identity: {} EOF # 3. Restart API server to pick up new config # 4. Re-encrypt all secrets with new key kubectl get secrets --all-namespaces -o json | \ kubectl replace -f - # 5. Remove old key from EncryptionConfiguration # 6. Restart API server again
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-04 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-16 | pass→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-09 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-03 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-11 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-23 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-07 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-21 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-19 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-10 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-18 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-05 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-02 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-06 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-20 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-14 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-13 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-15 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-01 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-22 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-12 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-08 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-17 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 23 cases were attempted. The headline lift of +4 percentage points is the difference between those two pass rates over the 23 comparable cases.
The per-case answers from this run were removed by the retention sweep, so the case table below shows the verdicts without the text either arm produced. The counts above were recorded at the time and are unaffected. Answers are now kept for 180 days.
Other measured skills in the registry, with their headline benchmark lift.