Secrets: handling passwords

Like config, but sensitive.

Beginner8 min · lesson 15 of 24
In plain terms
Same injection as ConfigMaps; default storage is base64, not encryption.

Two kinds of settings are not alike. The port your database listens on, 5432, can sit in ordinary config. The database password cannot. Kubernetes gives the sensitive half its own object: a Secret. The cluster stores it and hands it to pods. A ConfigMap is for non-sensitive settings. Both hold key/value pairs. The difference is how the cluster treats the object, and how carefully you have to treat it.

Use a Secret when a stranger reading the value would do real damage. A database password. An API key (application programming interface key): a token that lets the app prove who it is to another service. A TLS certificate, TLS being Transport Layer Security, the file behind the padlock in the browser that proves a site is who it claims to be. Keep the value out of the container image. The image is the frozen snapshot every server pulls. Bake a password into it and everyone who can pull the image has the password. Put it in a Secret instead, and Kubernetes injects it at startup from outside.

Make one without opening an editor

You talk to Kubernetes through a command-line tool called kubectl, meaning a tool you drive by typing commands instead of clicking buttons. Half the industry says 'cube-cuttle', half says 'cube-control', and nobody has ever settled it. For your first Secret you do not need to write a file at all. One command creates the record and handles the fiddly encoding on your behalf. The --from-literal flag is you saying: the value is right here, typed on the line. (A flag is an extra option you tack onto a command to change what it does.)

terminal
$ kubectl create secret generic db-secret --from-literal=DB_PASSWORD=supersecret
output
secret/db-secret created

Now ask the cluster to read back what it stored. The value comes out looking like line noise, and that appearance fools people constantly. It is not encrypted. Encryption means scrambling something so that only a person holding the right key can unscramble it. What you are looking at is base64, a reversible way of rewriting text so that awkward characters survive being copied from one system to another. Anyone allowed to read the Secret can turn that string back into the original password in one step. You are about to do exactly that.

terminal
$ kubectl get secret db-secret -o jsonpath='{.data.DB_PASSWORD}'
output
c3VwZXJzZWNyZXQ=
terminal
$ echo 'c3VwZXJzZWNyZXQ=' | base64 -d
output
supersecret

A Secret is like a ConfigMap with different defaults and more caution around it. The bytes land in etcd, the key-value store where Kubernetes keeps cluster state, and depending on the cluster they may still be only base64-encoded rather than encrypted. For production passwords, keep the source of truth in an external secret store and turn on encryption at rest so the copy in etcd is encrypted too.

That leads to one hard rule about files. Never commit a Secret manifest holding a live value to Git. Running kubectl get secret -o yaml prints base64, and base64 in a commit is a costume, not a lock. The usual ways around it, in rough order of how often you will meet them: sealed-secrets, which lets you commit an encrypted blob only your cluster can open; a CSI driver (Container Storage Interface, the standard plug for attaching outside storage to a Pod) that fetches the value from a vault when the Pod mounts it; or your cloud provider's own secret manager.

And if a credential does leak, the order of operations matters. Rotate it first, at the source, so the old string stops working. Then patch the Secret with the new value and restart everything that consumes it, because a Pod that read the old value at startup is still holding it in memory. Last, audit RBAC (role-based access control, the Kubernetes permission system that decides who may read which objects) to work out who could have read that Secret in the first place. The object is only as safe as your RBAC rules and your storage encryption, and no safer.

Try it yourself

Run the whole loop end to end. Create a Secret, mount or inject it into a Pod, confirm the app really can see the value, and watch how asking the cluster for it only ever gives you base64. Clean up at the end so nothing is left lying around.

terminal
$ kubectl create secret generic db --from-literal=password=S3cr3t!
secret/db created
$ kubectl get secret db -o jsonpath='{.data.password}{"\n"}'
UzNjcjN0IQ==
$ echo UzNjcjN0IQ== | base64 -d
S3cr3t!
$ kubectl run sec --image=busybox:1.36 --restart=Never --overrides='{"spec":{"containers":[{"name":"sec","image":"busybox:1.36","command":["sleep","300"],"env":[{"name":"DB_PASSWORD","valueFrom":{"secretKeyRef":{"name":"db","key":"password"}}}]}]}}'
pod/sec created
$ kubectl exec sec -- printenv DB_PASSWORD
S3cr3t!
$ kubectl delete pod sec; kubectl delete secret db
pod "sec" deleted
secret "db" deleted

Takeaway

Sensitive values belong in a Secret rather than a ConfigMap, and keeping that line clean is worth the effort. The encoding is not what protects them. base64 comes off in a single command, as you saw. What actually guards the password is the short list of people and service accounts your RBAC rules let near that Secret, whether the cluster encrypts its data store, and whether the production credential lives in a dedicated store to begin with. Rotate the moment you suspect anyone saw it.

base64 is a costume, not a lock
A Secret's values carry nothing but base64 by default, and you watched one command peel it off. 'It lives in a Secret' and 'it is safe' are two different claims. One habit protects you more than any other here: never commit a Secret file holding a real password into a Git repository, private ones included. (Git is the tool developers use to track changes to their files, and a repository is the tracked project folder, history and all.) The history is the trap. Delete the line tomorrow and the old commit still holds the password. If a live value ever lands in a repo, treat it as leaked and change it that day. The protection that actually counts comes later, and it is built specifically around Secrets: limiting who is allowed to read them, and encrypting the cluster's data store, the database where Kubernetes keeps everything it manages.

