Secrets

Manage sensitive data like passwords and API keys using Kubernetes Secrets. Learn best practices for secret management.

7 min read

Secrets

In the Previous Tutorial, we learned how to use ConfigMaps for configuration. But there's one thing ConfigMaps should NEVER hold — sensitive data. Passwords, API keys, certificates — these need special treatment.

You can't just throw your database password in a ConfigMap. They're stored in plain text, visible to anyone with cluster access. That's like writing your PIN on a post-it note and sticking it to your debit card.

Secrets provide a way to store and manage sensitive data with some additional safeguards.

What is a Secret?

So Secrets are encrypted ConfigMaps?

Not exactly — and this is important. A Secret is like a ConfigMap, but for sensitive data. Key differences:

  • Data is base64-encoded (not encrypted by default!)
  • Access can be restricted via RBAC
  • Kubernetes avoids writing Secrets to disk when possible
  • Can be encrypted at rest with additional configuration

Important reality check: Kubernetes Secrets are base64-encoded, NOT encrypted. Anyone with API access can decode them in about 2 seconds. For true security, use external secret managers (HashiCorp Vault, AWS Secrets Manager) or enable encryption at rest.

Wait, base64 isn't encryption? What's the point then?

Yeah, base64 is just encoding — like pig latin for data. The point of Secrets isn't encryption, it's separation of concerns and access control. Kubernetes can restrict who can read Secrets, even if the encoding itself is trivial to reverse.

Secret Types

TypeUse Case
OpaqueGeneric secrets (default)
kubernetes.io/basic-authUsername/password
kubernetes.io/ssh-authSSH private key
kubernetes.io/tlsTLS certificate and key
kubernetes.io/dockerconfigjsonDocker registry credentials

Create a Secret

Just like ConfigMaps, there are several ways to create Secrets. Let's go through them.

Method 1: From Literal Values

The quick way: kubectl create secret generic db-credentials
--from-literal=username=admin
--from-literal=password=supersecret123


Check it:

