Base64 ConfigMaps Meet etcd's Identity Provider Default
A ConfigMap holding a base64 database password looks encoded but is stored as flat, retrievable cleartext in etcd, in kubelet caches, and in every replica that mounts it. When etcd encryption at rest is left on its default identity provider, that same string also sits unencrypted in snapshots, Velero backups, and any copy of the etcd data directory, entirely outside the RBAC boundary that Secret objects are meant to enforce.
Operational summary
At a glance
- Symptom
- Any principal with configmaps get/list RBAC access, or access to an etcd snapshot or Velero backup, retrieves the credential in cleartext.
- Likely cause
- Credentials stored as base64 strings inside ConfigMap objects while etcd encryption at rest remains on the default identity provider.
- Impact
- Cluster-wide credential exposure across every namespace, backup, and replica, with no reliable way to enumerate which ConfigMaps hold live secrets during rotation.
- Verification signal
- Re-run the detection test and a controlled negative-path test.
- Safe correction
- Migrate credential data into Secret objects, block sensitive key patterns in ConfigMaps via Kyverno, and enable aescbc EncryptionConfiguration ahead of the identity provider.
- Rollback or recovery
- Restore the exported configuration if the new control blocks required production traffic, then narrow the policy before redeployment.
The Trap
Base64 ConfigMap Credentials Without etcd Encryption at Rest. Developers create a ConfigMap and populate its data field with a base64-encoded database password, API key, or TLS bootstrap token because the encoded string looks obfuscated. They pair this with a cluster running kube-apiserver’s default EncryptionConfiguration, which lists the identity provider first (or omits the file entirely), meaning etcd stores every object, ConfigMap or Secret, as unencrypted protobuf on disk.
The Default State
kubectl imposes no schema restriction on ConfigMap keys, so kubectl create configmap db-config --from-literal=password=cGFzc3dvcmQ= succeeds without warning. Base64 is an encoding, not a cipher; anyone with configmaps get/list reverses it with a single base64 -d call. Meanwhile the API server’s encryption-at-rest configuration, if set at all, typically ships with providers: [{identity: {}}] ahead of aescbc, so etcd itself never encrypts the value regardless of which object type stores it.
The Blast Radius
RBAC ClusterRoles built for read-only dashboards, CI pipelines, and Helm operators routinely grant broad get/list on configmaps because the resource is assumed low-sensitivity, while equivalent access to secrets is scoped tightly. That assumption fails silently once credentials live in ConfigMaps. Worse, etcd snapshots taken for disaster recovery, Velero backup archives, and any stolen or misconfigured PersistentVolume backing the etcd data directory now expose every credential in cleartext, with no encryption boundary to slow an attacker down. Rotation becomes guesswork because there is no consistent object type marking which ConfigMaps carry live secrets, so incident response cannot enumerate exposure with a single kubectl get secrets --all-namespaces query.
The Lead Mechanic Fix
Migrate every credential-bearing ConfigMap to a proper Secret object, then re-key etcd: set --encryption-provider-config=/etc/kubernetes/encryption-config.yaml on kube-apiserver with providers: [aescbc, identity] and a KMS-backed key, then force re-encryption with kubectl get secrets --all-namespaces -o json | kubectl replace -f -. Enforce a Kyverno validate policy that rejects ConfigMap keys matching patterns such as password, token, or apikey, redirecting authors to Secret objects at admission time.
Apply the safer control
Before you change production
Confirm the affected scope, export the current configuration, and test the replacement control in a non-production environment first.
Fix commands and configuration
--encryption-provider-config=/etc/kubernetes/encryption-config.yamlproviders: [aescbc, identity]kubectl get secrets --all-namespaces -o json | kubectl replace -f -Verify, roll back or escalate
Verify
Re-run the detection test and a controlled negative-path test. Confirm the unsafe behaviour is blocked while approved traffic still succeeds.
Rollback
Restore the exported configuration if the new control blocks required production traffic, then narrow the policy before redeployment.
Escalate
Escalate when the blast radius is uncertain, the control cannot be tested safely, or remediation requires an outage or security exception.