Module 4.2: Secrets
Complexity:
[MEDIUM]- Similar to ConfigMaps but with security considerationsTime to Complete: 40-50 minutes
Prerequisites: Module 4.1 (ConfigMaps), understanding of base64 encoding
Learning Outcomes
Section titled “Learning Outcomes”After completing this module, you will be able to:
- Create Secrets using
kubectl create secretfor generic, TLS, and docker-registry types - Configure pods to consume Secrets as environment variables and volume mounts securely
- Explain how Kubernetes stores Secrets (base64, not encrypted) and the security implications
- Debug authentication failures caused by incorrectly encoded or missing Secret data
Why This Module Matters
Section titled “Why This Module Matters”Secrets store sensitive data like passwords, API keys, and TLS certificates. While similar to ConfigMaps in usage, Secrets have additional security features and are designed specifically for sensitive information.
The CKAD exam tests your ability to:
- Create Secrets from literals, files, and YAML
- Consume Secrets as environment variables and volumes
- Understand different Secret types
- Know the security implications
The Safe Deposit Box Analogy
If ConfigMaps are like a public bulletin board, Secrets are like safe deposit boxes. The data is still accessible to authorized parties (pods), but it’s stored more carefully, handled differently, and you wouldn’t put anything there that you’d be comfortable posting publicly.
Creating Secrets
Section titled “Creating Secrets”Generic Secrets (from Literals)
Section titled “Generic Secrets (from Literals)”# Single key-valuek create secret generic db-secret --from-literal=password=mysecretpassword
# Multiple key-valuesk create secret generic db-secret \ --from-literal=username=admin \ --from-literal=password=mysecretpassword \ --from-literal=host=db.example.comFrom Files
Section titled “From Files”# Create files with sensitive dataecho -n 'admin' > username.txtecho -n 'mysecretpassword' > password.txt
# Create Secret from filesk create secret generic db-secret \ --from-file=username=username.txt \ --from-file=password=password.txt
# Cleanup filesrm username.txt password.txtFrom YAML (Base64 Encoded)
Section titled “From YAML (Base64 Encoded)”apiVersion: v1kind: Secretmetadata: name: db-secrettype: Opaquedata: username: YWRtaW4= # base64 of 'admin' password: bXlzZWNyZXQ= # base64 of 'mysecret'Pause and predict: If you run
echo 'mypassword' | base64versusecho -n 'mypassword' | base64, you get different results. Why? Which one would cause your application to fail authentication?
From YAML (Plain Text with stringData)
Section titled “From YAML (Plain Text with stringData)”apiVersion: v1kind: Secretmetadata: name: db-secrettype: OpaquestringData: username: admin password: mysecretNote: stringData is write-only and converts to data (base64) when stored.
Secret Types
Section titled “Secret Types”| Type | Purpose |
|---|---|
Opaque | Default, arbitrary user data |
kubernetes.io/service-account-token | ServiceAccount tokens |
kubernetes.io/dockerconfigjson | Docker registry credentials |
kubernetes.io/tls | TLS certificate and key |
kubernetes.io/basic-auth | Basic authentication |
kubernetes.io/ssh-auth | SSH credentials |
Docker Registry Secret
Section titled “Docker Registry Secret”k create secret docker-registry my-registry \ --docker-server=registry.example.com \ --docker-username=user \ --docker-password=pass \ --docker-email=user@example.comTLS Secret
Section titled “TLS Secret”k create secret tls my-tls \ --cert=path/to/cert.pem \ --key=path/to/key.pemConsuming Secrets
Section titled “Consuming Secrets”As Environment Variables
Section titled “As Environment Variables”Single variable:
apiVersion: v1kind: Podmetadata: name: appspec: containers: - name: app image: nginx env: - name: DB_PASSWORD valueFrom: secretKeyRef: name: db-secret key: passwordAll keys as variables:
apiVersion: v1kind: Podmetadata: name: appspec: containers: - name: app image: nginx envFrom: - secretRef: name: db-secretAs Volume Files
Section titled “As Volume Files”apiVersion: v1kind: Podmetadata: name: appspec: containers: - name: app image: nginx volumeMounts: - name: secret-volume mountPath: /etc/secrets readOnly: true volumes: - name: secret-volume secret: secretName: db-secretWith Specific Permissions
Section titled “With Specific Permissions”volumes:- name: secret-volume secret: secretName: db-secret defaultMode: 0400 # Read-only for ownerMount Specific Keys
Section titled “Mount Specific Keys”volumes:- name: secret-volume secret: secretName: db-secret items: - key: password path: db-passwordBase64 Encoding/Decoding
Section titled “Base64 Encoding/Decoding”# Encodeecho -n 'mysecret' | base64# bXlzZWNyZXQ=
# Decodeecho 'bXlzZWNyZXQ=' | base64 -d# mysecret
# View secret decodedk get secret db-secret -o jsonpath='{.data.password}' | base64 -dImportant: Use -n with echo to avoid newline being encoded!
Security Considerations
Section titled “Security Considerations”What Secrets Provide
Section titled “What Secrets Provide”- Base64 encoding (not encryption!)
- Stored in etcd (can be encrypted at rest with proper config)
- RBAC protection - control who can read secrets
- Limited exposure - not shown in
kubectl getoutput
Stop and think: A colleague says “Kubernetes Secrets are encrypted, so our passwords are safe.” Are they correct? What exactly does Kubernetes do with Secret data?
What Secrets Don’t Provide
Section titled “What Secrets Don’t Provide”- Encryption by default - base64 is encoding, not encryption
- Memory protection - secrets in pods are in plain text in memory
- Log protection - apps might log secret values
Best Practices
Section titled “Best Practices”# Mount as read-onlyvolumeMounts:- name: secrets mountPath: /etc/secrets readOnly: true
# Use specific permissionsvolumes:- name: secrets secret: secretName: my-secret defaultMode: 0400Visualization
Section titled “Visualization”┌─────────────────────────────────────────────────────────────┐│ Secrets Flow │├─────────────────────────────────────────────────────────────┤│ ││ Create Secret ││ ┌─────────────────────────────────────┐ ││ │ k create secret generic db-secret │ ││ │ --from-literal=pass=mysecret │ ││ └─────────────────────────────────────┘ ││ │ ││ ▼ ││ Stored in etcd (base64) ││ ┌─────────────────────────────────────┐ ││ │ data: │ ││ │ pass: bXlzZWNyZXQ= │ ││ └─────────────────────────────────────┘ ││ │ ││ ┌─────────┴─────────┐ ││ ▼ ▼ ││ ┌──────────────┐ ┌──────────────┐ ││ │ Environment │ │ Volume │ ││ │ Variable │ │ Mount │ ││ │ │ │ │ ││ │ $PASS= │ │ /secrets/ │ ││ │ "mysecret" │ │ pass file │ ││ │ (decoded) │ │ (decoded) │ ││ └──────────────┘ └──────────────┘ ││ │└─────────────────────────────────────────────────────────────┘Secrets vs ConfigMaps
Section titled “Secrets vs ConfigMaps”| Feature | ConfigMap | Secret |
|---|---|---|
| Data encoding | Plain text | Base64 |
| Purpose | Non-sensitive config | Sensitive data |
| Size limit | 1MB | 1MB |
| Encryption at rest | No | Optional |
| Special types | No | Yes (TLS, docker-registry) |
| Mount permissions | Default | Can restrict (0400) |
Quick Reference
Section titled “Quick Reference”# Createk create secret generic NAME --from-literal=KEY=VALUEk create secret generic NAME --from-file=FILEk create secret tls NAME --cert=CERT --key=KEYk create secret docker-registry NAME --docker-server=... --docker-username=...
# View (base64 encoded)k get secret NAME -o yaml
# Decode specific keyk get secret NAME -o jsonpath='{.data.KEY}' | base64 -d
# Editk edit secret NAME
# Deletek delete secret NAMEDid You Know?
Section titled “Did You Know?”-
Base64 is not encryption. Anyone with cluster access can decode secrets. It’s just encoding to handle binary data safely in YAML.
-
Kubernetes can encrypt secrets at rest in etcd using EncryptionConfiguration. This is cluster admin setup, not CKAD scope.
-
Secrets are namespaced. A pod can only access secrets in its own namespace (unless using RBAC to allow cross-namespace access).
-
Environment variables from secrets can leak in logs, crash dumps, or when printed by apps. Volume mounts are generally safer.
Common Mistakes
Section titled “Common Mistakes”| Mistake | Why It Hurts | Solution |
|---|---|---|
Forgetting -n when encoding | Newline gets encoded with data | Always use echo -n |
| Thinking base64 is secure | Anyone can decode | Use proper RBAC + encryption at rest |
| Logging secret env vars | Secrets exposed in logs | Mount as files, don’t log |
| Not setting readOnly | Container could modify mount | Always use readOnly: true |
| Committing secrets to git | Secrets exposed in repo | Use external secret management |
-
A developer creates a Secret with
echo 'dbpass123' | base64and puts the result in a Secret YAML underdata.password. Their application connects to the database but authentication fails every time, even though the password is correct. What went wrong?Answer
The developer forgot the `-n` flag on `echo`. Without it, `echo` appends a newline character (`\n`) to the string, so the base64 encoding includes the newline. When the Secret is decoded and passed to the application, the password becomes `dbpass123\n` instead of `dbpass123`. The database rejects it because the trailing newline is part of the string. Fix: always use `echo -n 'dbpass123' | base64`, or better yet, use `stringData` in the YAML which handles encoding automatically, or create it imperatively with `kubectl create secret generic --from-literal=password=dbpass123`. -
An application pod mounts a Secret as environment variables via
envFrom. During a security incident, the team discovers the database password appears in application crash dump logs. How did this happen, and what is a more secure approach?Answer
Environment variables are part of the process environment and are captured in crash dumps, core files, and often logged by application frameworks during startup or errors. They can also be viewed with `kubectl exec pod -- env` by anyone with exec access. A more secure approach is to mount the Secret as a volume file (e.g., at `/etc/secrets/db-password`) with `readOnly: true` and restrictive permissions (`defaultMode: 0400`). The application reads the file at startup instead of relying on environment variables. File-mounted secrets don't appear in crash dumps or process environment listings, reducing the attack surface. -
You run
kubectl get secret app-creds -o yamland see values underdata:that look like gibberish. A junior developer asks if this means the secrets are encrypted. What do you tell them, and what would you recommend for actual security?Answer
The values are base64-encoded, not encrypted. Anyone can decode them with `echo 'value' | base64 -d`. Base64 is just an encoding scheme to safely represent binary data in YAML — it provides zero security. For actual protection: (1) enable etcd encryption at rest via EncryptionConfiguration so secrets are encrypted in storage, (2) use RBAC to restrict who can read secrets (`get`, `list`, `watch` on secrets), (3) consider external secret management (HashiCorp Vault, AWS Secrets Manager) for production-critical credentials, and (4) never commit Secret YAMLs to git repositories. -
You need to provide your pod with Docker registry credentials to pull a private image. You create a generic Secret with the registry username and password. The pod still fails with
ImagePullBackOff. What type of Secret should you have created instead?Answer
Image pull credentials require a `kubernetes.io/dockerconfigjson` type Secret, not a generic `Opaque` Secret. Create it with: `kubectl create secret docker-registry my-registry --docker-server=registry.example.com --docker-username=user --docker-password=pass --docker-email=user@example.com`. Then reference it in the pod spec under `imagePullSecrets: - name: my-registry`. A generic Secret doesn't have the correct format that the kubelet expects when authenticating with a container registry. The docker-registry type encodes the credentials in the specific `.dockerconfigjson` format that container runtimes understand.
Hands-On Exercise
Section titled “Hands-On Exercise”Task: Create and consume secrets in multiple ways.
Setup:
# Create a secretk create secret generic app-secret \ --from-literal=api-key=supersecretkey123 \ --from-literal=db-password=dbpass456Part 1: Environment Variables
cat << 'EOF' | k apply -f -apiVersion: v1kind: Podmetadata: name: secret-envspec: containers: - name: app image: busybox command: ['sh', '-c', 'echo "API Key: $API_KEY" && echo "DB Pass: $DB_PASSWORD" && sleep 3600'] env: - name: API_KEY valueFrom: secretKeyRef: name: app-secret key: api-key - name: DB_PASSWORD valueFrom: secretKeyRef: name: app-secret key: db-passwordEOF
k logs secret-envPart 2: Volume Mount
cat << 'EOF' | k apply -f -apiVersion: v1kind: Podmetadata: name: secret-volspec: containers: - name: app image: busybox command: ['sh', '-c', 'ls -la /secrets && cat /secrets/api-key && sleep 3600'] volumeMounts: - name: secrets mountPath: /secrets readOnly: true volumes: - name: secrets secret: secretName: app-secret defaultMode: 0400EOF
k logs secret-volPart 3: Decode Secret
# View encodedk get secret app-secret -o yaml
# Decodek get secret app-secret -o jsonpath='{.data.api-key}' | base64 -decho # newlineCleanup:
k delete pod secret-env secret-volk delete secret app-secretPractice Drills
Section titled “Practice Drills”Drill 1: Create from Literals (Target: 1 minute)
Section titled “Drill 1: Create from Literals (Target: 1 minute)”k create secret generic drill1 --from-literal=pass=secret123k get secret drill1 -o yamlk delete secret drill1Drill 2: Decode Secret (Target: 2 minutes)
Section titled “Drill 2: Decode Secret (Target: 2 minutes)”k create secret generic drill2 --from-literal=token=mytoken123k get secret drill2 -o jsonpath='{.data.token}' | base64 -dechok delete secret drill2Drill 3: Environment Variable (Target: 3 minutes)
Section titled “Drill 3: Environment Variable (Target: 3 minutes)”k create secret generic drill3 --from-literal=DB_PASS=dbsecret
cat << 'EOF' | k apply -f -apiVersion: v1kind: Podmetadata: name: drill3spec: containers: - name: app image: busybox command: ['sh', '-c', 'echo $DB_PASS && sleep 3600'] env: - name: DB_PASS valueFrom: secretKeyRef: name: drill3 key: DB_PASSEOF
k logs drill3k delete pod drill3 secret drill3Drill 4: Volume Mount (Target: 3 minutes)
Section titled “Drill 4: Volume Mount (Target: 3 minutes)”k create secret generic drill4 --from-literal=cert=CERTIFICATE_DATA
cat << 'EOF' | k apply -f -apiVersion: v1kind: Podmetadata: name: drill4spec: containers: - name: app image: busybox command: ['sh', '-c', 'cat /certs/cert && sleep 3600'] volumeMounts: - name: certs mountPath: /certs readOnly: true volumes: - name: certs secret: secretName: drill4EOF
k logs drill4k delete pod drill4 secret drill4Drill 5: YAML with stringData (Target: 3 minutes)
Section titled “Drill 5: YAML with stringData (Target: 3 minutes)”cat << 'EOF' | k apply -f -apiVersion: v1kind: Secretmetadata: name: drill5type: OpaquestringData: username: admin password: supersecretEOF
# Verify it was encodedk get secret drill5 -o yaml | grep -A2 data
# Decodek get secret drill5 -o jsonpath='{.data.password}' | base64 -decho
k delete secret drill5Drill 6: Complete Scenario (Target: 5 minutes)
Section titled “Drill 6: Complete Scenario (Target: 5 minutes)”Scenario: Deploy app with database credentials.
# Create database secretk create secret generic drill6-db \ --from-literal=MYSQL_USER=appuser \ --from-literal=MYSQL_PASSWORD=apppass123 \ --from-literal=MYSQL_DATABASE=myapp
# Deploy app using all secrets as env varscat << 'EOF' | k apply -f -apiVersion: v1kind: Podmetadata: name: drill6spec: containers: - name: app image: busybox command: ['sh', '-c', 'env | grep MYSQL && sleep 3600'] envFrom: - secretRef: name: drill6-dbEOF
k logs drill6k delete pod drill6 secret drill6-dbNext Module
Section titled “Next Module”Module 4.3: Resource Requirements - Configure CPU and memory requests and limits.