KubernetesSecretsSecurityConfigMapKubectlDevOpsBase64RBACEnvironment VariablesVolumes Beginner to Intermediate 22 min read

Kubernetes Secrets

A complete guide to Kubernetes Secrets — what they are, why they exist, how they work internally, where they are used, and how to troubleshoot real-world secret issues with hands-on examples.

📋 Table of Contents

  1. What is a Kubernetes Secret?
  2. Why Do We Need Secrets?
  3. How Kubernetes Secrets Work Internally
  4. Types of Kubernetes Secrets
  5. Creating Secrets
  6. Using Secrets in Pods
  7. Where Secrets Are Used in Real Projects
  8. Secret vs ConfigMap — When to Use What
  9. Security Best Practices
  10. Troubleshooting Secrets — Real-World Examples
  11. Summary & Quick Reference

1. What is a Kubernetes Secret?

A Kubernetes Secret is a Kubernetes object designed to store and manage sensitive information — such as passwords, API tokens, SSH keys, TLS certificates, and database credentials — separately from your application code and container images.

In Kubernetes, everything is an “object” (Pod, Deployment, Service, etc.). A Secret is just another object — but it is treated specially by the cluster:

  • It is stored in etcd (Kubernetes’ key-value store) with optional encryption at rest
  • It is only sent to the nodes that need it
  • It is held in tmpfs (memory), never written to disk on the node
  • Access to it is controlled by RBAC (Role-Based Access Control)

📌 A Simple Definition

A Secret is a Kubernetes-native way to store small pieces of sensitive data and inject them into Pods at runtime, without hardcoding them anywhere in your source code.

🔑 What Can You Store in a Secret?

Sensitive DataExample
Database passwordpostgres_password=Sup3rS3cr3t!
API keysSTRIPE_API_KEY=sk_live_abc123...
OAuth tokensGITHUB_TOKEN=ghp_...
TLS/SSL certificates.crt and .key files
SSH private keysid_rsa
Docker registry credentialsUsername + password for pulling private images
JWT signing keysJWT_SECRET=myHmacKey

📸 [IMAGE SUGGESTION] A diagram showing a Secret object in the center, with arrows pointing to: Pod (as env var), Pod (as mounted file), and etcd (storage). Label the flow clearly. Use a lock icon on the Secret object.


2. Why Do We Need Secrets?

The Problem: Hardcoded Credentials

Imagine you have a Node.js app that connects to a PostgreSQL database:

// ❌ BAD — NEVER DO THIS
const client = new Client({
  host: 'postgres.example.com',
  user: 'admin',
  password: 'SuperSecret123!',   // Hardcoded in source code!
  database: 'myapp'
});

What happens if you commit this?

Git history is permanent.
Someone forks the repo.
A contractor gets access.
GitHub scans find it.
An attacker gains database access.

Even if you delete the line and push again, the password is still visible in git history.

The Naive Fix: Environment Variables in Docker

# ❌ Still bad — the password is visible in docker inspect
ENV DB_PASSWORD=SuperSecret123!

Or in a Kubernetes manifest:

# ❌ Still bad — password in plain text in your YAML file (which lives in Git!)
env:
- name: DB_PASSWORD
  value: "SuperSecret123!"

The Kubernetes Way: Secrets

# ✅ GOOD — The actual password lives in a Secret object, not your app YAML
env:
- name: DB_PASSWORD
  valueFrom:
    secretKeyRef:
      name: db-credentials    # name of the Secret object
      key: password           # key inside the Secret

Now your application YAML is safe to commit. The actual sensitive value lives in the Secret object, which is:

  • Stored separately in etcd
  • Access-controlled via RBAC
  • Never visible in your source code or Git

Why Not Just Use ConfigMaps?

ConfigMaps also store key-value data — but they store it in plain text. Anyone with read access to your cluster can read a ConfigMap. Secrets have:

  • Base64 encoding (obfuscation, not encryption — but signals intent)
  • Restricted RBAC defaults in well-configured clusters
  • Encryption at rest support via EncryptionConfiguration
  • Audit log tracking of who accessed them

📸 [IMAGE SUGGESTION] A side-by-side comparison: Left shows “Hardcoded → Git → Breach” risk chain. Right shows “Secret object → etcd → Pod (runtime only)” safe chain. Use red/green color coding.


3. How Kubernetes Secrets Work Internally

Understanding the internals helps you debug and secure secrets properly.

3.1 Storage in etcd

When you create a Secret, Kubernetes stores it in etcd — the cluster’s distributed key-value database.

