Secrets: Types, Creation, and Security
SummaryCovers the three main Secret types (Opaque, TLS,...
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
| Type | Created With | Keys | Use Case |
|---|---|---|---|
Opaque | kubectl create secret generic | Arbitrary | Passwords, tokens, API keys |
kubernetes.io/tls | kubectl create secret tls | tls.crt, tls.key | TLS certificates |
kubernetes.io/dockerconfigjson | kubectl create secret docker-registry | .dockerconfigjson | Private 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> -- envdisplays 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
tmpfsfilesystem (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> -- envdoes 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
- Use
kubectl create secretimperatively — it is faster than writing base64 values by hand. On the exam, speed matters. - Prefer
stringDataoverdatain YAML — if you must write declarative manifests,stringDataavoids manual base64 encoding and the errors that come with it. - Always use
readOnly: trueon Secret volume mounts — it prevents accidental writes and signals intent. - Watch for trailing newlines —
echo 's3cret' > fileincludes a newline. Useecho -n 's3cret' > fileorprintfinstead. - Know the decode command —
kubectl get secret <name> -o jsonpath='{.data.<key>}' | base64 -dis the fastest pattern. - Remember the distinction — ConfigMap for non-sensitive data, Secret for sensitive data. Same consumption methods, different security semantics.
- Understand Secret types — Know when to use
generic(passwords, tokens),tls(certificate + key pairs), anddocker-registry(private image pull credentials). The exam may specify a Secret type explicitly. - Practice volume mount syntax — The volume definition uses
secret.secretName, notsecret.nameorsecretRef. This subtle syntax difference from ConfigMaps (which useconfigMap.name) is a frequent source of validation errors on exam day.