Secrets in Depth¶
What You'll Learn¶
- The built-in Secret types and what each one is structured for
- Why base64-encoded data is not encrypted data, and what that means for who can read a Secret
- How to scope RBAC so only the workloads and people that need a Secret can read it
Why This Matters¶
A Secret object looks almost identical to a ConfigMap — same shape, same consumption patterns — which is exactly why it's dangerous to treat it as "ConfigMap but private." A Secret's data field is base64, not ciphertext, and by default anyone with get on secrets in that namespace can read every value in plaintext. Secrets are a convention and an API shape for sensitive data, not a security boundary by themselves — the security boundary is RBAC plus (ideally) encryption at rest and an external secret store layered on top.
Mental Model¶
A Secret is a ConfigMap with two differences: its values are base64-encoded rather than plaintext in the YAML, and
kubectl get/describeredact its values by default. Neither of those is encryption — both are just friction against accidental shoulder-surfing, not a defense against anyone who can query the API.
| Secret type | type: value |
Structure |
|---|---|---|
| Generic / arbitrary | Opaque |
Free-form key-value pairs — the default and most common |
| Docker registry credentials | kubernetes.io/dockerconfigjson |
A .dockerconfigjson key holding registry auth, referenced via imagePullSecrets |
| TLS certificate + key | kubernetes.io/tls |
Exactly tls.crt and tls.key keys |
| Service account token | kubernetes.io/service-account-token |
Auto-created/mounted for pod-to-API-server auth (or requested on demand via the TokenRequest API since 1.22+) |
How It Works¶
Opaque: the general-purpose type¶
apiVersion: v1
kind: Secret
metadata:
name: db-credentials
namespace: production
type: Opaque
data:
username: YWRtaW4= # echo -n 'admin' | base64
password: c3VwZXJzZWNyZXQ= # echo -n 'supersecret' | base64
stringData is often more convenient for authoring — write plaintext, and the API server base64-encodes it for you on write:
apiVersion: v1
kind: Secret
metadata:
name: db-credentials
type: Opaque
stringData:
username: admin
password: supersecret
dockerconfigjson: pulling from a private registry¶
kubectl create secret docker-registry regcred \
--docker-server=registry.example.com \
--docker-username=deploy \
--docker-password="$REGISTRY_TOKEN" \
--docker-email=deploy@example.com
spec:
imagePullSecrets:
- name: regcred
containers:
- name: app
image: registry.example.com/app:2.3.1
kubernetes.io/tls: serving certificates¶
apiVersion: v1
kind: Secret
metadata:
name: app-tls
type: kubernetes.io/tls
data:
tls.crt: <base64 cert>
tls.key: <base64 key>
Consumed as a volume mount by an Ingress controller or the application itself — this is the exact Secret shape cert-manager produces automatically when issuing certificates.
Service account tokens¶
Every pod is automatically issued a projected, time-bound service account token (since 1.22+, via serviceAccountToken in a projected volume, not a long-lived Secret) unless automountServiceAccountToken: false is set. This token authenticates the pod to the API server as its service account and is the backbone of in-cluster RBAC:
Consumption patterns¶
Env var and volume mount both work exactly as they do for ConfigMaps, with one meaningful difference: secret volumes are backed by tmpfs, never written to the node's disk — which is a real reason to prefer volume mounts for Secrets over env vars even beyond the general env-var-leakage concerns from the volumes page.
env:
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: db-credentials
key: password
volumeMounts:
- name: db-creds
mountPath: /etc/secrets/db
readOnly: true
volumes:
- name: db-creds
secret:
secretName: db-credentials
Base64 is encoding, not encryption¶
echo -n 'supersecret' | base64 # c3VwZXJzZWNyZXQ=
echo -n 'c3VwZXJzZWNyZXQ=' | base64 -d # supersecret — trivially reversible
Anyone who can kubectl get secret db-credentials -o jsonpath='{.data.password}' and pipe it through base64 -d has the plaintext. There is no key, no passphrase, nothing cryptographic about it — it's the same transformation that makes binary data safe to embed in YAML/JSON, applied here to sensitive values for no security reason at all. By default, Secret data is also stored unencrypted in etcd — anyone with etcd access (or an etcd backup) has the same plaintext. Turning that off requires enabling encryption at rest explicitly. See Secrets and Encryption at Rest for how to actually close that gap.
RBAC scoping¶
The real access control on a Secret is whatever RBAC rules grant get/list/watch on the secrets resource. Scope tightly, per namespace, per Secret name where possible:
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
namespace: production
name: db-secret-reader
rules:
- apiGroups: [""]
resources: ["secrets"]
resourceNames: ["db-credentials"]
verbs: ["get"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: app-reads-db-secret
namespace: production
subjects:
- kind: ServiceAccount
name: app-sa
namespace: production
roleRef:
kind: Role
name: db-secret-reader
apiGroup: rbac.authorization.k8s.io
Avoid granting blanket get/list on secrets across an entire namespace — list in particular returns every Secret's full content in one call, which is a common way overly broad RBAC turns a single compromised service account into a namespace-wide credential leak.
Common Mistakes¶
- Treating a Secret as encrypted because
kubectl get secretdoesn't print the value by default — it's redaction in the CLI output, not encryption of the underlying data. - Committing a Secret manifest (even base64-encoded) to a plain Git repo — this is the same as committing plaintext credentials.
- Granting
listonsecretscluster-wide or namespace-wide to a service account that only needs one specific Secret. - Forgetting that etcd itself needs encryption at rest configured — Secrets are plaintext in the datastore by default.
Interview Questions¶
- Why is "Secrets are base64-encoded" not a security answer, and what would actually make a Secret's data confidential at rest?
- What's the practical difference between mounting a Secret as an environment variable versus a volume?
- How would you scope RBAC so a service account can read one specific Secret but not enumerate every Secret in the namespace?
See Interview Prep for full answers.
Next¶
Continue to Injecting Config: Env and Volumes to see ConfigMaps and Secrets combined into a real application's configuration.