kubectl create secret → API Server → etcd
                               ↓
                     Stored as base64-encoded data
                     (optionally encrypted at rest)

By default, secrets are stored in etcd as base64-encoded values, which is not encryption. Anyone with direct etcd access can decode them. For true encryption, you must configure EncryptionConfiguration with a provider like aescbc, aesgcm, or a KMS provider.

3.2 How a Pod Gets a Secret

1. You create a Pod that references a Secret
      ↓
2. API Server validates the Pod spec
      ↓
3. Scheduler assigns the Pod to a Node
      ↓
4. kubelet on that Node fetches the Secret from the API Server
      ↓
5. kubelet injects the Secret into the Pod as:
   - Environment variable (copy into process env)
   - Volume mount (tmpfs — memory filesystem, not on disk)
      ↓
6. Container starts and reads the value

3.3 tmpfs — Why It Matters

When Secrets are mounted as volumes, Kubernetes mounts them on tmpfs — a temporary filesystem that lives in RAM, not on the node’s disk. This means:

  • Secrets are never written to disk storage on the node
  • They disappear when the node reboots
  • A disk forensic analysis cannot recover them
Pod
├── Container
│   └── /etc/secrets/db-password   ← tmpfs (in memory)
│       (NOT written to /var/lib/kubelet or any disk path)

3.4 Base64 Encoding

Secret values are stored as base64-encoded strings. This is not encryption — it’s just encoding to safely store binary data (like certificates) as text.

# Encoding
echo -n "MyPassword123" | base64
# Output: TXlQYXNzd29yZDEyMw==

# Decoding
echo "TXlQYXNzd29yZDEyMw==" | base64 -d
# Output: MyPassword123

⚠️ Important: Base64 is reversible by anyone. The security of a Secret comes from RBAC access control and etcd encryption, NOT from base64 encoding.

3.5 The Secret Lifecycle

Create Secret
     │
     ▼
Stored in etcd (base64 / encrypted)
     │
     ▼
Pod references Secret
     │
     ▼
