Secrets are Kubernetes' mechanism for handling sensitive data — passwords, tokens, TLS certificates, SSH keys. They look similar to ConfigMaps but have important security considerations that you must understand for both production and the CKS exam.
1. Secret Types
| Type | Use Case | Required Keys |
|---|---|---|
Opaque (default) | Arbitrary key-value data | None |
kubernetes.io/tls | TLS certificates | tls.crt, tls.key |
kubernetes.io/dockerconfigjson | Private registry credentials | .dockerconfigjson |
kubernetes.io/basic-auth | Basic authentication | username, password |
kubernetes.io/ssh-auth | SSH private key | ssh-privatekey |
kubernetes.io/service-account-token | ServiceAccount token (legacy) | Auto-generated |
bootstrap.kubernetes.io/token | Bootstrap tokens (kubeadm join) | token-id, token-secret |
Creating Secrets
# Opaque (generic): kubectl create secret generic db-creds \ --from-literal=username=admin \ --from-literal=password='S3cr3t!Pass' # TLS: kubectl create secret tls web-tls \ --cert=./tls.crt \ --key=./tls.key # Docker registry: kubectl create secret docker-registry regcred \ --docker-server=registry.example.com \ --docker-username=user \ --docker-password=pass \ --docker-email=user@example.com
Declarative YAML
apiVersion: v1 kind: Secret metadata: name: db-creds type: Opaque data: # ← base64 ENCODED (not encrypted!) username: YWRtaW4= # echo -n "admin" | base64 password: UzNjcjN0IVBhc3M= # echo -n "S3cr3t!Pass" | base64 # Alternative: use stringData (plain text, converted to base64 on create): stringData: # ← plain text (convenience) username: admin password: S3cr3t!Pass
data values are base64 encoded, NOT encrypted. Anyone who can read the Secret object can decode the values: echo "YWRtaW4=" | base64 -d → admin. Base64 is encoding, not security. Real security comes from encryption at rest and RBAC (covered later).
2. Consuming Secrets in Pods
Same methods as ConfigMaps — env vars or volume mounts — with one key difference in how volumes work.
As Environment Variables
spec:
containers:
- name: app
image: myapp:latest
env:
- name: DB_USERNAME
valueFrom:
secretKeyRef:
name: db-creds
key: username
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: db-creds
key: password
optional: false # Pod fails to start if Secret missing
As a Volume Mount
spec:
containers:
- name: app
image: myapp:latest
volumeMounts:
- name: tls-certs
mountPath: /etc/tls
readOnly: true # ← always set for secrets
volumes:
- name: tls-certs
secret:
secretName: web-tls
defaultMode: 0400 # ← restrictive file permissions
# Result:
# /etc/tls/tls.crt (0400, readable only by container user)
# /etc/tls/tls.key (0400)
Key Differences from ConfigMap Volumes
| Feature | ConfigMap Volume | Secret Volume |
|---|---|---|
| Storage medium | Node disk | tmpfs (RAM) — never written to disk |
| Default file mode | 0644 | 0644 (set 0400 manually!) |
| Auto-update | Yes (~60s) | Yes (~60s) |
Image Pull Secrets
# Reference a docker-registry secret for pulling private images:
spec:
imagePullSecrets:
- name: regcred
containers:
- name: app
image: registry.example.com/myapp:latest
# Can also be attached to a ServiceAccount (applies to all Pods using it):
kubectl patch serviceaccount default -p \
'{"imagePullSecrets": [{"name": "regcred"}]}'
3. Security Considerations
Where Secrets Are Exposed
| Location | Risk | Mitigation |
|---|---|---|
| etcd (at rest) | Anyone with etcd access reads all Secrets | Encryption at rest (EncryptionConfiguration) |
| API server (in transit) | Network snooping | TLS (always on by default) |
| Node (kubelet) | Only Secrets needed by Pods on that node are sent | RBAC: limit who can create Pods in a namespace |
| Container environment | /proc/1/environ exposes env vars | Prefer volume mounts over env vars for secrets |
| YAML manifests in Git | Base64 is not encryption — readable by anyone with repo access | Sealed Secrets, SOPS, External Secrets Operator |
| kubectl get secret -o yaml | Anyone with RBAC read access can decode | Restrict get verbs on Secrets to minimal roles |
data: password: UzNjcjN0 in a repo is trivially decoded. Never store Secret manifests in version control without encryption (Sealed Secrets, SOPS, or external secrets management).
Env Vars vs Volume Mounts for Secrets
# Env vars are visible via: kubectl exec pod -- env | grep PASSWORD # and in /proc/1/environ on the node (if compromised) # and in crash dumps, logging libraries that dump env # Volume mounts: # - Stored in tmpfs (RAM only) # - Can set restrictive file permissions (0400) # - Not visible in process environment # - Not accidentally logged
RBAC for Secrets
# Principle of least privilege:
# Most users should NOT have "get" access to Secrets
# Use separate Role for teams that need Secret access:
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: secret-reader
namespace: app-team
rules:
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get"] # Only get, not list (list exposes all!)
resourceNames: ["db-creds"] # Only specific secrets
list verb on Secrets is more dangerous than get. With get, you need to know the Secret name. With list, you see ALL Secrets in the namespace. CKS exam: always prefer minimal RBAC — grant get on specific resourceNames rather than broad access.
4. Encryption at Rest
By default, Secrets are stored unencrypted in etcd (just base64). To encrypt them at rest, configure the API server with an EncryptionConfiguration:
# /etc/kubernetes/enc/encryption-config.yaml
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources:
- secrets
providers:
- aescbc: # ← encryption provider
keys:
- name: key1
secret: c2VjcmV0LWtleS0xMjM0NTY3ODkwMTIzNA== # 32-byte base64
- identity: {} # ← fallback: read unencrypted (for migration)
# Enable on API server (static pod manifest):
# /etc/kubernetes/manifests/kube-apiserver.yaml
spec:
containers:
- command:
- kube-apiserver
- --encryption-provider-config=/etc/kubernetes/enc/encryption-config.yaml
volumeMounts:
- name: enc-config
mountPath: /etc/kubernetes/enc
readOnly: true
volumes:
- name: enc-config
hostPath:
path: /etc/kubernetes/enc
Encryption Providers
| Provider | Security | Use Case |
|---|---|---|
identity | None (plain text) | Default — no encryption |
aescbc | AES-CBC with PKCS#7 padding | Common, but key is stored on disk |
aesgcm | AES-GCM (must rotate keys frequently) | Faster, but nonce reuse risk if key not rotated |
secretbox | XSalsa20 + Poly1305 | Strong, recommended for new setups |
kms v2 | Envelope encryption with external KMS | Best — key never on disk (AWS KMS, GCP KMS, Azure Key Vault) |
After Enabling Encryption
# Existing Secrets are still unencrypted! Re-write them: kubectl get secrets --all-namespaces -o json | \ kubectl replace -f - # This reads and re-writes every Secret, encrypting it with the new provider # Verify encryption: etcdctl get /registry/secrets/default/db-creds | hexdump -C # Should show encrypted bytes (k8s:enc:aescbc:...) not plain text
--encryption-provider-config flag to API server, (3) re-write existing Secrets to encrypt them, (4) verify with etcdctl.
Summary
| Concept | Key Point |
|---|---|
| Secret types | Opaque, TLS, docker-registry, basic-auth, ssh-auth, SA token |
| Encoding | base64 in data: field — NOT encryption, just encoding |
stringData: | Convenience — plain text in YAML, converted to base64 on creation |
| tmpfs volumes | Secret volumes use RAM-backed tmpfs — never written to disk |
| Env vars risk | Visible in /proc, logs, crash dumps — prefer volume mounts |
| Encryption at rest | EncryptionConfiguration + API server flag — encrypt Secrets in etcd |
| KMS envelope | Best practice — DEK encrypted by external KMS, key never on disk |
| RBAC | Restrict get/list on Secrets; use resourceNames for precision |
| Git safety | Never commit Secret YAML to Git — use Sealed Secrets, SOPS, or ESO |
📝 Quiz: Secrets
Q1: You see data: password: cGFzc3dvcmQxMjM= in a Secret YAML. Is this secure?
echo "cGFzc3dvcmQxMjM=" | base64 -d → password123. Base64 provides zero security — it's just a transport encoding for binary-safe storage.Q2: What happens to Secret data when mounted as a volume? Where does it physically reside on the node?
Q3: Why should you prefer mounting Secrets as files rather than using environment variables?
/proc/[pid]/environ on the node, (2) logging libraries that dump environment, (3) crash reports, (4) child processes inherit them. File-mounted Secrets have restrictive permissions, aren't in the process environment, and aren't accidentally logged.Q4: You enabled encryption at rest with aescbc. Are existing Secrets now encrypted?
kubectl get secrets --all-namespaces -o json | kubectl replace -f -. This reads each Secret (through API server) and writes it back (now encrypted).Q5: A user has RBAC permission to create Pods in a namespace. Can they read Secrets in that namespace?
Q6: What's the difference between data: and stringData: in a Secret manifest?
data: values must be base64 encoded by you. stringData: accepts plain text — Kubernetes base64-encodes it automatically on creation. stringData is write-only (never returned by the API — kubectl get secret -o yaml always shows data: with base64). Use stringData for convenience in manifests.