A native Kubernetes Secret is, by default, protected by no encryption at all in etcd: its content is stored as-is after a simple base64 encoding, trivially reversible. EncryptionConfiguration fixes exactly that, under one condition rarely anticipated: activation only encrypts what gets written after it, never what already existed.

The file that changes what etcd actually stores

EncryptionConfiguration is a file provided to the API server at startup, defining how to encrypt certain resource types (Secrets first and foremost) before writing them to etcd. Once active, every new Secret write goes through this encryption before reaching etcd, invisible on the kubectl side since decryption happens transparently on read.

apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
  - resources: ["secrets"]
    providers:
      - aescbc:
          keys:
            - name: key1
              secret: <base64-encoded-32-byte-key>
      - identity: {}

Provider order decides behavior, not just encryption

The providers list isn’t a list of equivalent options: the first provider encrypts every new write, but subsequent providers stay active for reads, allowing decryption of data written under an older provider during a transition. Ending the list with identity: {} (no encryption) is common practice in development, never in production: this provider would silently accept a plaintext write if the first encrypted provider failed for any reason.

# aesgcm encrypts every new write;
# aescbc stays available read-only during the transition
providers:
  - aesgcm:
      keys:
        - name: key2
          secret: <base64-encoded-32-byte-key>
  - aescbc:
      keys:
        - name: key1
          secret: <base64-encoded-32-byte-key>

The trap that matters most: nothing is retroactive

Enabling EncryptionConfiguration on a cluster that already contains Secrets doesn’t automatically encrypt any of them: only writes after activation go through the new encryption. A Secret created before activation and never modified since stays stored in plaintext in etcd indefinitely, with no warning, no log, no signal that this particular Secret was never re-encrypted.

# Forces re-encryption of every existing Secret
# under the new provider, the only way to cover them
kubectl get secrets --all-namespaces -o json | kubectl replace -f -

This command reads then rewrites every Secret, triggering encryption for those that never had it. Without this explicit step, run once after any activation or provider change, part of the cluster stays protected only in appearance.

Key rotation follows the same logic

Rotating an encryption key (replacing key1 with a new key) follows exactly the same principle: the new key encrypts future writes, but Secrets encrypted under the old key stay readable (the old key needs to stay in the configuration, in secondary position) until an explicit re-encryption moves them over. Removing an old key from the configuration before re-encrypting everything depending on it makes those Secrets permanently unreadable.

Takeaway

EncryptionConfiguration encrypts Secrets at rest in etcd, but only for writes after activation: no existing Secret gets re-encrypted automatically, a trap that leaves part of the cluster protected only in appearance if the explicit re-encryption step gets forgotten. Provider order determines read behavior during a transition, never to end with identity: {} in production. Key rotation follows the same rule: the old key stays necessary until full re-encryption, one of the security details that matter the moment a Kubernetes migration handles sensitive Secrets.