Skip to main content
mastering ckad certified kubernetes application developer

Secrets: Types, Creation, and Security

9 min read Chapter 48 of 87
Summary

Covers the three main Secret types (Opaque, TLS,...

Covers the three main Secret types (Opaque, TLS, Docker registry), creation via kubectl and YAML, base64 encoding semantics, decoding commands, consumption methods identical to ConfigMaps, and security trade-offs between environment variables and volume mounts. Includes a complete Pod YAML consuming Secrets through both methods.

Secrets: Types, Creation, and Security

Secrets share the same structural DNA as ConfigMaps — namespaced objects storing key-value pairs — but they serve a fundamentally different purpose. Secrets are designed for sensitive data: database passwords, API tokens, SSH keys, TLS certificates. Kubernetes treats them differently at the API level: Secret values are base64-encoded in the API response, and RBAC policies can restrict Secret access independently from ConfigMap access.

That said, awareness of what Secrets are not is equally important. Base64 encoding is a reversible encoding scheme, not encryption. Anyone who can query the API server and has RBAC permission to access the Secret object can trivially decode the values. Without etcd encryption at rest (a cluster-level configuration outside CKAD scope), Secrets are stored in plaintext in etcd. The CKAD exam tests your ability to create, consume, and manage Secrets — but understanding their limitations is essential for making sound architectural decisions.

Secret Types

Kubernetes supports several built-in Secret types. The three you need for the exam:

Opaque (Default)

The generic catch-all type for arbitrary key-value data:

kubectl create secret generic db-secret \
  --from-literal=username=admin \
  --from-literal=password=s3cret

Inspect the result:

kubectl get secret db-secret -o yaml
apiVersion: v1
kind: Secret
metadata:
  name: db-secret
type: Opaque
data:
  password: czNjcmV0
  username: YWRtaW4=

The values czNjcmV0 and YWRtaW4= are base64-encoded versions of s3cret and admin. The key word here is encoded, not encrypted. We will decode them shortly.

kubernetes.io/tls

Stores a TLS certificate and its private key:

# Generate a self-signed certificate for testing
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
  -keyout tls.key -out tls.crt \
  -subj "/CN=myapp.example.com"

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

The resulting Secret has exactly two keys: tls.crt and tls.key. Kubernetes validates that the certificate and key are valid PEM-encoded data. This Secret type is commonly used with Ingress resources for TLS termination (as covered in Chapter 12).

kubectl get secret my-tls-secret -o yaml
apiVersion: v1
kind: Secret
metadata:
  name: my-tls-secret
type: kubernetes.io/tls
data:
  tls.crt: LS0tLS1CRUdJTi...
  tls.key: LS0tLS1CRUdJTi...

kubernetes.io/dockerconfigjson

Stores Docker registry credentials for pulling private images:

kubectl create secret docker-registry registry-secret \
  --docker-server=registry.example.com \
  --docker-username=deploy \
  --docker-password=token123 \
  [email protected]

Pods reference this Secret in spec.imagePullSecrets:

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

Summary of Secret Types

TypeCreated WithKeysUse Case
Opaquekubectl create secret genericArbitraryPasswords, tokens, API keys
kubernetes.io/tlskubectl create secret tlstls.crt, tls.keyTLS certificates
kubernetes.io/dockerconfigjsonkubectl create secret docker-registry.dockerconfigjsonPrivate registry auth

Creating Secrets

From Literals

kubectl create secret generic db-secret \
  --from-literal=password=s3cret \
  --from-literal=username=admin

From a File

Store the Secret value in a file and load it:

echo -n 's3cret' > password.txt
kubectl create secret generic db-secret --from-file=password=password.txt

The -n flag in echo prevents a trailing newline — a common source of bugs when mounting Secrets as files. A password file with an invisible newline character will cause authentication failures that are maddening to debug.

From YAML (Declarative)

When writing Secret manifests, you must base64-encode the values yourself:

echo -n 'admin' | base64
# YWRtaW4=

echo -n 's3cret' | base64
# czNjcmV0
apiVersion: v1
kind: Secret
metadata:
  name: db-secret
type: Opaque
data:
  username: YWRtaW4=
  password: czNjcmV0

Alternatively, use stringData to supply plaintext values — Kubernetes encodes them automatically:

apiVersion: v1
kind: Secret
metadata:
  name: db-secret
type: Opaque
stringData:
  username: admin
  password: s3cret

The stringData field is write-only: it does not appear when you read the Secret back with kubectl get. Kubernetes converts stringData entries into base64-encoded data entries on creation.

Decoding Secret Values

Retrieve and decode a specific key:

kubectl get secret db-secret -o jsonpath='{.data.password}' | base64 -d

Output:

s3cret

Decode all keys at once:

kubectl get secret db-secret -o jsonpath='{.data}' | python3 -c "
import json, base64, sys
data = json.load(sys.stdin)
for k, v in data.items():
    print(f'{k}: {base64.b64decode(v).decode()}')"

Or use kubectl with go-template:

kubectl get secret db-secret -o go-template='{{range $k, $v := .data}}{{$k}}: {{$v | base64decode}}{{"\n"}}{{end}}'

The jsonpath approach is the fastest for a single key during the exam. Memorize the pattern:

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

Consuming Secrets in Pods

Secret consumption is syntactically identical to ConfigMap consumption. The three methods — envFrom, env with valueFrom, and volume mounts — all work the same way, with secretRef and secretKeyRef replacing configMapRef and configMapKeyRef.

envFrom — All Keys as Environment Variables

envFrom:
  - secretRef:
      name: db-secret

Every key in db-secret becomes an environment variable. The values are automatically base64-decoded — your application sees the plaintext.

env with valueFrom — Specific Keys

env:
  - name: DB_PASSWORD
    valueFrom:
      secretKeyRef:
        name: db-secret
        key: password

Volume Mount — Keys as Files

volumes:
  - name: secret-volume
    secret:
      secretName: db-secret
spec:
  containers:
    - name: app
      volumeMounts:
        - name: secret-volume
          mountPath: /etc/secrets
          readOnly: true

Each key becomes a file in /etc/secrets/. The file password contains the plaintext s3cret. The file username contains admin. Setting readOnly: true is a best practice — containers should not modify mounted Secrets.

Complete Pod YAML: Secret as Env Var and Volume Mount

apiVersion: v1
kind: Pod
metadata:
  name: secret-demo
spec:
  volumes:
    - name: tls-certs
      secret:
        secretName: my-tls-secret
    - name: db-creds
      secret:
        secretName: db-secret
        items:
          - key: password
            path: db-password
  containers:
    - name: app
      image: nginx:1.25
      # Env var: specific key from Secret
      env:
        - name: DB_USER
          valueFrom:
            secretKeyRef:
              name: db-secret
              key: username
        - name: DB_PASS
          valueFrom:
            secretKeyRef:
              name: db-secret
              key: password
      volumeMounts:
        # TLS certs mounted as files
        - name: tls-certs
          mountPath: /etc/tls
          readOnly: true
        # DB password as a single file
        - name: db-creds
          mountPath: /etc/db
          readOnly: true

Create the prerequisite Secrets and apply the Pod:

# Create Opaque secret
kubectl create secret generic db-secret \
  --from-literal=username=admin \
  --from-literal=password=s3cret

# Create TLS secret (using previously generated cert/key)
kubectl create secret tls my-tls-secret --cert=tls.crt --key=tls.key

# Apply Pod
kubectl apply -f secret-demo.yaml

Verify:

# Check env vars
kubectl exec secret-demo -- env | grep DB_
# DB_USER=admin
# DB_PASS=s3cret

# Check mounted TLS files
kubectl exec secret-demo -- ls /etc/tls
# tls.crt  tls.key

# Check mounted DB password file
kubectl exec secret-demo -- cat /etc/db/db-password
# s3cret

Security: Environment Variables vs Volume Mounts

Both methods deliver Secret values to the container, but they carry different security profiles:

Environment Variables

  • Visible via inspect: kubectl exec <pod> -- env displays all environment variables, including Secrets. Container runtime inspection (docker inspect, crictl inspect) also exposes them.
  • Inherited by child processes: Any process forked from PID 1 inherits the full environment, including Secret values. A debug sidecar or a log collector running in the same Pod sees everything.
  • Logged accidentally: Applications that log their environment on startup (common in Java/Spring Boot frameworks) write Secret values to stdout. Those values end up in container logs, potentially shipped to centralized logging systems.
  • Core dumps: Environment variables are captured in process core dumps, which may be stored on persistent volumes or shipped to crash reporting systems.

Volume Mounts

  • File-based access: Secrets exist as files on a tmpfs filesystem (in-memory). They are never written to disk on the node. When the Pod is deleted, the tmpfs is reclaimed.
  • Granular permissions: You can set file permissions on mounted Secret files using defaultMode:
volumes:
  - name: secret-volume
    secret:
      secretName: db-secret
      defaultMode: 0400  # Owner read-only
  • Not inherited automatically: Child processes do not receive file contents through inheritance — they must explicitly read the file.
  • Not in inspect output: kubectl exec <pod> -- env does not reveal volume-mounted Secrets.

Recommendation: Prefer volume mounts for Secrets whenever the application supports file-based credential loading. Use environment variables only when the application requires them (e.g., DATABASE_URL in twelve-factor apps).

Update Behavior

Like ConfigMaps, the update semantics depend on the consumption method:

  • Environment variables: Static. Changing a Secret does not affect running containers. Requires Pod restart.
  • Volume mounts: Dynamic. The kubelet updates mounted Secret files within approximately 60 seconds. Applications must re-read the files.
  • Volume mounts with subPath: Static. Never auto-updated.

This is identical to ConfigMap behavior. The same table from the ConfigMap section applies.

Secret Best Practices for the CKAD

  1. Use kubectl create secret imperatively — it is faster than writing base64 values by hand. On the exam, speed matters.
  2. Prefer stringData over data in YAML — if you must write declarative manifests, stringData avoids manual base64 encoding and the errors that come with it.
  3. Always use readOnly: true on Secret volume mounts — it prevents accidental writes and signals intent.
  4. Watch for trailing newlinesecho 's3cret' > file includes a newline. Use echo -n 's3cret' > file or printf instead.
  5. Know the decode commandkubectl get secret <name> -o jsonpath='{.data.<key>}' | base64 -d is the fastest pattern.
  6. Remember the distinction — ConfigMap for non-sensitive data, Secret for sensitive data. Same consumption methods, different security semantics.
  7. Understand Secret types — Know when to use generic (passwords, tokens), tls (certificate + key pairs), and docker-registry (private image pull credentials). The exam may specify a Secret type explicitly.
  8. Practice volume mount syntax — The volume definition uses secret.secretName, not secret.name or secretRef. This subtle syntax difference from ConfigMaps (which use configMap.name) is a frequent source of validation errors on exam day.