<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:media="http://search.yahoo.com/mrss/"><channel><title>Secrets Management | FirewallSync</title><link>https://firewallsync.com/tags/secrets-management/</link><description>Practical cybersecurity writing for engineering teams that ship fast.</description><language>en-us</language><image><url>https://firewallsync.com/icon-512.png</url><title>FirewallSync</title><link>https://firewallsync.com/</link></image><lastBuildDate>Fri, 23 Oct 2026 04:45:00 +0000</lastBuildDate><atom:link href="https://firewallsync.com/tags/secrets-management/index.xml" rel="self" type="application/rss+xml"/><item><title>Kubernetes Secrets Encryption at Rest: What It Protects</title><link>https://firewallsync.com/posts/kubernetes-secrets-encryption-at-rest-what-it-protects/</link><guid>https://firewallsync.com/posts/kubernetes-secrets-encryption-at-rest-what-it-protects/</guid><pubDate>Fri, 23 Oct 2026 04:45:00 +0000</pubDate><dc:creator>Craig Leslie</dc:creator><category>Cloud Security</category><description>Kubernetes stores Secrets unencrypted in etcd unless you configure an EncryptionConfiguration. Turning it on protects etcd data, but not against anyone who can read Secrets through the API, create Pods, or, with local keys, read the control plane host. This guide maps each threat to the control that actually stops it, with a tested config checker.</description><enclosure url="https://firewallsync.com/posts/kubernetes-secrets-encryption-at-rest-what-it-protects/cover_hu14419892505578868447.jpg" type="image/jpeg" length="0"/><media:content url="https://firewallsync.com/posts/kubernetes-secrets-encryption-at-rest-what-it-protects/cover_hu14419892505578868447.jpg" medium="image" type="image/jpeg" width="1200" height="630"/><content:encoded><![CDATA[<p><img src="https://firewallsync.com/posts/kubernetes-secrets-encryption-at-rest-what-it-protects/cover_hu14419892505578868447.jpg" alt="Rows of ornate brass deposit boxes with keyholes and numbers 564 to 624, viewed at an angle" width="1200" height="630"></p><p>Kubernetes encryption at rest protects one thing: the Secret data stored in etcd. It stops someone who gets etcd&rsquo;s data, or a copy of it, from reading Secrets in plain text. It does not stop anyone the API server is willing to serve, and that group is larger than most teams assume: everyone with <code>get</code>, <code>list</code> or <code>watch</code> on Secrets, and everyone who can create a Pod in the namespace. Turn encryption on, but don&rsquo;t count it as your Secrets access control.</p>
<h2 id="what-kubernetes-does-by-default">What Kubernetes does by default</h2>
<p>The Kubernetes documentation is blunt: &ldquo;Kubernetes Secrets are, by default, stored unencrypted in the API server&rsquo;s underlying data store (etcd).&rdquo; The default provider is <code>identity</code>, which, in the docs&rsquo; words, &ldquo;does not encrypt stored data and provides <em>no</em> additional confidentiality protection.&rdquo;</p>
<p>The base64 you see in a Secret manifest doesn&rsquo;t change that. The good-practices page says it plainly: &ldquo;Base64 encoding is <em>not</em> an encryption method, it provides no additional confidentiality over plain text.&rdquo; A Secret manifest committed to Git is a plain-text credential in Git.</p>
<p>Managed Kubernetes services run the control plane for you, and their defaults may differ from upstream. This article describes upstream behavior; check your provider&rsquo;s documentation for what it encrypts by default and whether it supports envelope encryption with your own key.</p>
<h2 id="who-can-read-a-secret">Who can read a Secret</h2>
<p>Before choosing an encryption provider, list who can read Secrets at all. Encryption at rest changes only the last two rows of this table.</p>
<table>
  <thead>
      <tr>
          <th>Who</th>
          <th>How they read Secrets</th>
          <th>Does encryption at rest stop it?</th>
          <th>Control that does</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>A user or service account with <code>get</code> on Secrets</td>
          <td>Asks the API server</td>
          <td>No; the API server decrypts for them</td>
          <td>Least-privilege RBAC</td>
      </tr>
      <tr>
          <td>A subject with <code>list</code> or <code>watch</code> on Secrets</td>
          <td>A list response includes the contents of every Secret returned</td>
          <td>No</td>
          <td>Restrict <code>list</code>/<code>watch</code> to system components</td>
      </tr>
      <tr>
          <td>Anyone who can create Pods, Deployments or other workloads in the namespace</td>
          <td>Mounts any Secret in that namespace into a Pod they control</td>
          <td>No</td>
          <td>Separate namespaces by trust; RBAC on workload creation</td>
      </tr>
      <tr>
          <td>Someone with access to etcd, its disks or a copy of its data</td>
          <td>Reads stored bytes directly</td>
          <td><strong>Yes</strong></td>
          <td>Encryption at rest; restrict etcd access</td>
      </tr>
      <tr>
          <td>Someone with root on a control plane host</td>
          <td>Reads etcd data <em>and</em> the local key file</td>
          <td>Only with KMS</td>
          <td>KMS provider; host hardening</td>
      </tr>
  </tbody>
