Skip to main content
Updated Feb 23, 2026

Secrets Management

In January 2023, CircleCI disclosed a security incident affecting thousands of customer projects. Attackers had accessed stored secrets—API keys, OAuth tokens, SSH keys—because they were stored as environment variables visible in build logs and process listings. The incident affected companies from startups to enterprises, requiring emergency credential rotation across their entire infrastructure.

Your Task API needs database credentials, API keys, and service tokens. How you store and consume these secrets determines whether a container escape becomes a minor incident or a catastrophic breach. Environment variables—the most common approach—are also the most dangerous because they appear in process listings, crash dumps, and debugging output.

This lesson teaches you the secure pattern: volume mounts for secret consumption. You'll also understand where Kubernetes Secrets fit in the broader secrets hierarchy, from development conveniences to production-grade external secret managers.


The Problem with Environment Variables

Before learning the secure pattern, understand why the common pattern fails.

Why Teams Use Environment Variables

Environment variables are convenient:

# DANGEROUS - commonly used but insecure
containers:
- name: task-api
env:
- name: DATABASE_PASSWORD
valueFrom:
secretKeyRef:
name: db-credentials
key: password

The secret value becomes accessible via $DATABASE_PASSWORD in your application. Simple, readable, supported by every framework. But fundamentally insecure.

Environment Variable Attack Surface

Exposure VectorHow Secrets Leak
Process listingps auxe shows environment variables to any user on the node
Container inspectiondocker inspect exposes all env vars in plaintext
Crash dumpsCore dumps include environment in memory snapshot
LoggingLibraries often log startup configuration including env vars
Child processesSpawned processes inherit parent's environment
Kubernetes APIPod spec in etcd contains env var values

A single debugging command can expose your production database password:

# Anyone with node access can see container environment
docker inspect task-api-container | grep -A 50 "Env"

Output:

"Env": [
"DATABASE_PASSWORD=SuperSecretPassword123",
"API_KEY=sk-prod-abc123xyz..."
]

Volume Mounts: The Secure Alternative

Volume mounts project secrets as files inside the container. This approach eliminates most environment variable attack vectors.

How Volume Mounts Work

┌─────────────────────────────────────────────────────────────┐
│ Volume Mount Pattern │
├─────────────────────────────────────────────────────────────┤
│ │
│ K8s Secret ──► Volume ──► File in Container │
│ │
│ db-credentials /etc/secrets/ │
│ ├── username ──► ├── username (file containing value)│
│ └── password ──► └── password (file containing value)│
│ │
│ App reads: open("/etc/secrets/password").read() │
└─────────────────────────────────────────────────────────────┘

Your application reads credentials from files instead of environment variables. This simple change eliminates five of the six exposure vectors above.

Security Comparison

ConcernEnvironment VariablesVolume Mounts
Process listing (ps)ExposedNot visible
Container inspectionExposedNot visible
Child process inheritanceInheritedNot inherited
Crash dumpsIncludedNot included
Logging frameworksOften loggedRarely logged
Kubernetes etcdStored in pod specStored in Secret only
File permissionsN/AConfigurable (0400)

Volume mounts keep secrets out of the execution environment entirely. They exist only as files with restricted permissions.


Creating Kubernetes Secrets

Kubernetes Secrets store sensitive data as key-value pairs. Values are base64-encoded (NOT encrypted) in etcd.

Method 1: Create from Literals

For individual values, use --from-literal:

kubectl create secret generic db-credentials \
-n task-api \
--from-literal=username=task_api_user \
--from-literal=password='SuperSecretPassword123!'

Output:

secret/db-credentials created

Verify the Secret exists:

kubectl get secret db-credentials -n task-api -o yaml

Output:

apiVersion: v1
kind: Secret
metadata:
name: db-credentials
namespace: task-api
type: Opaque
data:
password: U3VwZXJTZWNyZXRQYXNzd29yZDEyMyE=
username: dGFza19hcGlfdXNlcg==

The values are base64-encoded. Decode to verify:

echo "U3VwZXJTZWNyZXRQYXNzd29yZDEyMyE=" | base64 -d

Output:

SuperSecretPassword123!
Base64 is NOT Encryption

Base64 encoding is reversible by anyone. It provides zero security—only encoding convenience. Anyone with kubectl get secret access can decode your credentials instantly. Kubernetes stores Secrets in etcd, which should have encryption-at-rest enabled in production.

Method 2: Create from Files

For certificates, keys, or complex credentials, use --from-file:

# Create files with credentials
echo -n "task_api_user" > username.txt
echo -n "SuperSecretPassword123!" > password.txt

