Skip to main content
RunBook Academy

KubernetesXXI · SecretsSecrets

Secrets — base64 is not encryption

Advanced⏱ ~17 minkubectlkubeadm

What you'll learn

  • Describe what Secrets are for and the Secret types (Opaque, kubernetes.io/tls, dockerconfigjson, service-account-token)
  • Explain why base64 is not encryption
  • Reason about the visibility of Secrets in etcd and via the API
  • Apply the right Secret type to a real workload

Prerequisites

Verified against Kubernetes 1.34.x · kubeadm 1.34.x · kubectl 1.34.x · etcd 3.6.x · CoreDNS 1.11.x · containerd 1.7.x / 2.x · 2026-08-16

Not yet marked complete on this device.

A Secret is the Kubernetes object for sensitive data: passwords, tokens, certificates, SSH keys. Like ConfigMaps, Secrets are key-value maps. Unlike ConfigMaps, Secrets have specific types (kubernetes.io/tls, dockerconfigjson, etc.) and stricter handling — but the underlying storage is plaintext unless encryption at rest is configured. This lesson covers the types, the encoding (base64 is not encryption), and the visibility rules.

What a Secret is

apiVersion: v1
kind: Secret
metadata:
  name: db-credentials
  namespace: prod
type: Opaque
data:
  username: YXBw           # base64 of "app"
  password: czNjcjN0       # base64 of "s3cr3t"
stringData:
  database_url: postgres://app:s3cr3t@db.prod/data

A Secret has data (base64-encoded values) and stringData (plaintext values that the API server base64-encodes on write). The two are equivalent; stringData is more convenient for authoring.

flowchart LR
    A["Authoring: stringData<br/>plaintext"] -->|API server| B["Encoded: data<br/>base64"]
    B -->|etcd| C[Stored plaintext]
    C -->|reader with get secret| D[Plaintext in memory]

The encoding happens once on write. Reading the Secret returns the encoded values; the API server does not auto-decode for kubectl get (with --output it does).

Base64 is not encryption

Base64 is an encoding scheme, not an encryption scheme. It maps binary data to a printable string. The mapping is reversible by anyone who knows base64:

$ echo 'czNjcjN0' | base64 -d
s3cr3t

Secret types

Kubernetes defines several Secret types with specific schemas:

TypeSchemaUse
OpaqueArbitrary key-valueGeneric credentials
kubernetes.io/tlstls.crt, tls.keyTLS certificates
kubernetes.io/dockerconfigjson.dockerconfigjsonImage registry credentials
kubernetes.io/dockerconfig.dockercfgLegacy registry credentials
kubernetes.io/basic-authusername, passwordHTTP basic auth
kubernetes.io/ssh-authssh-privatekeySSH private keys
kubernetes.io/service-account-tokentoken, ca.crt, namespaceServiceAccount tokens
bootstrap.kubernetes.io/tokentoken-id, token-secret, signing-keyNode bootstrap tokens

The type is metadata; the API server does not validate the content for Opaque but does for typed secrets (kubernetes.io/tls requires tls.crt and tls.key).

kubernetes.io/tls

apiVersion: v1
kind: Secret
metadata:
  name: web-tls
type: kubernetes.io/tls
data:
  tls.crt: <base64 of cert PEM>
  tls.key: <base64 of private key PEM>

Used by Ingress and Pods that need TLS material. The Ingress controller mounts this Secret and serves the cert.

dockerconfigjson

apiVersion: v1
kind: Secret
metadata:
  name: reg-credentials
type: kubernetes.io/dockerconfigjson
data:
  .dockerconfigjson: |
    {
      "auths": {
        "registry.example.com": {
          "username": "service-account",
          "password": "...",
          "auth": "..."
        }
      }
    }

Used as imagePullSecrets on a Pod to authenticate to a private image registry.

service-account-token

Created automatically when a ServiceAccount is referenced. Mounted into Pods that use the ServiceAccount; the kubelet periodically refreshes the token.

Creating Secrets

From literals:

kubectl create secret generic db-credentials -n prod \
  --from-literal=username=app \
  --from-literal=password=s3cr3t

From files:

kubectl create secret tls web-tls -n prod \
  --cert=tls.crt --key=tls.key

From a docker config:

kubectl create secret docker-registry reg-credentials -n prod \
  --docker-server=registry.example.com \
  --docker-username=service-account \
  --docker-password=...

From a manifest:

apiVersion: v1
kind: Secret
metadata:
  name: db-credentials
type: Opaque
stringData:
  password: s3cr3t

Where Secrets live

flowchart LR
    A[Pod uses Secret] --> B[API server]
    B --> C[etcd]
    C -->|plaintext| D["Anyone with<br/>etcd read access"]
    B -->|API response| E[Authorized reader]
    E --> F[Pod container]

By default, Secrets are stored plaintext in etcd. Encryption at rest is configured separately and is not the default.

The visibility chain:

  1. etcd direct access — anyone with file-system or network access to etcd can read the records.
  2. API server access — anyone with get secret permission can read via kubectl get secret -o yaml.
  3. Pod container access — Pods that mount the Secret can read the file or env var.
  4. Node access — anyone with root on a node where the Pod is running can read the file from /var/lib/kubelet/pods/<uid>/volumes/....

Encryption at rest protects the first; RBAC protects the second; Pod-level security contexts and Linux capabilities protect the third and fourth.

Inspecting a Secret

kubectl get secret db-credentials -n prod -o yaml
# apiVersion: v1
# kind: Secret
# metadata:
#   name: db-credentials
# data:
#   password: czNjcjN0       # base64
#   username: YXBw

kubectl get secret db-credentials -n prod -o jsonpath='{.data.password}' | base64 -d
# s3cr3t
Read-only / Safe
$ kubectl describe secret db-credentials -n prod
Name:         db-credentials
Namespace:    prod
Labels:       &lt;none&gt;
Annotations:  &lt;none&gt;

Type:  Opaque

Data
====
password:  6 bytes
username:  3 bytes

kubectl describe does not show the Secret values; only the sizes. This is a UX guard, not a security guard — anyone with get secret can still kubectl get -o yaml.

Quiz

Knowledge check · 4 questions

  1. Q1. Which Secret type is correct for a TLS certificate with a private key?

  2. Q2. Base64-encoding a Secret value provides real encryption.

  3. Q3. Your team has a Secret that contains a database password. The CI pipeline logs the Secret's YAML for debugging. The password leaks to logs. Diagnose and remediate.

    CI job runs kubectl get secret db-credentials -o yaml and prints the output to the build log. The base64-encoded password is visible in the log.

  4. Q4. Explain why a Secret that is only base64-encoded is not encrypted, and what protections Kubernetes actually provides.

Passing score: 75%. Answers are checked in this browser.

Production discipline

  • Secrets are not encrypted by default. Encryption at rest is opt-in; configure it before storing credentials.
  • Use the typed Secret variants. kubernetes.io/tls for TLS material, dockerconfigjson for registry credentials. The typed format is validated and prevents schema mistakes.
  • Treat get secret as a privileged permission. RBAC should restrict it to identities that need it; audit every binding that grants get.
  • Avoid kubectl get secret -o yaml in shared logs. The values are plaintext in the output; a CI pipeline that prints Secret YAMLs is leaking credentials.
  • Use external secret managers for production credentials. Vault, AWS Secrets Manager, External Secrets Operator. The Secret object is the rendering, not the source.

Secrets are the workhorse of Kubernetes credential management. They are not a security mechanism by themselves; they are a storage object that needs encryption, RBAC, and operational discipline to be secure.