```bash
kubectl get secret db-credentials -o yaml

Output:

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

The values are base64-encoded. Look at that gibberish! Let's decode them to prove they're reversible:

echo "c3VwZXJzZWNyZXQxMjM=" | base64 -d
# Output: supersecret123

See? Two seconds. That's why base64 isn't security — it's just data encoding.

Method 2: From Files

Create files with secret data:

echo -n 'admin' > ./username.txt
echo -n 'supersecret123' > ./password.txt

Create Secret:

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

Clean up the files:

rm ./username.txt ./password.txt

Method 3: YAML Definition

You can define Secrets in YAML, but you must base64-encode values yourself. A bit annoying, but here goes:

echo -n 'admin' | base64
# YWRtaW4=

echo -n 'supersecret123' | base64
# c3VwZXJzZWNyZXQxMjM=

Create db-secret.yaml:

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

Or use stringData to avoid manual encoding (much nicer!):

apiVersion: v1
kind: Secret
metadata:
  name: db-credentials
type: Opaque
stringData:
  username: admin
  password: supersecret123

Kubernetes encodes stringData automatically. No manual base64 gymnastics! Apply:

kubectl apply -f db-secret.yaml

Use Secrets in Pods

This works exactly like ConfigMaps — same patterns, different object name. If you understood ConfigMaps, you already know this.

1. Environment Variables (Single Keys)

apiVersion: v1
kind: Pod
metadata:
  name: app-with-secrets
spec:
  containers:
  - name: app
    image: nginx
    env:
    - name: DB_USERNAME
      valueFrom:
        secretKeyRef:
          name: db-credentials
          key: username
    - name: DB_PASSWORD
      valueFrom:
        secretKeyRef:
          name: db-credentials
          key: password

Apply and verify:

kubectl apply -f app-pod.yaml
kubectl exec app-with-secrets -- env | grep DB_
DB_USERNAME=admin
DB_PASSWORD=supersecret123

2. Environment Variables (All Keys)

apiVersion: v1
kind: Pod
metadata:
  name: app-all-secrets
spec:
  containers:
  - name: app
    image: nginx
    envFrom:
    - secretRef:
        name: db-credentials

3. Volume Mounts

Mount Secrets as files:

apiVersion: v1
kind: Pod
metadata:
  name: app-secret-volume
spec:
  containers:
  - name: app
    image: nginx
    volumeMounts:
    - name: secret-volume
      mountPath: /etc/secrets
      readOnly: true
  volumes:
  - name: secret-volume
    secret:
      secretName: db-credentials

Each key becomes a file inside the container:

kubectl apply -f app-secret-volume.yaml
kubectl exec app-secret-volume -- ls /etc/secrets
# Output: password  username

kubectl exec app-secret-volume -- cat /etc/secrets/password
# Output: supersecret123

The files contain the decoded values, not the base64 gibberish. Kubernetes handles the decoding for you.

Set file permissions for extra security:

volumes:
- name: secret-volume
  secret:
    secretName: db-credentials
    defaultMode: 0400  # Read-only for owner

TLS Secrets

Store TLS certificates for HTTPS.

Create a self-signed certificate (for testing):

openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
  -keyout tls.key -out tls.crt \
  -subj "/CN=example.com"

Create the Secret:

kubectl create secret tls my-tls-secret \
  --cert=tls.crt \
  --key=tls.key

Or with YAML:

apiVersion: v1
kind: Secret
metadata:
  name: my-tls-secret
type: kubernetes.io/tls
data:
  tls.crt: <base64-encoded-cert>
  tls.key: <base64-encoded-key>

Use in a Pod:

volumeMounts:
- name: tls
  mountPath: /etc/tls
  readOnly: true
volumes:
- name: tls
  secret:
    secretName: my-tls-secret

Docker Registry Secrets

Pull images from private registries.

Create the Secret:

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

Use in a Pod:

apiVersion: v1
kind: Pod
metadata:
  name: private-image-pod
spec:
  containers:
  - name: app
    image: registry.example.com/myapp:latest
  imagePullSecrets:
  - name: my-registry-secret

Or attach to a ServiceAccount (applies to all Pods using that account):

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

Practical Example: Database Connection

Let's put it all together with a real-world scenario — running a Postgres database with credentials from a Secret.

Create a Secret for database credentials:

apiVersion: v1
kind: Secret
metadata:
  name: postgres-credentials
type: Opaque
stringData:
  POSTGRES_USER: appuser
  POSTGRES_PASSWORD: "P@ssw0rd!2024"
  POSTGRES_DB: myapp

Use in a Deployment:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: postgres
spec:
  replicas: 1
  selector:
    matchLabels:
      app: postgres
  template:
    metadata:
      labels:
        app: postgres
    spec:
      containers:
      - name: postgres
        image: postgres:15
        envFrom:
        - secretRef:
            name: postgres-credentials
        ports:
        - containerPort: 5432

The database container reads credentials from environment variables automatically. No hardcoded passwords anywhere. Your security team will love you.

Security Best Practices

Alright, let's get serious for a moment. Secrets are only as secure as how you handle them.

1. Enable Encryption at Rest

By default, Secrets are stored unencrypted in etcd. Yeah, that's not great. Enable encryption:

# encryption-config.yaml (on control plane)
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
  - resources:
      - secrets
    providers:
      - aescbc:
          keys:
            - name: key1
              secret: <base64-encoded-32-byte-key>
      - identity: {}

2. Use RBAC

Restrict who can read Secrets:

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: secret-reader
rules:
- apiGroups: [""]
  resources: ["secrets"]
  resourceNames: ["db-credentials"]  # Specific secret only
  verbs: ["get"]

3. Avoid Secrets in Pod Specs

Never put Secret values directly in your Pod YAML. That defeats the whole purpose!

# Bad — value visible in Pod spec (why even use Secrets??)
env:
- name: PASSWORD
  value: "mysecretpassword"

# Good — value hidden in Secret object
env:
- name: PASSWORD
  valueFrom:
    secretKeyRef:
      name: my-secret
      key: password

4. Use External Secret Managers

For production, Kubernetes Secrets alone might not cut it. Consider the big guns:

  • HashiCorp Vault with the Vault Agent Injector
  • AWS Secrets Manager with External Secrets Operator
  • Azure Key Vault with CSI driver
  • Sealed Secrets for GitOps workflows

These provide real encryption, rotation, auditing, and all the security goodies.

5. Rotate Secrets Regularly

Update Secrets periodically. For volume mounts, Pods pick up changes automatically. For environment variables, restart the Pods.

Inspect Secrets

# List Secrets
kubectl get secrets

# View Secret (encoded)
kubectl get secret db-credentials -o yaml

# Decode a specific value
kubectl get secret db-credentials -o jsonpath='{.data.password}' | base64 -d

# Describe (shows metadata, not values)
kubectl describe secret db-credentials

Clean Up

kubectl delete pod app-with-secrets app-all-secrets app-secret-volume 2>/dev/null
kubectl delete secret db-credentials db-credentials-file my-tls-secret 2>/dev/null
rm tls.key tls.crt 2>/dev/null

What's Next?

You've learned to manage configuration (ConfigMaps) and sensitive data (Secrets). But here's another problem — containers are stateless. When they restart, all data is lost. Your database just lost everything? Oops.

In the next tutorial, you'll learn about Persistent Volumes — how to give your applications durable storage that survives Pod restarts. Because data matters. Let's go!