A review comment that says “it is in a Kubernetes Secret, so it is safe” should slow the deployment down. A Secret is the right Kubernetes object for credentials, tokens, and private keys, but the object name does not make the value encrypted, short-lived, or hard to reach.
Kubernetes Secrets need an encryption and access-control plan before they can be trusted for application credentials. The official Kubernetes Secret documentation says Secrets hold small amounts of sensitive data and reduce accidental exposure compared with putting those values directly in Pod specs or images. The same page also warns that Secrets are stored unencrypted in the API server backing store by default, and that anyone who can create a Pod in a namespace can use that path to read any Secret in that namespace.
If you came from Docker Compose, the shape is familiar: move credentials out of broad environment blocks and grant them only to the workload that needs them. The Orthogonal guide on Docker Compose secrets versus environment variables covers that smaller-stack version of the same problem. Kubernetes gives you more controls, but it also gives you more ways to grant accidental access.
On this page
Base64 is not the security boundary#
A Kubernetes Secret value is commonly stored in the object’s data field as base64. That encoding helps the API carry arbitrary bytes, but it is not encryption. The official Kubernetes Secrets good practices page says Secret values are base64 encoded and stored unencrypted by default unless at-rest encryption is configured. It also gives the direct warning that base64 provides no added confidentiality over plain text.
That means a Secret manifest checked into Git can still be a credential leak. Anyone who can read the repository can decode the value. If that has already happened, treat it as rotation work, not cleanup. The related guide on catching leaked secrets before commit is the right companion task because Kubernetes will not erase a leaked token from Git history.
For application teams, the first rule is simple: do not share Secret manifests that contain live values. Use a deployment path that injects the value from a trusted store, CI secret, or sealed workflow, and keep the generated object out of the source tree unless the value is encrypted in a form your cluster can decrypt safely.
Treat workload creation as Secret access#
Secret RBAC often looks safe when people only search for direct get secrets grants. That misses a larger path. Kubernetes says a user authorized to create a Pod in a namespace can use that access to read any Secret in that namespace, including indirect access through a Deployment. The RBAC good practices page makes the same point in a broader way: permission to create workloads in a namespace implicitly grants access to resources that Pods can mount, including Secrets and ConfigMaps.
Design reviews should treat workload creation as a sensitive permission, not only an app-deploy permission. If a namespace contains production database credentials, the set of identities allowed to create Pods in that namespace is part of the credential access list.
kubectl auth can-i get secrets --namespace demo --as user@example.com
kubectl auth can-i list secrets --namespace demo --as user@example.com
kubectl auth can-i watch secrets --namespace demo --as user@example.com
kubectl auth can-i create pods --namespace demo --as user@example.com
kubectl auth can-i create deployments.apps --namespace demo --as user@example.com
Those checks are a starting point for review, not a full access report. The Kubernetes RBAC good practices page recommends least privilege, namespace-level grants where possible, RoleBindings instead of ClusterRoleBindings when a namespace grant is enough, and avoiding wildcard permissions. It also warns that list and watch on Secrets reveal Secret contents, not just metadata.
Mount the value only where the app needs it#
A Pod can consume a Secret through an environment variable or a volume mount. Kubernetes supports both, but a volume mount is usually easier to limit and inspect. If a Pod has multiple containers, the good-practices page says only the container that needs the Secret should receive the mount or environment variable.
The manifest below shows the delivery shape without placing a live value in the file. In a real deployment, bind the volume to a pre-created Secret through your approved deployment system, keep ordinary settings separate, and avoid mounting a service account token unless the Pod needs Kubernetes API access.
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
namespace: demo
spec:
template:
spec:
containers:
- name: api
env:
- name: LOG_LEVEL
value: info
volumeMounts:
- name: runtime-private
mountPath: /var/run/private
readOnly: true
volumes:
- name: runtime-private
# Bind this volume to a pre-created Kubernetes Secret in your real manifest.
# Keep the live credential value outside source control.
This does not remove the need for careful application code. Kubernetes explicitly says applications must protect confidential data after reading it, including not logging it in clear text or sending it to an untrusted party. A well-scoped Secret mount can still leak if the app prints configuration on startup or includes headers in error reports.
Turn on at-rest encryption before etcd is the weak link#
Kubernetes at-rest encryption protects API resource data stored through the Kubernetes API, including Secret objects. The official Encrypting Confidential Data at Rest task says the API server stores plain-text resource data into etcd by default unless encryption is configured. It also explains that the API server uses an encryption-provider configuration to control how data is encrypted in etcd.
This is not a box to tick blindly. The same documentation explains that the first provider in the encryption configuration is used for new writes, and that the identity provider stores resources as plain text. It also warns that local key storage protects against an etcd compromise but does not protect against a control-plane host compromise, because local key material is present on the host. KMS-based envelope encryption changes that risk model by keeping the key-encryption key outside Kubernetes, but it adds an external dependency that must be protected and monitored.
One unknown is whether an existing cluster has fully migrated old Secret objects after enabling encryption. Kubernetes notes that existing data may remain in its previous stored form until rewritten. Do not assume a new encryption file has fixed old objects until your cluster runbook includes the migration check described by the Kubernetes task.
A small audit that catches most mistakes#
Start with one namespace that holds a real application credential. Confirm who can directly get, list, or watch Secrets there. Then check who can create Pods, Deployments, Jobs, or other workloads in that namespace, because those identities may be able to mount the same values indirectly.
Next, inspect how the application receives the Secret. Prefer a per-container volume mount for the sensitive value, keep non-secret settings in ConfigMaps or ordinary environment variables, and turn off service account token mounting for Pods that do not call the Kubernetes API. Finally, verify whether at-rest encryption is enabled and whether older Secret objects have been rewritten under the active provider.
Do that review before adding a new credential. Pick one Secret, one namespace, and one workload. If the Secret is in Git, rotate it. If too many identities can create Pods beside it, narrow the namespace or RBAC boundary. If etcd still stores API data as plain text, schedule the encryption work before treating Kubernetes Secrets as your safe default.
Leave a Reply