Kubernetes ConfigMaps have a hard 1 MiB size ceiling — inherited from etcd's default 1.5 MB request cap — and the upstream docs are explicit that they are "not designed to hold large chunks of data" or "intended to hold confidential data." Yet ConfigMaps are stored as plaintext key-value objects in etcd with no encryption at rest by default, while Secrets sit right next to them in the API and support envelope encryption via EncryptionConfiguration and, since the KMS v2 API went stable in Kubernetes 1.29, integration with AWS KMS, GCP KMS, Azure Key Vault, and HashiCorp Vault. That gap between the two object types is where a lot of real-world exposure happens: a database connection string or an API token gets templated into a ConfigMap because it's marginally easier to render at deploy time, and it ends up readable by every service account and human with get/list on configmaps in that namespace — usually a much larger population than the one scoped to secrets. This post lays out what actually belongs in a ConfigMap, when Secrets and external stores are the correct call, how to scope RBAC so a namespace compromise doesn't turn into a cluster-wide credential harvest, and what to watch for in your own manifests before it ships.
What should actually go in a ConfigMap versus a Secret?
ConfigMaps are for non-confidential configuration: feature flags, log levels, application .properties or .ini files, environment names, and non-sensitive URLs. Secrets exist for anything an attacker could use directly — passwords, API tokens, TLS private keys, OAuth client secrets, SSH keys. The distinction the Kubernetes documentation draws isn't about size or format, it's about the API's handling assumptions: Secrets get base64-encoding (not encryption, but a signal to tooling to treat the field as sensitive), can be excluded from kubectl describe output, and are the object type that encryption-at-rest and external KMS integrations are built around. ConfigMaps have none of that scaffolding. A common failure pattern is a Helm chart that renders DATABASE_URL: postgres://user:password@host/db straight into a ConfigMap because the chart author needed one object type for all app config and didn't split it. The fix isn't complicated — it's discipline at manifest-authoring time, ideally enforced by a policy check rather than code review alone, since connection strings and tokens are easy to miss in a 200-line values file.
Does encryption at rest actually apply to Secrets by default?
No — this is one of the most common misconceptions. A vanilla Kubernetes cluster stores Secrets in etcd with the same lack of default encryption as ConfigMaps; base64 is an encoding, not a cryptographic control, and anyone with direct etcd access or an etcd backup can read Secret values in plaintext. Encryption at rest for Secrets is an opt-in cluster-admin configuration: you pass --encryption-provider-config to kube-apiserver pointing at an EncryptionConfiguration resource that defines providers such as aescbc, secretbox, or a KMS plugin. Managed Kubernetes offerings vary here — some enable envelope encryption with a cloud KMS by default on new clusters, others leave it to the operator. The practical takeaway is that "it's a Secret" is not itself a security control; you still need to verify your cluster has encryption at rest configured, verify etcd backups are themselves encrypted and access-controlled, and treat Secrets as the necessary-but-not-sufficient first step rather than the finish line.
Where does volume-mount behavior create a gap between the two object types?
When Secrets are mounted into a Pod as a volume, kubelet backs that mount with tmpfs — an in-memory filesystem — specifically so secret material never lands on the node's persistent disk. ConfigMap volumes do not carry that same guarantee; ConfigMap data is not tmpfs-backed by default, meaning it can be written to node-local storage as ordinary files. That's usually fine for config, which is why the ConfigMap API exists — but it's a concrete reason to never treat "I'll just mount it like a ConfigMap" as an equivalent way to hand a container a credential. It also matters for node compromise scenarios: an attacker with filesystem access to a node can recover ConfigMap-mounted data from disk in ways that Secret-mounted data, living only in memory, resists. If you're debating whether a given value is sensitive enough to warrant a Secret, the volume-mount behavior alone is a reasonable tiebreaker in favor of Secrets.
What RBAC scoping mistakes turn a namespace read into a cluster-wide credential leak?
The recurring mistake documented in CIS Kubernetes Benchmark guidance is granting get, list, or watch on secrets — or on ConfigMaps holding quasi-sensitive data — through a ClusterRole bound cluster-wide, rather than a namespace-scoped Role. A ClusterRole with resources: ["secrets"] and a wildcard or broad verbs: ["list", "watch"], bound via a ClusterRoleBinding, hands whoever holds that role read access to every Secret in every namespace, including ones for services that team has no operational reason to touch. The CIS benchmark's control areas "Minimize wildcard use in Roles and ClusterRoles" and "Minimize access to secrets" both target this exact pattern, because it's one of the more direct privilege-escalation and lateral-movement paths inside a cluster: compromise one low-value workload's service account, and if that account's namespace has a Role granting secrets: list, you can potentially pull tokens for services well outside that workload's blast radius. The standard audit command is kubectl auth can-i list secrets --namespace <ns> run against every service account and role in the cluster, not just the ones you remember configuring — least-privilege audits of exactly this kind are a recommended control in the benchmark.
How does Safeguard help catch ConfigMap secret leaks before they ship?
The most common way sensitive values end up in a ConfigMap isn't a deliberate architecture decision — it's a credential typed into a YAML manifest during local testing that never gets pulled back out before commit. Safeguard's secrets scanning covers source code at HEAD, full Git history, and container images layer by layer, using issuer-specific pattern matching for 200+ providers plus entropy-based detection for anything generic, and it verifies each finding with a low-privilege call to the issuing service so a flagged AWS key or database credential is confirmed live rather than a guess. That coverage applies the same way to a Kubernetes manifest as it does to application code: a connection string hardcoded into a ConfigMap definition in a Helm chart or raw YAML file is caught by the same pattern and entropy layers, and the pre-push git hook can block it before it ever reaches a cluster. Paired with your own RBAC and encryption-at-rest configuration, that closes the gap between "we know Secrets exist for a reason" and actually keeping credentials out of the object type that was never built to hold them.