On this page
In the previous lesson you stored non-sensitive settings in ConfigMaps. Passwords, API keys, TLS certificates and database credentials need more care. ConfigMap data appears in plain text whenever someone runs kubectl get configmap -o yaml, and ConfigMaps are not designed to be locked down.
Kubernetes has a dedicated object for sensitive data: the Secret. If a ConfigMap is the settings sheet you hand to an app, a Secret is the sealed envelope with the keys to the building. You hand it over the same way, but you are much stricter about who may open it.
Secrets let you build container images without any credentials inside them. The same image runs in development, staging and production, and only the Secrets differ. Pods receive Secrets through explicit references in their manifests, and Secret volumes live on tmpfs (a RAM disk), so the files are never written to the node's disk.
What you will learn
- How Secrets differ from ConfigMaps, and which built-in Secret types exist
- Why base64 is encoding, not encryption, and how to protect Secrets in a real cluster
- How to create, inspect, decode and edit Secrets with
kubectl - How to give a .NET app database credentials as environment variables and TLS certificates as files
- How to pull images from a private registry with
imagePullSecrets
Secrets vs ConfigMaps
Secrets and ConfigMaps look alike: both are key-value stores that Pods consume. The differences matter as soon as security matters:
| Feature | ConfigMap | Secret |
|---|---|---|
| Intended data | Non-sensitive configuration | Passwords, tokens, keys, certificates |
| How values are written | Plain text in data (binary in binaryData) | Base64 in data, or plain text in stringData |
| Volume backing | Files on the node's disk | tmpfs (RAM disk), never written to the node's disk |
| Size limit | 1 MiB | 1 MiB |
kubectl describe | Shows the values | Shows only key names and sizes |
| Access control | RBAC, often readable by many users | RBAC, same model, but you grant it to as few users as possible |
| Encryption at rest in etcd | Possible, rarely enabled | Should be enabled with an EncryptionConfiguration |
Secret types
Every Secret has a type. It tells Kubernetes, and the people reading the manifest, what kind of data the Secret holds. Some types also make the API server check that the required keys are present.
| Type | Description |
|---|---|
Opaque | The default type. Holds any key-value data. |
kubernetes.io/tls | A TLS certificate and its private key. Requires the keys tls.crt and tls.key. |
kubernetes.io/dockerconfigjson | Credentials for private container registries. |
kubernetes.io/basic-auth | A username and a password. |
kubernetes.io/ssh-auth | An SSH private key. |
kubernetes.io/service-account-token | A long-lived ServiceAccount token. Legacy: modern clusters give Pods short-lived tokens automatically. |
Day to day you will mostly use Opaque (database passwords, API keys, tokens) and kubernetes.io/tls (HTTPS certificates).
Base64 is encoding, not encryption
Values in a Secret's data field are base64-encoded. Base64 is reversible, so anyone who can read the Secret can decode it in one command:
Expected output:
Base64 exists so that a Secret can carry binary data, such as certificates, inside YAML, which is a text format. On its own it provides no security at all.
If you want to write plain-text values in a manifest, use stringData instead of data. Kubernetes encodes the values for you when it stores the Secret. If the same key appears in both fields, the stringData value wins.
Protect Secrets in a real cluster
By default, Kubernetes stores Secrets unencrypted in etcd, the cluster's database. Anyone with access to etcd or with broad admin rights can read every Secret. Also, anyone who can create a Pod in a namespace can mount any Secret in that namespace. For production clusters:
- Enable encryption at rest with an
EncryptionConfigurationand a key you control, ideally from a cloud KMS. Managed clusters (AKS, EKS, GKE) offer this as a setting. - Use RBAC rules to limit who can
get,listandwatchSecrets, and who can create Pods in sensitive namespaces. - Consider the Secrets Store CSI Driver to read secrets straight from Azure Key Vault, AWS Secrets Manager or HashiCorp Vault, so they are never stored in etcd at all.
- Prefer mounted files over environment variables for the most sensitive values. Environment variables leak more easily through logs, crash dumps and child processes.
Naming rules for keys
Keys in Secrets and ConfigMaps follow the same rules:
- Only letters, digits, dashes (
-), underscores (_) and dots (.). No spaces, slashes or colons. - At most 253 characters.
- A key cannot be
.or.., and cannot start with... Kubernetes uses names like..datainternally inside mounted volumes.
| Valid key name | Invalid key name |
|---|---|
.auth_token | auth file.json (space) |
key.pem | certs/key.pem (slash) |
config_file | ..data (starts with ..) |
_password.txt | api:key (colon) |
Keys become file names when you mount a Secret, so pick names that read well as files: key.pem is clearer than tls-key when an app expects certificate files. If you load keys with envFrom, stick to letters, digits and underscores so every key is also a normal environment variable name.
Working with Secrets from the command line
The kubectl create secret command has a sub-command for each common Secret type.
Generic (Opaque) Secrets
Use generic to create an Opaque Secret from literal values. Wrap values in single quotes so your shell does not interpret characters such as ! or $:
You can also load values from files. Each file name becomes a key and the file content becomes its value:
This produces an Opaque Secret with the keys server.crt and server.key. For a proper TLS Secret, use the next command.
TLS Secrets
The tls sub-command checks that the certificate and key are valid and belong together:
This creates a Secret of type kubernetes.io/tls with the keys tls.crt and tls.key.
Docker registry Secrets
To store credentials for pulling images from a private registry:
This creates a Secret of type kubernetes.io/dockerconfigjson. The --docker-email flag is optional.
View Secrets
List the Secrets in the current namespace, then look at one of them:
describe shows key names and sizes but not the values, as a deliberate safety measure. Expected output (trimmed):
To see the base64-encoded values, print the YAML:
To decode a single value:
Expected output:
Edit a Secret
The editor shows the values base64-encoded, so encode any new value before you paste it in:
Expected output:
Often it is easier to regenerate the whole Secret from plain values and apply it over the existing one:
Delete a Secret
Just like with ConfigMaps, a Pod that references a deleted Secret keeps running, but it fails to start again (for example after a restart or a reschedule) until the Secret exists again.
Hands-on: secure a .NET Billing API
You will build a realistic setup: a .NET Billing API that needs database credentials as environment variables and a TLS certificate as files. You will create two Secrets and a Deployment that uses both.
As in the ConfigMaps lesson, the Deployment uses Microsoft's sample image mcr.microsoft.com/dotnet/samples:aspnetapp as a stand-in for your own Billing API image. It listens on port 8080 and answers health checks on /healthz. You also need openssl on your machine for Step 2.
Step 1 — Create the database credentials Secret
This Opaque Secret holds the database connection details. Save it as billing-api-db-credentials.yaml:
Every value under data is base64-encoded. Here is what each one decodes to:
| Key | Base64 value | Decoded value |
|---|---|---|
DB_HOST | cG9zdGdyZXMuaW50ZXJuYWwubG9jYWw= | postgres.internal.local |
DB_PORT | NTQzMg== | 5432 |
DB_NAME | YmlsbGluZ19kYg== | billing_db |
DB_USER | YmlsbGluZ19zdmM= | billing_svc |
DB_PASSWORD | WDlrIXZMcDJAZFFtNyE= | X9k!vLp2@dQm7! |
You can produce these values on the command line:
Expected output:
echo -n. Without -n, echo adds a newline, the newline gets encoded into the value, and your app fails with confusing login or connection errors. Writing plain values under stringData avoids the problem entirely.Apply the Secret:
Step 2 — Create the TLS Secret
A TLS Secret stores an HTTPS certificate and its private key. In YAML it has this shape:
The placeholders are not valid base64, so do not apply this file as it is. In production the certificate comes from a Certificate Authority such as Let's Encrypt. For this lesson, generate a self-signed certificate:
Then create the TLS Secret from the two files. This is the easiest way, because kubectl encodes the files and checks that the certificate and key match:
Expected output:
Step 3 — Create the Deployment that uses both Secrets
This Deployment injects the database credentials as environment variables and mounts the TLS certificate as files. Save it as billing-api-deployment.yaml:
Apply it and wait for the rollout:
Expected output:
Port 8443 is declared for when your own app serves HTTPS with the mounted certificate. The sample image only listens on 8080, which is why the probes use that port.
Step 4 — Inject individual keys with secretKeyRef
Each secretKeyRef picks one key from the Secret and exposes it under the environment variable name you choose:
This works exactly like configMapKeyRef, but points at a Secret. In .NET, the double underscore in Database__Host maps to the configuration path Database:Host in IConfiguration.
The last variable, ConnectionStrings__BillingDb, is built from the others. Kubernetes replaces $(Database__Host) and the other references with their values, so your app can call builder.Configuration.GetConnectionString("BillingDb") and get a complete connection string. This only works for variables defined earlier in the same env list.
You can also inject every key at once with envFrom:
This creates the variables DB_HOST, DB_PORT, DB_NAME, DB_USER and DB_PASSWORD, one for each key in the Secret.
Step 5 — Mount the TLS certificate as files
The certificate reaches the container through a Secret volume:
Each key becomes one file. Check it from a running Pod:
Expected output:
Run ls -la instead and you will also see hidden entries such as ..data. They are symbolic links Kubernetes uses to swap in new files at once when the Secret changes. Like ConfigMap volumes, Secret volumes update automatically, unless you mount them with subPath.
The folder is a tmpfs volume in RAM, so the key never touches the node's disk. You can see it in the Filesystem column:
Step 6 — Verify the environment variables
Confirm that the Secret values reached the container:
Expected output (the order may differ):
The values are plain text. Kubernetes decodes base64 automatically when it injects Secret data into environment variables or volume files, so your app never deals with base64.
kubectl rollout restart deployment/billing-api.Step 7 — Pull images from a private registry
When a Pod pulls an image from a private registry, such as Azure Container Registry or a private Docker Hub repository, the kubelet needs credentials. A registry Secret provides them. Create one:
Then reference it in any Pod that pulls from that registry with spec.imagePullSecrets:
imagePullSecrets tells the kubelet which Secret to use when it signs in to the registry. Without it, the registry rejects the pull and the Pod ends up in ImagePullBackOff. The Secret must live in the same namespace as the Pod.
If every Pod in a namespace pulls from the same registry, attach the Secret to the namespace's default ServiceAccount instead of repeating it in every manifest:
Step 8 — Clean up
Remove the resources from the hands-on part:
Or delete them by manifest file:
Then remove the Secrets from the command-line section, undo the ServiceAccount patch and delete the local key files:
Common mistakes
| Symptom | Likely cause | Fix |
|---|---|---|
| App fails to log in with a correct-looking password | A trailing newline was encoded (echo without -n) | Re-encode with echo -n, or use stringData |
kubectl apply fails with illegal base64 data | Plain text under data | Move the values to stringData, or encode them |
Pod stuck in CreateContainerConfigError | The Secret or key does not exist, or lives in another namespace | Create the Secret in the Pod's namespace and check key names |
Pod stuck in ImagePullBackOff for a private image | No imagePullSecrets, or wrong credentials | Add the registry Secret to the Pod or the ServiceAccount |
| App still uses the old password | The value comes from an environment variable | kubectl rollout restart deployment/<name> |
Summary
Secrets are the Kubernetes way to store and distribute sensitive data such as passwords, tokens and certificates. Pods use them just like ConfigMaps, but you store and guard them more carefully.
- A Secret stores sensitive key-value data, base64-encoded. Base64 is not encryption: anyone who can read the Secret can decode it.
- Common types are
Opaquefor general data,kubernetes.io/tlsfor certificates andkubernetes.io/dockerconfigjsonfor registry credentials. - Create Secrets imperatively with
kubectl create secret generic | tls | docker-registry, or declaratively in YAML withdata(base64) orstringData(plain text). - Consume them as environment variables (
secretKeyReforenvFromwithsecretRef) or as files on a tmpfs volume. - Image pull Secrets authenticate with private registries through
spec.imagePullSecretsor the default ServiceAccount. - Secrets are unencrypted in etcd by default. In production, enable encryption at rest, restrict access with RBAC, and consider the Secrets Store CSI Driver for external vaults.
- Key names may contain only letters, digits,
-,_and., and cannot start with... Choose names that work as file names, such askey.pem.
With ConfigMaps for settings and Secrets for credentials, your images stay generic and every environment gets exactly the configuration it needs.