kubelet fetches Secret → injects into Pod
     │
     ├── As ENV VAR: copied into process environment at container start
     │              (static — doesn't update if Secret changes)
     │
     └── As Volume: mounted as files on tmpfs
                    (can update automatically — kubelet syncs changes)
     │
     ▼
Pod terminates → Secret removed from node memory

📸 [IMAGE SUGGESTION] A flow diagram showing the full lifecycle: API Server → etcd → kubelet → Pod → tmpfs volume / env var. Label each component. Add a lock icon at etcd. Show the “base64 encode/decode” step clearly.


4. Types of Kubernetes Secrets

Kubernetes has several built-in Secret types. Each type tells Kubernetes (and your tools) how to interpret and use the data.

4.1 Opaque (Default)

The most common type. Arbitrary key-value pairs for any custom secret data.

apiVersion: v1
kind: Secret
metadata:
  name: my-app-secrets
type: Opaque                      # ← default type
data:
  db-password: U3VwZXJTZWNyZXQhMjM=    # base64 encoded
  api-key: c2tfdGVzdF9hYmMxMjM=

4.2 kubernetes.io/service-account-token

Automatically created by Kubernetes for each ServiceAccount. Contains a JWT token used by Pods to authenticate with the API Server.

# Every namespace has a default service account with this token
kubectl get secrets -n default
# NAME                  TYPE                                  DATA
# default-token-abc12   kubernetes.io/service-account-token   3

4.3 kubernetes.io/dockerconfigjson

Stores Docker registry credentials for pulling private container images.

kubectl create secret docker-registry my-registry-secret \
  --docker-server=registry.example.com \
  --docker-username=myuser \
  --docker-password=mypassword \
  --docker-email=me@example.com

Used in Pod specs as imagePullSecrets:

spec:
  imagePullSecrets:
  - name: my-registry-secret
  containers:
  - name: my-app
    image: registry.example.com/myapp:latest

4.4 kubernetes.io/tls

Stores TLS certificates and keys for HTTPS/SSL termination (typically used with Ingress controllers).

kubectl create secret tls my-tls-secret \
  --cert=path/to/tls.crt \
  --key=path/to/tls.key
# Used in Ingress
spec:
  tls:
  - hosts:
    - myapp.example.com
    secretName: my-tls-secret

4.5 kubernetes.io/basic-auth

For storing username and password for Basic Authentication.

apiVersion: v1
kind: Secret
type: kubernetes.io/basic-auth
data:
  username: YWRtaW4=       # admin
  password: cGFzc3dvcmQ=   # password

4.6 kubernetes.io/ssh-auth

Stores SSH private keys.

apiVersion: v1
kind: Secret
type: kubernetes.io/ssh-auth
data:
  ssh-privatekey: <base64 encoded private key>

Secret Types Summary

TypeUse Case
OpaqueGeneric — passwords, API keys, tokens
kubernetes.io/service-account-tokenPod-to-API-Server auth (auto-managed)
kubernetes.io/dockerconfigjsonPrivate Docker registry credentials
kubernetes.io/tlsTLS certificates for Ingress
kubernetes.io/basic-authHTTP basic auth credentials
kubernetes.io/ssh-authSSH private key

📸 [IMAGE SUGGESTION] A categorized card layout showing all 6 Secret types with an icon for each (lock, robot, docker whale, SSL shield, user, SSH terminal). Clean grid layout.


5. Creating Secrets

There are three ways to create Secrets: imperative (kubectl), declarative (YAML), and from files.

5.1 Method 1: Imperative — kubectl create secret

The fastest way. Kubernetes handles base64 encoding automatically.

# Create a generic (Opaque) secret with literal values
kubectl create secret generic db-credentials \
  --from-literal=username=admin \
  --from-literal=password='Sup3rS3cr3t!'

# Verify it was created
kubectl get secret db-credentials
# NAME             TYPE     DATA   AGE
# db-credentials   Opaque   2      5s

# View the secret (values are base64 encoded)
kubectl get secret db-credentials -o yaml

Output:

apiVersion: v1
data:
  password: U3VwM3JTM2NyM3Qh
  username: YWRtaW4=
kind: Secret
metadata:
  name: db-credentials
  namespace: default
type: Opaque

5.2 Method 2: From Files

Useful when the secret is already a file (certificate, SSH key, .env file).

# Create secret from a file
echo -n "Sup3rS3cr3t!" > ./db_password.txt
kubectl create secret generic db-credentials \
  --from-file=password=./db_password.txt

# Create from multiple files
kubectl create secret generic app-certs \
  --from-file=tls.crt=./server.crt \
  --from-file=tls.key=./server.key

5.3 Method 3: Declarative — YAML Manifest

When you need the Secret as code (e.g., stored in a GitOps repo with Sealed Secrets or SOPS encryption).

With data (base64 encoded values):

apiVersion: v1
kind: Secret
metadata:
  name: db-credentials
  namespace: production
type: Opaque
data:
  username: YWRtaW4=           # echo -n "admin" | base64
  password: U3VwM3JTM2NyM3Qh   # echo -n "Sup3rS3cr3t!" | base64

With stringData (plain text — Kubernetes encodes it automatically):

apiVersion: v1
kind: Secret
metadata:
  name: db-credentials
  namespace: production
type: Opaque
stringData:          # ← Use stringData for human-readable YAML
  username: admin
  password: Sup3rS3cr3t!

💡 Tip: Use stringData in your YAML files — it’s more readable and Kubernetes converts it to base64 automatically. On kubectl get, values always appear as base64 in the data field regardless.

5.4 Decode a Secret Value

# Get a specific secret value and decode it
kubectl get secret db-credentials \
  -o jsonpath='{.data.password}' | base64 -d

# Output: Sup3rS3cr3t!

6. Using Secrets in Pods

There are two primary ways to consume Secrets in your Pods.

6.1 Method 1: Environment Variables

Inject Secret values directly into the container’s process environment.

apiVersion: v1
kind: Pod
metadata:
  name: my-app-pod
spec:
  containers:
  - name: my-app
    image: my-app:1.0
    env:
    # Inject a single key from a Secret
    - name: DB_PASSWORD          # env var name in the container
      valueFrom:
        secretKeyRef:
          name: db-credentials   # Secret object name
          key: password          # key inside the Secret

    - name: DB_USERNAME
      valueFrom:
        secretKeyRef:
          name: db-credentials
          key: username

    # Or inject ALL keys from a Secret as env vars at once
    envFrom:
    - secretRef:
        name: db-credentials     # All keys become env vars

Your app then reads them normally:

import os
db_password = os.environ.get('DB_PASSWORD')
const dbPassword = process.env.DB_PASSWORD;

⚠️ Limitation of env var approach:

  • If the Secret changes, the Pod must be restarted to pick up the new value
  • The secret value is visible in kubectl describe pod (in process environment listings)
  • Environment variables can be leaked via verbose logging

Mount the Secret as files in the container filesystem (stored in tmpfs — in memory).

apiVersion: v1
kind: Pod
metadata:
  name: my-app-pod
spec:
  volumes:
  # Declare the secret as a volume
  - name: secret-volume
    secret:
      secretName: db-credentials   # Secret object name

  containers:
  - name: my-app
    image: my-app:1.0
    volumeMounts:
    # Mount the volume into the container filesystem
    - name: secret-volume
      mountPath: /etc/secrets      # Directory in the container
      readOnly: true               # Always mount secrets as readOnly

Inside the container, each key in the Secret becomes a file:

/etc/secrets/
├── username        ← contains "admin"
└── password        ← contains "Sup3rS3cr3t!"

Your app reads it like any file:

with open('/etc/secrets/password', 'r') as f:
    db_password = f.read().strip()
const fs = require('fs');
const dbPassword = fs.readFileSync('/etc/secrets/password', 'utf8').trim();

✅ Advantages of Volume Mount approach:

  • Secrets auto-update in the container when changed (kubelet syncs within ~1 minute) — no restart needed
  • Stored in tmpfs — not on disk
  • Can control file permissions with defaultMode
  • Specific keys can be projected to specific file paths

6.3 Mount Specific Keys with Custom File Names

volumes:
- name: secret-volume
  secret:
    secretName: db-credentials
    items:
    - key: password
      path: db/password.txt    # mounts as /etc/secrets/db/password.txt
      mode: 0400               # read-only for owner only
    - key: username
      path: db/username.txt

6.4 Using Secrets for Image Pull

apiVersion: v1
kind: Pod
spec:
  imagePullSecrets:
  - name: my-registry-secret   # Secret of type kubernetes.io/dockerconfigjson
  containers:
  - name: private-app
    image: private-registry.example.com/my-app:latest

📸 [IMAGE SUGGESTION] A Pod diagram with two inset boxes showing the two injection methods: (1) Arrow from Secret → env var (labeled “DB_PASSWORD=…”), (2) Arrow from Secret → volume mount (labeled “/etc/secrets/password”). Show tmpfs label on the volume mount path.


7. Where Secrets Are Used in Real Projects

7.1 Database Connections

# Secret
apiVersion: v1
kind: Secret
metadata:
  name: postgres-secret
stringData:
  DB_HOST: postgres-service
  DB_PORT: "5432"
  DB_NAME: myapp
  DB_USER: appuser
  DB_PASSWORD: Secr3tP@ss!
---
# Deployment using the secret
apiVersion: apps/v1
kind: Deployment
metadata:
  name: backend-api
spec:
  template:
    spec:
      containers:
      - name: api
        image: my-backend:v1.2
        envFrom:
        - secretRef:
            name: postgres-secret

7.2 Third-Party API Keys (Stripe, Twilio, SendGrid)

apiVersion: v1
kind: Secret
metadata:
  name: external-api-keys
stringData:
  STRIPE_SECRET_KEY: sk_live_abc123xyz
  SENDGRID_API_KEY: SG.abc123...
  TWILIO_AUTH_TOKEN: abc123...

7.3 TLS Termination with Ingress

# Create TLS secret
kubectl create secret tls myapp-tls \
  --cert=certs/tls.crt \
  --key=certs/tls.key

# Use in Ingress
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: myapp-ingress
spec:
  tls:
  - hosts:
    - myapp.example.com
    secretName: myapp-tls     # ← TLS Secret
  rules:
  - host: myapp.example.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: myapp-service
            port:
              number: 80

7.4 Private Docker Registry (Self-hosted or GCR/ECR)

# For AWS ECR
kubectl create secret docker-registry ecr-secret \
  --docker-server=123456789.dkr.ecr.us-east-1.amazonaws.com \
  --docker-username=AWS \
  --docker-password=$(aws ecr get-login-password --region us-east-1)

7.5 Git Repository Credentials (in CI/CD)

apiVersion: v1
kind: Secret
metadata:
  name: git-credentials
type: kubernetes.io/basic-auth
stringData:
  username: git-bot-user
  password: ghp_token_here

7.6 JWT / Session Signing Keys

apiVersion: v1
kind: Secret
metadata:
  name: jwt-secret
stringData:
  JWT_SECRET: "a-very-long-and-random-string-used-for-hmac-signing"
  SESSION_SECRET: "another-random-secret-for-express-sessions"

8. Secret vs ConfigMap — When to Use What

This is one of the most common questions in Kubernetes. The rule is simple:

CriterionSecretConfigMap
Sensitive data (passwords, tokens, keys)✅ Use Secret❌ Never
Non-sensitive config (URLs, feature flags, log levels)❌ Overkill✅ Use ConfigMap
Base64 encoded in etcdYesNo (plain text)
Encryption at rest supportYesNo
RBAC typically stricterYesLess so
Visible in kubectl describePartially hiddenFully visible

Decision Rule

Ask: "Would I be embarrassed if this value appeared in a log file or
     was committed to a public GitHub repo?"

YES → Use a Secret
NO  → Use a ConfigMap

Examples

# ✅ ConfigMap — non-sensitive configuration
apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config
data:
  LOG_LEVEL: "info"
  MAX_CONNECTIONS: "100"
  APP_ENV: "production"
  ALLOWED_ORIGINS: "https://myapp.com,https://api.myapp.com"

---
# ✅ Secret — sensitive credentials
apiVersion: v1
kind: Secret
metadata:
  name: app-secrets
stringData:
  DB_PASSWORD: "Secr3tP@ssw0rd"
  JWT_SECRET: "hmac-signing-key-abc123"
  REDIS_PASSWORD: "redis-pass-xyz"

📸 [IMAGE SUGGESTION] A two-column comparison table rendered as a visual card: ConfigMap (blue) on the left with non-sensitive examples, Secret (gold/yellow with lock icon) on the right with sensitive examples. Use a decision flowchart below it.


9. Security Best Practices

Kubernetes Secrets are only as secure as you make them. Here are production-grade practices.

9.1 Enable Encryption at Rest

By default, Secrets in etcd are only base64-encoded. Enable true encryption:

# /etc/kubernetes/encryption-config.yaml
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources:
  - secrets
  providers:
  - aescbc:
      keys:
      - name: key1
        secret: <base64-encoded-32-byte-key>
  - identity: {}   # fallback — allows reading unencrypted (during migration)

Pass to kube-apiserver:

--encryption-provider-config=/etc/kubernetes/encryption-config.yaml

9.2 Use RBAC to Restrict Secret Access

# Only allow the backend service account to read specific secrets
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: backend-secret-reader
  namespace: production
rules:
- apiGroups: [""]
  resources: ["secrets"]
  resourceNames: ["db-credentials", "jwt-secret"]  # specific secrets only
  verbs: ["get"]    # read-only — no list, no create
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: backend-reads-secrets
  namespace: production
subjects:
- kind: ServiceAccount
  name: backend-sa
  namespace: production
roleRef:
  kind: Role
  name: backend-secret-reader
  apiGroup: rbac.authorization.k8s.io

9.3 Never Store Secrets in Git Plaintext

Instead, use tools like:

ToolApproach
Sealed Secrets (Bitnami)Encrypt Secret before committing; only cluster can decrypt
SOPS (Mozilla)Encrypt specific values in YAML with KMS/PGP
HashiCorp VaultExternal secret management; dynamic secrets
External Secrets OperatorSync secrets from AWS SSM, GCP Secret Manager, Vault
Doppler / InfisicalSaaS secret management platforms

9.4 Rotate Secrets Regularly

Treat secrets like passwords — rotate them periodically:

# Update a secret value (patch in place)
kubectl patch secret db-credentials \
  --type='json' \
  -p='[{"op": "replace", "path": "/data/password", "value": "'$(echo -n "NewP@ssw0rd!" | base64)'"}]'

# Or recreate it
kubectl delete secret db-credentials
kubectl create secret generic db-credentials \
  --from-literal=password='NewP@ssw0rd!'

After rotation, Pods using volume-mounted secrets will pick up the change automatically within ~1 minute. Pods using env vars must be restarted.

9.5 Use readOnly: true on Volume Mounts

volumeMounts:
- name: secret-volume
  mountPath: /etc/secrets
  readOnly: true   # ← Always add this

9.6 Set Minimal File Permissions

volumes:
- name: secret-volume
  secret:
    secretName: db-credentials
    defaultMode: 0400    # ← Owner read-only (octal)

9.7 Avoid Logging Secret Values

# ❌ BAD — logs the actual secret
print(f"Connecting with password: {os.environ.get('DB_PASSWORD')}")

# ✅ GOOD — only confirm it's set
print(f"DB password set: {'yes' if os.environ.get('DB_PASSWORD') else 'no'}")

10. Troubleshooting Secrets — Real-World Examples

This section covers the most common Secret-related issues you’ll encounter in real Kubernetes environments, with exact commands and fixes.


🔴 Problem 1: Pod is in CreateContainerConfigError — Secret Not Found

Symptom:

kubectl get pods
# NAME          READY   STATUS                       RESTARTS   AGE
# my-app-pod    0/1     CreateContainerConfigError   0          30s

Diagnosis:

kubectl describe pod my-app-pod

Output shows:

Events:
  Warning  Failed  5s  kubelet  Error: secret "db-credentials" not found

Root Cause: The Pod references a Secret that doesn’t exist in the same namespace.

Fix:

# Check which namespace the pod is in
kubectl get pod my-app-pod -o jsonpath='{.metadata.namespace}'
# Output: production

# Check if the secret exists in that namespace
kubectl get secret db-credentials -n production
# Error: secrets "db-credentials" not found

# The secret exists in 'default' but not 'production'!
kubectl get secret db-credentials -n default
# NAME             TYPE     DATA   AGE
# db-credentials   Opaque   2      1d

# Fix: Recreate the secret in the correct namespace
kubectl get secret db-credentials -n default -o yaml \
  | sed 's/namespace: default/namespace: production/' \
  | kubectl apply -f -

Prevention: Always specify the namespace when creating secrets:

kubectl create secret generic db-credentials \
  --from-literal=password='Sup3rS3cr3t!' \
  -n production    # ← always specify namespace

🔴 Problem 2: Error: secret key "password" not found

Symptom: Pod starts but application crashes immediately.

Diagnosis:

kubectl logs my-app-pod
# Error: environment variable DB_PASSWORD is empty or not set

kubectl describe pod my-app-pod
# Events:
#   Warning  Failed  kubelet  Error: couldn't find key password in Secret default/db-credentials

Root Cause: The key name in the Pod spec doesn’t match the key name in the Secret.

# Check what keys actually exist in the Secret
kubectl get secret db-credentials -o jsonpath='{.data}' | python3 -m json.tool
# {
#   "db_password": "U3VwM3JTM2NyM3Qh",   ← underscore!
#   "db_username": "YWRtaW4="
# }

Pod spec has:

secretKeyRef:
  name: db-credentials
  key: password    # ← looking for "password" but Secret has "db_password"

Fix Option A: Update the Secret key name to match what Pod expects:

# Get current value
CURRENT_PASS=$(kubectl get secret db-credentials -o jsonpath='{.data.db_password}' | base64 -d)

# Recreate with correct key name
kubectl delete secret db-credentials
kubectl create secret generic db-credentials \
  --from-literal=password="$CURRENT_PASS" \
  --from-literal=username="admin"

Fix Option B: Update the Pod spec to use the correct key:

secretKeyRef:
  name: db-credentials
  key: db_password   # ← match the actual key name

🔴 Problem 3: Application Gets Wrong / Corrupted Secret Value

Symptom: Authentication fails even though the secret seems correct.

Diagnosis:

# Check the raw base64 value
kubectl get secret db-credentials -o jsonpath='{.data.password}'
# U3VwM3JTM2NyM3Qh==

# Decode it
kubectl get secret db-credentials -o jsonpath='{.data.password}' | base64 -d
# Sup3rS3cr3t!%    ← notice the % (trailing newline!) on some terminals

Root Cause: When creating secrets from files or with echo (without -n), a trailing newline gets encoded into the value.

# ❌ BAD — includes a newline character
echo "Sup3rS3cr3t!" | base64
# U3VwM3JTM2NyM3Qh Cg==  ← "Cg==" is the base64 of "\n"

# ✅ GOOD — -n flag suppresses trailing newline
echo -n "Sup3rS3cr3t!" | base64
# U3VwM3JTM2NyM3Qh

Fix:

kubectl delete secret db-credentials

# Use --from-literal (kubectl handles encoding correctly)
kubectl create secret generic db-credentials \
  --from-literal=password='Sup3rS3cr3t!'

Or if using a file:

# Strip trailing newline from file
printf '%s' 'Sup3rS3cr3t!' > ./password.txt   # printf has no trailing newline

kubectl create secret generic db-credentials \
  --from-file=password=./password.txt

🔴 Problem 4: Secret Changes Not Reflected in Running Pod

Symptom: You updated a Secret but the running application still uses the old value.

Diagnosis:

# You updated the secret
kubectl patch secret db-credentials \
  --type='merge' \
  -p '{"stringData": {"password": "NewP@ssw0rd2026!"}}'

# But the app still uses the old password
kubectl exec my-app-pod -- env | grep DB_PASSWORD
# DB_PASSWORD=OldP@ssw0rd!   ← still old value!

Root Cause: This depends on how the Secret is injected:

Injection MethodAuto-updates?Restart Required?
envFrom / env.valueFrom.secretKeyRef❌ No✅ Yes
volumeMount (files)✅ Yes (~1 min)❌ No

Environment variables are set once at container startup and never change during the container’s lifetime.

Fix for env vars — Rolling Restart:

# Restart all Pods in the Deployment to pick up new env var values
kubectl rollout restart deployment/my-app

# Watch the rollout
kubectl rollout status deployment/my-app
# Waiting for deployment "my-app" rollout to finish...
# deployment "my-app" successfully rolled out

Prevention — Use volume mounts for dynamic secrets:

# Instead of env vars, use volume mounts for secrets that rotate
volumeMounts:
- name: db-secret
  mountPath: /etc/secrets
  readOnly: true
volumes:
- name: db-secret
  secret:
    secretName: db-credentials

🔴 Problem 5: ImagePullBackOff — Private Registry Secret Issue

Symptom:

kubectl get pods
# NAME        READY   STATUS             RESTARTS
# my-app      0/1     ImagePullBackOff   0

Diagnosis:

kubectl describe pod my-app
# Events:
#   Warning  Failed  kubelet  Failed to pull image "registry.example.com/my-app:latest":
#            rpc error: unauthorized: authentication required

Root Cause: Missing or incorrect imagePullSecrets.

Fix:

# Step 1: Create the registry secret
kubectl create secret docker-registry registry-credentials \
  --docker-server=registry.example.com \
  --docker-username=myuser \
  --docker-password=mypassword \
  -n your-namespace

# Step 2: Verify the secret was created
kubectl get secret registry-credentials -o json | \
  python3 -c "import sys,json,base64; \
  d=json.load(sys.stdin); \
  print(base64.b64decode(d['data']['.dockerconfigjson']).decode())"
# Should show: {"auths":{"registry.example.com":{"username":"myuser",...}}}

# Step 3: Add imagePullSecrets to your Pod/Deployment
kubectl patch deployment my-app --patch \
  '{"spec":{"template":{"spec":{"imagePullSecrets":[{"name":"registry-credentials"}]}}}}'

# Step 4: Watch pods recover
kubectl get pods -w

Alternative — Attach imagePullSecrets to the ServiceAccount (so all Pods automatically get it):

kubectl patch serviceaccount default \
  -p '{"imagePullSecrets": [{"name": "registry-credentials"}]}'

🔴 Problem 6: RBAC — Error from server (Forbidden): secrets is forbidden

Symptom: A developer or CI/CD pipeline cannot read a secret.

kubectl get secret db-credentials
# Error from server (Forbidden): secrets "db-credentials" is forbidden:
# User "ci-bot" cannot get resource "secrets" in API group ""

Diagnosis:

# Check what permissions ci-bot has
kubectl auth can-i get secrets --as=ci-bot -n production
# no

kubectl auth can-i list secrets --as=ci-bot -n production
# no

Fix:

# Create a Role that allows reading specific secrets
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: ci-secret-reader
  namespace: production
rules:
- apiGroups: [""]
  resources: ["secrets"]
  resourceNames: ["db-credentials"]   # restrict to only this secret
  verbs: ["get"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: ci-reads-db-secret
  namespace: production
subjects:
- kind: User
  name: ci-bot
  apiGroup: rbac.authorization.k8s.io
roleRef:
  kind: Role
  name: ci-secret-reader
  apiGroup: rbac.authorization.k8s.io
kubectl apply -f ci-secret-rbac.yaml

# Verify
kubectl auth can-i get secrets/db-credentials --as=ci-bot -n production
# yes

🔴 Problem 7: Volume Mount — Permission Denied

Symptom: Container starts but cannot read secret files.

kubectl logs my-app-pod
# Error: EACCES: permission denied, open '/etc/secrets/password'

Diagnosis:

kubectl exec my-app-pod -- ls -la /etc/secrets/
# -r--------  1 root root 13 Jan 20 10:00 password    ← only root can read!

Root Cause: The secret file permissions (defaultMode) are too restrictive, and the container runs as a non-root user.

Fix Option A: Set permissive defaultMode on the secret volume:

volumes:
- name: secret-volume
  secret:
    secretName: db-credentials
    defaultMode: 0444    # ← world-readable (for non-sensitive but restricted info)
    # OR
    defaultMode: 0440    # ← owner + group readable

Fix Option B: Set fsGroup in Pod’s security context so the volume is group-accessible:

spec:
  securityContext:
    fsGroup: 1000        # ← files will be group-owned by GID 1000
  containers:
  - name: my-app
    securityContext:
      runAsUser: 1000    # ← container runs as UID 1000
    volumeMounts:
    - name: secret-volume
      mountPath: /etc/secrets
      readOnly: true

🛠️ Troubleshooting Cheat Sheet

# 1. Check if Secret exists in correct namespace
kubectl get secrets -n <namespace>

# 2. Check Secret keys and their (base64) values
kubectl get secret <name> -o yaml -n <namespace>

# 3. Decode a specific key
kubectl get secret <name> -n <namespace> \
  -o jsonpath='{.data.<key>}' | base64 -d

# 4. Check Pod events for Secret-related errors
kubectl describe pod <pod-name> | grep -A 20 "Events:"

# 5. Verify env vars inside a running container
kubectl exec <pod-name> -- env | grep <VAR_NAME>

# 6. Check if Secret is mounted correctly
kubectl exec <pod-name> -- ls -la /etc/secrets/
kubectl exec <pod-name> -- cat /etc/secrets/password

# 7. Check RBAC permissions
kubectl auth can-i get secrets -n <namespace>
kubectl auth can-i get secrets --as=<user> -n <namespace>

# 8. Describe the secret (shows metadata, not values)
kubectl describe secret <name> -n <namespace>

# 9. Force restart deployment after secret rotation
kubectl rollout restart deployment/<name> -n <namespace>

# 10. Check etcd encryption (admin only)
kubectl get secret <name> -n <namespace> -o json | \
  python3 -c "import sys,json,base64; \
  d=json.load(sys.stdin); \
  [print(k,'=', base64.b64decode(v).decode()) \
   for k,v in d['data'].items()]"

📸 [IMAGE SUGGESTION] A troubleshooting decision tree flowchart:

  • Start: “Pod has Secret issue”
  • Branch 1: CreateContainerConfigError → “Secret not found” → check namespace
  • Branch 2: App crashes → “Key not found” → check key names
  • Branch 3: Wrong value → “Trailing newline?” → recreate with –from-literal
  • Branch 4: Old value after update → “Env var?” → restart; “Volume?” → auto-updates Use color-coded boxes (red for errors, green for fixes).

11. Summary & Quick Reference

Core Concepts

ConceptDescription
WhatA K8s object to store sensitive data (passwords, tokens, certs)
WhySeparate sensitive config from code; enforce access control
StorageStored in etcd, base64-encoded (optionally encrypted at rest)
DeliveryInjected into Pods as env vars or volume-mounted files
SecurityProtected by RBAC; use EncryptionConfiguration for at-rest encryption

Quick Command Reference

# Create
kubectl create secret generic <name> --from-literal=key=value
kubectl create secret generic <name> --from-file=key=./file.txt
kubectl create secret tls <name> --cert=tls.crt --key=tls.key
kubectl create secret docker-registry <name> --docker-server=... --docker-username=... --docker-password=...

# Read
kubectl get secrets
kubectl get secret <name> -o yaml
kubectl get secret <name> -o jsonpath='{.data.<key>}' | base64 -d

# Update
kubectl patch secret <name> --type='merge' -p '{"stringData":{"key":"newvalue"}}'

# Delete
kubectl delete secret <name>

# Restart Pods to pick up env var changes
kubectl rollout restart deployment/<name>

Secret Injection Methods

Method 1: Environment Variable
  ✅ Simple to use
  ❌ No auto-update on secret change (requires pod restart)
  ❌ Visible via kubectl describe

Method 2: Volume Mount (Recommended for production)
  ✅ Auto-updates within ~1 minute of secret change
  ✅ Stored in tmpfs (memory only, not disk)
  ✅ Granular file permissions
  ❌ Slightly more YAML to write

Common Errors at a Glance

ErrorCauseFix
secret "x" not foundWrong namespaceRecreate secret in correct namespace
key "x" not foundKey name mismatchCheck actual keys with kubectl get secret -o yaml
Corrupted value / auth failsTrailing newline in valueUse echo -n or --from-literal
Old value after rotationEnv var (static)Restart Pods with kubectl rollout restart
ImagePullBackOffMissing imagePullSecretsCreate docker-registry secret and add to Pod spec
Forbidden: secretsRBAC not configuredCreate Role + RoleBinding
Permission denied on mountdefaultMode too restrictiveSet defaultMode: 0444 or configure fsGroup

📸 [IMAGE SUGGESTION — Summary] A single-page “Kubernetes Secrets Cheat Sheet” infographic with four quadrants: (1) Creation commands, (2) Injection methods, (3) Common errors + fixes, (4) Best practices checklist. This makes a great printable reference card.


Part of the Kubernetes Configuration & Security module. Next: Kubernetes ConfigMaps | Previous: Kubernetes Namespaces