# Create Secret from files
kubectl create secret generic db-credentials-file \
-n task-api \
--from-file=username=username.txt \
--from-file=password=password.txt

# Clean up plaintext files immediately
rm username.txt password.txt

Output:

secret/db-credentials-file created

The --from-file approach is essential for multi-line values like TLS certificates:

kubectl create secret tls task-api-tls \
-n task-api \
--cert=server.crt \
--key=server.key

Output:

secret/task-api-tls created

Consuming Secrets via Volume Mount

Mount your Secret as a volume in the pod spec:

apiVersion: apps/v1
kind: Deployment
metadata:
name: task-api
namespace: task-api
spec:
replicas: 2
selector:
matchLabels:
app: task-api
template:
metadata:
labels:
app: task-api
spec:
serviceAccountName: task-api-sa
containers:
- name: task-api
image: ghcr.io/your-org/task-api:v1.0.0
ports:
- containerPort: 8000
volumeMounts:
- name: db-credentials
mountPath: /etc/secrets/db
readOnly: true
volumes:
- name: db-credentials
secret:
secretName: db-credentials
defaultMode: 0400 # Read-only for owner

Key elements:

ElementPurpose
volumeMounts.mountPathDirectory where secrets appear as files
volumeMounts.readOnly: truePrevent accidental writes
volumes.secret.secretNameReference to your Secret
defaultMode: 0400File permissions (owner read-only)

Apply the deployment:

kubectl apply -f task-api-deployment.yaml

Output:

deployment.apps/task-api created

Verify Volume Mount

Check that secrets are mounted correctly:

kubectl exec -n task-api deploy/task-api -- ls -la /etc/secrets/db/

Output:

total 0
drwxrwxrwt 3 root root 120 Jan 15 10:30 .
drwxr-xr-x 3 root root 60 Jan 15 10:30 ..
lrwxrwxrwx 1 root root 15 Jan 15 10:30 password -> ..data/password
lrwxrwxrwx 1 root root 15 Jan 15 10:30 username -> ..data/username

Read the secret value:

kubectl exec -n task-api deploy/task-api -- cat /etc/secrets/db/password

Output:

SuperSecretPassword123!

Application Code Pattern

Your application reads credentials from files:

# Python example
def get_db_password():
with open('/etc/secrets/db/password', 'r') as f:
return f.read().strip()

# Use in connection string
db_url = f"postgresql://{get_db_username()}:{get_db_password()}@postgres:5432/tasks"
// TypeScript example
import { readFileSync } from 'fs';

function getDbPassword(): string {
return readFileSync('/etc/secrets/db/password', 'utf-8').trim();
}

Mounting Specific Keys

You can mount individual keys to specific file paths:

volumes:
- name: db-credentials
secret:
secretName: db-credentials
items:
- key: password
path: db-password # Creates /etc/secrets/db/db-password
- key: username
path: db-username # Creates /etc/secrets/db/db-username

This is useful when your application expects specific filenames.


The Secrets Management Hierarchy

Kubernetes Secrets are the foundation, but production environments need more:

┌─────────────────────────────────────────────────────────────┐
│ Secrets Management Hierarchy │
├─────────────────────────────────────────────────────────────┤
│ │
│ Level 3: External Secrets Operator (Production) │
│ ├── Syncs from: Vault, AWS Secrets Manager, Azure KV │
│ ├── Features: Rotation, audit, centralized management │
│ └── Best for: Multi-cluster, enterprise compliance │
│ │ │
│ Level 2: Sealed Secrets (GitOps) │
│ ├── Encrypted secrets safe for git repositories │
│ ├── Features: GitOps-compatible, cluster-specific keys │
│ └── Best for: GitOps workflows, single-cluster │
│ │ │
│ Level 1: K8s Secrets (Development) │
│ ├── Base64-encoded in etcd │
│ ├── Features: Simple, native, no external dependencies │
│ └── Best for: Development, quick prototypes │
│ │
└─────────────────────────────────────────────────────────────┘

When to Use Each Level

ScenarioRecommended LevelWhy
Local developmentK8s SecretsSimple, no setup required
Single-cluster GitOpsSealed SecretsEncrypted secrets in git
Multi-cluster productionExternal Secrets OperatorCentralized management
Compliance requirements (SOC2, HIPAA)External Secrets OperatorAudit trails, rotation
Secret rotation neededExternal Secrets OperatorAutomatic sync

Sealed Secrets Overview

Sealed Secrets (Bitnami) encrypts secrets with a cluster-specific key. The encrypted SealedSecret resource is safe to commit to git:

# This is SAFE to commit to git
apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
name: db-credentials
namespace: task-api
spec:
encryptedData:
password: AgBy8hCi...long-encrypted-string...
username: AgCtr4Kx...long-encrypted-string...

Only the controller running in your cluster can decrypt it. Different clusters have different keys, so secrets stay cluster-specific.

External Secrets Operator Overview

External Secrets Operator (ESO) syncs secrets from external stores into Kubernetes:

# ExternalSecret definition
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: db-credentials
namespace: task-api
spec:
refreshInterval: 1h
secretStoreRef:
name: vault-backend
kind: SecretStore
target:
name: db-credentials
data:
- secretKey: password
remoteRef:
key: secret/data/task-api/db
property: password

ESO creates a standard Kubernetes Secret that your pods consume via volume mount—the same pattern you learned above. The difference is where the source of truth lives (external vault) and that ESO handles rotation automatically.


Task API Secret Configuration

Here's the complete Secret and volume mount configuration for Task API. Create task-api-secrets.yaml:

---
apiVersion: v1
kind: Secret
metadata:
name: task-api-secrets
namespace: task-api
type: Opaque
stringData: # stringData auto-encodes to base64
db-password: "YourSecurePassword123!"
api-key: "sk-prod-your-api-key-here"
jwt-secret: "your-jwt-signing-secret"
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: task-api
namespace: task-api
spec:
replicas: 2
selector:
matchLabels:
app: task-api
template:
metadata:
labels:
app: task-api
spec:
serviceAccountName: task-api-sa
automountServiceAccountToken: false
securityContext:
runAsNonRoot: true
runAsUser: 1000
fsGroup: 1000
containers:
- name: task-api
image: ghcr.io/your-org/task-api:v1.0.0
ports:
- containerPort: 8000
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
volumeMounts:
- name: secrets
mountPath: /etc/secrets
readOnly: true
- name: tmp
mountPath: /tmp
volumes:
- name: secrets
secret:
secretName: task-api-secrets
defaultMode: 0400
- name: tmp
emptyDir: {}

Apply the configuration:

kubectl apply -f task-api-secrets.yaml

Output:

secret/task-api-secrets created
deployment.apps/task-api created

Reflect on Your Skill

Test your cloud-security skill against secrets management:

Using my cloud-security skill, generate a Secret and Deployment
configuration for an application that needs:
- Database credentials (username and password)
- An external API key
- A TLS certificate and key

Use volume mounts, not environment variables.

Evaluation questions:

  1. Does your skill default to volume mounts instead of environment variables?
  2. Does your skill set defaultMode: 0400 for restrictive file permissions?
  3. Does your skill include the readOnly: true flag on volumeMounts?
  4. Does your skill warn about base64 encoding vs encryption?
  5. Does your skill mention when to escalate to Sealed Secrets or ESO?

If any answers are "no," update your skill with the patterns from this lesson.


Try With AI

Practice secrets management patterns and troubleshooting.

Prompt 1:

My application can't read secrets from /etc/secrets/db/password.
The file doesn't exist. Here's my pod spec:

volumeMounts:
- name: credentials
mountPath: /etc/secrets/db
volumes:
- name: credentials
secret:
secretName: database-creds

The Secret database-creds exists. What's wrong?

What you're learning: Common volume mount debugging. The issue could be namespace mismatch (Secret in different namespace than pod), Secret key names don't match expected paths, or the Secret was created after the pod started. The skill should walk through verification steps: checking Secret exists in same namespace, verifying key names, and restarting the pod if Secret was created late.

Prompt 2:

Convert this environment variable configuration to use volume mounts:

containers:
- name: app
env:
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: db-creds
key: password
- name: API_KEY
valueFrom:
secretKeyRef:
name: api-creds
key: key

What you're learning: Migration pattern from environment variables to volume mounts. Notice how the conversion requires both volumeMount entries and volume definitions. The application code also needs updating to read from files instead of environment variables.

Prompt 3:

Our security team requires that all secrets:
1. Rotate every 90 days automatically
2. Have audit trails for access
3. Work across 5 Kubernetes clusters

Should we use K8s Secrets, Sealed Secrets, or External Secrets Operator?
Explain your recommendation.

What you're learning: How to select the appropriate level in the secrets hierarchy based on requirements. Rotation and audit requirements point to External Secrets Operator with a backend like HashiCorp Vault. Multi-cluster also favors centralized management. The skill should explain why K8s Secrets and Sealed Secrets don't meet these requirements.

Security Reminder

Never commit plaintext secrets to git, even temporarily. Use kubectl create secret imperatively or Sealed Secrets for GitOps workflows. Base64 encoding provides zero security—anyone with cluster access can decode your secrets instantly.