Hand the Secret to your Pod

Now let something actually use it. A Pod is the smallest thing Kubernetes runs for you: one or more containers wrapped together, placed onto a machine, sharing the same small sandbox. There are two ways in. You can mount the Secret as a file, where Kubernetes makes the value appear as a file inside the container at a path you choose. Or you can expose it as an environment variable, a named value the operating system hands your program the moment it starts, sitting there waiting to be read. The example below takes the environment-variable route. The file itself is a manifest, a YAML description of what you want the cluster to create. (YAML is a plain-text format for structured settings, built on names, values and indentation.) Look at stringData: writing the password there means Kubernetes does the base64 encoding for you, and you never type an encoded string by hand.

app.yaml
apiVersion: v1
kind: Secret
metadata:
name: db-secret
type: Opaque
stringData:
DB_PASSWORD: supersecret
---
apiVersion: v1
kind: Pod
metadata:
name: web
spec:
containers:
- name: web
image: nginx:1.27
env:
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: db-secret
key: DB_PASSWORD
terminal
$ kubectl apply -f app.yaml
output
secret/db-secret configured
pod/web created

Here comes the satisfying part. Step inside the running container and check that the password made it. The exec command runs a program inside a Pod for you, and printenv is a tiny utility that prints the value of one environment variable.

terminal
$ kubectl exec web -- printenv DB_PASSWORD
output
supersecret

DB_PASSWORD is now sitting in the app's environment exactly as if you had typed it there by hand, while the real value lives in the cluster and never in the image. Your code reads it the ordinary way, which in most languages is one line that looks up an environment variable by name. The app has no idea the value came from a Secret, and it does not need one. A caveat before you move on. Mounting a Secret as a file is usually the safer of the two routes, because environment variables have a habit of escaping: into log files, and down into every other program your app launches. Both routes work, and your code barely changes between them.

When the key name does not line up

One mistake catches nearly everyone in their first week, so it helps to meet it deliberately, on your own terms. The key you name under secretKeyRef has to match a key that really exists inside the Secret, spelled identically. Say your finger slipped and you wrote key: DB_PASS while the Secret holds DB_PASSWORD. Kubernetes takes the file happily and creates the Pod. The container never starts. Ask for the Pod's status and here is what comes back.

terminal
$ kubectl get pod web
output
NAME READY STATUS RESTARTS AGE
web 0/1 CreateContainerConfigError 0 14s

CreateContainerConfigError is Kubernetes telling you it could not assemble the container's startup settings, because something they point at is missing. That names the symptom and stops there. For the cause, describe the Pod and read the Events list at the bottom, which is a play-by-play of everything that has happened to it.

terminal
$ kubectl describe pod web
output
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled 22s default-scheduler Successfully assigned default/web to node-1
Normal Pulled 6s (x3 over 21s) kubelet Container image "nginx:1.27" already present on machine
Warning Failed 6s (x3 over 21s) kubelet Error: couldn't find key DB_PASS in Secret default/db-secret

Read it top down, oldest line first. The scheduler, the part of Kubernetes that picks which machine a Pod runs on, did its job: Successfully assigned web to node-1. Then the kubelet, the agent running on that machine, hit the wall and said so in plain words: couldn't find key DB_PASS. Fix the key name in the manifest so it matches the Secret, apply again, and the container comes up. There is a close cousin of this error, secret "db-secret" not found, which shows up when the Secret is missing altogether, for instance if you applied the Pod before creating the Secret.

Which object should hold this value?
A value your app needs
plain config, or a credential?
safe to read
ConfigMap
ports, URLs, feature flags
sensitive
Secret
passwords, API keys, TLS certs
Both keep the value out of your image, and both inject it the same way. Pick a Secret the moment a stranger reading the value would cause harm.
Quick check
01You run kubectl get secret db-secret -o yaml and the password shows up as c3VwZXJzZWNyZXQ=. What has that string actually told you?
Incorrect — No. That is base64, and one command turns it straight back into the password. Encoding is not encryption.
Correct — base64 only reshapes the text. The real protection is who may read the Secret and whether the data store is encrypted.
Incorrect — Nothing is broken. base64 is the normal way a Secret stores its data.
02The lesson calls a file-mounted Secret the safer choice over an environment variable. What reason does it give?
Incorrect — Neither one is encrypted by default. A mounted Secret value is no more encrypted than an env var.
Incorrect — Length is not the worry raised here. Both can carry the value without trouble.
Correct — Env vars spill into logs and into child processes, so a file is the lower-exposure option.
Incorrect — Auto-refresh is a ConfigMap-file behavior, and it is not the security reason given here.
03db-secret exists and holds the key DB_PASSWORD, but your Pod's secretKeyRef asks for key: DB_PASS, and the Pod is stuck at CreateContainerConfigError. Which describe event fits, and what fixes it?
Incorrect — That error means the whole Secret is missing. Here it exists, so you get a key error instead.
Correct — the key you reference has to exist and be spelled exactly, so fixing DB_PASS to DB_PASSWORD lets the container start.
Incorrect — That is an image problem and has nothing to do with a Secret key mismatch.
Incorrect — A missing key stops the container from starting. Kubernetes does not quietly fall back to an empty value.

Try this

Run kubectl create secret generic db-secret --from-literal=DB_PASSWORD=supersecret on a scratch host or disposable cluster and read the output against what this lesson described. Then change one input so it fails, and re-run: the error you get is the one you will meet in production.

Related