Skip to the document
Madhuopen lab
The Kubernetes Ninja PathTrack 1 — Kubernetes from the ground up

Chapter 6

ConfigMaps & Secrets — Managing App Configuration

5 min read read1,567 wordsIntermediate6 recall cards

Before you read, guess

How do ConfigMaps and Secrets differ in their handling of sensitive data?

Take ten seconds and guess — even a wrong guess makes the answer stick. Tap to see where the chapter lands, or just read on.

ConfigMaps hold config that's safe to be public. Secrets hold sensitive config — but by default they're only base64-encoded, not encrypted. That's a different format, not a lock.

Somewhere in your app's code right now there's probably a line like const dbHost = "prod-db.internal". It made sense the day you wrote it. It's been quietly causing pain ever since. Let's fix that the Kubernetes way.

The problem: your image shouldn't know where it's running

In Post 4 you built a Deployment and rolled out an update. The whole point of that exercise: you build one container image — the packaged app plus everything it needs to run — and promote that exact same image through dev, staging, and prod, unchanged. That promise breaks the moment you hard-code an environment-specific value into the image: a database URL, a feature flag, an API key, a log level. Now your "prod image" and "staging image" are actually two different files, you're rebuilding just to flip one flag, and nobody can be sure what passed tests in staging is really what's running in prod.

The fix: pull configuration out of the image, hand it to the app when it starts. Kubernetes gives you two objects for this. A ConfigMap holds config that's fine for anyone to see. A Secret holds config you need to keep private.

ConfigMap: config as its own object

A ConfigMap is a key-value store — a simple list of names paired with values, like APP_ENV: production — that lives as its own object, separate from your Pod. You create it once. Any Pod that needs those values just references the ConfigMap by name. Need to point your app at a different environment? Edit the ConfigMap. No image rebuild, no code change.

It's for values that are fine to be public: an environment name, a log level, a feature-flag toggle, the address of another service. If someone reads it over your shoulder, nothing bad happens.

Secret: same idea, for the stuff that matters if it leaks

A Secret works the same way — key-value pairs, referenced by name — but it's for values where exposure is a real problem: passwords, API tokens, TLS certificates. Kubernetes stores Secrets as a separate object type, and kubectl hides the values by default when you print one.

Here's the part that trips people up, so I'll say it plainly: by default, a Secret's values are base64-encoded, not encrypted. Those are not the same thing. Encoding just rewrites data in a different format — anyone can reverse it, no key needed. Encryption scrambles data so only someone holding the right key can read it back. Base64 is encoding, not encryption: one command turns it back into the original text, no password required. All it does is stop a value from showing up as plaintext when you casually run kubectl get. It does nothing against someone with real access to the cluster, or to etcd — the database Kubernetes stores all its objects in.

Analogy: A ConfigMap is a recipe card taped to the kitchen wall — anyone walking by can read it, and that's fine, it's just instructions. A Secret is a key kept in a drawer instead of pinned to the wall. But the drawer isn't actually locked. It's just closed. Anyone who opens it is holding the exact same key as if it had been left on the wall — nothing about the key itself changed. That's exactly what base64 encoding is: not a lock, just a different way of writing the same value down. Closing the drawer — putting a value in a Secret instead of a ConfigMap — stops casual stumbling. It does not stop anyone who's actually allowed to open drawers.

So what actually protects a Secret? Three layers. First, RBAC — the rules deciding which users and service accounts may run commands like kubectl get secret — locked down so only whoever truly needs a value can read it. A Secret nobody can read is safe no matter how it's encoded. Second, encryption at rest turned on for etcd, so the raw data isn't sitting on disk as plaintext. Third, in production, a dedicated secrets tool: something like Sealed Secrets, for storing encrypted secrets safely in Git, or GCP Secret Manager, which you'll meet later in this series. Kubernetes Secrets are the right shape for wiring values into Pods — the storage behind them just isn't real protection on its own. Don't let "it's a Secret" fool you into thinking "it's encrypted."

Two ways to get values into a Pod

Whether it's a ConfigMap or a Secret, a Pod can consume it in one of two ways:

As environment variables. The values get injected into the container's environment when it starts — the same kind of environment variables your app already reads. Simple, and it's what most apps expect. The catch: they're set once, at process start. Update the ConfigMap or Secret afterward, and a running container has no idea — it holds the old values in memory until something restarts it.

