Lesson  in  DevSecOps in Practice: Container & Kubernetes Security

Kubernetes Secrets: Safe Consumption

Prove that base64 is not encryption — decode a Kubernetes Secret in two commands. Then see what we are actually avoiding: hardcoded credentials in manifests that end up in git history and the API server forever. Both env vars and volume mounts solve the problem by keeping credentials out of manifests entirely.

base64 is NOT Encryption

Anyone with kubectl get secret access can read your passwords in plaintext. Two commands:

kubectl create namespace secrets-demo

kubectl create secret generic db-credentials \
  --from-literal=username=admin \
  --from-literal=password=SuperSecretPassword123 \
  -n secrets-demo

kubectl get secret db-credentials -n secrets-demo -o yaml

Look at the data section. That value is base64. Decode it:

kubectl get secret db-credentials -n secrets-demo \
  -o jsonpath='{.data.password}' | base64 -d
echo ""

Expected: SuperSecretPassword123

No special tools needed. Zero effort. The point of Kubernetes Secrets is not encryption — it is keeping credentials out of your manifests and images, where they would end up in git history, container registries, and build logs permanently.

What We Are Avoiding: Hardcoded Credentials

The anti-pattern is embedding credentials directly in a manifest or baking them into an image. Here is what that looks like — do not do this:

cat > pod-hardcoded.yaml << 'EOF'
apiVersion: v1
kind: Pod
metadata:
  name: pod-hardcoded
  namespace: secrets-demo
spec:
  containers:
  - name: app
    image: public.ecr.aws/docker/library/busybox:1.36
    command: ["sh", "-c", "echo connecting to db with $DB_PASS && sleep 3600"]
    env:
    - name: DB_PASS
      value: "SuperSecretPassword123"
  restartPolicy: Never
EOF
kubectl apply -f pod-hardcoded.yaml
kubectl get pod pod-hardcoded -n secrets-demo -o yaml | grep -A2 "env:"

The password is visible to anyone who can read the Pod spec — kubectl get pod, kubectl describe pod, the API server audit log, and anything that exports cluster state. It also lives permanently in your git history the moment the file is committed.

Pattern 1: Environment Variable from Secret

The credential is stored in a Secret object and injected at runtime — it never appears in the manifest:

cat > pod-env-secret.yaml << 'EOF'
apiVersion: v1
kind: Pod
metadata:
  name: pod-env-secret
  namespace: secrets-demo
spec:
  containers:
  - name: app
    image: public.ecr.aws/docker/library/busybox:1.36
    command: ["sh", "-c", "echo DB_USER=$DB_USER && echo DB_PASS set: $(test -n \"$DB_PASS\" && echo yes || echo no) && sleep 3600"]
    env:
    - name: DB_USER
      valueFrom:
        secretKeyRef:
          name: db-credentials
          key: username
    - name: DB_PASS
      valueFrom:
        secretKeyRef:
          name: db-credentials
          key: password
    securityContext:
      runAsNonRoot: true
      runAsUser: 1000
      allowPrivilegeEscalation: false
  restartPolicy: Never
EOF
kubectl apply -f pod-env-secret.yaml
kubectl wait --for=condition=Ready pod/pod-env-secret -n secrets-demo --timeout=30s
kubectl logs pod/pod-env-secret -n secrets-demo

The manifest references the Secret by name — the value itself is never written into the YAML.

Pattern 2: Volume Mount from Secret

The Secret is mounted as files inside the container. The application reads credentials from disk at runtime:

cat > pod-volume-secret.yaml << 'EOF'
apiVersion: v1
kind: Pod
metadata:
  name: pod-volume-secret
  namespace: secrets-demo
spec:
  containers:
  - name: app
    image: public.ecr.aws/docker/library/busybox:1.36
    command: ["sh", "-c", "echo username=$(cat /etc/db-creds/username) && echo password=$(cat /etc/db-creds/password) && sleep 3600"]
    volumeMounts:
    - name: db-creds
      mountPath: /etc/db-creds
      readOnly: true
  volumes:
  - name: db-creds
    secret:
      secretName: db-credentials
  restartPolicy: Never
EOF
kubectl apply -f pod-volume-secret.yaml
kubectl wait --for=condition=Ready pod/pod-volume-secret -n secrets-demo --timeout=30s
kubectl logs pod/pod-volume-secret -n secrets-demo

Expected:

username=admin
password=SuperSecretPassword123

Same result — credential never appears in the manifest.

Summary

PatternCredential in manifest?Credential in git?
Hardcoded valueYesYes — permanently
secretKeyRef (env var)No — reference onlyNo
Volume mountNo — reference onlyNo

Both secretKeyRef and volume mounts solve the core problem. The next lab (Vault + External Secrets Operator) goes further — secrets are not stored in Kubernetes at all, and rotation happens without touching a manifest.

Cleanup

kubectl delete namespace secrets-demo