</table>
<p>The middle row surprises people. The Secret documentation says &ldquo;anyone who is authorized to create a Pod in a namespace can use that access to read any Secret in that namespace; this includes indirect access such as the ability to create a Deployment.&rdquo; Our <a href="/posts/kubernetes-rbac-audit-11-permissions-that-escalate-access/">Kubernetes RBAC audit</a> covers how to find these grants in your bindings; this article stays on the storage side.</p>
<h2 id="how-encryption-at-rest-is-configured">How encryption at rest is configured</h2>
<p>You pass an <code>EncryptionConfiguration</code> file to <code>kube-apiserver</code> with <code>--encryption-provider-config</code>. A minimal example for Secrets with a local AES-CBC key, shaped like the one in the Kubernetes task page:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">apiVersion</span>: <span style="color:#ae81ff">apiserver.config.k8s.io/v1</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">EncryptionConfiguration</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">resources</span>:
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">resources</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">secrets</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">providers</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">aescbc</span>:
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">keys</span>:
</span></span><span style="display:flex;"><span>            - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">key1</span>
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">secret</span>: <span style="color:#ae81ff">&lt;BASE64-ENCODED 32-BYTE KEY&gt;</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">identity</span>: {} <span style="color:#75715e"># lets the API server read Secrets written before encryption</span>
</span></span></code></pre></div><p>Three rules from the documentation decide what this file does:</p>
<ul>
<li><strong>The first provider encrypts.</strong> &ldquo;The first provider in the list is used to encrypt resources written into the storage.&rdquo; If <code>identity</code> is first, new writes are plain text even if other providers are listed.</li>
<li><strong>Later providers only decrypt.</strong> On reads, each matching provider is tried in order. The trailing <code>identity</code> is what lets the API server keep reading old plain-text Secrets during a migration. Once everything is rewritten, you can remove it.</li>
<li><strong>Order across rules matters too.</strong> The <code>resources</code> list is processed in order, so an earlier rule that names <code>secrets</code> with <code>identity</code> will win over a later <code>*.*</code> wildcard that encrypts. Wildcards are <code>*.</code> for the core group (which includes Secrets) and <code>*.*</code> for everything.</li>
</ul>
<h2 id="which-provider-stops-which-attacker">Which provider stops which attacker</h2>
<table>
  <thead>
      <tr>
          <th>Provider</th>
          <th>Key location</th>
          <th>Kubernetes docs say</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>identity</code></td>
          <td>None</td>
          <td>No encryption; the default</td>
      </tr>
      <tr>
          <td><code>aescbc</code></td>
          <td>Local file on each control plane host</td>
          <td>Not recommended: CBC is vulnerable to padding oracle attacks</td>
      </tr>
      <tr>
          <td><code>aesgcm</code></td>
          <td>Local file</td>
          <td>Not recommended unless you automate key rotation</td>
      </tr>
      <tr>
          <td><code>secretbox</code></td>
          <td>Local file</td>
          <td>Newer cryptography that may not pass high-review environments</td>
      </tr>
      <tr>
          <td><code>kms</code> v1</td>
          <td>External KMS</td>
          <td>Deprecated since Kubernetes v1.28</td>
      </tr>
      <tr>
          <td><code>kms</code> v2</td>
          <td>External KMS</td>
          <td>Strongest; envelope encryption with AES-GCM data keys; stable from v1.29</td>
      </tr>
  </tbody>