As a mounted volume. A volume, in Kubernetes terms, is just storage attached to a container's filesystem — we'll cover it properly in the next post. Here, each key shows up as a file inside the container, with the file's contents equal to the value. The advantage: Kubernetes updates these files live when the ConfigMap or Secret changes — no restart needed. Genuinely useful for something like a rotated TLS certificate. The catch mirrors the first one: the file updating doesn't mean your app notices. Unless your app is actively watching that file and reloading it, it's still running on whatever it read at startup.

Neither approach reloads your app for you. The real difference is where the update lands, and how fast.

Hands-on: create, inspect, decode

Create a ConfigMap the quick, imperative way, setting keys and values directly with --from-literal:

$ kubectl create configmap app-config \
    --from-literal=APP_ENV=production \
    --from-literal=LOG_LEVEL=info
configmap/app-config created

Look at what got stored:

$ kubectl get configmap app-config -o yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config
  namespace: default
data:
  APP_ENV: production
  LOG_LEVEL: info

Plain text, sitting right there in the object. Exactly what you'd expect for a recipe card taped to the wall. Now let's create a Secret:

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

$ kubectl get secret db-secret -o yaml
apiVersion: v1
kind: Secret
metadata:
  name: db-secret
  namespace: default
type: Opaque
data:
  DB_PASSWORD: c3VwZXJzZWNyZXQ=

That string, c3VwZXJzZWNyZXQ=, looks like ciphertext if you don't know better. It isn't — it's base64. Prove it to yourself:

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

One pipe, and you're back to plaintext. No key, no password. That's the "drawer isn't really locked" point, right on your screen — the value was never turned into ciphertext, just rewritten in a different format.

Try it: Run that decode command against the db-secret you just created. Then, in a real cluster, check who has RBAC permission to run `kubectl get secret` in your namespace — that permission is the real security boundary, not the encoding.

Wiring both into a Pod

Here's a Pod spec that does both: pulls every key from the ConfigMap in via envFrom, and pulls one key out of the Secret via valueFrom.secretKeyRef:

apiVersion: v1
kind: Pod
metadata:
  name: web
spec:
  containers:
  - name: web
    image: myapp:1.0
    envFrom:
    - configMapRef:
        name: app-config
    env:
    - name: DB_PASSWORD
      valueFrom:
        secretKeyRef:
          name: db-secret
          key: DB_PASSWORD

envFrom dumps every key from the ConfigMap into the environment, keeping the same key names — fast when you want the whole set at once. valueFrom.secretKeyRef does the opposite: it pulls one named key out of a Secret and lets you rename it as it lands in the container, which is exactly the precision you want for something sensitive.

Exam trap: The CKA loves testing this wiring. "Inject this ConfigMap as environment variables" means envFrom. "Inject this one key as an environment variable named X" means env plus valueFrom.secretKeyRef. Mix the two up and you fail the task. The next trap sits right beside it: editing a ConfigMap does not restart Pods already using it as env vars. The Pod keeps old values until its containers get recreated — on a Deployment, run kubectl rollout restart deployment <name> (the command from Post 4) after updating the config. Don't just edit the ConfigMap and assume the change is live.

Key Takeaways

  • Keep configuration out of your container image, so the same image can move through dev, staging, and prod unchanged.
  • ConfigMaps hold config that's safe to be public. Secrets hold sensitive config — but by default they're only base64-encoded, not encrypted. That's a different format, not a lock.
  • Real protection for a Secret comes from RBAC, encryption at rest on etcd, and, in production, a dedicated tool like Sealed Secrets or GCP Secret Manager.
  • A Pod can consume a ConfigMap or Secret as env vars (simple, but frozen at container start) or as a mounted volume (updates live, but your app still has to notice).
  • envFrom injects an entire ConfigMap; valueFrom.secretKeyRef injects one specific Secret key — know the syntax difference cold for the exam.
  • Updating a ConfigMap does not restart Pods using it as env vars — run kubectl rollout restart deployment to pick up the new values.

Next up: Volumes & Persistent Storage — what happens to your data when a container restarts, and how to make it survive.

Before you go

In one sentence, what was this chapter about?

From memory, without scrolling up. Writing it is what makes it yours; the grade is only to show you what you had.

How sure?