base64 is an encoding
The problem. Teams put database passwords and API keys into Kubernetes Secrets and think they are protected. They are only encoded. This lesson shows who can really read a Secret and what to do about each path.
What you need to know already: Secrets and base64 (15.33), RBAC and list (17.30), env vars in /proc/PID/environ (2.21), etcd (15.5), tmpfs (11.22).
kubectl create secret generic NAME --from-literal=key=value creates a Secret from the command line (generic = an ordinary key/value Secret):
# an illustration: pay/db-creds and psp-key-v7 (the Secrets mission)
$ kubectl create secret generic db-creds -n pay --from-literal=username=ledger --from-literal=password='s3cr3t-p@ssw0rd'
secret/db-creds created
$ kubectl get secret db-creds -n pay -o yaml
apiVersion: v1
data:
password: czNjcjN0LXBAc3N3MHJk
username: bGVkZ2Vy
kind: Secret
metadata:
name: db-creds
namespace: pay
type: Opaque
$ kubectl get secret db-creds -n pay -o jsonpath='{.data.password}' | base64 -d; echo
s3cr3t-p@ssw0rd
base64 exists so that binary data (TLS keys, keystores) survives JSON and YAML. It has no key; anyone who can read the object reads the secret. kubectl describe secret hides the values (password: 15 bytes) as a courtesy against shoulder-surfing, not as protection - -o yaml is one flag away.
So "who can read a Secret" is the whole question, and the answer has four layers:
- RBAC - who may
get/list/watchsecrets through the API. Rememberlistreturns full objects (17.30), andeditincludes secrets. Also: anyone who can create pods in the namespace can mount any secret in that namespace into a pod and read it - pod creation is secret access. - etcd - by default secrets are stored in etcd in plain text (well, base64 inside protobuf, the binary format etcd stores). An etcd backup file, a stolen disk, or anyone with the etcd client certificate reads them all.
- the node - a mounted secret is a tmpfs file on the node; root on the node reads it.
- the process - an env var is visible in
/proc/PID/environ(as in 2.21), in crash dumps, in "print all env" debug endpoints, and to every child process.
Encryption at rest
The apiserver can encrypt secrets before writing them to etcd. A KMS (Key Management Service) is an external service that holds the encryption key so it never sits on the node's disk; an HSM (hardware security module) is a tamper-proof device that stores keys:
# /etc/kubernetes/enc/encryption-config.yaml, passed as --encryption-provider-config
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources: ["secrets"]
providers:
- kms: # production: an external KMS (a cloud key vault, an HSM) holds the key
apiVersion: v2
name: cloud-kms
endpoint: unix:///var/run/kmsplugin/socket.sock
- aescbc: # or a local key in the file (better than nothing, key on disk)
keys:
- name: key1
secret: <32 random bytes, base64>
- identity: {} # read-only fallback so existing plaintext data stays readable
The first provider encrypts new writes; the others can decrypt. After enabling it, existing secrets are still plaintext until rewritten: kubectl get secrets -A -o json | kubectl replace -f - (read every Secret and write it back unchanged). You verify by reading etcd directly with etcdctl get /registry/secrets/... (etcdctl = etcd's own command-line client): plaintext shows the value, encrypted data starts with k8s:enc:aescbc:v1:key1: or k8s:enc:kms:v2:. Managed cloud clusters offer the same as a setting.
What to do instead, in a bank
- Keep the source of truth outside the cluster: a dedicated secrets manager (a "vault" service - HashiCorp Vault, or your cloud provider's key vault), with its own access policies, audit log, rotation and HSM-backed keys.
- Get secrets into pods with the Secrets Store CSI driver: a CSI volume (the storage-plugin interface from 16.39) that fetches the secret from the vault at pod start, using the pod's own identity, and mounts it as a file - no Secret object needed (optionally synced to one for env vars). Or the External Secrets Operator (an operator = a controller that manages one kind of thing for you), which copies vault entries into Kubernetes Secrets.
Later (Ch 22): Azure Key Vault is the vault you will use, with pods authenticating through workload identity.
- Encrypt etcd with KMS anyway, restrict RBAC on secrets to the workloads that need them, never give humans
list secretsin production, and audit-log secret reads. - Prefer mounted files over env vars: files can be rotated without a restart (the kubelet updates mounted secrets within about a minute; env vars are fixed at container start - chapter 15), and they do not leak through
env. - Never put Secret manifests into a Git repository. If the repository must carry them, encrypt them first (tools such as SOPS or Sealed Secrets) - encrypted in the repository, decrypted only in the cluster.
Two useful Secret features
apiVersion: v1
kind: Secret
metadata:
name: psp-key-v7
immutable: true # cannot be changed, only deleted: no accidental edits,
type: Opaque # and the kubelet stops watching it (less apiserver load)
stringData: # write plain text, the apiserver base64-encodes into data
key: sk_live_4f9a2c77
# an illustration: pay/db-creds and psp-key-v7 (the Secrets mission)
kubectl edit secret psp-key-v7 # change the key, save
error: secrets "psp-key-v7" is invalid
The Secret "psp-key-v7" is invalid: data: Forbidden: field is immutable when `immutable` is set
Rotation with immutable secrets = create psp-key-v8, point the Deployment at it (a rollout), delete v7. Every change is visible and reversible.
What you can now do
- Decode any Secret you can read, and list the four ways a secret leaks (RBAC, etcd, node, process).
- Explain encryption at rest and why an external secrets manager is the real answer.
- Rotate a secret by name with
immutable: true.