In this episode, we'll explore Kubernetes Secrets — the native mechanism for storing and consuming sensitive data like passwords, API keys, and TLS certificates. You'll learn how Secrets work, their types, security limitations, and how to use them safely in production.

Note
If you want to read the previous episode, you can click the Episode 21.1 thumbnail below
In the previous episode, we deep-dived into PersistentVolume and PersistentVolumeClaim for durable storage. In this episode (21.2), we'll focus on Secrets — Kubernetes' native object for managing sensitive data such as database passwords, API tokens, TLS certificates, and SSH keys.
Note: Here I'll be using a Kubernetes Cluster installed through K3s.
Most real-world applications need credentials of some kind. Without a proper mechanism, developers end up hardcoding secrets in container images or environment variable lists, creating security liabilities. Kubernetes Secrets give you a structured, API-driven way to separate sensitive data from application code and configuration.
A Secret is a Kubernetes API object that stores a small amount of sensitive data in key-value pairs. Secrets are stored in etcd and can be consumed by Pods as environment variables, files via volumes, or used by the kubelet to pull private container images.
Think of Secrets like a locked safe that only authorized people can open. The application doesn't carry its own password vault — it requests access to the vault at runtime.
Key characteristics of Secrets:
EncryptionConfigurationWarning
Kubernetes Secrets are base64-encoded by default, not encrypted. Anyone with kubectl get secret access or read access to etcd can decode them trivially. Always enable encryption at rest and use strict RBAC in production.
Both Secrets and ConfigMaps store key-value data. The difference is intent and handling:
| Feature | Secret | ConfigMap |
|---|---|---|
| Intended for | Sensitive data | Non-sensitive config |
| Storage format | Base64-encoded | Plain text |
| Encryption at rest | Supported (opt-in) | Not typically encrypted |
Mounted as tmpfs | Yes (in-memory) | No |
| RBAC granularity | secrets resource | configmaps resource |
Visible in kubectl describe pod | Values hidden | Values shown |
Use ConfigMaps for application configuration. Use Secrets for anything that would be harmful if leaked: passwords, tokens, certificates, private keys.
Kubernetes supports several built-in Secret types, each designed for a specific use case.
The default type for arbitrary user-defined data.
apiVersion: v1
kind: Secret
metadata:
name: app-credentials
namespace: default
type: Opaque
data:
username: YWRtaW4= # base64 of "admin"
password: c2VjcmV0cGFzcw== # base64 of "secretpass"Or use stringData to provide values in plain text (Kubernetes encodes them automatically):
apiVersion: v1
kind: Secret
metadata:
name: app-credentials
type: Opaque
stringData:
username: admin
password: secretpassTip
Prefer stringData in your manifests — it's more readable and avoids manual base64 encoding mistakes. Kubernetes will automatically convert the values to base64 when storing them.
Used for TLS certificates and private keys. Consumed by Ingress controllers and other TLS-aware components.
apiVersion: v1
kind: Secret
metadata:
name: tls-secret
namespace: default
type: kubernetes.io/tls
data:
tls.crt: <base64-encoded-cert>
tls.key: <base64-encoded-private-key>Creating TLS secrets from files is more common:
sudo kubectl create secret tls tls-secret \
--cert=path/to/tls.crt \
--key=path/to/tls.keyUsed for authenticating to private container image registries.
sudo kubectl create secret docker-registry regcred \
--docker-server=registry.example.com \
--docker-username=myuser \
--docker-password=mypassword \
--docker-email=me@example.comReference it in a Pod spec:
apiVersion: v1
kind: Pod
metadata:
name: private-image-pod
spec:
imagePullSecrets:
- name: regcred
containers:
- name: app
image: registry.example.com/myapp:latestAutomatically created by Kubernetes for ServiceAccounts. Used by Pods to authenticate to the API server. Modern clusters use projected service account tokens instead.
| Type | Use Case |
|---|---|
Opaque | Generic key-value secrets |
kubernetes.io/tls | TLS certificates and keys |
kubernetes.io/dockerconfigjson | Registry authentication |
kubernetes.io/basic-auth | Username/password basic auth |
kubernetes.io/ssh-auth | SSH private keys |
kubernetes.io/service-account-token | ServiceAccount tokens |
bootstrap.kubernetes.io/token | Node bootstrap tokens |
# From literal values
sudo kubectl create secret generic db-credentials \
--from-literal=username=admin \
--from-literal=password=supersecret
# From files
sudo kubectl create secret generic app-certs \
--from-file=ca.crt \
--from-file=server.crt \
--from-file=server.keyapiVersion: v1
kind: Secret
metadata:
name: db-credentials
namespace: production
labels:
app: myapp
env: production
type: Opaque
stringData:
DB_HOST: postgres.production.svc.cluster.local
DB_PORT: "5432"
DB_NAME: myappdb
DB_USER: appuser
DB_PASSWORD: Sup3rS3cur3P@ssw0rd!Caution
Never commit Secret manifests with real values to version control. Your git history is permanent. Use sealed-secrets, External Secrets Operator, or Vault to manage secrets as code safely. More on this in the best practices section.
Secrets can be consumed in two ways: environment variables or volume mounts. Each has distinct trade-offs.
The simplest pattern — directly inject secret values as container environment variables.
apiVersion: v1
kind: Pod
metadata:
name: app-pod
spec:
containers:
- name: app
image: myapp:latest
env:
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: db-credentials
key: DB_PASSWORD
- name: DB_USER
valueFrom:
secretKeyRef:
name: db-credentials
key: DB_USERapiVersion: v1
kind: Pod
metadata:
name: app-pod
spec:
containers:
- name: app
image: myapp:latest
envFrom:
- secretRef:
name: db-credentialsThis mounts every key in db-credentials as an environment variable. The key names become the variable names.
Warning
Environment variables are visible in process listings (/proc/<pid>/environ) and may be logged by third-party libraries or crash reports. For maximum security, prefer volume mounts for sensitive credentials.
Secrets mounted as volumes are stored on tmpfs (in-memory filesystem), which is safer than disk. Each key becomes a file inside the mount directory.
apiVersion: v1
kind: Pod
metadata:
name: app-pod
spec:
containers:
- name: app
image: myapp:latest
volumeMounts:
- name: credentials
mountPath: /etc/secrets
readOnly: true
volumes:
- name: credentials
secret:
secretName: db-credentials
defaultMode: 0400 # owner read-onlyInside the container, the files will be:
/etc/secrets/DB_HOST
/etc/secrets/DB_PORT
/etc/secrets/DB_NAME
/etc/secrets/DB_USER
/etc/secrets/DB_PASSWORDvolumes:
- name: credentials
secret:
secretName: db-credentials
items:
- key: DB_PASSWORD
path: password.txt
mode: 0400
- key: DB_USER
path: username.txt
mode: 0444This mounts only the specified keys, with custom file names and permissions.
| Aspect | Environment Variable | Volume Mount |
|---|---|---|
| Visibility in process | cat /proc/self/environ | Not exposed |
| Storage | Memory (env) | tmpfs (in-memory) |
| Dynamic updates | Requires Pod restart | Updated without restart |
| File permissions | N/A | Configurable (mode) |
| Audit trail | Harder to audit | Easier (file access logging) |
| Best for | Simple apps, legacy compat | Production, security-sensitive |
A typical pattern: inject database credentials securely into a web application.
apiVersion: v1
kind: Secret
metadata:
name: postgres-credentials
namespace: production
type: Opaque
stringData:
POSTGRES_USER: appuser
POSTGRES_PASSWORD: Sup3rS3cur3P@ss!
POSTGRES_DB: myappdbapiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: app-ingress
namespace: production
annotations:
nginx.ingress.kubernetes.io/ssl-redirect: "true"
spec:
tls:
- hosts:
- myapp.example.com
secretName: tls-secret
rules:
- host: myapp.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: web-app
port:
number: 8080apiVersion: v1
kind: ServiceAccount
metadata:
name: app-service-account
namespace: production
imagePullSecrets:
- name: regcredAttaching the pull secret to the ServiceAccount means all Pods using this ServiceAccount automatically get access to the private registry — no need to specify imagePullSecrets on every Pod.
# Method 1: Edit directly
sudo kubectl edit secret db-credentials -n production
# Method 2: Apply updated manifest
sudo kubectl apply -f db-credentials.yml
# Method 3: Patch a specific key (base64 encode first)
NEW_PASS=$(echo -n "newpassword" | base64)
sudo kubectl patch secret db-credentials \
-p "{\"data\":{\"DB_PASSWORD\":\"${NEW_PASS}\"}}"Note
Pods consuming Secrets as volume mounts will see the updated secret within ~1 minute (kubelet sync period) without restart. Pods consuming Secrets as environment variables require a restart to pick up the new values.
By default, Kubernetes stores Secrets in etcd as base64-encoded plain text. Anyone with etcd access can read them. To enable true encryption at rest, configure an EncryptionConfiguration.
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources:
- secrets
providers:
- aescbc:
keys:
- name: key1
secret: <base64-encoded-32-byte-key>
- identity: {}Apply this by passing --encryption-provider-config flag to the API server. After enabling, encrypt existing secrets:
# Re-write all existing secrets to trigger encryption
sudo kubectl get secrets --all-namespaces -o json | \
sudo kubectl replace -f -Important
Managed Kubernetes services (GKE, EKS, AKS) typically provide encryption at rest for Secrets by default using their cloud KMS. Always verify this is enabled in your cloud provider's documentation.
Problem: Committing Secret YAML files with real values to version control. The values persist in git history even after deletion.
Solution: Use one of these approaches:
# DO NOT commit files containing real secrets
git add db-credentials.yml # ❌
git commit -m "add database secret"Problem: Granting broad get secrets access exposes all secrets in the Namespace.
Solution: Follow least privilege. Grant access only to specific secrets:
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: db-secret-reader
namespace: production
rules:
- apiGroups: [""]
resources: ["secrets"]
resourceNames: ["db-credentials"] # specific secret only
verbs: ["get"]Problem: Environment variables are visible to all processes in the container and may be logged.
Solution: Mount sensitive data as volume files with restrictive permissions:
# Less secure: env var (visible in /proc)
env:
- name: PRIVATE_KEY
valueFrom:
secretKeyRef:
name: app-secret
key: private_key
# More secure: volume mount (tmpfs, file permissions)
volumeMounts:
- name: app-secret
mountPath: /run/secrets
readOnly: trueProblem: Secret files mounted in volumes are readable by all users in the container.
Solution: Set restrictive defaultMode and per-item mode:
volumes:
- name: credentials
secret:
secretName: db-credentials
defaultMode: 0400 # owner read-onlyProblem: Static, long-lived credentials are a liability. A leaked credential remains valid indefinitely.
Solution: Implement secret rotation. Use short-lived tokens where possible (e.g., Vault dynamic secrets, AWS IAM roles for service accounts).
Don't manage secrets natively in Kubernetes at scale. Use an external secrets backend:
# External Secrets Operator syncing from AWS SSM
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: db-credentials
namespace: production
spec:
refreshInterval: 1h
secretStoreRef:
name: aws-secretsmanager
kind: ClusterSecretStore
target:
name: db-credentials
creationPolicy: Owner
data:
- secretKey: DB_PASSWORD
remoteRef:
key: /production/myapp/db-passwordAlways enable EncryptionConfiguration pointing to a KMS provider (not just aescbc with a static key) in production.
Restrict who can list and get secrets:
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: secret-reader-restricted
rules:
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get"]
# No "list" verb — prevents bulk enumerationTip
Removing the list verb prevents users from enumerating all Secret names in a Namespace — a useful defense-in-depth measure even if they can get specific secrets by name.
Always add readOnly: true to Secret volume mounts:
volumeMounts:
- name: credentials
mountPath: /etc/secrets
readOnly: trueEnable Kubernetes audit logging with a policy that captures get and watch operations on Secrets:
apiVersion: audit.k8s.io/v1
kind: Policy
rules:
- level: Metadata
resources:
- group: ""
resources: ["secrets"]Mark Secrets as immutable to prevent accidental modification and reduce API server load (Kubernetes stops watching for changes):
apiVersion: v1
kind: Secret
metadata:
name: app-credentials
type: Opaque
immutable: true
stringData:
API_KEY: my-api-key-valueKubernetes Secrets are convenient but not always the right tool:
Note
Kubernetes Secrets are fine for simple deployments and development. For production at scale, always layer them with proper encryption, external secrets management, and strict RBAC.
In episode 21.2, we've covered Kubernetes Secrets comprehensively — from what they are and their types, to how to create and consume them, to securing them properly in production.
Key takeaways:
Opaque, kubernetes.io/tls, kubernetes.io/dockerconfigjsonlist permissionsimmutable: true for secrets that don't change to reduce API server overheadProper secrets management is one of the most security-critical aspects of running Kubernetes in production. Getting it right from the start saves painful security incidents later.
Are you getting a clearer picture of how Kubernetes manages sensitive data? Keep the momentum going and look forward to the next episode!
Note
If you want to continue to the next episode, you can click the Episode 22 thumbnail below