CKS — Kubernetes Secrets Deep Dive
Kubernetes Secrets Deep Dive
CKS Exam Domain: Minimize Microservice Vulnerabilities (20%)
Kubernetes Secrets are the native mechanism for storing sensitive data — passwords, tokens, keys, certificates. But “native” does not mean “secure by default.” Secrets are base64-encoded, not encrypted, and stored in etcd as plaintext. Understanding exactly how Secrets work — and where they fail — is essential for the CKS exam.
1. How Secrets Are Stored in etcd
When you create a Secret, the API server writes it to etcd. By default, the data is stored base64-encoded — which is an encoding, not encryption. Anyone with direct etcd access can decode every Secret in the cluster.
kubectl create secret → API Server → etcd
│
▼
/registry/secrets/<namespace>/<name>
│
├── key: "password"
└── value: "cGFzc3dvcmQxMjM=" ← base64 of "password123"
Base64 is trivially reversible:
echo "cGFzc3dvcmQxMjM=" | base64 -d
# password123
Security Impact: Without encryption at rest, an attacker who gains access to etcd backups, etcd data directories, or etcd API endpoints has access to every Secret in your cluster.
2. Secret Types
Kubernetes defines several built-in Secret types. Each type enforces specific data key requirements.
| Type | Description | Required Keys | Use Case |
|---|---|---|---|
Opaque |
Default type, arbitrary key-value pairs | None (any keys) | Generic passwords, API keys |
kubernetes.io/dockerconfigjson |
Docker registry credentials | .dockerconfigjson |
Image pull authentication |
kubernetes.io/tls |
TLS certificate and key pair | tls.crt, tls.key |
Ingress TLS, mutual TLS |
kubernetes.io/basic-auth |
Basic authentication credentials | username, password |
HTTP basic auth |
kubernetes.io/ssh-auth |
SSH private key | ssh-privatekey |
Git operations, SSH tunnels |
kubernetes.io/service-account-token |
ServiceAccount token (legacy) | Auto-generated | API authentication |
Creating Different Types
# Opaque (default)
kubectl create secret generic db-creds \
--from-literal=username=admin \
--from-literal=password=s3cur3P@ss
# Docker registry
kubectl create secret docker-registry reg-cred \
--docker-server=registry.example.com \
--docker-username=admin \
--docker-password=dckr_pat_xxxx \
--docker-email=admin@example.com
# TLS
kubectl create secret tls app-tls \
--cert=tls.crt \
--key=tls.key
# From file
kubectl create secret generic ssh-key \
--from-file=ssh-privatekey=/path/to/id_rsa
3. Consuming Secrets — Environment Variables vs Volume Mounts
There are two primary ways Pods consume Secrets. They are not equivalent from a security perspective.
Option A: Environment Variables
apiVersion: v1
kind: Pod
metadata:
name: env-secret-pod
spec:
containers:
- name: app
image: nginx:1.25
env:
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: db-creds
key: password
Option B: Volume Mounts
apiVersion: v1
kind: Pod
metadata:
name: vol-secret-pod
spec:
containers:
- name: app
image: nginx:1.25
volumeMounts:
- name: secret-vol
mountPath: /etc/secrets
readOnly: true
volumes:
- name: secret-vol
secret:
secretName: db-creds
Security Comparison
| Factor | Environment Variables | Volume Mounts |
|---|---|---|
Visibility in kubectl describe |
Yes — shown in Pod spec | No — only mount path shown |
| Exposure in crash dumps | Yes — env is part of process info | No — files on disk |
| Exposure in application logs | High risk — logging frameworks dump env | Lower risk — must explicitly read file |
| Available to child processes | Yes — env inherited by all children | No — must explicitly read files |
| Update without Pod restart | No | Yes — kubelet refreshes mounted Secrets |
Usable by envFrom (bulk inject) |
Yes — but unsafe, injects ALL keys | N/A |
CKS Best Practice: Always prefer volume mounts over environment variables for Secret consumption. Environment variables leak in too many ways.
4. Immutable Secrets
Kubernetes supports marking Secrets as immutable (stable since v1.21). Once set, the Secret cannot be updated — only deleted and recreated.
apiVersion: v1
kind: Secret
metadata:
name: db-creds
namespace: production
type: Opaque
data:
username: YWRtaW4= # admin
password: czNjdXIzUEBzcw== # s3cur3P@ss
immutable: true
Benefits
- Prevents accidental or malicious modification of critical credentials
- Reduces etcd watch load — the API server stops tracking changes for immutable Secrets
- Improves cluster performance at scale — fewer watches = less etcd CPU
Limitations
- To rotate an immutable Secret, you must delete it and create a new one
- Pods referencing the Secret must be restarted to pick up the new version
- Cannot be “un-immutabled” — the field is one-way
# Trying to update an immutable secret fails:
kubectl edit secret db-creds
# Error: secret "db-creds" is immutable
# To rotate: delete and recreate
kubectl delete secret db-creds
kubectl create secret generic db-creds \
--from-literal=username=admin \
--from-literal=password=n3wP@ssw0rd
5. Secret Risks — Attack Vectors
Risk 1: etcd Direct Access
# If an attacker has etcd credentials, they can read all secrets:
ETCDCTL_API=3 etcdctl get /registry/secrets/default/db-creds \
--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
# Output contains base64-encoded secret data in plaintext
Mitigation: Enable encryption at rest (next topic in this chapter).
Risk 2: Overpermissioned RBAC
# Check who can list all secrets in a namespace:
kubectl auth can-i list secrets --as=system:serviceaccount:default:default -n default
# yes ← DANGEROUS — this SA can read every secret in the namespace
# Check who can get secrets cluster-wide:
kubectl auth can-i get secrets --all-namespaces --as=developer
Mitigation: Restrict RBAC — never grant list or watch on secrets unless absolutely required.
Risk 3: Environment Variable Exposure
# Anyone who can describe a pod sees env-injected secrets:
kubectl describe pod env-secret-pod
# ...
# Environment:
# DB_PASSWORD: <set to the key 'password' in secret 'db-creds'>
While kubectl describe doesn’t show the value directly, kubectl get pod -o yaml does:
kubectl get pod env-secret-pod -o jsonpath='{.spec.containers[0].env}'
# Shows the secretKeyRef — and anyone with get-pods + get-secrets can read the actual value
Mitigation: Use volume mounts. Restrict Pod get permissions to trusted identities.
Risk 4: Logging and Debug Endpoints
Applications that log all environment variables will dump secrets:
# Dangerous Python code:
import os
print(os.environ) # Logs DB_PASSWORD to stdout
# Dangerous shell pattern:
env | tee /tmp/debug.log # Writes all env vars including secrets
Mitigation: Never log environment variables. Use volume-mounted secrets and read files directly.
Risk 5: etcd Backups
etcd snapshots contain all Secrets in plaintext (unless encryption at rest is enabled). A stolen backup = a stolen credential store.
# etcd snapshot contains everything:
ETCDCTL_API=3 etcdctl snapshot save /tmp/etcd-backup.db
# /tmp/etcd-backup.db contains ALL secrets in base64
Mitigation: Encrypt etcd backups. Enable encryption at rest. Restrict access to backup storage.
6. Creating Secrets — Exam Commands
These are the commands you need to know by heart for the CKS exam:
# Create from literals
kubectl create secret generic my-secret \
--from-literal=key1=value1 \
--from-literal=key2=value2
# Create from file (key name = filename by default)
kubectl create secret generic my-secret \
--from-file=config.json
# Create from file with custom key name
kubectl create secret generic my-secret \
--from-file=app-config=config.json
# Create in a specific namespace
kubectl create secret generic my-secret \
--from-literal=password=secret123 \
-n production
# Create from env file
kubectl create secret generic my-secret \
--from-env-file=secrets.env
# Dry run + YAML output (for manifests)
kubectl create secret generic my-secret \
--from-literal=password=secret123 \
--dry-run=client -o yaml > secret.yaml
7. Viewing and Decoding Secrets
# List secrets in a namespace
kubectl get secrets -n default
# View secret metadata (does NOT show data)
kubectl describe secret db-creds
# View base64-encoded data
kubectl get secret db-creds -o yaml
# Decode a specific key using jsonpath
kubectl get secret db-creds -o jsonpath='{.data.password}' | base64 -d
# s3cur3P@ss
# Decode all keys
kubectl get secret db-creds -o json | jq -r '.data | to_entries[] | "\(.key): \(.value | @base64d)"'
# username: admin
# password: s3cur3P@ss
# Quick one-liner for a single field
kubectl get secret db-creds -o go-template='{{ .data.password | base64decode }}'
8. ServiceAccount Token Secrets
Prior to Kubernetes v1.24, creating a ServiceAccount automatically generated a long-lived Secret containing a JWT token. This was a security risk — the token never expired.
Legacy Behavior (pre-1.24)
# Auto-created secret:
apiVersion: v1
kind: Secret
metadata:
name: my-sa-token-xxxxx
annotations:
kubernetes.io/service-account.name: my-sa
type: kubernetes.io/service-account-token
data:
token: <base64-encoded JWT>
ca.crt: <base64-encoded CA>
namespace: <base64-encoded namespace>
Current Behavior (v1.24+)
- Tokens are no longer auto-created as Secrets
- The TokenRequest API issues short-lived, audience-bound tokens
- Tokens are projected into Pods via the
projectedvolume (default) - Long-lived tokens can still be created manually if needed (but avoid this)
# Create a time-limited token (preferred):
kubectl create token my-sa --duration=1h
# Create a legacy long-lived token (avoid in production):
kubectl apply -f - <<EOF
apiVersion: v1
kind: Secret
metadata:
name: my-sa-token
annotations:
kubernetes.io/service-account.name: my-sa
type: kubernetes.io/service-account-token
EOF
CKS Exam Tip: Know that projected service account tokens (short-lived, audience-bound) are the secure default since v1.24. Legacy Secrets-based tokens are a security risk.
Summary
| Aspect | Key Takeaway |
|---|---|
| Storage | Secrets are base64-encoded in etcd — not encrypted by default |
| Consumption | Prefer volume mounts over environment variables |
| Immutability | Use immutable: true for critical secrets |
| RBAC | Never grant list/watch on secrets to unnecessary identities |
| SA Tokens | Use TokenRequest API (v1.24+) — avoid long-lived token Secrets |
| Encryption | Enable encryption at rest (next topic) |
Practice Questions
Question 1
What encoding format does Kubernetes use to store Secret values in etcd by default?
Show Answer
Base64 encoding. Kubernetes does NOT encrypt Secrets by default. The values in .data fields are base64-encoded, which is trivially reversible:
echo "cGFzc3dvcmQxMjM=" | base64 -d
# password123
This means anyone with etcd access can read all Secrets. To actually encrypt Secrets, you must configure encryption at rest via EncryptionConfiguration.
Question 2
Why are volume-mounted Secrets preferred over environment-variable Secrets from a security perspective?
Show Answer
Volume-mounted Secrets are preferred because:
- Environment variables leak — they appear in
kubectl describe pod, crash dumps, child process environments, and logging framework output. - Volume mounts are opaque — only the mount path is visible in the Pod spec, not the values.
- Volume mounts auto-update — when a Secret changes, the kubelet refreshes the mounted files (with a delay). Environment variables require a Pod restart.
- Environment variables propagate to child processes — any subprocess inherits the parent environment, including secrets.
# Preferred:
volumeMounts:
- name: secret-vol
mountPath: /etc/secrets
readOnly: true
# Avoid:
env:
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: db-creds
key: password
Question 3
What does immutable: true do on a Secret, and what are its benefits?
Show Answer
Setting immutable: true on a Secret means:
- The Secret cannot be updated after creation (only deleted and recreated)
- The API server stops watching the Secret for changes, reducing etcd load
- This protects against accidental or malicious modification of credentials
apiVersion: v1
kind: Secret
metadata:
name: critical-creds
type: Opaque
data:
api-key: c3VwZXJzZWNyZXQ=
immutable: true
To rotate an immutable Secret, you must delete it and create a new one, then restart Pods that reference it.
Question 4
A team grants their CI/CD ServiceAccount list and watch permissions on Secrets in the production namespace. Why is this dangerous?
Show Answer
list and watch on Secrets means the ServiceAccount can read every Secret in the namespace, not just specific ones. There is no way to scope list/watch to individual Secret names in RBAC.
# DANGEROUS — grants access to ALL secrets in production:
rules:
- apiGroups: [""]
resources: ["secrets"]
verbs: ["list", "watch"]
If the CI/CD pipeline or its Pod is compromised, the attacker gets every credential in production.
Better approach: Grant get on specific Secret resourceNames:
rules:
- apiGroups: [""]
resources: ["secrets"]
resourceNames: ["ci-deploy-token"]
verbs: ["get"]
Question 5
How do projected ServiceAccount tokens (v1.24+) improve security over legacy Secret-based SA tokens?
Show Answer
Projected tokens (via the TokenRequest API) are more secure because:
- Time-limited — they expire (default: 1 hour, configurable). Legacy tokens never expire.
- Audience-bound — they specify which API server they’re valid for. Legacy tokens work against any API server.
- Not stored as Secrets — they’re generated on-demand and projected into the Pod. No Secret object exists in etcd.
- Auto-rotated — the kubelet refreshes the token before it expires.
# Short-lived token (preferred):
kubectl create token my-sa --duration=1h
# Legacy token (avoid):
# Creates a persistent Secret in etcd that never expires
For the CKS exam, know that v1.24+ uses the TokenRequest API by default, and creating long-lived SA token Secrets is a security anti-pattern.
| Previous | Up | Next |
|---|---|---|
| Secrets Management | Secrets Management | Encryption at Rest |