</table>
<p>The key-location column is the one that maps to threats. With a local key, the docs say encryption &ldquo;protects against an etcd compromise, but it fails to protect against a host compromise,&rdquo; because the key sits in the configuration file on the host. With KMS, Kubernetes encrypts data with a data encryption key and the KMS wraps that key, so an attacker &ldquo;would need to compromise etcd <strong>and</strong> the third-party KMS provider.&rdquo;</p>
<p>A local key is still worth having. It turns a leaked etcd snapshot or a mis-scoped etcd client certificate into ciphertext rather than credentials. Our view: use KMS v2 where you have a KMS to point at, and treat a local <code>aescbc</code> key as a stopgap you plan to replace.</p>
<h2 id="checking-a-configuration-file">Checking a configuration file</h2>
<p>We wrote a short checker that reads an <code>EncryptionConfiguration</code> and flags the weak settings above. It reads only the file and does not contact a cluster. It needs Python 3 and PyYAML; we ran it with Python 3 and PyYAML 6.0.3 against three sample files. It is illustrative: it does not validate the whole schema, and it reads <code>kms</code> without an <code>apiVersion</code> as v1.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#75715e">#!/usr/bin/env python3</span>
</span></span><span style="display:flex;"><span><span style="color:#e6db74">&#34;&#34;&#34;Flag weak settings in a Kubernetes EncryptionConfiguration file.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">Usage: python3 check_encryption_config.py /path/to/encryption-config.yaml
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">Reads the file only; it does not contact a cluster.&#34;&#34;&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> sys
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> yaml
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>LOCAL_KEY <span style="color:#f92672">=</span> {<span style="color:#e6db74">&#34;aescbc&#34;</span>, <span style="color:#e6db74">&#34;aesgcm&#34;</span>, <span style="color:#e6db74">&#34;secretbox&#34;</span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">covers_secrets</span>(resources):
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> any(r <span style="color:#f92672">in</span> (<span style="color:#e6db74">&#34;secrets&#34;</span>, <span style="color:#e6db74">&#34;*.&#34;</span>, <span style="color:#e6db74">&#34;*.*&#34;</span>) <span style="color:#66d9ef">for</span> r <span style="color:#f92672">in</span> resources)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">main</span>(path):
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">with</span> open(path) <span style="color:#66d9ef">as</span> f:
</span></span><span style="display:flex;"><span>        cfg <span style="color:#f92672">=</span> yaml<span style="color:#f92672">.</span>safe_load(f)
</span></span><span style="display:flex;"><span>    findings <span style="color:#f92672">=</span> []
</span></span><span style="display:flex;"><span>    secrets_rule_seen <span style="color:#f92672">=</span> <span style="color:#66d9ef">False</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> i, rule <span style="color:#f92672">in</span> enumerate(cfg<span style="color:#f92672">.</span>get(<span style="color:#e6db74">&#34;resources&#34;</span>, [])):
</span></span><span style="display:flex;"><span>        res <span style="color:#f92672">=</span> rule<span style="color:#f92672">.</span>get(<span style="color:#e6db74">&#34;resources&#34;</span>, [])
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> <span style="color:#f92672">not</span> covers_secrets(res):
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">continue</span>
</span></span><span style="display:flex;"><span>        <span style="color:#75715e"># The first matching rule wins, so only the first rule covering secrets counts.</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> secrets_rule_seen:
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">continue</span>
</span></span><span style="display:flex;"><span>        secrets_rule_seen <span style="color:#f92672">=</span> <span style="color:#66d9ef">True</span>
</span></span><span style="display:flex;"><span>        providers <span style="color:#f92672">=</span> rule<span style="color:#f92672">.</span>get(<span style="color:#e6db74">&#34;providers&#34;</span>, [])
</span></span><span style="display:flex;"><span>        first <span style="color:#f92672">=</span> next(iter(providers[<span style="color:#ae81ff">0</span>])) <span style="color:#66d9ef">if</span> providers <span style="color:#66d9ef">else</span> <span style="color:#66d9ef">None</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> first <span style="color:#f92672">==</span> <span style="color:#e6db74">&#34;identity&#34;</span>:
</span></span><span style="display:flex;"><span>            findings<span style="color:#f92672">.</span>append(<span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;rule </span><span style="color:#e6db74">{</span>i<span style="color:#e6db74">}</span><span style="color:#e6db74">: first provider is identity, so new Secrets are written in plain text&#34;</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">elif</span> first <span style="color:#f92672">in</span> LOCAL_KEY:
</span></span><span style="display:flex;"><span>            findings<span style="color:#f92672">.</span>append(<span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;rule </span><span style="color:#e6db74">{</span>i<span style="color:#e6db74">}</span><span style="color:#e6db74">: first provider is </span><span style="color:#e6db74">{</span>first<span style="color:#e6db74">}</span><span style="color:#e6db74">; key sits in this file on every control plane host&#34;</span>)
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">if</span> first <span style="color:#f92672">==</span> <span style="color:#e6db74">&#34;aescbc&#34;</span>:
</span></span><span style="display:flex;"><span>                findings<span style="color:#f92672">.</span>append(<span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;rule </span><span style="color:#e6db74">{</span>i<span style="color:#e6db74">}</span><span style="color:#e6db74">: aescbc is not recommended by the Kubernetes docs (padding oracle)&#34;</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">elif</span> first <span style="color:#f92672">==</span> <span style="color:#e6db74">&#34;kms&#34;</span>:
</span></span><span style="display:flex;"><span>            ver <span style="color:#f92672">=</span> providers[<span style="color:#ae81ff">0</span>][<span style="color:#e6db74">&#34;kms&#34;</span>]<span style="color:#f92672">.</span>get(<span style="color:#e6db74">&#34;apiVersion&#34;</span>, <span style="color:#e6db74">&#34;v1&#34;</span>)
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">if</span> ver <span style="color:#f92672">!=</span> <span style="color:#e6db74">&#34;v2&#34;</span>:
</span></span><span style="display:flex;"><span>                findings<span style="color:#f92672">.</span>append(<span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;rule </span><span style="color:#e6db74">{</span>i<span style="color:#e6db74">}</span><span style="color:#e6db74">: KMS v1 is deprecated since Kubernetes v1.28; move to KMS v2&#34;</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> any(<span style="color:#e6db74">&#34;identity&#34;</span> <span style="color:#f92672">in</span> p <span style="color:#66d9ef">for</span> p <span style="color:#f92672">in</span> providers[<span style="color:#ae81ff">1</span>:]):
</span></span><span style="display:flex;"><span>            findings<span style="color:#f92672">.</span>append(<span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;rule </span><span style="color:#e6db74">{</span>i<span style="color:#e6db74">}</span><span style="color:#e6db74">: identity fallback present; unencrypted Secrets can still be read (expected during migration)&#34;</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#f92672">not</span> secrets_rule_seen:
</span></span><span style="display:flex;"><span>        findings<span style="color:#f92672">.</span>append(<span style="color:#e6db74">&#34;no rule covers secrets: Secrets are stored unencrypted&#34;</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> f_ <span style="color:#f92672">in</span> findings:
</span></span><span style="display:flex;"><span>        print(<span style="color:#e6db74">&#34;WARN&#34;</span>, f_)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#f92672">not</span> findings:
</span></span><span style="display:flex;"><span>        print(<span style="color:#e6db74">&#34;OK no weak settings found for secrets&#34;</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#ae81ff">1</span> <span style="color:#66d9ef">if</span> findings <span style="color:#66d9ef">else</span> <span style="color:#ae81ff">0</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> __name__ <span style="color:#f92672">==</span> <span style="color:#e6db74">&#34;__main__&#34;</span>:
</span></span><span style="display:flex;"><span>    sys<span style="color:#f92672">.</span>exit(main(sys<span style="color:#f92672">.</span>argv[<span style="color:#ae81ff">1</span>]))
</span></span></code></pre></div><p>Results on our samples: the <code>aescbc</code>-plus-<code>identity</code> file above produced three warnings and exit code 1; a file using <code>kms</code> with <code>apiVersion: v2</code> printed <code>OK</code> and exited 0; a file that only configured ConfigMaps reported that no rule covers Secrets. The non-zero exit code lets you run it in CI against the configuration you deploy to control plane nodes.</p>
<h2 id="rolling-it-out-without-locking-yourself-out">Rolling it out without locking yourself out</h2>
<p>The Kubernetes task page walks through the full procedure. The steps that most often go wrong:</p>
<ol>
<li>
<p><strong>Every control plane host needs the same file and key.</strong> The docs say to copy the key to every control plane host over a secure channel, and to restart each API server. An API server without the key can&rsquo;t read Secrets another one encrypted.</p>
</li>
<li>
<p><strong>Existing Secrets are not touched.</strong> Encryption applies on write. Rewrite everything once all API servers have the new configuration:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-shell" data-lang="shell"><span style="display:flex;"><span>kubectl get secrets --all-namespaces -o json | kubectl replace -f -
</span></span></code></pre></div></li>
<li>
<p><strong>Verify in etcd, not through the API.</strong> The API always returns decrypted values, so <code>kubectl get secret</code> proves nothing. Read a Secret with <code>etcdctl get /registry/secrets/&lt;namespace&gt;/&lt;name&gt;</code> and check that the stored value starts with <code>k8s:enc:&lt;provider&gt;:v1:&lt;key-name&gt;:</code>, for example <code>k8s:enc:aescbc:v1:key1:</code>.</p>
</li>
<li>
<p><strong>Rotate keys in two passes.</strong> The docs&rsquo; zero-downtime procedure: add the new key as the <em>second</em> entry on every control plane node and restart all API servers so each can decrypt with it; back the key up; then make it the first entry, restart again, rewrite all Secrets, and only then remove the old key. Losing every copy of a key means deleting the resources encrypted with it.</p>
</li>
</ol>
<h2 id="controls-that-matter-as-much-as-encryption">Controls that matter as much as encryption</h2>
<p>Encryption at rest is one line in the Kubernetes Secrets good-practices list. The others address the readers in the first table:</p>
<ul>
<li><strong>Restrict <code>get</code>, <code>list</code> and <code>watch</code>.</strong> The docs say to limit <code>watch</code> and <code>list</code> to &ldquo;the most privileged, system-level components&rdquo; and caution that <code>list</code> &ldquo;implicitly lets the subject fetch the contents of the Secrets.&rdquo;</li>
<li><strong>Separate namespaces by trust.</strong> Pod creation reaches every Secret in a namespace, so a namespace is the practical boundary. Enforce <a href="/posts/kubernetes-pod-security-admission-namespace-labels-modes-and-a-safe-rollout-from-warn-to-enforce/">Pod Security Admission</a> where workload creators are less trusted, and consider <a href="/posts/kubernetes-validatingadmissionpolicy-cel-rules-for-registry-allowlists-and-memory-limits-with-a-warn-first-rollout/">ValidatingAdmissionPolicy</a> for rules Pod Security doesn&rsquo;t cover.</li>
<li><strong>Audit Secret access at the Metadata level.</strong> The example audit policy in the Kubernetes docs logs Secrets and ConfigMaps at <code>Metadata</code>, which records who accessed what without writing values into your logs. The good-practices page suggests alerting on events such as one user reading many Secrets at once.</li>
<li><strong>Use short-lived credentials and external stores.</strong> The docs recommend short-lived Secrets and mention the Secrets Store CSI Driver for keeping values in an external store. The same reasoning drives <a href="/posts/aws-iam-least-privilege-a-practical-guide-to-finding-excess-permissions/">least privilege in cloud IAM</a>: a credential that expires in an hour is worth less to whoever reads it.</li>
</ul>
<h2 id="limits-of-this-article">Limits of this article</h2>
<p>This describes upstream Kubernetes as documented on kubernetes.io at the time of writing. We tested the checker against sample files only, not against a live cluster or a managed service. We have not tested the encryption rollout procedure ourselves; follow the Kubernetes task page and your distribution&rsquo;s documentation, and rehearse on a non-production cluster first.</p>
<h2 id="sources-and-verification">Sources and verification</h2>
<table>
  <thead>
      <tr>
          <th>Important claim</th>
          <th>Source</th>
          <th>Verification</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Secrets stored unencrypted in etcd by default; Pod creators can read any Secret in the namespace</td>
          <td><a href="https://kubernetes.io/docs/concepts/configuration/secret/">Kubernetes: Secrets</a></td>
          <td>Verified</td>
      </tr>
      <tr>
          <td>Base64 is not encryption; list access reveals contents; short-lived Secrets, audit alerting, Secrets Store CSI Driver</td>
          <td><a href="https://kubernetes.io/docs/concepts/security/secrets-good-practices/">Kubernetes: Good practices for Kubernetes Secrets</a></td>
          <td>Verified</td>
      </tr>
      <tr>
          <td>list and watch reveal contents; workload creation grants namespace Secret access</td>
          <td><a href="https://kubernetes.io/docs/concepts/security/rbac-good-practices/">Kubernetes: RBAC good practices</a></td>
          <td>Verified</td>
      </tr>
      <tr>
          <td>identity default; first provider encrypts; rule ordering; provider ratings; local vs KMS key threat model; KMS v1 deprecated v1.28; KMS v2 stable v1.29; rewrite command; k8s:enc: prefix; key rotation steps</td>
          <td><a href="https://kubernetes.io/docs/tasks/administer-cluster/encrypt-data/">Kubernetes: Encrypting confidential data at rest</a></td>
          <td>Verified</td>
      </tr>
      <tr>
          <td>Example audit policy logs Secrets and ConfigMaps at Metadata level</td>
          <td><a href="https://kubernetes.io/docs/tasks/debug/debug-cluster/audit/">Kubernetes: Auditing</a></td>
          <td>Verified</td>
      </tr>
      <tr>
          <td>A local key turns a leaked etcd copy into ciphertext</td>
          <td>Editorial inference from the encrypt-data page&rsquo;s statement that local keys protect against etcd compromise</td>
          <td>Qualified</td>
      </tr>
  </tbody>
</table>
]]></content:encoded></item><item><title>How to Detect and Rotate Leaked API Keys Before Attackers Use Them</title><link>https://firewallsync.com/posts/how-to-detect-and-rotate-leaked-api-keys-before-attackers-use-them/</link><guid>https://firewallsync.com/posts/how-to-detect-and-rotate-leaked-api-keys-before-attackers-use-them/</guid><pubDate>Mon, 05 Oct 2026 09:00:00 +0000</pubDate><dc:creator>FirewallSync Editorial</dc:creator><category>AppSec &amp; APIs</category><description>A leaked API key isn't automatically compromised, but you often can't prove it wasn't used. Here's how to find exposure fast and rotate without breaking production.</description><enclosure url="https://firewallsync.com/posts/how-to-detect-and-rotate-leaked-api-keys-before-attackers-use-them/cover_hu4123451916895706154.jpg" type="image/jpeg" length="0"/><media:content url="https://firewallsync.com/posts/how-to-detect-and-rotate-leaked-api-keys-before-attackers-use-them/cover_hu4123451916895706154.jpg" medium="image" type="image/jpeg" width="1200" height="630"/><content:encoded><![CDATA[<p><img src="https://firewallsync.com/posts/how-to-detect-and-rotate-leaked-api-keys-before-attackers-use-them/cover_hu4123451916895706154.jpg" alt="Colorful JavaScript code on a dark screen" width="1200" height="630"></p><p>An API key committed to a public repository is not automatically compromised. The problem is that you usually cannot prove who copied it, when, or what they did with it. Scanners and bots index public GitHub commits within minutes of a push, so the window between &ldquo;leaked&rdquo; and &ldquo;found by someone else&rdquo; is often shorter than the window between &ldquo;leaked&rdquo; and &ldquo;found by you.&rdquo;</p>
<h2 id="how-keys-actually-get-exposed">How keys actually get exposed</h2>
<p>Most leaks don&rsquo;t come from someone deliberately posting a credential. They come from ordinary developer workflow:</p>
<ul>
<li>A <code>.env</code> file committed because <code>.gitignore</code> was added after the first commit, not before it</li>
<li>A key hardcoded during local testing and never removed before the branch was pushed</li>
<li>A key embedded in client-side JavaScript because it was easier than building a backend proxy</li>
<li>A key pasted into a CI log for debugging and left there, readable by anyone with log access</li>
<li>A key shared in a support ticket, Slack thread, or screen-recorded demo</li>
</ul>
<p>Client-side exposure deserves special attention because it isn&rsquo;t a mistake in the traditional sense — a key embedded in a frontend bundle is, by definition, sent to every visitor&rsquo;s browser. If a key must be used from the browser, it needs to be scoped so that exposure is expected and harmless, not treated as a leak when it inevitably surfaces.</p>
<h2 id="finding-out-youre-exposed">Finding out you&rsquo;re exposed</h2>
<p>Don&rsquo;t wait for a scanner to tell you. Check these sources directly:</p>
<ul>
<li><strong>GitHub secret scanning</strong> — enabled by default on public repos for known token formats; push protection can block the commit before it lands if you turn it on</li>
<li><strong>Your own git history</strong> — a key removed from the current file is still in every prior commit unless you&rsquo;ve rewritten history; <code>git log -p</code> and grep for the key pattern across the full history, not just HEAD</li>
<li><strong>CI/CD logs</strong> — check whether your pipeline prints environment variables or command arguments that include secrets, especially in debug or verbose modes</li>
<li><strong>Client-side bundles</strong> — search your built JS/CSS output for the key string directly; source maps can also re-expose something that was minified out of the readable bundle</li>
</ul>
<h2 id="rotating-without-breaking-production">Rotating without breaking production</h2>
<p>Rotation fails when the old key is deactivated before every service using it has the new one. The order matters:</p>
<ol>
<li><strong>Issue a new key</strong> without deactivating the old one yet — most providers let both be valid simultaneously</li>
<li><strong>Deploy the new key</strong> to every service, script, and integration that uses it, including ones that aren&rsquo;t obviously &ldquo;yours&rdquo; — third-party tools, scheduled jobs, and one-off scripts are the most common things forgotten</li>
<li><strong>Verify old-key usage has dropped to zero</strong> using the provider&rsquo;s access logs or usage dashboard before deactivating anything; if usage doesn&rsquo;t drop, something is still using the old key and you haven&rsquo;t found it yet</li>
<li><strong>Deactivate the old key</strong>, not just rotate the value in your own config — a key still valid on the provider&rsquo;s side is still usable by whoever leaked it, regardless of what your app currently points at</li>
</ol>
<h2 id="common-mistakes">Common mistakes</h2>
<p>The most common rotation failure isn&rsquo;t forgetting to rotate — it&rsquo;s rotating the key in your secrets manager without confirming every consumer actually redeployed with the new value. A key that&rsquo;s &ldquo;rotated&rdquo; in Vault but still cached in a running process&rsquo;s memory, or in a service that reads secrets only at startup, means the old key is still functionally live until that service restarts.</p>
<p>The second most common mistake is treating rotation as the end of the incident. If the key had broad permissions, rotation stops future misuse but does nothing about anything already done with it during the exposure window — check the provider&rsquo;s audit log for activity on that key specifically, not just your own application logs.</p>
<h2 id="detection-and-verification-checklist">Detection and verification checklist</h2>
<ul>
<li>Search full git history, not just current files, for the key pattern</li>
<li>Check CI/CD logs for accidental secret printing</li>
<li>Search built frontend bundles and source maps</li>
<li>Confirm the provider&rsquo;s own access log shows old-key usage before deactivating it</li>
<li>Review what the leaked key had permission to do, and audit that specific window of activity</li>
<li>Scope any client-exposed keys narrowly enough that exposure is expected, not catastrophic</li>
</ul>
<h2 id="preventing-the-next-one">Preventing the next one</h2>
<p>Push protection (blocking commits containing recognizable secret patterns before they land) closes the most common exposure path at the point of failure, rather than relying on someone noticing afterward. Pair it with short-lived, scoped credentials where the provider supports them — a key with a narrow permission set and a defined expiry limits the damage of the next leak you don&rsquo;t catch immediately, which, at some point, you won&rsquo;t.</p>
]]></content:encoded></item><item><title>The API Key Mistakes That Keep Making Breach Reports</title><link>https://firewallsync.com/posts/the-api-key-mistakes-that-keep-making-breach-reports/</link><guid>https://firewallsync.com/posts/the-api-key-mistakes-that-keep-making-breach-reports/</guid><pubDate>Mon, 24 Aug 2026 00:00:00 +0000</pubDate><dc:creator>FirewallSync Editorial</dc:creator><category>AppSec &amp; APIs</category><description>Secrets scanning tools have gotten good. The breaches keep happening anyway, mostly for three preventable reasons.</description><enclosure url="https://firewallsync.com/posts/the-api-key-mistakes-that-keep-making-breach-reports/cover_hu17581053162965116179.jpg" type="image/jpeg" length="0"/><media:content url="https://firewallsync.com/posts/the-api-key-mistakes-that-keep-making-breach-reports/cover_hu17581053162965116179.jpg" medium="image" type="image/jpeg" width="1200" height="630"/><content:encoded><![CDATA[<p><img src="https://firewallsync.com/posts/the-api-key-mistakes-that-keep-making-breach-reports/cover_hu17581053162965116179.jpg" alt="Laptop displaying program code" width="1200" height="630"></p><p>Secrets scanning is mature technology at this point — most CI pipelines can catch a hardcoded key before it merges. Yet leaked API keys remain one of the most common root causes in breach disclosures. The gap isn&rsquo;t tooling. It&rsquo;s three specific habits that scanners don&rsquo;t catch.</p>
<h2 id="keys-with-no-expiry">Keys with no expiry</h2>
<p>A scanner can tell you a key is exposed in a commit. It can&rsquo;t tell you that the key was never going to expire anyway, so rotating it after the leak doesn&rsquo;t actually close the window — the same key pattern gets reused in the next service, with the same no-expiry default. Default every new key to expire, and make the renewal request the normal path, not the exception.</p>
<h2 id="shared-keys-across-environments">Shared keys across environments</h2>
<p>Using the same API key for staging and production is still common because it&rsquo;s convenient — one less secret to manage. It also means a staging environment with weaker access controls becomes a direct path to production data. Environment-scoped keys should be a hard requirement in your provisioning process, not a &ldquo;best practice&rdquo; that gets skipped under deadline pressure.</p>
<h2 id="keys-stored-in-places-scanners-dont-look">Keys stored in places scanners don&rsquo;t look</h2>
<p>Secrets scanners are tuned for source code. They generally don&rsquo;t scan Slack messages, shared docs, or ticket comments — all places engineers paste a key &ldquo;just this once&rdquo; while debugging. That key often outlives the debugging session by months. If your threat model assumes secrets only live in git, you&rsquo;re missing the majority of where they actually leak from in practice.</p>
<h2 id="where-to-focus-first">Where to focus first</h2>
<ol>
<li>Audit for non-expiring keys before adding more scanning tools</li>
<li>Split shared staging/production keys this quarter, not next</li>
<li>Treat chat and docs as a secrets surface, with periodic manual sweeps</li>
</ol>
<p>Scanning your repos is necessary. It was never sufficient.</p>
]]></content:encoded></item></channel></rss>