Secrets are Kubernetes' mechanism for handling sensitive data — passwords, tokens, TLS certificates, SSH keys. They look similar to ConfigMaps but have important security considerations that you must understand for both production and the CKS exam.

1. Secret Types

TypeUse CaseRequired Keys
Opaque (default)Arbitrary key-value dataNone
kubernetes.io/tlsTLS certificatestls.crt, tls.key
kubernetes.io/dockerconfigjsonPrivate registry credentials.dockerconfigjson
kubernetes.io/basic-authBasic authenticationusername, password
kubernetes.io/ssh-authSSH private keyssh-privatekey
kubernetes.io/service-account-tokenServiceAccount token (legacy)Auto-generated
bootstrap.kubernetes.io/tokenBootstrap tokens (kubeadm join)token-id, token-secret

Creating Secrets

# Opaque (generic):
kubectl create secret generic db-creds \
  --from-literal=username=admin \
  --from-literal=password='S3cr3t!Pass'

# TLS:
kubectl create secret tls web-tls \
  --cert=./tls.crt \
  --key=./tls.key

# Docker registry:
kubectl create secret docker-registry regcred \
  --docker-server=registry.example.com \
  --docker-username=user \
  --docker-password=pass \
  --docker-email=user@example.com

Declarative YAML

apiVersion: v1
kind: Secret
metadata:
  name: db-creds
type: Opaque
data:                              # ← base64 ENCODED (not encrypted!)
  username: YWRtaW4=               # echo -n "admin" | base64
  password: UzNjcjN0IVBhc3M=      # echo -n "S3cr3t!Pass" | base64

# Alternative: use stringData (plain text, converted to base64 on create):
stringData:                        # ← plain text (convenience)
  username: admin
  password: S3cr3t!Pass
data values are base64 encoded, NOT encrypted. Anyone who can read the Secret object can decode the values: echo "YWRtaW4=" | base64 -dadmin. Base64 is encoding, not security. Real security comes from encryption at rest and RBAC (covered later).

2. Consuming Secrets in Pods

Same methods as ConfigMaps — env vars or volume mounts — with one key difference in how volumes work.

As Environment Variables

spec:
  containers:
    - name: app
      image: myapp:latest
      env:
        - name: DB_USERNAME
          valueFrom:
            secretKeyRef:
              name: db-creds
              key: username
        - name: DB_PASSWORD
          valueFrom:
            secretKeyRef:
              name: db-creds
              key: password
              optional: false      # Pod fails to start if Secret missing

As a Volume Mount

spec:
  containers:
    - name: app
      image: myapp:latest
      volumeMounts:
        - name: tls-certs
          mountPath: /etc/tls
          readOnly: true           # ← always set for secrets
  volumes:
    - name: tls-certs
      secret:
        secretName: web-tls
        defaultMode: 0400          # ← restrictive file permissions
# Result:
# /etc/tls/tls.crt  (0400, readable only by container user)
# /etc/tls/tls.key  (0400)

Key Differences from ConfigMap Volumes

FeatureConfigMap VolumeSecret Volume
Storage mediumNode disktmpfs (RAM) — never written to disk
Default file mode06440644 (set 0400 manually!)
Auto-updateYes (~60s)Yes (~60s)
Secret volumes use tmpfs. The secret data is stored in RAM only — it never touches the node's disk. This means even if the node's disk is compromised, secret data isn't there. ConfigMap volumes don't have this protection.

Image Pull Secrets

# Reference a docker-registry secret for pulling private images:
spec:
  imagePullSecrets:
    - name: regcred
  containers:
    - name: app
      image: registry.example.com/myapp:latest

# Can also be attached to a ServiceAccount (applies to all Pods using it):
kubectl patch serviceaccount default -p \
  '{"imagePullSecrets": [{"name": "regcred"}]}'

3. Security Considerations

Where Secrets Are Exposed

LocationRiskMitigation
etcd (at rest)Anyone with etcd access reads all SecretsEncryption at rest (EncryptionConfiguration)
API server (in transit)Network snoopingTLS (always on by default)
Node (kubelet)Only Secrets needed by Pods on that node are sentRBAC: limit who can create Pods in a namespace
Container environment/proc/1/environ exposes env varsPrefer volume mounts over env vars for secrets
YAML manifests in GitBase64 is not encryption — readable by anyone with repo accessSealed Secrets, SOPS, External Secrets Operator
kubectl get secret -o yamlAnyone with RBAC read access can decodeRestrict get verbs on Secrets to minimal roles
The #1 Secret leak vector: committing Secret YAML to Git. data: password: UzNjcjN0 in a repo is trivially decoded. Never store Secret manifests in version control without encryption (Sealed Secrets, SOPS, or external secrets management).

Env Vars vs Volume Mounts for Secrets

# Env vars are visible via:
kubectl exec pod -- env | grep PASSWORD
# and in /proc/1/environ on the node (if compromised)
# and in crash dumps, logging libraries that dump env

# Volume mounts:
# - Stored in tmpfs (RAM only)
# - Can set restrictive file permissions (0400)
# - Not visible in process environment
# - Not accidentally logged
Best practice: mount Secrets as files, not env vars. Environment variables leak into logs, crash dumps, and child processes. File-based Secrets are visible only to processes that explicitly open the file, with enforced file permissions.

RBAC for Secrets

# Principle of least privilege:
# Most users should NOT have "get" access to Secrets
# Use separate Role for teams that need Secret access:
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: secret-reader
  namespace: app-team
