18.4 Secrets: Base64-Encoded Sensitive Data
Alright, let’s talk about Secrets. You’ve just learned about ConfigMaps, and you’re thinking, “Great! I’ll just shove my database password in one of those!” Please, for the love of all that is holy, do not do that. That’s why we have Secrets. They’re the ConfigMap’s more paranoid, security-conscious cousin who whispers instead of shouting.
The core idea is simple: Secrets are a Kubernetes object for storing sensitive data like passwords, API keys, TLS certificates, and OAuth tokens. The key difference from a ConfigMap? They’re not just plain text. Well, sort of. Here’s the first thing you need to know, and it’s a bit of a doozy: the data in a Secret is base64-encoded, not encrypted. Let me say that again for the people in the back. It is not encrypted. Base64 is an encoding scheme, designed to avoid weird binary/control characters, not a encryption cipher designed to keep prying eyes out. Anyone with kubectl get secret my-secret -o yaml can see the encoded data, and any mildly curious intern can run echo 'dGhpcyBpcyBzb21lIHNlY3JldA==' | base64 --decode on their laptop to reveal the plain text “this is some secret”. This is the first and most important pitfall. Secrets are a way to avoid accidentally shoulder-surfing a password, not a way to secure it against a determined attacker. For real encryption at rest, you need to enable and configure the EncryptionConfiguration for the Kubernetes API server, which is a whole other chapter of pain.
Why Base64 at All?
You might be wondering, “If it’s not for security, why bother with the base64 song and dance?” It’s a practical choice, not a security one. Secrets can hold two types of data: simple UTF-8 text (like a password) and binary data (like a TLS certificate or key). Base64 provides a common, simple text-based wrapper for both. It ensures that whatever binary blob or special character you shove into a Secret will be safely transmitted and stored without any YAML or JSON parser getting confused and mangling it. It’s about consistency and reliability, not security.
Creating a Generic Secret
The easiest way to create a secret is from the command line. Let’s create one for a database connection.
# Create a secret from literal values. The --dry-run bit is a best practice to preview.
kubectl create secret generic my-db-credentials \
--from-literal=username=prod-admin \
--from-literal=password='S0m3Tr1cKyS7r0ngP@ss!' \
--dry-run=client -o yaml > my-secret.yaml
This --dry-run command will generate the YAML for you. Let’s look at it.
apiVersion: v1
kind: Secret
metadata:
name: my-db-credentials
type: Opaque
data:
username: cHJvZC1hZG1pbg==
password: UzBtM1RyMWNLWVM3cjBuZ1BAc3Mh
See those data fields? That’s the base64-encoded version of our strings. You can absolutely write this YAML by hand, but you must remember to encode the values yourself: echo -n 'prod-admin' | base64. Notice the -n flag? That’s critical. It removes the trailing newline character, which would otherwise get encoded and cause all sorts of frustrating mismatches when your app tries to use the password. Forgetting -n is a classic “why doesn’t my auth work?!” pitfall.
Using Secrets in Pods
Now, how do you get this sensitive data into your application? The same two ways as ConfigMaps: environment variables and volume mounts. Environment variables are convenient but often riskier; they can be printed in logs or shown in shell history. Mounting as a file is generally the more secure and idiomatic way.
As Environment Variables:
apiVersion: v1
kind: Pod
metadata:
name: my-app
spec:
containers:
- name: app
image: my-app:1.0
env:
- name: DB_USERNAME
valueFrom:
secretKeyRef:
name: my-db-credentials
key: username
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: my-db-credentials
key: password
As a Volume Mount (The Safer Bet):
apiVersion: v1
kind: Pod
metadata:
name: my-app
spec:
containers:
- name: app
image: my-app:1.0
volumeMounts:
- name: creds-volume
mountPath: "/etc/app-secrets"
readOnly: true
volumes:
- name: creds-volume
secret:
secretName: my-db-credentials
This will create two files in /etc/app-secrets: username and password. Your application just needs to read the files. This avoids the environment variable pitfalls and is the method most security teams prefer.
The stringData Field: A Useful Illusion
Sometimes, managing the base64 encoding is annoying for literal values in YAML. Enter stringData. This field is write-only. You put plain text in, and Kubernetes will read it, base64-encode it, and merge it into the data field when you create/update the Secret. It’s a convenience feature.
apiVersion: v1
kind: Secret
metadata:
name: example-with-stringdata
type: Opaque
stringData:
config.yaml: |
database:
host: "db.prod.svc.cluster.local"
port: 5432
If you get this secret, the data field will contain a base64-encoded blob of that YAML block. stringData will not be present in the output. It’s a great trick for injecting larger blocks of configuration that contain sensitive values.
Best Practices and The Eternal Vigilance
- RBAC, RBAC, RBAC: The primary security for Secrets is Kubernetes Role-Based Access Control. Lock down who can
get,list, andcreatesecrets in each namespace. If an attacker can’t access the API, they can’t read your secrets. - Use dedicated Secret types: We used
type: Opaque(the generic catch-all), but Kubernetes has built-in types forkubernetes.io/service-account-token,kubernetes.io/dockerconfigjson(for private image registries), andkubernetes.io/tls. Using these can sometimes enable additional integrations or validation. - Consider External Secrets: For a truly robust setup, especially in a large organization, look into the External Secrets Operator. It allows you to sync secrets from a real secret manager (like AWS Secrets Manager, HashiCorp Vault, or Azure Key Vault) into your Kubernetes cluster. This is the professional move, as it gives you proper auditing, rotation, and access policies.
- Rotate, Rotate, Rotate: Secrets aren’t set-and-forget. You need a process to rotate them. Remember, if you update a Secret, any Pods using it will eventually get the update, but the timing depends on if it’s mounted as a volume (kubelet periodically updates it) or used as an env variable (only updated on Pod restart). This is a crucial nuance in your rotation strategy.
So, use Secrets. They’re the right tool for the job. Just go in with your eyes wide open, knowing their base64-encoded, RBAC-protected reality. They’re a significant step up from a ConfigMap, but they’re not a silver bullet.