CoursesKubernetes fundamentalsSecrets: handling passwords

Secrets: handling passwords

Like config, but sensitive.

Beginner8 min · lesson 15 of 24
In plain terms
A Secret is that settings sheet for passwords — kept aside and handled with care, though by default it’s only folded shut, not truly locked.

Two of the settings your app needs are not alike at all. The port your database listens on, 5432, could be printed on a poster in the lobby and nothing bad would happen. The database password could not. Kubernetes takes that difference seriously enough to give the sensitive half its own kind of record, called a Secret. (Your cluster, the group of machines Kubernetes runs your apps across, is what stores these records and hands them out.) Here is an easy way to keep the two straight. A ConfigMap, which is Kubernetes' record for plain, non-sensitive settings, is a note pinned to the office bulletin board where anyone walking past can read it. A Secret is that same note, sealed in an envelope and locked in a drawer. Inside, both hold ordinary key-and-value pairs. What changes is how carefully the cluster treats the envelope, and how carefully you have to treat it too.

Reach for a Secret the moment a value would do real damage if a stranger read it. A database password. An API key (application programming interface key), a token that lets your app prove who it is to another service, working a bit like a key cut for one specific door. A TLS certificate, TLS being short for Transport Layer Security, the file sitting behind the little padlock in your browser that proves a website is who it claims to be. The rule you met with plain config still holds here. Keep the value out of your container image. A container image is the frozen, ready-to-run snapshot your app ships as, and every server that runs the app pulls its own copy. Bake a password into that snapshot and you have handed the password to everyone who can pull the image, permanently. Put it in a Secret instead, and Kubernetes passes the value to the app 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

So a Secret is a ConfigMap with different defaults and a lot more caution built up around it. Follow the bytes and you see why the caution matters. They end up in etcd, the key-value database where Kubernetes keeps a record of everything it manages, and depending on how your cluster was set up they may sit there still only base64-encoded rather than properly encrypted. A Secret object is a labelled envelope, not a bank vault. For passwords guarding real production data, keep the master copy in a dedicated external secret store and switch on encryption at rest so the copy in etcd is scrambled as well.

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