rules:
  - apiGroups: [""]
    resources: ["secrets"]
    verbs: ["get"]              # Only get, not list (list exposes all!)
    resourceNames: ["db-creds"] # Only specific secrets
The list verb on Secrets is more dangerous than get. With get, you need to know the Secret name. With list, you see ALL Secrets in the namespace. CKS exam: always prefer minimal RBAC — grant get on specific resourceNames rather than broad access.

4. Encryption at Rest

By default, Secrets are stored unencrypted in etcd (just base64). To encrypt them at rest, configure the API server with an EncryptionConfiguration:

# /etc/kubernetes/enc/encryption-config.yaml
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
  - resources:
      - secrets
    providers:
      - aescbc:                    # ← encryption provider
          keys:
            - name: key1
              secret: c2VjcmV0LWtleS0xMjM0NTY3ODkwMTIzNA==  # 32-byte base64
      - identity: {}               # ← fallback: read unencrypted (for migration)
# Enable on API server (static pod manifest):
# /etc/kubernetes/manifests/kube-apiserver.yaml
spec:
  containers:
    - command:
        - kube-apiserver
        - --encryption-provider-config=/etc/kubernetes/enc/encryption-config.yaml
      volumeMounts:
        - name: enc-config
          mountPath: /etc/kubernetes/enc
          readOnly: true
  volumes:
    - name: enc-config
      hostPath:
        path: /etc/kubernetes/enc

Encryption Providers

ProviderSecurityUse Case
identityNone (plain text)Default — no encryption
aescbcAES-CBC with PKCS#7 paddingCommon, but key is stored on disk
aesgcmAES-GCM (must rotate keys frequently)Faster, but nonce reuse risk if key not rotated
secretboxXSalsa20 + Poly1305Strong, recommended for new setups
kms v2Envelope encryption with external KMSBest — key never on disk (AWS KMS, GCP KMS, Azure Key Vault)

After Enabling Encryption

# Existing Secrets are still unencrypted! Re-write them:
kubectl get secrets --all-namespaces -o json | \
  kubectl replace -f -
# This reads and re-writes every Secret, encrypting it with the new provider

# Verify encryption:
etcdctl get /registry/secrets/default/db-creds | hexdump -C
# Should show encrypted bytes (k8s:enc:aescbc:...) not plain text
CKS exam: configuring encryption at rest is a guaranteed topic. Know: (1) create the EncryptionConfiguration file, (2) add --encryption-provider-config flag to API server, (3) re-write existing Secrets to encrypt them, (4) verify with etcdctl.
Envelope encryption (KMS) is the gold standard. The data encryption key (DEK) encrypts the Secret. The DEK itself is encrypted by an external KMS. Even if etcd is compromised, the attacker needs KMS access to decrypt. The encryption key never exists on disk.

Summary

ConceptKey Point
Secret typesOpaque, TLS, docker-registry, basic-auth, ssh-auth, SA token
Encodingbase64 in data: field — NOT encryption, just encoding
stringData:Convenience — plain text in YAML, converted to base64 on creation
tmpfs volumesSecret volumes use RAM-backed tmpfs — never written to disk
Env vars riskVisible in /proc, logs, crash dumps — prefer volume mounts
Encryption at restEncryptionConfiguration + API server flag — encrypt Secrets in etcd
KMS envelopeBest practice — DEK encrypted by external KMS, key never on disk
RBACRestrict get/list on Secrets; use resourceNames for precision
Git safetyNever commit Secret YAML to Git — use Sealed Secrets, SOPS, or ESO

📝 Quiz: Secrets

Q1: You see data: password: cGFzc3dvcmQxMjM= in a Secret YAML. Is this secure?

No. It's base64 encoded, not encrypted. Anyone can decode it: echo "cGFzc3dvcmQxMjM=" | base64 -dpassword123. Base64 provides zero security — it's just a transport encoding for binary-safe storage.

Q2: What happens to Secret data when mounted as a volume? Where does it physically reside on the node?

It's stored in a tmpfs filesystem (RAM), not on the node's disk. If the node is powered off, the data disappears. This protects against disk theft or forensic analysis of decommissioned nodes.

Q3: Why should you prefer mounting Secrets as files rather than using environment variables?

Environment variables are exposed via: (1) /proc/[pid]/environ on the node, (2) logging libraries that dump environment, (3) crash reports, (4) child processes inherit them. File-mounted Secrets have restrictive permissions, aren't in the process environment, and aren't accidentally logged.

Q4: You enabled encryption at rest with aescbc. Are existing Secrets now encrypted?

No. Encryption only applies to new writes. Existing Secrets remain unencrypted in etcd until you re-write them. Force re-encryption: kubectl get secrets --all-namespaces -o json | kubectl replace -f -. This reads each Secret (through API server) and writes it back (now encrypted).

Q5: A user has RBAC permission to create Pods in a namespace. Can they read Secrets in that namespace?

Effectively yes. They can create a Pod that mounts any Secret in the namespace and then exec into it to read the file. This is why Pod creation permission is nearly equivalent to Secret read permission. In multi-tenant clusters, isolate teams into separate namespaces and restrict Pod creation carefully.

Q6: What's the difference between data: and stringData: in a Secret manifest?

data: values must be base64 encoded by you. stringData: accepts plain text — Kubernetes base64-encodes it automatically on creation. stringData is write-only (never returned by the API — kubectl get secret -o yaml always shows data: with base64). Use stringData for convenience in manifests.