<?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>Process | FirewallSync</title><link>https://firewallsync.com/tags/process/</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>Sun, 18 Oct 2026 06:06:00 +0000</lastBuildDate><atom:link href="https://firewallsync.com/tags/process/index.xml" rel="self" type="application/rss+xml"/><item><title>SSVC Deployer Model Explained: How Exploitation, Exposure, Automatable and Human Impact Set Patch Priority</title><link>https://firewallsync.com/posts/ssvc-deployer-patch-priority-model-explained-exploitation-exposure-automatable-and-human-impact/</link><guid>https://firewallsync.com/posts/ssvc-deployer-patch-priority-model-explained-exploitation-exposure-automatable-and-human-impact/</guid><pubDate>Sun, 18 Oct 2026 06:06:00 +0000</pubDate><dc:creator>Craig Leslie</dc:creator><category>Security Operations</category><description>The CERT/CC SSVC deployer model turns four inputs into Defer, Scheduled, Out-of-cycle or Immediate. This guide reproduces all 72 rows of its example decision table, shows which inputs CISA's public CVE data already supplies, and flags where the model's own pages disagree.</description><enclosure url="https://firewallsync.com/posts/ssvc-deployer-patch-priority-model-explained-exploitation-exposure-automatable-and-human-impact/cover_hu9859181224393853406.jpg" type="image/jpeg" length="0"/><media:content url="https://firewallsync.com/posts/ssvc-deployer-patch-priority-model-explained-exploitation-exposure-automatable-and-human-impact/cover_hu9859181224393853406.jpg" medium="image" type="image/jpeg" width="1200" height="630"/><content:encoded><![CDATA[<p><img src="https://firewallsync.com/posts/ssvc-deployer-patch-priority-model-explained-exploitation-exposure-automatable-and-human-impact/cover_hu9859181224393853406.jpg" alt="A snowy, nearly empty downtown street at dusk with red traffic lights, a glass skywalk bridge between tall office buildings and one car in the distance" width="1200" height="630"></p><p>Most teams prioritize patches by a severity score, then argue about the exceptions. CERT/CC&rsquo;s SSVC takes a different route: it asks four questions about the vulnerability and your own systems, and a fixed lookup table returns a priority. This article explains the <strong>deployer</strong> version of that model, reproduces its full decision table so you can check it yourself, and shows how much of the answer CISA&rsquo;s public CVE data can and cannot provide.</p>
<p><strong>Scope and method.</strong> We read the CERT/CC SSVC documentation on October 18, 2026: the &ldquo;Prioritizing Patch Deployment&rdquo; page (the deployer model), and the reference pages for Human Impact, Safety Impact and Mission Impact. We extracted the 72-row decision table from the page and checked it programmatically (counts, completeness and consistency, described below). We also read the CISA Vulnrichment SSVC values and CISA Known Exploited Vulnerabilities (KEV) entries in six CVE records from the CVE Services API on October 18, 2026. The CERT/CC pages show no publication date, so we cite the read date. We did not test SSVC in a production vulnerability-management program, and this is research-based analysis, not firsthand operating experience. If you only want to read CISA&rsquo;s own three values, see our <a href="/posts/cisa-vulnrichment-how-to-read-ssvc-values-in-a-cve-record-and-turn-them-into-a-patch-decision/">Vulnrichment guide</a>; this article covers the other decision a deployer faces.</p>
<h2 id="the-short-answer">The short answer</h2>
<ul>
<li><strong>Who it is for.</strong> The &ldquo;deployer&rdquo;: an organization that receives a patch or mitigation from a supplier and decides how fast to apply it to systems it runs. CERT/CC recommends the unit of work be the pairing of a remediation or mitigation with one product-version instance.</li>
<li><strong>Four inputs:</strong> Exploitation, System Exposure, Automatable, Human Impact.</li>
<li><strong>Four outputs:</strong> Defer, Scheduled, Out-of-cycle, Immediate.</li>
<li><strong>How many inputs you supply yourself:</strong> two (System Exposure and Human Impact). Exploitation and Automatable can come from public sources such as CISA&rsquo;s data, if you accept them.</li>
</ul>
<h2 id="the-four-inputs">The four inputs</h2>
<table>
  <thead>
      <tr>
          <th>Input (version)</th>
          <th>Values</th>
          <th>What it asks</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Exploitation (v1.1.0)</td>
          <td>None, Public PoC, Active</td>
          <td>None: no evidence of active exploitation and no public proof of concept. Public PoC: a typical public PoC exists (for example in Metasploit or ExploitDB) or the vulnerability has a well-known method of exploitation. Active: shared, observable, reliable evidence of use in the wild by real attackers, with credible public reporting</td>
      </tr>
      <tr>
          <td>System Exposure (v1.0.1)</td>
          <td>Small, Controlled, Open</td>
          <td>Small: local service or program, highly controlled network. Controlled: networked service with some access restrictions or mitigations already in place. Open: internet or another widely accessible network where access cannot plausibly be restricted</td>
      </tr>
      <tr>
          <td>Automatable (v2.0.0)</td>
          <td>No, Yes</td>
          <td>Can an attacker reliably automate steps 1 to 4 of the kill chain (reconnaissance, weaponization, delivery, exploitation)?</td>
      </tr>
      <tr>
          <td>Human Impact (v2.0.2)</td>
          <td>Low, Medium, High, Very High</td>
          <td>A combination of Safety Impact and Mission Impact, explained below</td>
      </tr>
  </tbody>
</table>
<p>&ldquo;PoC&rdquo; means proof of concept. Note the definition of <strong>Controlled</strong>: CERT/CC says a successful mitigation must reliably interrupt the adversary&rsquo;s attack, which requires that the attack be detectable both reliably and quickly enough to respond. A firewall rule nobody monitors does not necessarily qualify.</p>
<h3 id="human-impact-the-lookup-table">Human Impact: the lookup table</h3>
<p>Human Impact combines Safety Impact (v2.0.1: Negligible, Marginal, Critical, Catastrophic, based on IEC 61508 with modifications) with Mission Impact (v2.0.0: Degraded, MEF Support Crippled, MEF Failure, Mission Failure). &ldquo;MEF&rdquo; means Mission Essential Function. This is the lookup table published on the Human Impact page:</p>
<table>
  <thead>
      <tr>
          <th>Safety Impact</th>
          <th>Degraded</th>
          <th>MEF Support Crippled</th>
          <th>MEF Failure</th>
          <th>Mission Failure</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Negligible</td>
          <td>Low</td>
          <td>Low</td>
          <td>Medium</td>
          <td>Very High</td>
      </tr>
      <tr>
          <td>Marginal</td>
          <td>Low</td>
          <td>Low</td>
          <td>Medium</td>
          <td>Very High</td>
      </tr>
      <tr>
          <td>Critical</td>
          <td>Medium</td>
          <td>High</td>
          <td>High</td>
          <td>Very High</td>
      </tr>
      <tr>
          <td>Catastrophic</td>
          <td>Very High</td>
          <td>Very High</td>
          <td>Very High</td>
          <td>Very High</td>
      </tr>
  </tbody>
</table>
<p><strong>A documentation inconsistency you should know about.</strong> The same page also gives prose definitions of each Human Impact level, and they do not match the table in four cells:</p>
<table>
  <thead>
      <tr>
          <th>Safety / Mission</th>
          <th>Table says</th>
          <th>Prose says</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Marginal / Degraded</td>
          <td>Low</td>
          <td>Medium</td>
      </tr>
      <tr>
          <td>Marginal / MEF Support Crippled</td>
          <td>Low</td>
          <td>Medium</td>
      </tr>
      <tr>
          <td>Marginal / MEF Failure</td>
          <td>Medium</td>
          <td>High</td>
      </tr>
      <tr>
          <td>Critical / Degraded</td>
          <td>Medium</td>
          <td>High</td>
      </tr>
  </tbody>
</table>
<p>In addition, the prose does not mention Critical combined with MEF Failure at all, while the table gives High. We use the table, because it is the full 16-cell mapping and the prose definitions do not cover every combination. This is our judgment, not a CERT/CC statement. Before adopting the model, check which one your tooling implements, and check the current CERT/CC page, as this may be corrected.</p>
<h2 id="the-full-decision-table">The full decision table</h2>
<p>This is the example decision table from the CERT/CC deployer page (identified there as <code>ssvc:DT_DP:1.0.0</code>), reorganized so each row shows the outcome for all four Human Impact levels. The original lists 72 rows (3 Exploitation values × 3 System Exposure values × 2 Automatable values × 4 Human Impact values); we confirmed that each of the 72 combinations appears exactly once.</p>
<table>
  <thead>
      <tr>
          <th>Exploitation</th>
          <th>System Exposure</th>
          <th>Automatable</th>
          <th>Human Impact: Low</th>
          <th>Medium</th>
          <th>High</th>
          <th>Very High</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>None</td>
          <td>Small</td>
          <td>No</td>
          <td>Defer</td>
          <td>Defer</td>
          <td>Scheduled</td>
          <td>Scheduled</td>
      </tr>
      <tr>
          <td>None</td>
          <td>Small</td>
          <td>Yes</td>
          <td>Defer</td>
          <td>Scheduled</td>
          <td>Scheduled</td>
          <td>Scheduled</td>
      </tr>
      <tr>
          <td>None</td>
          <td>Controlled</td>
          <td>No</td>
          <td>Defer</td>
          <td>Scheduled</td>
          <td>Scheduled</td>
          <td>Scheduled</td>
      </tr>
      <tr>
          <td>None</td>
          <td>Controlled</td>
          <td>Yes</td>
          <td>Scheduled</td>
          <td>Scheduled</td>
          <td>Scheduled</td>
          <td>Scheduled</td>
      </tr>
      <tr>
          <td>None</td>
          <td>Open</td>
          <td>No</td>
          <td>Defer</td>
          <td>Scheduled</td>
          <td>Scheduled</td>
          <td>Scheduled</td>
      </tr>
      <tr>
          <td>None</td>
          <td>Open</td>
          <td>Yes</td>
          <td>Scheduled</td>
          <td>Scheduled</td>
          <td>Scheduled</td>
          <td>Out-of-cycle</td>
      </tr>
      <tr>
          <td>Public PoC</td>
          <td>Small</td>
          <td>No</td>
          <td>Defer</td>
          <td>Scheduled</td>
          <td>Scheduled</td>
          <td>Scheduled</td>
      </tr>
      <tr>
          <td>Public PoC</td>
          <td>Small</td>
          <td>Yes</td>
          <td>Scheduled</td>
          <td>Scheduled</td>
          <td>Scheduled</td>
          <td>Scheduled</td>
      </tr>
      <tr>
          <td>Public PoC</td>
          <td>Controlled</td>
          <td>No</td>
          <td>Defer</td>
          <td>Scheduled</td>
          <td>Scheduled</td>
          <td>Scheduled</td>
      </tr>
      <tr>
          <td>Public PoC</td>
          <td>Controlled</td>
          <td>Yes</td>
          <td>Scheduled</td>
          <td>Scheduled</td>
          <td>Scheduled</td>
          <td>Out-of-cycle</td>
      </tr>
      <tr>
          <td>Public PoC</td>
          <td>Open</td>
          <td>No</td>
          <td>Scheduled</td>
          <td>Scheduled</td>
          <td>Scheduled</td>
          <td>Out-of-cycle</td>
      </tr>
      <tr>
          <td>Public PoC</td>
          <td>Open</td>
          <td>Yes</td>
          <td>Scheduled</td>
          <td>Scheduled</td>
          <td>Out-of-cycle</td>
          <td>Out-of-cycle</td>
      </tr>
      <tr>
          <td>Active</td>
          <td>Small</td>
          <td>No</td>
          <td>Scheduled</td>
          <td>Scheduled</td>
          <td>Out-of-cycle</td>
          <td>Out-of-cycle</td>
      </tr>
      <tr>
          <td>Active</td>
          <td>Small</td>
          <td>Yes</td>
          <td>Scheduled</td>
          <td>Out-of-cycle</td>
          <td>Out-of-cycle</td>
          <td>Out-of-cycle</td>
      </tr>
      <tr>
          <td>Active</td>
          <td>Controlled</td>
          <td>No</td>
          <td>Scheduled</td>
          <td>Scheduled</td>
          <td>Out-of-cycle</td>
          <td>Out-of-cycle</td>
      </tr>
      <tr>
          <td>Active</td>
          <td>Controlled</td>
          <td>Yes</td>
          <td>Out-of-cycle</td>
          <td>Out-of-cycle</td>
          <td>Out-of-cycle</td>
          <td>Out-of-cycle</td>
      </tr>
      <tr>
          <td>Active</td>
          <td>Open</td>
          <td>No</td>
          <td>Scheduled</td>
          <td>Out-of-cycle</td>
          <td>Out-of-cycle</td>
          <td>Immediate</td>
      </tr>
      <tr>
          <td>Active</td>
          <td>Open</td>
          <td>Yes</td>
          <td>Out-of-cycle</td>
          <td>Out-of-cycle</td>
          <td>Immediate</td>
          <td>Immediate</td>
      </tr>
  </tbody>
</table>
<h3 id="what-the-table-shows-our-counts">What the table shows (our counts)</h3>
<p>We counted outcomes across the 72 rows and checked the shape of the table:</p>
<table>
  <thead>
      <tr>
          <th>Outcome</th>
          <th>Rows out of 72</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Defer</td>
          <td>7</td>
      </tr>
      <tr>
          <td>Scheduled</td>
          <td>42</td>
      </tr>
      <tr>
          <td>Out-of-cycle</td>
          <td>20</td>
      </tr>
      <tr>
          <td>Immediate</td>
          <td>3</td>
      </tr>
  </tbody>
</table>
<ul>
<li><strong>Active exploitation never yields Defer.</strong> This matches a note on the CERT/CC page.</li>
<li><strong>Immediate needs Active exploitation and Open exposure.</strong> All three Immediate rows are Active, Open and one of: Automatable No with Very High Human Impact, or Automatable Yes with High or Very High Human Impact.</li>
<li><strong>No exploitation evidence is mostly Defer or Scheduled.</strong> The one exception is None, Open, Automatable Yes and Very High Human Impact, which is Out-of-cycle. The CERT/CC page makes the same observation.</li>
<li><strong>The table is monotonic.</strong> We compared every pair of rows: raising any single input, with the others unchanged or also raised, never lowered the priority in the table (zero violations out of all comparable pairs).</li>
</ul>
<h2 id="what-cisas-public-data-can-feed-in">What CISA&rsquo;s public data can feed in</h2>
<p>CISA&rsquo;s Vulnrichment adds an SSVC block to many CVE records, with three decision points: Exploitation, Automatable and Technical Impact. Exploitation and Automatable share names with two deployer inputs. We read six records on October 18, 2026; values were stamped by CISA on the dates shown and could have changed since:</p>
<table>
  <thead>
      <tr>
          <th>CVE</th>
          <th>Product family (see our article)</th>
          <th>CISA SSVC timestamp</th>
          <th>Exploitation</th>
          <th>Automatable</th>
          <th>In KEV (date added)</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>CVE-2026-88771</td>
          <td><a href="/posts/citrix-netscaler-zero-days-cve-2026-88771-cve-2026-88772-patch-and-hunt/">Citrix NetScaler</a></td>
          <td>2026-09-27</td>
          <td>active</td>
          <td>yes</td>
          <td>Yes (2026-09-27)</td>
      </tr>
      <tr>
          <td>CVE-2026-102489</td>
          <td><a href="/posts/zammad-zero-days-cve-2026-102489-and-cve-2026-102490-what-is-fixed-and-what-is-not/">Zammad</a></td>
          <td>2026-10-02</td>
          <td>active</td>
          <td>no</td>
          <td>Yes (2026-10-02)</td>
      </tr>
      <tr>
          <td>CVE-2026-105207</td>
          <td><a href="/posts/zitadel-account-takeover-cve-2026-105207-cve-2026-105209-cve-2026-105215-fixed-versions/">ZITADEL</a></td>
          <td>2026-10-06</td>
          <td>none</td>
          <td>yes</td>
          <td>Not in the record</td>
      </tr>
      <tr>
          <td>CVE-2026-76504</td>
          <td><a href="/posts/cisco-catalyst-sd-wan-manager-cve-2026-76504-authentication-bypass-fixed-releases-and-log-checks/">Cisco Catalyst SD-WAN Manager</a></td>
          <td>2026-09-30</td>
          <td>active</td>
          <td>yes</td>
          <td>Yes (2026-09-30)</td>
      </tr>
      <tr>
          <td>CVE-2026-104286</td>
          <td><a href="/posts/fortimail-zero-day-cve-2026-104286-no-patch-yet-what-to-do/">FortiMail</a></td>
          <td>2026-10-02</td>
          <td>active</td>
          <td>yes</td>
          <td>Yes (2026-10-01)</td>
      </tr>
      <tr>
          <td>CVE-2026-88779</td>
          <td><a href="/posts/netscaler-cve-2026-88779-saml-denial-of-service-exploited-patched-last-weeks-zero-days/">NetScaler SAML</a></td>
          <td>2026-10-04</td>
          <td>active</td>
          <td>yes</td>
          <td>Yes (2026-10-04)</td>
      </tr>
  </tbody>
</table>
<p>Three cautions apply before you reuse these as deployer inputs:</p>
<ol>
<li><strong>Definitions may differ.</strong> CISA labels its blocks with the role &ldquo;CISA Coordinator&rdquo; and tree version 2.0.3. We did not compare CISA&rsquo;s decision-point versions against the deployer model&rsquo;s versions line by line. The names and the value sets (none/poc/active; no/yes) match, but confirm before automating.</li>
<li><strong>A timestamp is a snapshot.</strong> CVE-2026-105207 was marked <code>none</code> on October 6; &ldquo;none&rdquo; says no evidence at that time. Recheck KEV and the vendor advisory.</li>
<li><strong>A missing block is missing data.</strong> No CISA block does not mean low risk. See the <a href="/posts/cisa-kev-json-feed-what-each-field-means-and-how-to-match-it-against-a-scanner-export/">KEV feed guide</a> for matching KEV to your inventory.</li>
</ol>
<p>Technical Impact is part of CISA&rsquo;s own tree but is not one of the deployer model&rsquo;s four inputs. Do not feed it in as if it were.</p>
<h2 id="worked-examples-same-flaw-different-answers">Worked examples: same flaw, different answers</h2>
<p>Suppose the affected product is running in your environment. We take Exploitation and Automatable from the table above, then show the full range of outcomes across your two inputs. All outcomes are read from the decision table.</p>
<table>
  <thead>
      <tr>
          <th>CVE (Exploitation / Automatable)</th>
          <th>Best case: Small exposure, Low Human Impact</th>
          <th>Worst case: Open exposure, Very High Human Impact</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>CVE-2026-88771 (Active / Yes)</td>
          <td>Scheduled</td>
          <td>Immediate</td>
      </tr>
      <tr>
          <td>CVE-2026-102489 (Active / No)</td>
          <td>Scheduled</td>
          <td>Immediate</td>
      </tr>
      <tr>
          <td>CVE-2026-105207 (None / Yes)</td>
          <td>Defer</td>
          <td>Out-of-cycle</td>
      </tr>
  </tbody>
</table>
<p>More useful are the in-between cases. For CVE-2026-88771 (Active, Automatable Yes):</p>
<table>
  <thead>
      <tr>
          <th>System Exposure</th>
          <th>Low</th>
          <th>Medium</th>
          <th>High</th>
          <th>Very High</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Small</td>
          <td>Scheduled</td>
          <td>Out-of-cycle</td>
          <td>Out-of-cycle</td>
          <td>Out-of-cycle</td>
      </tr>
      <tr>
          <td>Controlled</td>
          <td>Out-of-cycle</td>
          <td>Out-of-cycle</td>
          <td>Out-of-cycle</td>
          <td>Out-of-cycle</td>
      </tr>
      <tr>
          <td>Open</td>
          <td>Out-of-cycle</td>
          <td>Out-of-cycle</td>
          <td>Immediate</td>
          <td>Immediate</td>
      </tr>
  </tbody>
</table>
<p>Read that second table carefully. An internet-facing instance (Open) supporting a function with Human Impact Medium is Out-of-cycle, not Immediate; the model reserves Immediate for High or Very High impact on an Open system. An instance with Controlled exposure is Out-of-cycle at every Human Impact level. This is the model&rsquo;s output, not an endorsement of those timings for your organization.</p>
<p>For CVE-2026-102489 (Active, Automatable No) an Open system with Medium Human Impact is Out-of-cycle, and a Controlled system with Medium Human Impact is only Scheduled, even though the flaw is listed in KEV. That is the kind of result a severity score alone would not give you, and the kind a risk owner may want to override. SSVC does not stop you from doing so.</p>
<h2 id="using-it-in-practice">Using it in practice</h2>
<ol>
<li><strong>Pre-assign Human Impact by asset class.</strong> Record, once, the Safety Impact and Mission Impact level for each class of system, then look up Human Impact in the table. Re-deciding it per CVE is where inconsistency creeps in.</li>
<li><strong>Define Exposure per instance, not per product.</strong> The same product can be Open in one place and Small in another. Apply CERT/CC&rsquo;s strict wording for Controlled.</li>
<li><strong>Treat mitigations as input changes.</strong> CERT/CC says a mitigation that successfully changes a decision point can lower the priority; it also warns that mitigations can fail, for example a firewall rule that is later changed. If you rely on one, record who owns it.</li>
<li><strong>Keep legal deadlines separate.</strong> Binding timelines, such as those in <a href="/posts/cisa-bod-26-04-explained-kev-deadlines-forensic-triage-and-what-private-teams-can-borrow/">BOD 26-04</a> for federal agencies, are not the same thing as SSVC priorities.</li>
<li><strong>Compare with your current method.</strong> Our comparison of <a href="/posts/cvss-vs-epss-vs-kev-which-signal-should-decide-what-you-patch-first/">CVSS, EPSS and KEV</a> shows what each signal answers; SSVC&rsquo;s deployer table consumes an exploitation signal and then adds your context.</li>
<li><strong>Reassess when inputs change.</strong> CERT/CC says remediation is not a final state, because rollbacks or new deployments can reintroduce a vulnerability.</li>
</ol>
<h2 id="limits-of-this-article">Limits of this article</h2>
<ul>
<li>We read the CERT/CC example table; we did not read the underlying repository files or test CERT/CC&rsquo;s calculator tool.</li>
<li>The CERT/CC pages show no dates, so we cannot say when the table or the Human Impact inconsistency was last changed.</li>
<li>The CVE examples assume the affected product is present in your environment and are not statements about any real organization.</li>
<li>Values in live CVE records can change after October 18, 2026.</li>
<li>The outcome names describe urgency, not a service-level agreement. CERT/CC does not give hour or day limits for them on the pages we read, so we give none.</li>
<li>We did not evaluate how well SSVC performs compared with other methods.</li>
</ul>
<h2 id="sources-and-verification">Sources and verification</h2>
<p>CERT/CC pages and CVE records read October 18, 2026.</p>
<table>
  <thead>
      <tr>
          <th>Important claim</th>
          <th>Source</th>
          <th>Verification</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Deployer model unit of work, four decision points, four outcomes and their descriptions; mitigation and remediation notes</td>
          <td><a href="https://certcc.github.io/SSVC/howto/deployer_tree/">CERT/CC SSVC: Prioritizing Patch Deployment</a></td>
          <td>Verified: page shows no date</td>
      </tr>
      <tr>
          <td>72-row deployer decision table (ssvc:DT_DP:1.0.0), notes on Active and None; our counts (7, 42, 20, 3), completeness and monotonicity checks</td>
          <td>Same page; our calculation from the extracted table</td>
          <td>Qualified: counts and checks are ours; the table is presented by CERT/CC as an example</td>
      </tr>
      <tr>
          <td>Exploitation, System Exposure and Automatable values and definitions</td>
          <td>Same page; <a href="https://certcc.github.io/SSVC/reference/decision_points/exploitation/">Exploitation</a></td>
          <td>Verified</td>
      </tr>
      <tr>
          <td>Human Impact definitions, Safety and Mission Impact value names, and the 16-cell lookup table</td>
          <td><a href="https://certcc.github.io/SSVC/reference/decision_points/human_impact/">CERT/CC SSVC: Human Impact</a>, <a href="https://certcc.github.io/SSVC/reference/decision_points/safety_impact/">Safety Impact</a>, <a href="https://certcc.github.io/SSVC/reference/decision_points/mission_impact/">Mission Impact</a></td>
          <td>Verified</td>
      </tr>
      <tr>
          <td>Four cells where the Human Impact prose and table differ; Critical with MEF Failure missing from the prose</td>
          <td>Same Human Impact page; our comparison</td>
          <td>Qualified: our comparison; the use of the table is our judgment</td>
      </tr>
      <tr>
          <td>Vulnrichment values and KEV dates for six CVEs</td>
          <td>CVE Services records, e.g. <a href="https://cveawg.mitre.org/api/cve/CVE-2026-88771">CVE-2026-88771</a>, <a href="https://cveawg.mitre.org/api/cve/CVE-2026-102489">CVE-2026-102489</a>, <a href="https://cveawg.mitre.org/api/cve/CVE-2026-105207">CVE-2026-105207</a>, <a href="https://cveawg.mitre.org/api/cve/CVE-2026-76504">CVE-2026-76504</a>, <a href="https://cveawg.mitre.org/api/cve/CVE-2026-104286">CVE-2026-104286</a>, <a href="https://cveawg.mitre.org/api/cve/CVE-2026-88779">CVE-2026-88779</a></td>
          <td>Qualified: live records read October 18, 2026; values can change</td>
      </tr>
      <tr>
          <td>Worked-example outcomes</td>
          <td>Table above, our lookup</td>
          <td>Qualified: assumes the product is present and uses our stated exposure and impact assumptions</td>
      </tr>
      <tr>
          <td>Practical steps and their rationale</td>
          <td>Our analysis</td>
          <td>Qualified: not tested in a production program</td>
      </tr>
  </tbody>
</table>
]]></content:encoded></item><item><title>Kubernetes MutatingAdmissionPolicy: CEL Mutations with Apply Configuration or JSON Patch, and When a Webhook Is Still Needed</title><link>https://firewallsync.com/posts/kubernetes-mutatingadmissionpolicy-cel-mutations-apply-configuration-or-json-patch-and-when-a-webhook-is-still-needed/</link><guid>https://firewallsync.com/posts/kubernetes-mutatingadmissionpolicy-cel-mutations-apply-configuration-or-json-patch-and-when-a-webhook-is-still-needed/</guid><pubDate>Sun, 18 Oct 2026 06:05:00 +0000</pubDate><dc:creator>John Marshall</dc:creator><category>Cloud Security</category><description>MutatingAdmissionPolicy lets the Kubernetes API server change incoming objects with CEL expressions instead of calling a webhook. This guide covers the three resources, the two patch types, reinvocation, failure behavior, what the docs do not say, and a cautious rollout.</description><enclosure url="https://firewallsync.com/posts/kubernetes-mutatingadmissionpolicy-cel-mutations-apply-configuration-or-json-patch-and-when-a-webhook-is-still-needed/cover_hu18373587765124506723.jpg" type="image/jpeg" length="0"/><media:content url="https://firewallsync.com/posts/kubernetes-mutatingadmissionpolicy-cel-mutations-apply-configuration-or-json-patch-and-when-a-webhook-is-still-needed/cover_hu18373587765124506723.jpg" medium="image" type="image/jpeg" width="1200" height="630"/><content:encoded><![CDATA[<p><img src="https://firewallsync.com/posts/kubernetes-mutatingadmissionpolicy-cel-mutations-apply-configuration-or-json-patch-and-when-a-webhook-is-still-needed/cover_hu18373587765124506723.jpg" alt="A close-up of a red industrial valve with black solenoid connectors, an orange cable and silver pipework, with a blurred corridor behind" width="1200" height="630"></p><p>A mutating admission policy answers a narrow question: when a request to create or update an object reaches the Kubernetes API server, should something in that object be changed before it is stored? Until recently the usual answer was a mutating webhook, which means running and securing a separate HTTPS service in the request path. <strong>MutatingAdmissionPolicy</strong> (MAP) moves the logic into the API server as declarative CEL expressions. This guide explains how it is built, what the documentation guarantees, and where you should still be careful.</p>
<p><strong>Scope and method.</strong> This is research-based analysis of the Kubernetes v1.37 documentation, read on October 18, 2026: the Mutating Admission Policy page (last modified September 28, 2026), the MutatingAdmissionPolicy and MutatingAdmissionPolicyBinding API references (both last modified August 26, 2026), the Admission Controllers reference (last modified September 28, 2026) and the Dynamic Admission Control page. We did not run a Kubernetes cluster and did not test any policy against an API server. The only thing we ran ourselves is a check of generic JSON Patch behavior, described in the JSON Patch section and labelled there.</p>
<h2 id="the-short-answer">The short answer</h2>
<ul>
<li><strong>What it is.</strong> A declarative, in-process alternative to mutating admission webhooks. Mutations are written in the Common Expression Language (CEL), the small expression language Kubernetes uses for admission rules.</li>
<li><strong>Status.</strong> The Kubernetes documentation says it is stable since v1.36 and enabled by default, and first became available in v1.30.</li>
<li><strong>What you create.</strong> A <code>MutatingAdmissionPolicy</code> (the logic), a <code>MutatingAdmissionPolicyBinding</code> (the scope), and optionally a parameter resource (the values). At least a policy and a matching binding must exist for anything to happen.</li>
<li><strong>Where it runs.</strong> In the mutating phase of admission, before the validating phase.</li>
<li><strong>The catch.</strong> It is part of the control plane. The documentation says to write and deploy these policies &ldquo;with great caution&rdquo;.</li>
</ul>
<h2 id="the-three-resources">The three resources</h2>
<table>
  <thead>
      <tr>
          <th>Resource</th>
          <th>What it does</th>
          <th>Notes from the documentation</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>MutatingAdmissionPolicy</td>
          <td>The abstract logic: what to match, which mutations to apply</td>
          <td><code>matchConstraints</code> and <code>mutations</code> are required; <code>mutations</code> cannot be an empty list</td>
      </tr>
      <tr>
          <td>Parameter resource (optional)</td>
          <td>Concrete values, for example from a ConfigMap or a custom resource</td>
          <td>Referenced by <code>spec.paramKind</code> in the policy and <code>paramRef</code> in the binding</td>
      </tr>
      <tr>
          <td>MutatingAdmissionPolicyBinding</td>
          <td>Links a policy to parameters and scopes it</td>
          <td>Has three fields: <code>policyName</code>, <code>paramRef</code> and <code>matchResources</code></td>
      </tr>
  </tbody>
</table>
<p>Two details are easy to miss. If a binding names a policy that does not exist, the API reference says the binding is &ldquo;considered invalid and will be ignored&rdquo;. If the policy declares a <code>paramKind</code> and the binding&rsquo;s <code>paramRef</code> points to a resource that does not exist, the binding is treated as misconfigured and the policy&rsquo;s <code>failurePolicy</code> applies. Also, per binding, the policy is evaluated once, or once per selected parameter object if parameters are used, and each evaluation has a runtime cost budget.</p>
<h2 id="which-requests-a-policy-sees">Which requests a policy sees</h2>
<p>Matching is layered, and each layer can only narrow the previous one:</p>
<ol>
<li><strong><code>matchConstraints</code></strong> on the policy (required) selects resources and operations. The API reference says the operations CREATE, UPDATE and CONNECT are allowed and DELETE may not be matched.</li>
<li><strong><code>matchResources</code></strong> on the binding narrows further. If it is unset, it does not constrain matching.</li>
<li><strong><code>matchConditions</code></strong> on the policy are CEL expressions evaluated last. If any condition is false the policy is skipped; if none is false but one errors, <code>failurePolicy</code> decides. A policy may have at most 64 match conditions.</li>
</ol>
<p>The documentation notes that <code>namespaceObject</code> is always null in match conditions and that <code>variables</code> cannot be used there, because match conditions are evaluated before the rest of the policy. To select by namespace labels, use a <code>namespaceSelector</code> in <code>matchConstraints</code> or in the binding&rsquo;s <code>matchResources</code>.</p>
<p>One more limit that affects design: the documentation says that only <code>apiVersion</code>, <code>kind</code>, <code>metadata.name</code>, <code>metadata.generateName</code> and <code>metadata.labels</code> are accessible from the root of the object, and that no other metadata properties are accessible. A rule that depends on an annotation, for example, cannot read it through <code>object</code>. This is the documented behavior; we have not tested workarounds.</p>
<h2 id="two-ways-to-express-a-mutation">Two ways to express a mutation</h2>
<p>Each entry in <code>spec.mutations</code> has a <code>patchType</code>, either <code>ApplyConfiguration</code> or <code>JSONPatch</code>.</p>
<h3 id="applyconfiguration">ApplyConfiguration</h3>
<p>The expression must evaluate to a CEL object created with <code>Object{...}</code>, and the result is merged using the server-side apply merge strategy. This is the Kubernetes documentation&rsquo;s own example, which adds a <code>mesh-proxy</code> init container to new Pods that do not already have one:</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">admissionregistration.k8s.io/v1</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">MutatingAdmissionPolicy</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#e6db74">&#34;sidecar-policy.example.com&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">paramKind</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">kind</span>: <span style="color:#ae81ff">Sidecar</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">apiVersion</span>: <span style="color:#ae81ff">mutations.example.com/v1</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">matchConstraints</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">resourceRules</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">apiGroups</span>:   [<span style="color:#e6db74">&#34;&#34;</span>]
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">apiVersions</span>: [<span style="color:#e6db74">&#34;v1&#34;</span>]
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">operations</span>:  [<span style="color:#e6db74">&#34;CREATE&#34;</span>]
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">resources</span>:   [<span style="color:#e6db74">&#34;pods&#34;</span>]
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">matchConditions</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">does-not-already-have-sidecar</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">expression</span>: <span style="color:#e6db74">&#34;!object.spec.initContainers.exists(ic, ic.name == \&#34;mesh-proxy\&#34;)&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">failurePolicy</span>: <span style="color:#ae81ff">Fail</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">reinvocationPolicy</span>: <span style="color:#ae81ff">IfNeeded</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">mutations</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">patchType</span>: <span style="color:#e6db74">&#34;ApplyConfiguration&#34;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">applyConfiguration</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">expression</span>: &gt;<span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">          Object{
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            spec: Object.spec{
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">              initContainers: [
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">                Object.spec.initContainers{
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">                  name: &#34;mesh-proxy&#34;,
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">                  image: &#34;mesh/proxy:v1.0.0&#34;,
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">                  args: [&#34;proxy&#34;, &#34;sidecar&#34;],
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">                  restartPolicy: &#34;Always&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">                }
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">              ]
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            }
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">          }</span>          
</span></span></code></pre></div><p>The documented restriction to remember: apply configurations may not modify atomic structs, maps or arrays, because of the risk of accidentally deleting values the configuration does not mention.</p>
<h3 id="jsonpatch">JSONPatch</h3>
<p>The expression must return an array of <code>JSONPatch{op, path, value}</code> values, following the <a href="https://jsonpatch.com/">JSON Patch</a> format. The documentation&rsquo;s examples include a conditional change using a <code>test</code> operation before a <code>replace</code>, and a helper, <code>jsonpatch.escapeKey()</code>, for path keys that contain <code>/</code> or <code>~</code>. It escapes <code>~</code> as <code>~0</code> and <code>/</code> as <code>~1</code>, so a label such as <code>example.com/environment</code> can be addressed.</p>
<p><strong>Read the documentation&rsquo;s two sidecar examples side by side.</strong> The page says the JSON Patch version writes &ldquo;the same mutation&rdquo;, but the two examples are not identical: the apply-configuration example uses the image <code>mesh/proxy:v1.0.0</code> and sets <code>args</code>, while the JSON Patch example uses the image string <code>mesh-proxy/v1.0.0</code> and no <code>args</code>. Treat them as illustrations of syntax, not as a verified equivalence, and do not copy the image reference from either.</p>
<h3 id="what-we-tested-about-json-patch">What we tested about JSON Patch</h3>
<p>On October 18, 2026 we ran a generic JSON Patch library (Python <code>jsonpatch</code> 1.35, an implementation of the RFC 6902 patch format) on small sample documents. This does not run the Kubernetes API server&rsquo;s implementation, so it shows how the format behaves, not how your cluster will.</p>
<table>
  <thead>
      <tr>
          <th>Patch</th>
          <th>Document</th>
          <th>Result</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>add</code> to <code>/spec/initContainers/-</code></td>
          <td>Pod with an existing <code>initContainers</code> list</td>
          <td>Appended the new item</td>
      </tr>
      <tr>
          <td>Same <code>add</code></td>
          <td>Pod with no <code>initContainers</code> field</td>
          <td>Error: member <code>initContainers</code> not found</td>
      </tr>
      <tr>
          <td><code>add</code> to <code>/metadata/labels/example.com~1environment</code></td>
          <td>Empty <code>labels</code> map</td>
          <td>Added the key <code>example.com/environment</code></td>
      </tr>
      <tr>
          <td><code>add</code> to <code>/metadata/labels/example.com/environment</code> (unescaped)</td>
          <td>Empty <code>labels</code> map</td>
          <td>Error: member <code>example.com</code> not found</td>
      </tr>
      <tr>
          <td><code>test</code> of <code>/spec/example</code> equal to <code>&quot;Red&quot;</code>, then <code>replace</code> with <code>&quot;Green&quot;</code></td>
          <td><code>example</code> is <code>&quot;Red&quot;</code></td>
          <td>Replaced with <code>&quot;Green&quot;</code></td>
      </tr>
      <tr>
          <td>Same pair</td>
          <td><code>example</code> is <code>&quot;Blue&quot;</code></td>
          <td>Error: the test failed, nothing replaced</td>
      </tr>
  </tbody>
</table>
<p>The practical lesson is that a positional JSON Patch needs the parent path to exist and the key to be escaped. The documentation&rsquo;s sidecar example uses a match condition that reads <code>object.spec.initContainers</code>; whether that expression behaves well when the field is absent is something we did not verify and you should test. The Kubernetes documentation we read does not say what the API server does when a JSON Patch fails to apply, so do not assume it is silently skipped.</p>
<h2 id="reinvocation-why-your-mutation-must-be-safe-to-repeat">Reinvocation: why your mutation must be safe to repeat</h2>
<p><code>reinvocationPolicy</code> is a required field on the policy, with two values:</p>
<ul>
<li><strong>Never:</strong> the mutations are not called more than once per binding in a single admission evaluation.</li>
<li><strong>IfNeeded:</strong> they may be called more than once per binding for a single admission request, only when other mutations changed the object after this one ran. The API reference states there is no guarantee of order relative to other admission plugins, webhooks, bindings of this policy and other admission policies.</li>
</ul>
<p>The reason is explained on the Dynamic Admission Control page: no single ordering of mutating plugins works for every case, so mutating plugins can be re-run so that they see changes made by others. That page also states that mutating webhooks must be idempotent, meaning able to process an object they have already modified. Our reading is that the same discipline applies to a policy using <code>IfNeeded</code>, which is why the documentation&rsquo;s example begins with a match condition that skips Pods that already have the <code>mesh-proxy</code> container. For comparison, the webhook field <code>reinvocationPolicy</code> defaults to <code>Never</code>.</p>
<h2 id="failure-behavior">Failure behavior</h2>
<p><code>failurePolicy</code> accepts <code>Fail</code> or <code>Ignore</code> and defaults to <code>Fail</code>. The API reference says failures can come from CEL parse errors, type-check errors, runtime errors, and invalid or misconfigured policy definitions or bindings, such as a <code>paramKind</code> that refers to a non-existent kind. It does not define how validations that evaluate to false are handled; that is a validation concept, not a mutation one.</p>
<p>For a control-plane component, the difference matters. With <code>Fail</code>, an error in your expression can reject writes to every object you matched. With <code>Ignore</code>, the same error means the mutation is silently not applied, and anything that depended on it, such as a validating rule that expects the label to be there, may then reject the object or let a bad one through. The Admission Controllers page says that if any MutatingAdmissionPolicy fails, the request fails.</p>
<h2 id="how-it-fits-with-other-admission-mechanisms">How it fits with other admission mechanisms</h2>
<p>The Admission Controllers reference says admission runs in two phases: mutating controllers first, then validating ones, and a rejection in either phase rejects the whole request.</p>
<ul>
<li><strong>With Pod Security Admission.</strong> The reference lists the <code>PodSecurity</code> plugin as validating. Pods are therefore checked after mutation. See our guide to <a href="/posts/kubernetes-pod-security-admission-namespace-labels-modes-and-a-safe-rollout-from-warn-to-enforce/">Pod Security Admission</a>.</li>
<li><strong>With ValidatingAdmissionPolicy.</strong> A mutating policy can set a value that a validating policy then checks. Our <a href="/posts/kubernetes-validatingadmissionpolicy-cel-rules-for-registry-allowlists-and-memory-limits-with-a-warn-first-rollout/">ValidatingAdmissionPolicy guide</a> covers the validating side, which has Deny, Warn and Audit actions that the MAP binding does not.</li>
<li><strong>With image signature checks.</strong> Verifying cosign signatures at admission is done by a webhook such as Sigstore&rsquo;s policy-controller; see our guide to <a href="/posts/verify-signed-container-images-in-kubernetes-sigstore-policy-controller-keyless-policy-and-rollout/">verifying signed container images in Kubernetes</a>.</li>
<li><strong>With webhooks.</strong> Built-in mutating plugins are re-run if a mutating webhook modifies an object, per the Dynamic Admission Control page.</li>
</ul>
<h3 id="exempt-kinds">Exempt kinds</h3>
<p>Policies created through the REST API cannot mutate ValidatingAdmissionPolicies, their bindings, MutatingAdmissionPolicies or their bindings. The documentation says a policy loaded through manifest-based admission control can intercept those kinds. Separately, the non-persisted authentication and authorization review kinds (TokenReviews, SelfSubjectReviews, LocalSubjectAccessReviews, SelfSubjectAccessReviews, SelfSubjectRulesReviews and SubjectAccessReviews) are exempt from all admission policies, and the page says that starting with Kubernetes v1.37 admission webhooks also exclude them by default.</p>
<h2 id="map-vap-or-webhook-a-decision-guide">MAP, VAP or webhook: a decision guide</h2>
<table>
  <thead>
      <tr>
          <th>Your need</th>
          <th>Fits</th>
          <th>Why (from the documentation)</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Reject requests that break a rule</td>
          <td>ValidatingAdmissionPolicy</td>
          <td>The MAP page calls it &ldquo;a simpler and more effective alternative&rdquo; for prevention-only rules</td>
      </tr>
      <tr>
          <td>Add or default fields from the request, an existing object field or a parameter</td>
          <td>MutatingAdmissionPolicy</td>
          <td>Declarative, in-process, CEL-based, parameterizable</td>
      </tr>
      <tr>
          <td>Decision needs an outside system or data not in the request or parameters</td>
          <td>Mutating webhook</td>
          <td>MAP is described as free of external HTTP callouts</td>
      </tr>
  </tbody>
</table>
<p>This table is our summary of the documented capabilities, not a benchmark. We did not measure latency or reliability differences between policies and webhooks, and we make no claim about them.</p>
<h2 id="a-cautious-rollout">A cautious rollout</h2>
<p>The MAP binding has no Warn or Audit action, so you cannot trial a policy in a non-enforcing mode the way you can with a validating binding. Our suggestions, all untested by us:</p>
<ol>
<li><strong>Start in a throwaway cluster</strong> on the Kubernetes minor version you run, because the feature became stable in v1.36 and your cluster may be older.</li>
<li><strong>Scope the binding narrowly</strong> with <code>matchResources</code> and a <code>namespaceSelector</code>. The <code>kubernetes.io/metadata.name</code> label that the API server sets on namespaces, as covered in our ValidatingAdmissionPolicy guide, gives you a name-based selector without labelling anything.</li>
<li><strong>Make the mutation idempotent</strong> and put the &ldquo;already done&rdquo; check in <code>matchConditions</code>.</li>
<li><strong>Decide on <code>failurePolicy</code> deliberately.</strong> Use <code>Ignore</code> while developing so a typo does not block writes, then switch to <code>Fail</code> once you have shown the mutation applies.</li>
<li><strong>Re-read the resulting object</strong>, and verify that your validating rules and Pod Security levels still accept it.</li>
</ol>
<h2 id="limits-of-this-article">Limits of this article</h2>
<ul>
<li>We did not run a Kubernetes cluster, so no policy in this article was tested against an API server.</li>
<li>The JSON Patch results come from a generic Python library, not from Kubernetes.</li>
<li>We did not find, in the pages we read, statements about what happens when a JSON Patch fails to apply, or about the ordering of several mutating policies; we have not claimed any.</li>
<li>The Admission Controllers page still describes the MutatingAdmissionPolicy plugin as needing the <code>mutatingadmissionpolicy</code> feature gate and the <code>admissionregistration.k8s.io/v1alpha1</code> API group, while its default-plugin list for 1.37 includes it and the MAP page says stable and enabled by default. We follow the latter two and flag the inconsistency.</li>
<li>Documentation changes. Re-check the pages below for your Kubernetes version.</li>
</ul>
<h2 id="sources-and-verification">Sources and verification</h2>
<p>Documentation read October 18, 2026 (site version v1.37).</p>
<table>
  <thead>
      <tr>
          <th>Important claim</th>
          <th>Source</th>
          <th>Verification</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>MAP is a declarative, in-process CEL alternative to mutating webhooks; stable since v1.36, enabled by default, first available in v1.30</td>
          <td><a href="https://kubernetes.io/docs/reference/access-authn-authz/mutating-admission-policy/">Mutating Admission Policy</a> (last modified Sep 28, 2026)</td>
          <td>Verified</td>
      </tr>
      <tr>
          <td>Policy, binding and optional parameter resource; a policy and a matching binding are required; <code>.spec.mutations</code> cannot be empty; names ending <code>.static.k8s.io</code> are reserved</td>
          <td><a href="https://kubernetes.io/docs/reference/access-authn-authz/mutating-admission-policy/">Mutating Admission Policy</a></td>
          <td>Verified</td>
      </tr>
      <tr>
          <td>ApplyConfiguration must evaluate to a CEL object and may not modify atomic structs, maps or arrays; <code>jsonpatch.escapeKey</code> escapes <code>~</code> and <code>/</code>; only apiVersion, kind, metadata.name, metadata.generateName and metadata.labels are accessible from the object root</td>
          <td><a href="https://kubernetes.io/docs/reference/access-authn-authz/mutating-admission-policy/">Mutating Admission Policy</a></td>
          <td>Verified</td>
      </tr>
      <tr>
          <td>matchConditions semantics, maximum of 64, <code>namespaceObject</code> and <code>variables</code> not usable there</td>
          <td><a href="https://kubernetes.io/docs/reference/kubernetes-api/policy-resources/mutating-admission-policy-v1/">MutatingAdmissionPolicy API reference</a> and the MAP page (last modified Aug 26, 2026)</td>
          <td>Verified</td>
      </tr>
      <tr>
          <td><code>failurePolicy</code> Fail or Ignore, default Fail; <code>reinvocationPolicy</code> required, Never or IfNeeded with the stated semantics; CREATE, UPDATE, CONNECT allowed, DELETE not</td>
          <td><a href="https://kubernetes.io/docs/reference/kubernetes-api/policy-resources/mutating-admission-policy-v1/">MutatingAdmissionPolicy API reference</a></td>
          <td>Verified</td>
      </tr>
      <tr>
          <td>Binding fields (<code>policyName</code>, <code>paramRef</code>, <code>matchResources</code>); missing policy means binding ignored; evaluation per parameter; cost budget</td>
          <td><a href="https://kubernetes.io/docs/reference/kubernetes-api/policy-resources/mutating-admission-policy-binding-v1/">MutatingAdmissionPolicyBinding API reference</a> (last modified Aug 26, 2026)</td>
          <td>Verified</td>
      </tr>
      <tr>
          <td>Two-phase admission, mutating then validating; rejection in either phase rejects the request; PodSecurity is validating; any failing MutatingAdmissionPolicy fails the request</td>
          <td><a href="https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers/">Admission Controllers reference</a></td>
          <td>Verified</td>
      </tr>
      <tr>
          <td>MutatingAdmissionPolicy appears in the Kubernetes 1.37 default-enabled plugin list</td>
          <td><a href="https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers/">Admission Controllers reference</a></td>
          <td>Qualified: the per-plugin text on the same page still mentions v1alpha1 prerequisites</td>
      </tr>
      <tr>
          <td>Built-in mutating plugins are re-run if a webhook modifies the object; webhooks must be idempotent; webhook <code>reinvocationPolicy</code> defaults to Never</td>
          <td><a href="https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#reinvocation-policy">Dynamic Admission Control</a></td>
          <td>Verified</td>
      </tr>
      <tr>
          <td>Exempt kinds, including virtual authentication and authorization kinds, and the v1.37 webhook default exclusion</td>
          <td><a href="https://kubernetes.io/docs/reference/access-authn-authz/mutating-admission-policy/">Mutating Admission Policy</a></td>
          <td>Verified</td>
      </tr>
      <tr>
          <td>The documentation&rsquo;s JSON Patch sidecar example differs from its apply-configuration example in image string and <code>args</code></td>
          <td><a href="https://kubernetes.io/docs/reference/access-authn-authz/mutating-admission-policy/">Mutating Admission Policy</a></td>
          <td>Verified (our comparison of the two published examples)</td>
      </tr>
      <tr>
          <td>JSON Patch behaviors in the test table</td>
          <td>Our run of Python <code>jsonpatch</code> 1.35, October 18, 2026</td>
          <td>Qualified: generic RFC 6902 library, not the Kubernetes API server</td>
      </tr>
      <tr>
          <td>Rollout suggestions and the reading that <code>IfNeeded</code> calls for idempotent mutations</td>
          <td>Our analysis</td>
          <td>Qualified: untested on a cluster</td>
      </tr>
  </tbody>
</table>
]]></content:encoded></item><item><title>Kubernetes ValidatingAdmissionPolicy: CEL Rules for Registry Allowlists and Memory Limits, with a Warn-First Rollout</title><link>https://firewallsync.com/posts/kubernetes-validatingadmissionpolicy-cel-rules-for-registry-allowlists-and-memory-limits-with-a-warn-first-rollout/</link><guid>https://firewallsync.com/posts/kubernetes-validatingadmissionpolicy-cel-rules-for-registry-allowlists-and-memory-limits-with-a-warn-first-rollout/</guid><pubDate>Sat, 17 Oct 2026 06:05:00 +0000</pubDate><dc:creator>John Marshall</dc:creator><category>Cloud Security</category><description>ValidatingAdmissionPolicy lets the Kubernetes API server enforce custom rules written in CEL, with no webhook to run. This guide shows the policy, binding and parameter model, two worked rules that Pod Security Admission cannot express, and a rollout order that goes from Warn and Audit to Deny.</description><enclosure url="https://firewallsync.com/posts/kubernetes-validatingadmissionpolicy-cel-rules-for-registry-allowlists-and-memory-limits-with-a-warn-first-rollout/cover_hu6289360861179626159.jpg" type="image/jpeg" length="0"/><media:content url="https://firewallsync.com/posts/kubernetes-validatingadmissionpolicy-cel-rules-for-registry-allowlists-and-memory-limits-with-a-warn-first-rollout/cover_hu6289360861179626159.jpg" medium="image" type="image/jpeg" width="1200" height="630"/><content:encoded><![CDATA[<p><img src="https://firewallsync.com/posts/kubernetes-validatingadmissionpolicy-cel-rules-for-registry-allowlists-and-memory-limits-with-a-warn-first-rollout/cover_hu6289360861179626159.jpg" alt="Large architectural floor-plan drawings printed on white paper and spread overlapping across a table" width="1200" height="630"></p><p>Kubernetes has a built-in way to reject a workload for breaking <em>your</em> rules, not just the generic ones: <strong>ValidatingAdmissionPolicy</strong> (VAP). You write a rule such as &ldquo;every container image must start with <code>registry.example.com/</code>&rdquo; in the Common Expression Language (CEL), and the API server evaluates it whenever a matching object is created or updated. There is no webhook server to run, patch or keep available. This guide explains the three resources involved, shows two worked rules, and gives a rollout order that does not break deployments on day one.</p>
<p><strong>Scope.</strong> This is research-based analysis of the Kubernetes documentation pages listed under Sources and verification, read on October 17, 2026 (the site&rsquo;s latest version is v1.37). We did not run a cluster. We did evaluate the CEL expressions below against sample Pod objects with the open-source <code>cel-go</code> library (version 0.32.0), which confirms the logic but is not the Kubernetes API server&rsquo;s own CEL environment. Treat the manifests as untested drafts and try them in a non-production cluster first.</p>
<h2 id="what-vap-is-and-where-it-sits">What VAP is, and where it sits</h2>
<p>The Kubernetes <a href="https://kubernetes.io/docs/reference/access-authn-authz/validating-admission-policy/">Validating Admission Policy page</a> describes validating admission policies as &ldquo;a declarative, in-process alternative to validating admission webhooks&rdquo;, using CEL to declare validation rules. The page marks the feature &ldquo;Stable since Kubernetes v1.30&rdquo;. An <em>admission controller</em> is a component that intercepts API requests after authentication and authorization and before the object is stored; the <a href="https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers/">admission controllers reference</a> lists <code>ValidatingAdmissionPolicy</code> among the plugins enabled by default in Kubernetes 1.37.</p>
<p>How it compares with the options you may already know:</p>
<table>
  <thead>
      <tr>
          <th></th>
          <th>Pod Security Admission</th>
          <th>ValidatingAdmissionPolicy</th>
          <th>Validating webhook</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Rules</td>
          <td>Three fixed levels</td>
          <td>Your own CEL expressions</td>
          <td>Your own code</td>
      </tr>
      <tr>
          <td>Runs</td>
          <td>In the API server</td>
          <td>In the API server</td>
          <td>In a separate service the API server calls over HTTP</td>
      </tr>
      <tr>
          <td>Can modify the object</td>
          <td>No</td>
          <td>No (validating only; mutation is a separate feature, MutatingAdmissionPolicy)</td>
          <td>No (mutating webhooks are a separate type)</td>
      </tr>
      <tr>
          <td>Best for</td>
          <td>Pod hardening baseline</td>
          <td>Field-level rules on any resource</td>
          <td>Logic CEL cannot express, or external lookups</td>
      </tr>
  </tbody>
</table>
<p>PSA&rsquo;s own documentation points to third-party engines for rules such as registry allowlists; see our <a href="/posts/kubernetes-pod-security-admission-namespace-labels-modes-and-a-safe-rollout-from-warn-to-enforce/">Pod Security Admission guide</a>. VAP is the built-in route for many of those rules.</p>
<h2 id="the-three-resources">The three resources</h2>
<p>Per the docs, a policy generally has three parts:</p>
<ul>
<li><strong>ValidatingAdmissionPolicy</strong>: the abstract logic and the resource types it applies to (<code>matchConstraints</code>).</li>
<li><strong>ValidatingAdmissionPolicyBinding</strong>: links a policy to a scope and chooses what happens on failure (<code>validationActions</code>). &ldquo;At least a ValidatingAdmissionPolicy and a corresponding ValidatingAdmissionPolicyBinding must be defined for a policy to have an effect.&rdquo;</li>
<li><strong>A parameter resource</strong> (optional): a ConfigMap or custom resource that supplies concrete values. The docs&rsquo; example is a <code>ReplicaLimit</code> resource with <code>maxReplicas: 3</code> for test namespaces and another with <code>maxReplicas: 100</code> for the rest, both used by the same policy through two bindings.</li>
</ul>
<p>If you use no parameters, leave <code>spec.paramKind</code> unset.</p>
<h2 id="worked-rule-1-image-registry-allowlist-for-pods">Worked rule 1: image registry allowlist for Pods</h2>
<p><strong>Goal.</strong> Reject Pods in which any container or init container uses an image outside <code>registry.example.com/</code>. The registry name is a placeholder.</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">admissionregistration.k8s.io/v1</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">ValidatingAdmissionPolicy</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">allowed-registry.example.com</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">failurePolicy</span>: <span style="color:#ae81ff">Fail</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">matchConstraints</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">resourceRules</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">apiGroups</span>:   [<span style="color:#e6db74">&#34;&#34;</span>]
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">apiVersions</span>: [<span style="color:#e6db74">&#34;v1&#34;</span>]
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">operations</span>:  [<span style="color:#e6db74">&#34;CREATE&#34;</span>, <span style="color:#e6db74">&#34;UPDATE&#34;</span>]
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">resources</span>:   [<span style="color:#e6db74">&#34;pods&#34;</span>]
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">variables</span>:
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">allContainers</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">expression</span>: &gt;-<span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      object.spec.containers +
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      (has(object.spec.initContainers) ? object.spec.initContainers : [])</span>      
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">validations</span>:
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">expression</span>: <span style="color:#e6db74">&#34;variables.allContainers.all(c, c.image.startsWith(&#39;registry.example.com/&#39;))&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">messageExpression</span>: &gt;-<span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      &#39;images not from registry.example.com: &#39; +
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      variables.allContainers.filter(c, !c.image.startsWith(&#39;registry.example.com/&#39;))
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        .map(c, c.image).join(&#39;, &#39;)</span>      
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">reason</span>: <span style="color:#ae81ff">Forbidden</span>
</span></span></code></pre></div><p>How to read it:</p>
<ul>
<li><code>variables</code> are named sub-expressions, evaluated lazily, that later expressions reference as <code>variables.&lt;name&gt;</code>. The docs recommend them when an expression gets complicated or is reused.</li>
<li><code>has(object.spec.initContainers)</code> guards the optional field. The docs explain that CEL&rsquo;s <code>has()</code> checks whether a field is present before the expression reads it.</li>
<li>The trailing <code>/</code> in the prefix matters. <code>registry.example.com.evil.net/app</code> does not start with <code>registry.example.com/</code>, so a look-alike hostname fails the check. We confirmed this case in our <code>cel-go</code> run.</li>
<li><code>messageExpression</code> builds the denial message from the offending images. Per the docs, if the message expression fails to evaluate, the static <code>message</code> is used instead, and a multi-line result is discarded.</li>
<li><code>reason: Forbidden</code> is one of the four documented values (<code>Unauthorized</code>, <code>Forbidden</code>, <code>Invalid</code>, <code>RequestEntityTooLarge</code>). It sets the machine-readable reason and HTTP status of the first failing validation.</li>
</ul>
<p><strong>Our <code>cel-go</code> results</strong> (sample Pods, expression result): a Pod with only registry-prefixed images, with and without init containers, evaluated <code>true</code>; a Pod using <code>docker.io/library/nginx:1</code> evaluated <code>false</code>; a Pod with a bad image only in an init container evaluated <code>false</code>; the look-alike hostname evaluated <code>false</code>.</p>
<p><strong>What this rule does not cover:</strong></p>
<ul>
<li><em>Ephemeral containers.</em> They are added through a different API subresource that <code>resources: [&quot;pods&quot;]</code> does not match. We did not test how the API server handles them.</li>
<li><em>Digest pinning and signatures.</em> A prefix does not stop someone pushing a malicious image to your own registry. For that, see <a href="/posts/verify-signed-container-images-in-kubernetes-sigstore-policy-controller-keyless-policy-and-rollout/">verifying signed images with policy-controller</a> and <a href="/posts/container-image-scanning-what-ci-pipelines-still-miss/">what image scanning misses</a>.</li>
<li><em>Existing Pods on UPDATE.</em> Because the policy matches <code>UPDATE</code> as well as <code>CREATE</code>, our reading (untested) is that editing an already-running Pod that violates the rule would also be evaluated. Drop <code>UPDATE</code> if you only want to gate new Pods.</li>
</ul>
<h2 id="worked-rule-2-every-container-needs-a-memory-limit">Worked rule 2: every container needs a memory limit</h2>
<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">admissionregistration.k8s.io/v1</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">ValidatingAdmissionPolicy</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">require-memory-limit.example.com</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">failurePolicy</span>: <span style="color:#ae81ff">Fail</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">matchConstraints</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">resourceRules</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">apiGroups</span>:   [<span style="color:#e6db74">&#34;&#34;</span>]
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">apiVersions</span>: [<span style="color:#e6db74">&#34;v1&#34;</span>]
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">operations</span>:  [<span style="color:#e6db74">&#34;CREATE&#34;</span>]
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">resources</span>:   [<span style="color:#e6db74">&#34;pods&#34;</span>]
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">validations</span>:
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">expression</span>: &gt;-<span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      object.spec.containers.all(c,
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        has(c.resources) &amp;&amp; has(c.resources.limits) &amp;&amp; &#39;memory&#39; in c.resources.limits)</span>      
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">message</span>: <span style="color:#e6db74">&#34;every container must set resources.limits.memory&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">reason</span>: <span style="color:#ae81ff">Invalid</span>
</span></span></code></pre></div><p>The docs&rsquo; advice for maps is to use the <code>in</code> operator to test whether a key exists, which is what <code>'memory' in c.resources.limits</code> does. In our <code>cel-go</code> run, a container with a memory limit evaluated <code>true</code>; one with no <code>resources</code> block and one with only a CPU limit both evaluated <code>false</code>. This rule checks init containers only if you extend it the same way as rule 1. Be aware that a cluster may apply default limits through a LimitRange, in which case Pods may already carry limits by the time admission policies see them. We did not verify the ordering of those two mechanisms, so test this in your own cluster.</p>
<h2 id="bindings-scope-and-actions">Bindings: scope and actions</h2>
<p>A binding says where the policy applies and what a failure does. This one excludes the <code>kube-system</code> namespace and starts in observation mode:</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">admissionregistration.k8s.io/v1</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">ValidatingAdmissionPolicyBinding</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">allowed-registry-binding.example.com</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">policyName</span>: <span style="color:#ae81ff">allowed-registry.example.com</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">validationActions</span>: [<span style="color:#ae81ff">Warn, Audit]</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">matchResources</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">namespaceSelector</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">matchExpressions</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">key</span>: <span style="color:#ae81ff">kubernetes.io/metadata.name</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">operator</span>: <span style="color:#ae81ff">NotIn</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">values</span>: [<span style="color:#e6db74">&#34;kube-system&#34;</span>]
</span></span></code></pre></div><p>The Kubernetes <a href="https://kubernetes.io/docs/reference/labels-annotations-taints/">well-known labels page</a> says the API server sets <code>kubernetes.io/metadata.name</code> on all namespaces, with the namespace&rsquo;s name as the value, and that it cannot be changed. That makes it a dependable selector. Whether <code>kube-system</code> should be excluded at all is your call; system components may use images from other registries.</p>
<p>The three actions, per the docs:</p>
<table>
  <thead>
      <tr>
          <th>Action</th>
          <th>What a failure does</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>Deny</code></td>
          <td>The request is rejected</td>
      </tr>
      <tr>
          <td><code>Warn</code></td>
          <td>The request is allowed and the client receives a warning</td>
      </tr>
      <tr>
          <td><code>Audit</code></td>
          <td>The request is allowed and the failure is recorded in the audit event for the request</td>
      </tr>
  </tbody>
</table>
<p>Deny and Warn &ldquo;may not be used together&rdquo;. Deny with Audit is therefore the production end state, and Warn with Audit is the observation state.</p>
<h2 id="what-happens-when-several-policies-or-bindings-apply">What happens when several policies or bindings apply</h2>
<p>For each request, the API server evaluates every matching (policy, binding, parameter) combination, and the request is admitted only if all of them pass. The docs state that a policy may be evaluated several times if multiple bindings match, and that overlapping bindings are allowed. The practical effect: adding a second, stricter binding never loosens an existing one.</p>
<h2 id="failurepolicy-the-setting-that-can-silently-disable-a-rule"><code>failurePolicy</code>: the setting that can silently disable a rule</h2>
<p><code>failurePolicy</code> is set on the policy and governs errors in the policy itself, such as a misconfiguration or a CEL expression that errors at runtime. <code>Fail</code> (the default) rejects the request; <code>Ignore</code> lets it continue. Two consequences:</p>
<ul>
<li>Under <code>Ignore</code>, a broken expression enforces nothing and nobody is told. If you choose <code>Ignore</code> to avoid blocking deployments, watch for errors another way.</li>
<li>Under <code>Fail</code>, a policy that errors on a field you did not guard with <code>has()</code> can block every matching request. This is why the Warn-first rollout below matters.</li>
</ul>
<p>The docs also say a validation that simply evaluates to false is always enforced according to the binding&rsquo;s actions, whatever <code>failurePolicy</code> says.</p>
<h2 id="catch-mistakes-before-they-run">Catch mistakes before they run</h2>
<ul>
<li><strong>Type checking.</strong> When a policy is created or updated, expressions are parsed and syntax errors cause rejection. Referenced fields are then type-checked against the matched resource types, with results in <code>status.typeChecking</code>. Per the docs, type checking does not affect behavior, does not apply to custom resources, skips wildcard matches, and covers at most 10 matched types. Inspect it with <code>kubectl get validatingadmissionpolicy &lt;name&gt; -o yaml</code> after creation.</li>
<li><strong>Cost limits.</strong> The <a href="https://kubernetes.io/docs/reference/using-api/cel/">CEL in Kubernetes page</a> says all expressions run under a runtime cost budget, and that the API server may reject expressions whose worst-case estimated cost is too high when they are written. Deeply nested <code>all</code>/<code>filter</code> loops over large lists are where you would run into this.</li>
<li><strong><code>matchConditions</code></strong> filter requests with CEL before the policy runs. Per the docs, <code>namespaceObject</code> is null in match conditions, so use a <code>namespaceSelector</code> to select by namespace labels. If any match condition is false, the policy is skipped; if a match condition errors and none is false, <code>failurePolicy</code> decides.</li>
</ul>
<h2 id="a-rollout-order">A rollout order</h2>
<p>Our suggested order, built on the documented actions. We have not run it on a cluster.</p>
<ol>
<li><strong>Write the policy and a binding with <code>[Warn, Audit]</code></strong>, scoped to one non-production namespace.</li>
<li><strong>Check <code>status.typeChecking</code></strong> for the policy and fix any warnings.</li>
<li><strong>Apply a known-good and a known-bad manifest</strong> in that namespace and confirm the client sees a warning for the bad one only. Look in the API server audit log for the validation-failure annotation (see the docs&rsquo; <a href="https://kubernetes.io/docs/reference/access-authn-authz/validating-admission-policy/#audit-annotations">audit annotations</a> section). Audit-log availability depends on your cluster&rsquo;s audit policy.</li>
<li><strong>Widen the namespace selector</strong> and let real deployments generate warnings for a period you choose. Fix or exempt what shows up.</li>
<li><strong>Switch the binding to <code>[Deny, Audit]</code></strong>, one namespace group at a time.</li>
<li><strong>Keep the policy in version control.</strong> The docs warn that &ldquo;Validating Admission Policy is part of the cluster control-plane. You should write and deploy them with great caution.&rdquo;</li>
</ol>
<p>One more documented limit: policies created through the REST API cannot intercept ValidatingAdmissionPolicy or binding objects themselves, nor TokenReview-style authentication and authorization review kinds, which are exempt to avoid lock-out. So a user who can edit bindings can switch a policy off; guard that with RBAC.</p>
<h2 id="limits-of-this-article">Limits of this article</h2>
<p>We did not run a Kubernetes cluster. The <code>cel-go</code> check uses generic dynamic types, not Kubernetes&rsquo; typed objects or its CEL environment, so a policy can pass our check and still draw a type-checking warning in a real cluster. We did not test how LimitRange defaults, ephemeral containers or <code>UPDATE</code> operations interact with these rules. Behavior may differ by Kubernetes version and by managed-service provider; the documented feature state is for v1.30 and later.</p>
<h2 id="sources-and-verification">Sources and verification</h2>
<p>Documentation read October 17, 2026. The Validating Admission Policy page shows &ldquo;Last modified September 28, 2026&rdquo;; the CEL page shows August 13, 2026.</p>
<table>
  <thead>
      <tr>
          <th>Important claim</th>
          <th>Source</th>
          <th>Verification</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>VAP is a declarative, in-process alternative to validating webhooks using CEL; stable since v1.30</td>
          <td><a href="https://kubernetes.io/docs/reference/access-authn-authz/validating-admission-policy/">Validating Admission Policy</a></td>
          <td>Verified</td>
      </tr>
      <tr>
          <td>Policy plus binding required; optional parameter resources; example <code>ReplicaLimit</code> bindings</td>
          <td><a href="https://kubernetes.io/docs/reference/access-authn-authz/validating-admission-policy/">Validating Admission Policy</a></td>
          <td>Verified</td>
      </tr>
      <tr>
          <td>Actions Deny, Warn, Audit; Deny and Warn cannot be combined</td>
          <td><a href="https://kubernetes.io/docs/reference/access-authn-authz/validating-admission-policy/#validation-actions">Validating Admission Policy: validation actions</a></td>
          <td>Verified</td>
      </tr>
      <tr>
          <td><code>failurePolicy</code> values Fail (default) and Ignore; failing validations always follow binding actions</td>
          <td><a href="https://kubernetes.io/docs/reference/access-authn-authz/validating-admission-policy/#failure-policy">Validating Admission Policy: failure policy</a></td>
          <td>Verified</td>
      </tr>
      <tr>
          <td>All matching policy/binding/parameter combinations must pass</td>
          <td><a href="https://kubernetes.io/docs/reference/access-authn-authz/validating-admission-policy/#parameter-resources">Validating Admission Policy: parameter resources</a></td>
          <td>Verified</td>
      </tr>
      <tr>
          <td><code>has()</code>, <code>in</code> for map keys; variables; messageExpression behavior; reason values</td>
          <td><a href="https://kubernetes.io/docs/reference/access-authn-authz/validating-admission-policy/">Validating Admission Policy</a></td>
          <td>Verified</td>
      </tr>
      <tr>
          <td>matchConditions semantics; <code>namespaceObject</code> null in match conditions</td>
          <td><a href="https://kubernetes.io/docs/reference/access-authn-authz/validating-admission-policy/#matching-requests-matchconditions">Validating Admission Policy: matchConditions</a></td>
          <td>Verified</td>
      </tr>
      <tr>
          <td>Type-checking behavior and limits</td>
          <td><a href="https://kubernetes.io/docs/reference/access-authn-authz/validating-admission-policy/#type-checking">Validating Admission Policy: type checking</a></td>
          <td>Verified</td>
      </tr>
      <tr>
          <td>Exempt kinds for REST-created policies</td>
          <td><a href="https://kubernetes.io/docs/reference/access-authn-authz/validating-admission-policy/#api-kinds-exempt-from-admission-validation">Validating Admission Policy: exempt kinds</a></td>
          <td>Verified</td>
      </tr>
      <tr>
          <td>ValidatingAdmissionPolicy is in the default-enabled plugin list for Kubernetes 1.37</td>
          <td><a href="https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers/">Admission Controllers reference</a></td>
          <td>Qualified: the same page&rsquo;s per-plugin text still mentions v1alpha1 prerequisites; we rely on the default list</td>
      </tr>
      <tr>
          <td>API server sets immutable <code>kubernetes.io/metadata.name</code> label on all namespaces</td>
          <td><a href="https://kubernetes.io/docs/reference/labels-annotations-taints/">Well-Known Labels, Annotations and Taints</a></td>
          <td>Verified</td>
      </tr>
      <tr>
          <td>Runtime cost budget; estimated-cost rejection at write time</td>
          <td><a href="https://kubernetes.io/docs/reference/using-api/cel/">CEL in Kubernetes</a></td>
          <td>Verified</td>
      </tr>
      <tr>
          <td>The two example expressions give the stated true/false results on sample Pods</td>
          <td>Our run of <code>cel-go</code> v0.32.0, October 17, 2026</td>
          <td>Qualified: not the Kubernetes CEL environment; not run on a cluster</td>
      </tr>
      <tr>
          <td>Ephemeral containers, LimitRange ordering and UPDATE effects</td>
          <td>Our inference</td>
          <td>Qualified: not tested</td>
      </tr>
  </tbody>
</table>
]]></content:encoded></item><item><title>Broken Access Control: Real-World Examples and How to Prevent It</title><link>https://firewallsync.com/posts/broken-access-control-real-world-examples-and-how-to-prevent-it/</link><guid>https://firewallsync.com/posts/broken-access-control-real-world-examples-and-how-to-prevent-it/</guid><pubDate>Thu, 15 Oct 2026 09:00:00 +0000</pubDate><dc:creator>FirewallSync Editorial</dc:creator><category>AppSec &amp; APIs</category><description>Broken access control has topped the OWASP Top 10 since 2021 for a reason: it's not one vulnerability type, it's an entire category of ways authorization checks get skipped.</description><enclosure url="https://firewallsync.com/posts/broken-access-control-real-world-examples-and-how-to-prevent-it/cover_hu8235627336239014093.jpg" type="image/jpeg" length="0"/><media:content url="https://firewallsync.com/posts/broken-access-control-real-world-examples-and-how-to-prevent-it/cover_hu8235627336239014093.jpg" medium="image" type="image/jpeg" width="1200" height="630"/><content:encoded><![CDATA[<p><img src="https://firewallsync.com/posts/broken-access-control-real-world-examples-and-how-to-prevent-it/cover_hu8235627336239014093.jpg" alt="Old key hanging from a ring" width="1200" height="630"></p><p>Broken access control has held the top position in the OWASP Top 10 since the 2021 edition, and it&rsquo;s the category with the most reported occurrences in OWASP&rsquo;s own contributed testing data — over 318,000 recorded instances across the applications analyzed. That volume isn&rsquo;t because it&rsquo;s one specific bug pattern; it&rsquo;s because &ldquo;broken access control&rdquo; covers several distinct ways authorization checks fail, each common enough on its own to matter.</p>
<h2 id="insecure-direct-object-references-idor">Insecure direct object references (IDOR)</h2>
<p>The most familiar pattern: an application exposes a reference to an internal object — an order ID, a document ID, a user ID — directly in a URL or request parameter, and fails to verify that the requesting user is actually authorized to access that specific object. Changing <code>/invoices/1024</code> to <code>/invoices/1025</code> and getting another customer&rsquo;s invoice back is the canonical example. This happens because authentication (confirming who&rsquo;s logged in) gets implemented and tested thoroughly, while object-level authorization (confirming that specific logged-in user is allowed to see that specific object) gets assumed rather than explicitly checked on every request.</p>
<h2 id="missing-function-level-access-control">Missing function-level access control</h2>
<p>Distinct from IDOR: this is when an application correctly restricts what&rsquo;s shown in the UI for a given role, but doesn&rsquo;t enforce the same restriction on the backend endpoint itself. A standard user who can&rsquo;t see an admin panel in the interface can often still call the admin API endpoint directly if the endpoint itself doesn&rsquo;t independently verify the caller&rsquo;s role — the UI hid the button, but nothing stopped the request. Any access control decision made only in frontend code is not actually an access control decision.</p>
<h2 id="privilege-escalation-vertical-and-horizontal">Privilege escalation, vertical and horizontal</h2>
<p>Vertical escalation is a standard user gaining admin-level capability — often through the function-level gap above, or through a parameter that controls role or permission level and isn&rsquo;t validated server-side (a request body that includes <code>&quot;role&quot;: &quot;admin&quot;</code> and gets trusted rather than ignored). Horizontal escalation is accessing another user&rsquo;s data at the same privilege level — which is effectively IDOR viewed through a different lens, since both involve one user reaching resources that belong to another.</p>
<h2 id="cors-misconfiguration-as-an-access-control-failure">CORS misconfiguration as an access control failure</h2>
<p>A permissive CORS policy — particularly one that reflects an arbitrary request origin back as trusted, combined with credentialed requests — effectively extends your access control boundary to any site that can get a victim to load it, since the browser will now let that third-party origin make authenticated requests on the victim&rsquo;s behalf. This is access control failing at the browser trust boundary rather than the server logic, but it belongs in the same category because the end result is the same: someone accessing something they shouldn&rsquo;t.</p>
<h2 id="why-this-keeps-happening">Why this keeps happening</h2>
<p>Authentication has mature, well-tested standard patterns that most frameworks handle reasonably well by default. Object and function-level authorization is inherently application-specific — the framework can&rsquo;t know on its own whether the currently authenticated user should be allowed to see order #1025, because that depends entirely on your data model and business logic. This means authorization checks have to be deliberately written for each resource type and each endpoint, and any one that&rsquo;s missed is a gap, not a framework misconfiguration you can patch centrally.</p>
<h2 id="how-to-actually-prevent-it">How to actually prevent it</h2>
<p><strong>Deny by default, allow explicitly.</strong> Every endpoint and every resource access should require an explicit authorization check to succeed, rather than being open unless something blocks it. This flips the failure mode from &ldquo;silently overexposed&rdquo; to &ldquo;loudly broken,&rdquo; which is far easier to catch in testing.</p>
<p><strong>Enforce authorization server-side, every time, regardless of what the client already checked.</strong> Never trust a client-supplied value (a role field, a permission flag) without independently verifying it against the actual authenticated identity&rsquo;s real permissions on the server.</p>
<p><strong>Test authorization explicitly, not just authentication.</strong> A test suite that confirms &ldquo;unauthenticated users are rejected&rdquo; but never confirms &ldquo;user A cannot access user B&rsquo;s resources&rdquo; is testing less than half of what actually matters here. Include this as its own category of test, per resource type, not an afterthought bundled into general functional testing.</p>
<p><strong>Centralize authorization logic where possible.</strong> Scattering ad hoc permission checks across individual route handlers makes it easy for one to be forgotten. A centralized authorization layer or middleware that every request passes through, with each route declaring what it requires, reduces how many places the check has to be independently remembered and correctly implemented.</p>
<p>Broken access control isn&rsquo;t a single vulnerability to patch. It&rsquo;s a category that requires deliberate, per-resource enforcement — which is exactly why it remains the most common category in real-world findings even after years at the top of the list.</p>
]]></content:encoded></item><item><title>Kubernetes Pod Security Admission: Namespace Labels, Modes and a Safe Rollout from Warn to Enforce</title><link>https://firewallsync.com/posts/kubernetes-pod-security-admission-namespace-labels-modes-and-a-safe-rollout-from-warn-to-enforce/</link><guid>https://firewallsync.com/posts/kubernetes-pod-security-admission-namespace-labels-modes-and-a-safe-rollout-from-warn-to-enforce/</guid><pubDate>Thu, 15 Oct 2026 06:06:00 +0000</pubDate><dc:creator>FirewallSync Editorial</dc:creator><category>Cloud Security</category><description>Pod Security Admission is the Kubernetes built-in that blocks risky pod settings per namespace. This guide explains the three levels, the three modes, the labels that control them, what it does not cover, and a rollout order that does not break running workloads.</description><enclosure url="https://firewallsync.com/posts/kubernetes-pod-security-admission-namespace-labels-modes-and-a-safe-rollout-from-warn-to-enforce/cover_hu8938934897327753420.jpg" type="image/jpeg" length="0"/><media:content url="https://firewallsync.com/posts/kubernetes-pod-security-admission-namespace-labels-modes-and-a-safe-rollout-from-warn-to-enforce/cover_hu8938934897327753420.jpg" medium="image" type="image/jpeg" width="1200" height="630"/><content:encoded><![CDATA[<p><img src="https://firewallsync.com/posts/kubernetes-pod-security-admission-namespace-labels-modes-and-a-safe-rollout-from-warn-to-enforce/cover_hu8938934897327753420.jpg" alt="A silver combination padlock with a rusty shackle hanging on a black chain-link fence, with a blurred pale background" width="1200" height="630"></p><p>Many Kubernetes clusters run workloads that could mount the host filesystem, share the host network or run as root, simply because nothing told the API server not to allow it. <strong>Pod Security Admission</strong> (PSA) is the built-in answer: a check that runs when a pod is created and compares its settings with a named standard. This guide explains how it works and gives an order of operations that avoids breaking deployments.</p>
<p><strong>Scope.</strong> This is research-based analysis of the official Kubernetes documentation as published for v1.37 on October 15, 2026 (the site&rsquo;s configured latest version). We did not run a cluster for this article; the manifests and commands below are taken from, or built from, the documentation and are untested by us. If you run an older Kubernetes version, read that version&rsquo;s docs: some controls carry version markers, such as host probes, which the docs tag &ldquo;v1.34+&rdquo;.</p>
<h2 id="what-psa-is-and-where-it-sits">What PSA is and where it sits</h2>
<p>The <a href="https://kubernetes.io/docs/concepts/security/pod-security-admission/">Pod Security Admission documentation</a> describes a built-in <em>admission controller</em>, a component that inspects API requests after authentication and authorization and before objects are stored, and enforces the <a href="https://kubernetes.io/docs/concepts/security/pod-security-standards/">Pod Security Standards</a>. Restrictions are applied at the <em>namespace</em> level when pods are created. The <a href="https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers/#podsecurity">admission controllers reference</a> lists it as a validating controller, meaning it can accept or reject a request but not modify it.</p>
<p>Two practical consequences follow:</p>
<ul>
<li>It looks only at the <strong>pod specification</strong>. It does not check which image is running, whether the image is signed (see our guide to <a href="/posts/verify-signed-container-images-in-kubernetes-sigstore-policy-controller-keyless-policy-and-rollout/">verifying signed images with policy-controller</a>), whether the image is vulnerable (<a href="/posts/container-image-scanning-what-ci-pipelines-still-miss/">what scanning misses</a>), or what network and RBAC permissions the pod has.</li>
<li>It offers <strong>three fixed levels and no custom rules</strong>. If you need, for instance, an allowlist of registries, the Kubernetes docs point to third-party tools such as Kubewarden, Kyverno and OPA Gatekeeper, and say that using any of them is better than doing nothing.</li>
</ul>
<h2 id="the-three-levels">The three levels</h2>
<p>The standards are cumulative, from permissive to restrictive:</p>
<table>
  <thead>
      <tr>
          <th>Level</th>
          <th>What the docs say it is for</th>
          <th>Practical meaning</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>privileged</code></td>
          <td>Unrestricted; &ldquo;purposely-open&rdquo;; aimed at system and infrastructure workloads run by privileged, trusted users</td>
          <td>Pods may bypass normal container isolation, for example use the node&rsquo;s host network</td>
      </tr>
      <tr>
          <td><code>baseline</code></td>
          <td>Minimally restrictive; prevents known privilege escalations; allows the default, minimally specified pod</td>
          <td>Blocks, for example, privileged containers, host namespaces (<code>hostNetwork</code>, <code>hostPID</code>, <code>hostIPC</code>), hostPath volumes and added capabilities outside a short allowed list</td>
      </tr>
      <tr>
          <td><code>restricted</code></td>
          <td>Heavily restricted; follows current pod hardening best practices</td>
          <td>Everything in baseline, plus the controls in the next section</td>
      </tr>
  </tbody>
</table>
<h3 id="what-restricted-adds-on-top-of-baseline">What <code>restricted</code> adds on top of baseline</h3>
<p>From the Pod Security Standards page for this version:</p>
<table>
  <thead>
      <tr>
          <th>Control</th>
          <th>Requirement</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Volume types</td>
          <td>Only <code>configMap</code>, <code>csi</code>, <code>downwardAPI</code>, <code>emptyDir</code>, <code>ephemeral</code>, <code>persistentVolumeClaim</code>, <code>projected</code> and <code>secret</code> volumes</td>
      </tr>
      <tr>
          <td>Privilege escalation</td>
          <td><code>allowPrivilegeEscalation</code> must be <code>false</code> (Linux pods only)</td>
      </tr>
      <tr>
          <td>Run as non-root</td>
          <td><code>runAsNonRoot</code> must be <code>true</code> (may be set at pod level)</td>
      </tr>
      <tr>
          <td>Non-root user</td>
          <td><code>runAsUser</code> must not be <code>0</code> (undefined is allowed)</td>
      </tr>
      <tr>
          <td>Seccomp</td>
          <td>Profile type must be set to <code>RuntimeDefault</code> or <code>Localhost</code>; unset and <code>Unconfined</code> are not allowed (Linux pods only)</td>
      </tr>
      <tr>
          <td>Capabilities</td>
          <td>Containers must drop <code>ALL</code>; only <code>NET_BIND_SERVICE</code> may be added back (Linux pods only)</td>
      </tr>
  </tbody>
</table>
<p>Baseline itself also restricts, among other things, <code>hostPort</code> (the built-in controller does not support the docs&rsquo; &ldquo;known list&rdquo; option, so effectively no host ports), the SELinux type and the AppArmor profile type. Read the <a href="https://kubernetes.io/docs/concepts/security/pod-security-standards/">full tables</a> before choosing a level for workloads such as node agents or ingress controllers, which often need exceptions.</p>
<h2 id="the-three-modes-and-the-labels-that-control-them">The three modes and the labels that control them</h2>
<p>A namespace opts in with labels. The <a href="https://kubernetes.io/docs/concepts/security/pod-security-admission/">docs</a> define the modes this way:</p>
<table>
  <thead>
      <tr>
          <th>Mode</th>
          <th>Effect on a violating pod</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>enforce</code></td>
          <td>The pod is rejected</td>
      </tr>
      <tr>
          <td><code>audit</code></td>
          <td>Allowed, but an annotation is added to the event in the <a href="https://kubernetes.io/docs/tasks/debug/debug-cluster/audit/">audit log</a></td>
      </tr>
      <tr>
          <td><code>warn</code></td>
          <td>Allowed, but the user sees a warning</td>
      </tr>
  </tbody>
</table>
<p>For each mode there are two labels:</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">pod-security.kubernetes.io/&lt;MODE&gt;</span>: <span style="color:#ae81ff">&lt;LEVEL&gt;         </span> <span style="color:#75715e"># privileged | baseline | restricted</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">pod-security.kubernetes.io/&lt;MODE&gt;-version</span>: <span style="color:#ae81ff">&lt;VERSION&gt;</span> <span style="color:#75715e"># a Kubernetes minor version such as v1.37, or latest</span>
</span></span></code></pre></div><p>A namespace may set any or all modes, at different levels. The version label pins the rules to those that shipped with a given Kubernetes minor version. The docs&rsquo; own example pins to the current version, v1.37. Pinning avoids a surprise when a cluster upgrade tightens a level, while <code>latest</code> always tracks the running version. The docs suggest a useful pattern: keep <code>enforce</code> pinned and set <code>audit</code> and <code>warn</code> to the same level at <code>latest</code>, which surfaces settings that older rules allowed but current best practice does not.</p>
<p>Here is the docs&rsquo; example, which blocks pods failing <code>baseline</code> and warns about, and audits, anything failing <code>restricted</code>:</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">v1</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">Namespace</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">my-baseline-namespace</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">labels</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">pod-security.kubernetes.io/enforce</span>: <span style="color:#ae81ff">baseline</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">pod-security.kubernetes.io/enforce-version</span>: <span style="color:#ae81ff">v1.37</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">pod-security.kubernetes.io/audit</span>: <span style="color:#ae81ff">restricted</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">pod-security.kubernetes.io/audit-version</span>: <span style="color:#ae81ff">v1.37</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">pod-security.kubernetes.io/warn</span>: <span style="color:#ae81ff">restricted</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">pod-security.kubernetes.io/warn-version</span>: <span style="color:#ae81ff">v1.37</span>
</span></span></code></pre></div><h3 id="unlabeled-namespaces-default-to-privileged">Unlabeled namespaces default to privileged</h3>
<p>This is the most common misunderstanding. The <a href="https://kubernetes.io/docs/tasks/configure-pod-container/enforce-standards-admission-controller/">admission controller configuration page</a> shows cluster-wide defaults applied &ldquo;when a mode label is not set,&rdquo; and lists <code>privileged</code> as the default level for each mode and <code>latest</code> as the default version. So a namespace with no labels is, by default, unrestricted, unless an administrator changed it. The &ldquo;Enforcing Pod Security Standards&rdquo; page calls namespaces lacking any configuration &ldquo;significant gaps in your cluster security model&rdquo; and suggests treating them as &ldquo;yet to be evaluated.&rdquo;</p>
<p>Cluster defaults and exemptions are set through an <code>AdmissionConfiguration</code> file passed to the API server with <code>--admission-control-config-file</code>. On managed Kubernetes services you may not control API server flags; we did not check any provider&rsquo;s options.</p>
<h2 id="pods-versus-deployments-where-enforcement-happens">Pods versus Deployments: where enforcement happens</h2>
<p>Pods are usually created indirectly, by a controller acting on a Deployment or Job template. The docs state that audit and warn modes apply to those workload resources too, &ldquo;to help catch violations early,&rdquo; but that <strong>enforce mode is not applied to workload resources, only to the resulting pod objects</strong>.</p>
<p>Our reading, which we have not tested: a Deployment that violates an enforced level can be accepted by the API server while its pods are then refused, so the failure appears as missing pods rather than a rejected <code>kubectl apply</code>. If you see a Deployment with zero ready replicas after labeling a namespace, look at the events on its ReplicaSet. Having <code>warn</code> set at the same level gives the person applying the manifest an immediate message.</p>
<h2 id="exemptions-and-the-one-that-backfires">Exemptions, and the one that backfires</h2>
<p>PSA can skip checks by exempting usernames, runtime class names or entire namespaces. Exempted requests are ignored entirely, with all three modes skipped. The documentation adds a caution: most pods are created by controllers, so exempting an end user exempts them only when creating pods directly, not when creating workload resources; and controller service accounts such as <code>system:serviceaccount:kube-system:replicaset-controller</code> &ldquo;should generally not be exempted,&rdquo; because that would implicitly exempt anyone who can create the corresponding workload.</p>
<p>Separately, updates that change only certain fields are not rechecked, so an already-running violating pod can still receive those updates: metadata changes (except seccomp or AppArmor annotations), valid updates to <code>.spec.activeDeadlineSeconds</code>, and valid updates to <code>.spec.tolerations</code>.</p>
<h2 id="a-rollout-that-follows-the-kubernetes-guidance">A rollout that follows the Kubernetes guidance</h2>
<p>Apply this order, adapted from the <a href="https://kubernetes.io/docs/tasks/configure-pod-container/enforce-standards-namespace-labels/">namespace labels task</a> and the <a href="https://kubernetes.io/docs/setup/best-practices/enforcing-pod-security-standards/">enforcement best practices</a>:</p>
<ol>
<li><strong>Inventory.</strong> Find namespaces with no <code>enforce</code> label (the docs&rsquo; command):
<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-bash" data-lang="bash"><span style="display:flex;"><span>kubectl get namespaces --selector<span style="color:#f92672">=</span><span style="color:#e6db74">&#39;!pod-security.kubernetes.io/enforce&#39;</span>
</span></span></code></pre></div></li>
<li><strong>Turn on visibility, not blocking.</strong> The docs&rsquo; suggested first step for all namespaces:
<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-bash" data-lang="bash"><span style="display:flex;"><span>kubectl label --overwrite ns --all <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  pod-security.kubernetes.io/audit<span style="color:#f92672">=</span>baseline <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  pod-security.kubernetes.io/warn<span style="color:#f92672">=</span>baseline
</span></span></code></pre></div>The docs deliberately leave <code>enforce</code> unset so that unevaluated namespaces remain distinguishable. Collect the warnings and audit annotations, then set the target level per namespace. In the docs&rsquo; words: &ldquo;set to the desired level and version you would eventually like to enforce.&rdquo;</li>
<li><strong>Dry-run the enforce label.</strong> The docs&rsquo; server-side dry run shows how the new level would treat existing pods without changing anything:
<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-bash" data-lang="bash"><span style="display:flex;"><span>kubectl label --dry-run<span style="color:#f92672">=</span>server --overwrite ns --all <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  pod-security.kubernetes.io/enforce<span style="color:#f92672">=</span>baseline
</span></span></code></pre></div>The docs note that when an enforce label is added or changed, the admission plugin tests every pod in the namespace and returns violations as warnings.</li>
<li><strong>Enforce namespace by namespace</strong>, pinned to a version:
<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-bash" data-lang="bash"><span style="display:flex;"><span>kubectl label --overwrite ns my-existing-namespace <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  pod-security.kubernetes.io/enforce<span style="color:#f92672">=</span>restricted <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  pod-security.kubernetes.io/enforce-version<span style="color:#f92672">=</span>v1.37
</span></span></code></pre></div></li>
<li><strong>Keep <code>warn</code> and <code>audit</code> on</strong> at the same level as enforce so authors get feedback.</li>
<li><strong>Put privileged workloads in their own namespaces</strong> and document why, as the docs advise: namespaces allowing <code>privileged</code> workloads &ldquo;should establish and enforce appropriate access controls.&rdquo;</li>
</ol>
<p>Also consider monitoring. The kube-apiserver exposes <code>pod_security_evaluations_total</code>, <code>pod_security_exemptions_total</code> and <code>pod_security_errors_total</code>. The docs warn that non-fatal evaluation errors &ldquo;may result in the latest restricted profile being used for enforcement,&rdquo; so a rise in errors is worth an alert.</p>
<h2 id="an-example-pod-that-should-satisfy-restricted">An example pod that should satisfy <code>restricted</code></h2>
<p>We built this from the controls above; we have not run it against a cluster. The image name is a placeholder, and your image must really be able to run as a non-root user, otherwise the container will start and then fail.</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">v1</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">Pod</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">restricted-example</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">securityContext</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">runAsNonRoot</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">seccompProfile</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">type</span>: <span style="color:#ae81ff">RuntimeDefault</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">containers</span>:
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">app</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">image</span>: <span style="color:#ae81ff">registry.example.com/team/app:1.0.0  </span> <span style="color:#75715e"># placeholder</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">securityContext</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">allowPrivilegeEscalation</span>: <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">capabilities</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">drop</span>: [<span style="color:#e6db74">&#34;ALL&#34;</span>]
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">volumeMounts</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">tmp</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">mountPath</span>: <span style="color:#ae81ff">/tmp</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">volumes</span>:
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">tmp</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">emptyDir</span>: {}
</span></span></code></pre></div><p>Check against the table: <code>emptyDir</code> is an allowed volume type; <code>allowPrivilegeEscalation</code> is <code>false</code>; <code>runAsNonRoot</code> is <code>true</code> at pod level, which the docs permit in place of per-container values; seccomp is <code>RuntimeDefault</code> at pod level; <code>ALL</code> capabilities are dropped and none are added. The usual failure causes when you move an existing workload to <code>restricted</code> are a missing seccomp profile, a missing <code>drop: [&quot;ALL&quot;]</code>, an image whose default user is root, and a volume type outside the allowed list such as <code>hostPath</code>.</p>
<h2 id="where-psa-is-not-enough">Where PSA is not enough</h2>
<ul>
<li><strong>Windows pods.</strong> Several restricted controls (privilege escalation, seccomp, Linux capabilities) apply only when <code>spec.os.name</code> is not <code>windows</code>, and the docs warn that clusters containing nodes older than v1.24 should pin restricted policies to a version before v1.25.</li>
<li><strong>Policy beyond pod fields.</strong> Registry allowlists, required labels, resource limits and image signatures need another policy engine.</li>
<li><strong>Pods that need privilege.</strong> Node-level agents and some storage or networking components legitimately need <code>privileged</code>; isolate them rather than weakening everything.</li>
<li><strong>Detection.</strong> PSA reduces what can be created; it does not tell you what already runs. Pair it with the practices in our <a href="/posts/cloud-misconfiguration-checklists-dont-work-heres-what-does/">cloud misconfiguration guide</a> and identity hygiene from <a href="/posts/least-privilege-access-controls-that-dont-slow-teams-down/">least privilege without slowing teams</a>.</li>
</ul>
<h2 id="limits-of-this-article">Limits of this article</h2>
<p>We did not run any of the commands or manifests. We read the Kubernetes documentation source for the v1.37 site on October 15, 2026; older and newer versions may differ, and we did not check any managed Kubernetes provider&rsquo;s behavior. The statement about where enforce failures surface for Deployments is our inference from the documented scope of enforce mode.</p>
<h2 id="sources-and-verification">Sources and verification</h2>
<p>Checked on October 15, 2026, against the Kubernetes documentation source (kubernetes/website, main branch; site latest version v1.37).</p>
<table>
  <thead>
      <tr>
          <th>Important claim</th>
          <th>Source</th>
          <th>Verification</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>PSA enforces Pod Security Standards per namespace at pod creation; stable v1.25; levels; modes and labels; version label; workload-resource behavior; exemptions and caution; field-update exemptions; metrics</td>
          <td><a href="https://kubernetes.io/docs/concepts/security/pod-security-admission/">Pod Security Admission</a></td>
          <td>Verified</td>
      </tr>
      <tr>
          <td>Level definitions; baseline and restricted controls; Windows/OS-specific controls; alternatives (Kubewarden, Kyverno, OPA Gatekeeper)</td>
          <td><a href="https://kubernetes.io/docs/concepts/security/pod-security-standards/">Pod Security Standards</a></td>
          <td>Verified</td>
      </tr>
      <tr>
          <td>Example labels, <code>kubectl</code> commands, enforce-label warning behavior, dry run</td>
          <td><a href="https://kubernetes.io/docs/tasks/configure-pod-container/enforce-standards-namespace-labels/">Enforce Pod Security Standards with Namespace Labels</a></td>
          <td>Verified; commands not run by us</td>
      </tr>
      <tr>
          <td>Default level privileged and version latest; AdmissionConfiguration; <code>--admission-control-config-file</code>; beta in v1.23, GA from v1.25</td>
          <td><a href="https://kubernetes.io/docs/tasks/configure-pod-container/enforce-standards-admission-controller/">Enforce Pod Security Standards by Configuring the Built-in Admission Controller</a></td>
          <td>Verified</td>
      </tr>
      <tr>
          <td>Unlabeled namespaces as gaps; multi-mode strategy; least privilege; intent to replace PodSecurityPolicies</td>
          <td><a href="https://kubernetes.io/docs/setup/best-practices/enforcing-pod-security-standards/">Enforcing Pod Security Standards</a></td>
          <td>Verified</td>
      </tr>
      <tr>
          <td>PSA is a validating admission controller</td>
          <td><a href="https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers/#podsecurity">Admission Controllers reference</a></td>
          <td>Verified</td>
      </tr>
      <tr>
          <td>Existing running pods are not terminated by an enforce label</td>
          <td>Inferred from &ldquo;applied when pods are created&rdquo; and the warning behavior</td>
          <td>Qualified: not tested</td>
      </tr>
      <tr>
          <td>Failure for Deployments shows as missing pods / ReplicaSet events</td>
          <td>Our inference from documented enforce scope</td>
          <td>Qualified: not tested</td>
      </tr>
      <tr>
          <td>Example restricted pod compliance</td>
          <td>Our check against the restricted control table</td>
          <td>Qualified: not run on a cluster</td>
      </tr>
  </tbody>
</table>
]]></content:encoded></item><item><title>CISA Vulnrichment SSVC: Read CVE Record Values with jq</title><link>https://firewallsync.com/posts/cisa-vulnrichment-how-to-read-ssvc-values-in-a-cve-record-and-turn-them-into-a-patch-decision/</link><guid>https://firewallsync.com/posts/cisa-vulnrichment-how-to-read-ssvc-values-in-a-cve-record-and-turn-them-into-a-patch-decision/</guid><pubDate>Wed, 14 Oct 2026 06:07:00 +0000</pubDate><dc:creator>FirewallSync Editorial</dc:creator><category>Security Operations</category><description>CISA adds three SSVC fields (Exploitation, Automatable, Technical Impact) to many CVE records. Here is where they sit in the JSON, how to pull them with one jq command, what each value means, and how CISA's published decision table turns them into Track, Track*, Attend or Act.</description><enclosure url="https://firewallsync.com/posts/cisa-vulnrichment-how-to-read-ssvc-values-in-a-cve-record-and-turn-them-into-a-patch-decision/cover_hu5517198160202342200.jpg" type="image/jpeg" length="0"/><media:content url="https://firewallsync.com/posts/cisa-vulnrichment-how-to-read-ssvc-values-in-a-cve-record-and-turn-them-into-a-patch-decision/cover_hu5517198160202342200.jpg" medium="image" type="image/jpeg" width="1200" height="630"/><content:encoded><![CDATA[<p><img src="https://firewallsync.com/posts/cisa-vulnrichment-how-to-read-ssvc-values-in-a-cve-record-and-turn-them-into-a-patch-decision/cover_hu5517198160202342200.jpg" alt="A black compass and a silver map-measuring instrument with a dial, resting on an old black-and-white street map" width="1200" height="630"></p><p>If a CVE record has been analyzed by CISA, it contains three small fields that answer a triage question faster than most advisories do: is this being exploited, can attackers automate it, and what do they get if it works. They sit inside the record under CISA&rsquo;s Authorized Data Publisher (ADP) container, and you can read them with one <code>curl</code> and one <code>jq</code> command. This article shows where the fields are, what each value means, what the data cannot tell you, and how CISA&rsquo;s own decision table converts the three values plus one input from you into one of four actions.</p>
<p>We read CISA&rsquo;s Vulnrichment README and flowcharts, the SSVC project&rsquo;s definitions of the decision points, and CISA&rsquo;s published decision tree file, and we ran the commands below against the live CVE Services API on October 14, 2026. This is research-based explanation plus our own command-line checks of public records; we did not scan any systems.</p>
<h2 id="the-short-answer">The short answer</h2>
<ul>
<li><strong>What it is:</strong> CISA&rsquo;s Vulnrichment project enriches public CVE records. For each CVE CISA analyzes it adds three SSVC (Stakeholder-Specific Vulnerability Categorization) decision points: Exploitation, Automatable and Technical Impact. For some it also adds a CWE weakness identifier or a CVSS score.</li>
<li><strong>Where to read it:</strong> the <code>adp</code> array in the CVE record, in the entry whose <code>providerMetadata.shortName</code> is <code>CISA-ADP</code>.</li>
<li><strong>What to do with it:</strong> combine it with your own exposure and asset criticality. CISA&rsquo;s decision table needs a fourth input, which only you can supply.</li>
</ul>
<h2 id="where-the-data-lives">Where the data lives</h2>
<p>A CVE record in the CVE Record Format has a CNA container, written by the organization that assigned the CVE, and zero or more ADP containers, written by other authorized publishers. CISA&rsquo;s README says CISA never overwrites the CNA&rsquo;s data in the CNA container; it writes its additions to its own ADP container.</p>
<p>You can fetch any record from the CVE Services API and filter for CISA&rsquo;s entry. This is the command we ran (it needs <code>curl</code> and <code>jq</code>):</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-bash" data-lang="bash"><span style="display:flex;"><span>curl -s https://cveawg.mitre.org/api/cve/CVE-2024-4947 | jq <span style="color:#e6db74">&#39;[
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">  .containers.adp[]?
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">  | select(.providerMetadata.shortName == &#34;CISA-ADP&#34;)
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">  | .metrics[]?
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">  | select(.other.type == &#34;ssvc&#34;)
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">  | .other.content
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">  | {version, timestamp, options: (.options | add)}
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">]&#39;</span>
</span></span></code></pre></div><p>On October 14, 2026 it returned, for CVE-2024-4947:</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-json" data-lang="json"><span style="display:flex;"><span>[
</span></span><span style="display:flex;"><span>  {
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;version&#34;</span>: <span style="color:#e6db74">&#34;2.0.3&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;timestamp&#34;</span>: <span style="color:#e6db74">&#34;2024-05-23T20:01:07.846921Z&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;options&#34;</span>: {
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&#34;Exploitation&#34;</span>: <span style="color:#e6db74">&#34;active&#34;</span>,
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&#34;Automatable&#34;</span>: <span style="color:#e6db74">&#34;no&#34;</span>,
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&#34;Technical Impact&#34;</span>: <span style="color:#e6db74">&#34;total&#34;</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>]
</span></span></code></pre></div><p>If CISA has not analyzed a CVE, the command returns <code>[]</code>. Replace <code>&quot;ssvc&quot;</code> with <code>&quot;kev&quot;</code> and <code>.other.content</code> with <code>.other.content.dateAdded</code> to read the date CISA&rsquo;s ADP container recorded the CVE being added to the Known Exploited Vulnerabilities (KEV) catalog; for CVE-2024-4947 that was <code>2024-05-20</code>.</p>
<p>Two fields deserve attention:</p>
<ul>
<li><strong><code>timestamp</code></strong> is when CISA produced that SSVC result. The record above is from May 2024. Exploitation can change after that date.</li>
<li><strong><code>version</code></strong> follows a major.minor.patch pattern. CISA&rsquo;s README says major.minor is the SSVC version and the patch number is the version of CISA&rsquo;s decision tree, so <code>2.0.3</code> means SSVC 2.0 and CISA Coordinator tree version 3. The README asks consumers to check this before validating or processing the JSON.</li>
</ul>
<h2 id="what-the-three-values-mean">What the three values mean</h2>
<table>
  <thead>
      <tr>
          <th>Decision point</th>
          <th>Values CISA publishes</th>
          <th>What it means</th>
          <th>Source definition</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Exploitation</td>
          <td><code>none</code>, <code>poc</code>, <code>active</code></td>
          <td>The state of exploitation at analysis time: no evidence and no public proof of concept (PoC); a public PoC or well-known exploitation method; or shared, observable, reliable evidence of exploitation in the wild</td>
          <td>SSVC project, Exploitation 1.1.0</td>
      </tr>
      <tr>
          <td>Automatable</td>
          <td><code>no</code>, <code>yes</code></td>
          <td><code>yes</code> means attackers can reliably automate steps 1 to 4 of the kill chain: reconnaissance, weaponization, delivery and exploitation</td>
          <td>SSVC project, Automatable 2.0.0</td>
      </tr>
      <tr>
          <td>Technical Impact</td>
          <td><code>partial</code>, <code>total</code></td>
          <td><code>total</code> means the attacker gains total control over the behavior of the affected software, or full disclosure of all information on the affected system; <code>partial</code> means limited influence or visibility</td>
          <td>SSVC project, Technical Impact 1.0.0</td>
      </tr>
  </tbody>
</table>
<p>Some details change how you should read them:</p>
<ul>
<li><strong>Exploitation is present-tense, not predictive.</strong> The SSVC documentation says it measures current exploitation, not predicted future exploitation, and points to predictive systems such as <a href="/posts/epss-data-feed-columns-percentile-vs-probability-and-how-to-join-it-to-a-scanner-export/">EPSS</a> for that. A <code>none</code> value means no exploitation evidence at the time of analysis, not that exploitation will not happen.</li>
<li><strong>The definitions of <code>poc</code> differ between versions.</strong> The SSVC project says Exploitation version 1.0.0 also counted privately attested exploitation and widespread hearsay under <code>poc</code>; version 1.1.0 limits it to public PoCs and well-known methods. CISA&rsquo;s Coordinator tree file we read (v2.0.3) still carries the broader 1.0.0-style wording. Treat <code>poc</code> as &ldquo;exploit knowledge exists&rdquo;, not as a measured exploit-development effort.</li>
<li><strong>Automatable defaults to yes.</strong> The SSVC documentation says that if nothing is known, assume Yes, and that remote code execution or command injection should generally be scored Yes. Reasons for No include a component that is not discoverable on the network, weaponization that needs manual work per target, delivery blocked by common network controls, or exploit-prevention features that make exploitation unreliable.</li>
<li><strong>Technical Impact is relative to the vulnerable component.</strong> The SSVC documentation says control is judged relative to the affected component where the flaw resides. It does not describe how much of your network the attacker then reaches.</li>
</ul>
<h3 id="how-cisa-picks-which-cves-get-more-data">How CISA picks which CVEs get more data</h3>
<p>CISA&rsquo;s README says that CVEs rated Total Technical Impact, Automatable, or with Exploitation <code>poc</code> or <code>active</code> get further analysis for a CWE identifier and a CVSS score, where there is enough information, and that CISA may provide those even for lower-rated CVEs. The README also says that if the CNA later provides its own CWE, CVSS or CPE data, CISA removes its own, and if both exist the CNA&rsquo;s data takes precedence. As of December 10, 2024, CISA no longer adds CPE strings.</p>
<h2 id="four-records-four-profiles">Four records, four profiles</h2>
<p>We pulled these records from the CVE Services API on October 14, 2026. The three values are as CISA recorded them at the timestamp shown.</p>
<table>
  <thead>
      <tr>
          <th>CVE</th>
          <th>CISA SSVC (Exploitation / Automatable / Technical Impact)</th>
          <th>CISA analysis timestamp</th>
          <th>KEV block in the record</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>CVE-2024-4947</td>
          <td>active / no / total</td>
          <td>2024-05-23</td>
          <td>Yes, added 2024-05-20</td>
      </tr>
      <tr>
          <td>CVE-2024-25522</td>
          <td>poc / yes / total</td>
          <td>2024-05-24</td>
          <td>No</td>
      </tr>
      <tr>
          <td>CVE-2024-34974</td>
          <td>none / yes / partial</td>
          <td>2024-05-10</td>
          <td>No</td>
      </tr>
      <tr>
          <td>CVE-2021-44228 (Log4Shell)</td>
          <td>active / yes / total</td>
          <td>2025-02-04</td>
          <td>Yes, added 2021-12-10</td>
      </tr>
  </tbody>
</table>
<p>CVE-2024-25522 shows why you need more than one field. CISA&rsquo;s ADP container also carries a CVSS v3.1 base score of 9.4 for it (vector <code>CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:L</code>), yet its SSVC profile is only <code>poc</code>, with no exploitation evidence. A near-critical severity score and an unexploited status can coexist; they answer different questions. Our companion article on <a href="/posts/cvss-vs-epss-vs-kev-which-signal-should-decide-what-you-patch-first/">CVSS, EPSS and KEV</a> covers how to combine them.</p>
<p>For records published in September 2026, the same command returned <code>active / yes / total</code> for CVE-2026-76504 (KEV block dated 2026-09-30, and covered in our <a href="/posts/cisco-catalyst-sd-wan-manager-cve-2026-76504-authentication-bypass-fixed-releases-and-log-checks/">Cisco SD-WAN Manager article</a>) and for CVE-2026-88771 (KEV block dated 2026-09-27, covered in our <a href="/posts/citrix-netscaler-zero-days-cve-2026-88771-cve-2026-88772-patch-and-hunt/">NetScaler article</a>).</p>
<h2 id="from-three-values-to-a-decision">From three values to a decision</h2>
<p>CISA&rsquo;s SSVC page says its tree uses five decision points (exploitation status, technical impact, automatable, mission prevalence and public well-being impact) and ends in one of four outcomes. It describes them as follows:</p>
<table>
  <thead>
      <tr>
          <th>Outcome</th>
          <th>CISA&rsquo;s description</th>
          <th>Remediation timing CISA says to apply</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Track</td>
          <td>No action required at this time; keep monitoring and reassess if new information emerges</td>
          <td>Standard update timelines</td>
      </tr>
      <tr>
          <td>Track*</td>
          <td>Specific characteristics that may require closer monitoring for changes</td>
          <td>Standard update timelines</td>
      </tr>
      <tr>
          <td>Attend</td>
          <td>Needs supervisory-level attention, which may include requesting help or information and publishing an internal or external notification</td>
          <td>Sooner than standard timelines</td>
      </tr>
      <tr>
          <td>Act</td>
          <td>Requires attention from the organization&rsquo;s internal, supervisory-level and leadership-level individuals; leadership coordinates the response</td>
          <td>As soon as possible</td>
      </tr>
  </tbody>
</table>
<p>Vulnrichment fills in three of the five. The other two, Mission Prevalence and Public Well-being Impact, are combined in CISA&rsquo;s tree file into a single input called Mission and Well-being, with three values:</p>
<ul>
<li><strong>Low:</strong> Mission Prevalence is Minimal and Public Well-being Impact is Minimal.</li>
<li><strong>Medium:</strong> Mission Prevalence is Support and Public Well-being is Minimal or Material, or Mission Prevalence is Minimal or Support and Public Well-being is Material.</li>
<li><strong>High:</strong> Mission Prevalence is Essential, or Public Well-being Impact is Irreversible.</li>
</ul>
<p>Our reading, not stated in the README, is that these two are left out of the public record because they depend on who you are and what the component does in your environment, which a public CVE record cannot know.</p>
<p>CISA&rsquo;s tree file lists all 36 combinations. The table below is the full result for the three Vulnrichment values and each Mission and Well-being level. We extracted it directly from the file (<code>CISA-Coordinator.json</code>, title &ldquo;CISA Coordinator v2.0.3&rdquo;) and checked that each of the 12 combinations has exactly one outcome per level.</p>
<table>
  <thead>
      <tr>
          <th>Exploitation</th>
          <th>Automatable</th>
          <th>Technical Impact</th>
          <th>Mission and Well-being: low</th>
          <th>medium</th>
          <th>high</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>none</td>
          <td>no</td>
          <td>partial</td>
          <td>Track</td>
          <td>Track</td>
          <td>Track</td>
      </tr>
      <tr>
          <td>none</td>
          <td>no</td>
          <td>total</td>
          <td>Track</td>
          <td>Track</td>
          <td>Track*</td>
      </tr>
      <tr>
          <td>none</td>
          <td>yes</td>
          <td>partial</td>
          <td>Track</td>
          <td>Track</td>
          <td>Attend</td>
      </tr>
      <tr>
          <td>none</td>
          <td>yes</td>
          <td>total</td>
          <td>Track</td>
          <td>Track</td>
          <td>Attend</td>
      </tr>
      <tr>
          <td>poc</td>
          <td>no</td>
          <td>partial</td>
          <td>Track</td>
          <td>Track</td>
          <td>Track*</td>
      </tr>
      <tr>
          <td>poc</td>
          <td>no</td>
          <td>total</td>
          <td>Track</td>
          <td>Track*</td>
          <td>Attend</td>
      </tr>
      <tr>
          <td>poc</td>
          <td>yes</td>
          <td>partial</td>
          <td>Track</td>
          <td>Track</td>
          <td>Attend</td>
      </tr>
      <tr>
          <td>poc</td>
          <td>yes</td>
          <td>total</td>
          <td>Track</td>
          <td>Track*</td>
          <td>Attend</td>
      </tr>
      <tr>
          <td>active</td>
          <td>no</td>
          <td>partial</td>
          <td>Track</td>
          <td>Track</td>
          <td>Attend</td>
      </tr>
      <tr>
          <td>active</td>
          <td>no</td>
          <td>total</td>
          <td>Track</td>
          <td>Attend</td>
          <td>Act</td>
      </tr>
      <tr>
          <td>active</td>
          <td>yes</td>
          <td>partial</td>
          <td>Attend</td>
          <td>Attend</td>
          <td>Act</td>
      </tr>
      <tr>
          <td>active</td>
          <td>yes</td>
          <td>total</td>
          <td>Attend</td>
          <td>Act</td>
          <td>Act</td>
      </tr>
  </tbody>
</table>
<p>Applying it to the records above:</p>
<ul>
<li><strong>CVE-2024-4947 (active / no / total):</strong> Track if the affected software is of minimal mission relevance to you, Attend at medium, Act at high.</li>
<li><strong>CVE-2024-25522 (poc / yes / total):</strong> Track at low, Track* at medium, Attend at high. Despite the 9.4 CVSS score, the tree does not send it to Act at any level.</li>
<li><strong>CVE-2024-34974 (none / yes / partial):</strong> Track at low and medium, Attend at high.</li>
<li><strong>CVE-2021-44228 (active / yes / total):</strong> Attend at low, Act at medium and high.</li>
</ul>
<p>Two cautions on this table. First, it uses a data file from the CERT/CC SSVC repository&rsquo;s main branch, which the Vulnrichment README links as the tree CISA currently uses; a newer tree version would change both the version string in records and possibly the table. Second, &ldquo;Track&rdquo; in the tree is not &ldquo;never patch&rdquo;: CISA&rsquo;s description says remediation still follows standard update timelines.</p>
<h2 id="how-to-use-this-without-over-trusting-it">How to use this without over-trusting it</h2>
<ol>
<li><strong>Read the timestamp first.</strong> A May 2024 <code>none</code> for Exploitation says nothing about October 2026. Cross-check the <a href="/posts/cisa-kev-json-feed-what-each-field-means-and-how-to-match-it-against-a-scanner-export/">KEV feed</a> and the vendor advisory.</li>
<li><strong>Treat a missing block as missing data.</strong> CISA&rsquo;s README describes Vulnrichment as covering new and recent CVEs that CISA assesses. No <code>CISA-ADP</code> entry means no CISA SSVC result, not low risk.</li>
<li><strong>Decide your Mission and Well-being input per asset class, once.</strong> Write down which systems are Minimal, Support or Essential for your organization, so triage does not reopen the question for every CVE. That classification is yours; CISA&rsquo;s data cannot supply it.</li>
<li><strong>Keep your own exposure facts separate.</strong> The tree does not know whether the vulnerable service is reachable from the internet in your environment. An internet-facing instance can justify a faster timeline than the table&rsquo;s output.</li>
<li><strong>Do not confuse SSVC outcomes with legal deadlines.</strong> Our <a href="/posts/cisa-bod-26-04-explained-kev-deadlines-forensic-triage-and-what-private-teams-can-borrow/">BOD 26-04 explainer</a> covers the KEV deadlines that bind federal agencies. SSVC&rsquo;s Track, Attend and Act are decision aids, not deadlines.</li>
</ol>
<h2 id="limits-of-this-article">Limits of this article</h2>
<ul>
<li>We ran read-only queries against public records and read documentation; we did not test any automation at scale or measure how complete Vulnrichment coverage is.</li>
<li>The records in the tables are examples chosen to show different profiles, not a statistical sample.</li>
<li>Values in live records can change; the numbers above are as of October 14, 2026.</li>
<li>We did not read CISA&rsquo;s separate SSVC Guide PDF, so the tree logic is taken from the published JSON file rather than from CISA&rsquo;s prose explanation.</li>
</ul>
<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>Vulnrichment adds SSVC decision points via CISA&rsquo;s ADP container; never overwrites CNA data; further CWE/CVSS analysis rules; CNA data takes precedence; CPE additions stopped December 10, 2024; version string meaning</td>
          <td><a href="https://github.com/cisagov/vulnrichment/blob/develop/README.md">CISA Vulnrichment README</a></td>
          <td>Verified: read October 14, 2026</td>
      </tr>
      <tr>
          <td>Location and shape of SSVC and KEV data in CVE records; commands and outputs</td>
          <td><a href="https://cveawg.mitre.org/api/cve/CVE-2024-4947">CVE Services API</a> (example record), run by us</td>
          <td>Verified: live output October 14, 2026; values may change</td>
      </tr>
      <tr>
          <td>Exploitation values and definitions; version difference; not predictive</td>
          <td><a href="https://certcc.github.io/SSVC/reference/decision_points/exploitation/">SSVC project: Exploitation</a></td>
          <td>Verified</td>
      </tr>
      <tr>
          <td>Automatable definition, kill chain steps, default to Yes</td>
          <td><a href="https://certcc.github.io/SSVC/reference/decision_points/automatable/">SSVC project: Automatable</a></td>
          <td>Verified</td>
      </tr>
      <tr>
          <td>Technical Impact definition and values</td>
          <td><a href="https://certcc.github.io/SSVC/reference/decision_points/technical_impact/">SSVC project: Technical Impact</a></td>
          <td>Verified</td>
      </tr>
      <tr>
          <td>CISA tree uses five decision points and four outcomes; outcome descriptions and timelines</td>
          <td><a href="https://www.cisa.gov/stakeholder-specific-vulnerability-categorization-ssvc">CISA SSVC page</a></td>
          <td>Verified</td>
      </tr>
      <tr>
          <td>36-row decision table and Mission and Well-being definitions</td>
          <td><a href="https://github.com/CERTCC/SSVC/blob/main/docs/ssvc-calc/CISA-Coordinator.json">CISA-Coordinator.json, CERT/CC SSVC repository (main branch)</a></td>
          <td>Qualified: main branch file read October 14, 2026; table computed by us from it; tree may change</td>
      </tr>
      <tr>
          <td>Why Mission and Well-being is absent from public records</td>
          <td>Our reading</td>
          <td>Qualified: editorial inference, labelled as such</td>
      </tr>
  </tbody>
</table>
]]></content:encoded></item><item><title>CISA BOD 26-04 Explained: KEV Deadlines, Forensic Triage and What Private Teams Can Borrow</title><link>https://firewallsync.com/posts/cisa-bod-26-04-explained-kev-deadlines-forensic-triage-and-what-private-teams-can-borrow/</link><guid>https://firewallsync.com/posts/cisa-bod-26-04-explained-kev-deadlines-forensic-triage-and-what-private-teams-can-borrow/</guid><pubDate>Mon, 12 Oct 2026 06:15:00 +0000</pubDate><dc:creator>FirewallSync Editorial</dc:creator><category>Security Operations</category><description>CISA's Binding Operational Directive 26-04, issued June 10, 2026, replaces fixed patch deadlines for U.S. federal agencies with risk-based timelines and a three-day forensic triage requirement. A batch of five years-old flaws added to the KEV catalog on October 8, 2026 shows it in action.</description><enclosure url="https://firewallsync.com/posts/cisa-bod-26-04-explained-kev-deadlines-forensic-triage-and-what-private-teams-can-borrow/cover_hu14614838678526543170.jpg" type="image/jpeg" length="0"/><media:content url="https://firewallsync.com/posts/cisa-bod-26-04-explained-kev-deadlines-forensic-triage-and-what-private-teams-can-borrow/cover_hu14614838678526543170.jpg" medium="image" type="image/jpeg" width="1200" height="630"/><content:encoded><![CDATA[<p><img src="https://firewallsync.com/posts/cisa-bod-26-04-explained-kev-deadlines-forensic-triage-and-what-private-teams-can-borrow/cover_hu14614838678526543170.jpg" alt="A small wooden hourglass with blue sand standing at a tilt on pebbles" width="1200" height="630"></p><p>If you work for a U.S. federal civilian agency, <strong>Binding Operational Directive 26-04</strong> (BOD 26-04) sets how fast you must fix known exploited vulnerabilities. If you do not, it still matters, because CISA&rsquo;s Known Exploited Vulnerabilities (KEV) catalog entries now carry its deadlines and its forensic-triage language, and many private teams use KEV as a patching signal. This article explains what the directive says, what we could and could not verify, and which parts transfer to organizations it does not bind.</p>
<p>We read CISA&rsquo;s directive page, CISA&rsquo;s implementation guidance page, the KEV catalog and National Vulnerability Database (NVD) records on October 12, 2026. This is research-based analysis; we did not test any system.</p>
<h2 id="what-bod-26-04-is-and-who-it-covers">What BOD 26-04 is and who it covers</h2>
<table>
  <thead>
      <tr>
          <th>Item</th>
          <th>What CISA&rsquo;s page says</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Title</td>
          <td>Prioritizing Security Updates Based on Risk</td>
      </tr>
      <tr>
          <td>Page date</td>
          <td>June 10, 2026</td>
      </tr>
      <tr>
          <td>Replaces</td>
          <td>Revokes BOD 22-01 and BOD 19-02 (vulnerability remediation guidance that had been split across them)</td>
      </tr>
      <tr>
          <td>Applies to</td>
          <td>Assets in any federal information system of Federal Civilian Executive Branch (FCEB) agencies</td>
      </tr>
      <tr>
          <td>Does not apply to</td>
          <td>National security systems and certain Department of War and Intelligence Community systems</td>
      </tr>
      <tr>
          <td>Contractors</td>
          <td>Generally excluded unless a governing procurement contract directs otherwise</td>
      </tr>
      <tr>
          <td>Cloud</td>
          <td>Agencies must inventory third-party hosted systems; FedRAMP offerings are handled through the FedRAMP Program Management Office</td>
      </tr>
  </tbody>
</table>
<p>Source: <a href="https://www.cisa.gov/news-events/directives/bod-26-04-prioritizing-security-updates-based-risk">CISA, BOD 26-04</a>.</p>
<h2 id="the-four-inputs-that-set-the-deadline">The four inputs that set the deadline</h2>
<p>Instead of one deadline for every KEV entry, the directive uses a table (Table 1, Appendix A) with four yes/no style decision points, measured in calendar days:</p>
<ol>
<li><strong>KEV status:</strong> is the vulnerability in the KEV catalog?</li>
<li><strong>Publicly exposed:</strong> is the asset reachable by unauthenticated or untrusted parties over public networks, wherever it physically sits? If several methods are used and any finds the asset exposed, the value is &ldquo;yes.&rdquo;</li>
<li><strong>Automatable:</strong> can exploitation be scripted reliably? CISA&rsquo;s heuristic: if a public proof-of-concept achieves remote code execution and reliably runs against a vulnerable system, it is automatable. CISA publishes results through its Vulnrichment Program.</li>
<li><strong>Technical impact:</strong> <em>total</em> if the attacker can run arbitrary software, trigger all actions of the component, get a full-privilege account, or both CVSS confidentiality and integrity impacts are High; otherwise <em>partial</em>. Denial of service generally counts as partial.</li>
</ol>
<p>The clock starts at whichever comes first: CISA adding the CVE to KEV, or the agency identifying the vulnerability on an asset and updating its Continuous Diagnostics and Mitigation (CDM) dashboard. Timelines are dynamic: removing an asset from public exposure can lengthen its deadline, and a KEV addition shortens it.</p>
<h3 id="what-the-deadlines-actually-are">What the deadlines actually are</h3>
<p>This is where we must be careful. CISA&rsquo;s text states that where the table says &ldquo;&amp; forensic triage,&rdquo; the agency must remediate or mitigate <strong>within three days</strong> and also perform forensic triage. The page also describes &ldquo;fix on system upgrade&rdquo; for lower-risk cases. The individual day counts for every row are in an image of Table 1 that our reading tool could not extract, so we have not verified them against CISA&rsquo;s own table.</p>
<p>For orientation only, Tenable&rsquo;s <a href="https://www.tenable.com/blog/cisa-bod-26-04-FAQ-vulnerability-remediation-impact">secondary summary</a> describes tiers of 3 days with triage, 3 days without triage, 14 days, 60 days and fix-on-upgrade. Treat that as a pointer, and read Appendix A on CISA&rsquo;s site before relying on a specific number.</p>
<h2 id="phase-dates">Phase dates</h2>
<p>The directive phases its requirements. Phase I applies immediately; Phase II within 60 days of issuance; Phase III within 180 days.</p>
<table>
  <thead>
      <tr>
          <th>Phase</th>
          <th>Requirement (summary)</th>
          <th>Date if counted from June 10, 2026</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>I</td>
          <td>Update vulnerability management policies; monitor KEV and remediate to KEV timelines; report KEV status via CDM or manually every two weeks</td>
          <td>Immediately</td>
      </tr>
      <tr>
          <td>II</td>
          <td>Update processes to cover vulnerabilities in the CVE database as well as KEV</td>
          <td>August 9, 2026</td>
      </tr>
      <tr>
          <td>III</td>
          <td>Remediate within Table 1 timelines; continuously tag externally reachable assets (manual reporters every seven days)</td>
          <td>December 7, 2026</td>
      </tr>
  </tbody>
</table>
<p>The dates in the last column are our arithmetic from the page date. The directive measures from issuance, and we found no separate issuance date, so treat them as approximate. On that count, Phase III had not yet begun as of October 12, 2026.</p>
<h2 id="case-study-five-old-cves-added-on-october-8-2026">Case study: five old CVEs added on October 8, 2026</h2>
<p>The KEV catalog lists five entries added on 2026-10-08, each with a due date of 2026-10-11. Each entry&rsquo;s required action refers to BOD 26-04 and CISA&rsquo;s &ldquo;Forensics Triage Requirements,&rdquo; but the catalog&rsquo;s forensic-triage field is not the same for all five: it is marked &ldquo;Yes&rdquo; for four and &ldquo;No&rdquo; for the BIND denial-of-service flaw, CVE-2015-5477.</p>
<table>
  <thead>
      <tr>
          <th>CVE</th>
          <th>Product</th>
          <th>Published (NVD)</th>
          <th>What NVD says</th>
          <th>CVSS v3.1 base score (CISA enrichment)</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>CVE-2015-3306</td>
          <td>ProFTPD 1.3.5</td>
          <td>May 18, 2015</td>
          <td>mod_copy lets remote attackers read and write arbitrary files via site cpfr and cpto commands</td>
          <td>10.0</td>
      </tr>
      <tr>
          <td>CVE-2021-3199</td>
          <td>ONLYOFFICE Document Server before 5.6.3</td>
          <td>Jan 26, 2021</td>
          <td>Directory traversal with remote code execution in /upload when JWT is used</td>
          <td>9.8</td>
      </tr>
      <tr>
          <td>CVE-2023-22894</td>
          <td>Strapi through 4.5.5</td>
          <td>Apr 19, 2023</td>
          <td>Admin-panel users can infer sensitive user details, including password hashes and reset tokens for a super admin, via the query filter</td>
          <td>7.2 (NIST&rsquo;s own NVD score: 4.9)</td>
      </tr>
      <tr>
          <td>CVE-2016-3081</td>
          <td>Apache Struts 2.3.19 to 2.3.28 (specified ranges)</td>
          <td>Apr 26, 2016</td>
          <td>Remote code execution via method: prefix when Dynamic Method Invocation is enabled</td>
          <td>8.1</td>
      </tr>
      <tr>
          <td>CVE-2015-5477</td>
          <td>ISC BIND 9.x before 9.9.7-P2 and 9.10.x before 9.10.2-P3</td>
          <td>Jul 29, 2015</td>
          <td>Denial of service via TKEY queries (assertion failure and daemon exit)</td>
          <td>7.5</td>
      </tr>
  </tbody>
</table>
<p>Sources: <a href="https://www.cisa.gov/known-exploited-vulnerabilities-catalog">CISA KEV catalog</a> and the NVD records (<a href="https://nvd.nist.gov/vuln/detail/CVE-2015-3306">CVE-2015-3306</a>, <a href="https://nvd.nist.gov/vuln/detail/CVE-2021-3199">CVE-2021-3199</a>, <a href="https://nvd.nist.gov/vuln/detail/CVE-2023-22894">CVE-2023-22894</a>, <a href="https://nvd.nist.gov/vuln/detail/CVE-2016-3081">CVE-2016-3081</a>, <a href="https://nvd.nist.gov/vuln/detail/CVE-2015-5477">CVE-2015-5477</a>). The scores are the CVSS v3.1 scores CISA added to each CVE record (its &ldquo;ADP&rdquo; enrichment), as updated on October 9, 2026; NIST&rsquo;s own NVD analysis has scored only the Strapi entry, at 4.9, and shows no v3.1 score for the BIND entry. Scores may change.</p>
<p>What this shows, and what it does not:</p>
<ul>
<li><strong>Age is not safety.</strong> These flaws date from 2015 to 2023, yet were added with a three-day due date. The three-day interval is what the catalog and NVD show; we cannot tell from public data which row of Table 1 applies to any agency&rsquo;s asset, since that depends on exposure.</li>
<li><strong>CVSS and KEV answer different questions.</strong> The Strapi entry has an NVD base score of 4.9 yet is in KEV. Our guide to <a href="/posts/cvss-vs-epss-vs-kev-which-signal-should-decide-what-you-patch-first/">CVSS, EPSS and KEV</a> explains why exploitation evidence outranks severity.</li>
<li><strong>We do not know how or by whom these were exploited.</strong> The catalog entries we read do not say, and we have not assumed it.</li>
<li><strong>Dates differ by product version.</strong> If you run any of these products, check your exact version against the NVD affected ranges and your vendor&rsquo;s fixes; backported distribution patches can change the answer.</li>
</ul>
<h2 id="forensic-triage-what-three-days-includes">Forensic triage: what &ldquo;three days&rdquo; includes</h2>
<p>CISA&rsquo;s <a href="https://www.cisa.gov/news-events/directives/bod-26-04-implementation-guidance-prioritizing-security-updates-based-risk">implementation guidance</a> presents baseline steps as best practice, and says the directive requires an adequate triage but not these specific timings. If a KEV entry has its own triage steps, those take precedence.</p>
<table>
  <thead>
      <tr>
          <th>Window</th>
          <th>Step</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>First 2 hours after KEV addition</td>
          <td>Scope: activate the team, define affected systems, set up out-of-band communication</td>
      </tr>
      <tr>
          <td>2 to 24 hours</td>
          <td>Preserve evidence, volatile data first; patch and stabilize after collection</td>
      </tr>
      <tr>
          <td>6 to 24 hours</td>
          <td>Contain and isolate affected and suspect systems</td>
      </tr>
      <tr>
          <td>24 to 48 hours</td>
          <td>Analyze: unauthorized access, persistence, lateral movement, data staging</td>
      </tr>
      <tr>
          <td>48 to 72 hours</td>
          <td>Report and decide: no evidence, suspected, or confirmed compromise (confirmed goes to CISA&rsquo;s reporting system)</td>
      </tr>
  </tbody>
</table>
<p>The ordering matters: evidence is collected before remediation where possible, because patching can destroy the traces you need.</p>
<h2 id="what-non-federal-teams-can-borrow">What non-federal teams can borrow</h2>
<p>The directive does not bind you, so these are our suggestions, not requirements.</p>
<ol>
<li><strong>Make exposure a first-class input.</strong> Tag assets by environment, exposure and type, as the directive&rsquo;s Phase III tagging requires, so &ldquo;is it internet-reachable?&rdquo; is a query rather than a meeting.</li>
<li><strong>Treat KEV as a trigger for compromise checks, not only patching.</strong> An exploited flaw that was exposed before you patched may already have been used. Compare with how we handled <a href="/posts/fortimail-zero-day-cve-2026-104286-no-patch-yet-what-to-do/">recent zero-day roundups</a> and the <a href="/posts/cisco-catalyst-sd-wan-manager-cve-2026-76504-authentication-bypass-fixed-releases-and-log-checks/">Cisco SD-WAN log checks</a>.</li>
<li><strong>Rehearse the 72 hours.</strong> Decide who runs scoping, where evidence goes and how you communicate if email is suspect. Our piece on <a href="/posts/the-vulnerability-backlog-that-never-shrinks/">why vulnerability backlogs never shrink</a> covers the process side.</li>
<li><strong>Inventory old software.</strong> The October 8 batch is a reminder that forgotten FTP daemons, unpatched Struts apps and DNS servers can matter. If you cannot list them, you cannot fix them.</li>
<li><strong>Know your response to a zero-day</strong> before one lands: see <a href="/posts/what-is-a-zero-day-vulnerability-and-how-should-you-actually-respond-to-one/">what to do when a zero-day is announced</a>.</li>
</ol>
<h2 id="limits-of-this-article">Limits of this article</h2>
<p>We could not read Table 1 as text and have not verified individual day counts beyond the three-day forensic triage requirement. We did not find a separate issuance date, so the phase dates are approximate. We did not verify whether any of the five products is exploited in a particular environment, nor the exploitation details behind the KEV additions. We did not review agencies&rsquo; compliance with the directive.</p>
<h2 id="sources-and-verification">Sources and verification</h2>
<p>Checked on October 12, 2026.</p>
<table>
  <thead>
      <tr>
          <th>Important claim</th>
          <th>Source</th>
          <th>Verification</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>BOD 26-04 page dated June 10, 2026; revokes BOD 22-01 and BOD 19-02; scope and exclusions; phases (immediate, 60, 180 days); four decision points; clock start; exposure, automatable and technical impact definitions</td>
          <td><a href="https://www.cisa.gov/news-events/directives/bod-26-04-prioritizing-security-updates-based-risk">CISA, BOD 26-04</a></td>
          <td>Qualified: no separate issuance date found; Table 1 day counts not readable as text</td>
      </tr>
      <tr>
          <td>Three-day remediation plus forensic triage where the table says so</td>
          <td>Same page</td>
          <td>Verified</td>
      </tr>
      <tr>
          <td>Tier descriptions (3, 14, 60 days, fix-on-upgrade)</td>
          <td><a href="https://www.tenable.com/blog/cisa-bod-26-04-FAQ-vulnerability-remediation-impact">Tenable analysis</a></td>
          <td>Qualified: secondary source, not checked against Table 1</td>
      </tr>
      <tr>
          <td>Triage steps and timings; directive requires adequate triage, not these timings</td>
          <td><a href="https://www.cisa.gov/news-events/directives/bod-26-04-implementation-guidance-prioritizing-security-updates-based-risk">CISA implementation guidance</a></td>
          <td>Verified</td>
      </tr>
      <tr>
          <td>Five CVEs added to KEV 2026-10-08, due 2026-10-11</td>
          <td><a href="https://www.cisa.gov/known-exploited-vulnerabilities-catalog">CISA KEV catalog</a> and the five NVD records linked above</td>
          <td>Verified as of October 12, 2026</td>
      </tr>
      <tr>
          <td>Publication dates, descriptions, affected versions</td>
          <td>NVD records linked above</td>
          <td>Verified</td>
      </tr>
      <tr>
          <td>CVSS v3.1 scores (CISA enrichment) and the NIST score of 4.9 for Strapi</td>
          <td><a href="https://github.com/CVEProject/cvelistV5">CVE records in the CVE Program&rsquo;s cvelistV5 repository</a>, updated October 9, 2026; NVD page for CVE-2023-22894</td>
          <td>Qualified: scores may change</td>
      </tr>
      <tr>
          <td>Forensic triage field &ldquo;Yes&rdquo; for four entries, &ldquo;No&rdquo; for CVE-2015-5477</td>
          <td><a href="https://github.com/cisagov/kev-data">CISA KEV data file</a>, catalog version 2026.10.08</td>
          <td>Verified</td>
      </tr>
      <tr>
          <td>Phase II and III dates (August 9 and December 7, 2026)</td>
          <td>Our calculation from June 10, 2026</td>
          <td>Qualified: assumes issuance date equals page date</td>
      </tr>
      <tr>
          <td>Suggestions for non-federal teams</td>
          <td>Our analysis</td>
          <td>Qualified: editorial judgment</td>
      </tr>
  </tbody>
</table>
<h2 id="correction">Correction</h2>
<p><em>October 12, 2026:</em> An earlier version of this article described the CVSS v3.1 scores in the KEV table as &ldquo;listed in NVD&rdquo;. They are CISA&rsquo;s enrichment (ADP) scores in the CVE records, not NIST&rsquo;s own NVD scores. It also said every entry added on October 8 was marked for forensic triage; CVE-2015-5477 (BIND) is not. Both points have been corrected.</p>
]]></content:encoded></item><item><title>CVSS vs EPSS vs KEV: Which Signal Should Decide What You Patch First?</title><link>https://firewallsync.com/posts/cvss-vs-epss-vs-kev-which-signal-should-decide-what-you-patch-first/</link><guid>https://firewallsync.com/posts/cvss-vs-epss-vs-kev-which-signal-should-decide-what-you-patch-first/</guid><pubDate>Wed, 07 Oct 2026 08:30:00 +0000</pubDate><dc:creator>FirewallSync Editorial</dc:creator><category>Security Operations</category><description>CVSS says how bad a flaw could be, EPSS estimates how likely it is to be attacked, and KEV records that it already has been. Seven recently exploited flaws show why the order you check them in matters.</description><enclosure url="https://firewallsync.com/posts/cvss-vs-epss-vs-kev-which-signal-should-decide-what-you-patch-first/cover_hu9987397115794223768.jpg" type="image/jpeg" length="0"/><media:content url="https://firewallsync.com/posts/cvss-vs-epss-vs-kev-which-signal-should-decide-what-you-patch-first/cover_hu9987397115794223768.jpg" medium="image" type="image/jpeg" width="1200" height="630"/><content:encoded><![CDATA[<p><img src="https://firewallsync.com/posts/cvss-vs-epss-vs-kev-which-signal-should-decide-what-you-patch-first/cover_hu9987397115794223768.jpg" alt="Colorful sticky notes pinned to a board" width="1200" height="630"></p><p>Check the CISA <a href="/posts/cisa-kev-json-feed-what-each-field-means-and-how-to-match-it-against-a-scanner-export/">Known Exploited Vulnerabilities (KEV) catalog</a> first, then EPSS, then CVSS, and apply your own knowledge of exposure at every step. KEV tells you a flaw has been exploited. EPSS estimates how likely exploitation is when you have no such evidence. CVSS tells you how bad exploitation would be. Using any one of them alone produces a predictable failure, and seven flaws from the past two weeks show each failure clearly.</p>
<p>This article explains what each signal measures, tests them against those seven flaws, and gives an order of operations you can implement.</p>
<h2 id="what-each-one-measures">What each one measures</h2>
<table>
  <thead>
      <tr>
          <th></th>
          <th>CVSS</th>
          <th>EPSS</th>
          <th>KEV</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Full name</td>
          <td>Common Vulnerability Scoring System</td>
          <td>Exploit Prediction Scoring System</td>
          <td>Known Exploited Vulnerabilities catalog</td>
      </tr>
      <tr>
          <td>Published by</td>
          <td>FIRST (Forum of Incident Response and Security Teams) maintains the standard; vendors and analysts assign scores</td>
          <td>FIRST&rsquo;s EPSS special interest group; scores generated by Empirical Security</td>
          <td>CISA (US Cybersecurity and Infrastructure Security Agency)</td>
      </tr>
      <tr>
          <td>Question it answers</td>
          <td>How severe is this flaw?</td>
          <td>How likely is exploitation activity in the next 30 days?</td>
          <td>Is there reliable evidence this flaw has been exploited?</td>
      </tr>
      <tr>
          <td>Output</td>
          <td>Score from 0.0 to 10.0 plus a vector string</td>
          <td>Probability from 0 to 1, plus a percentile rank</td>
          <td>A yes or no listing, with a date added and a due date</td>
      </tr>
      <tr>
          <td>Changes over time</td>
          <td>Base score normally does not</td>
          <td>Recalculated daily</td>
          <td>Entries are added as evidence emerges</td>
      </tr>
      <tr>
          <td>Knows about your environment</td>
          <td>No, unless you supply Environmental metrics</td>
          <td>No</td>
          <td>No</td>
      </tr>
  </tbody>
</table>
<h3 id="cvss-severity-not-risk">CVSS: severity, not risk</h3>
<p>The CVSS v4.0 specification describes the base score as reflecting a vulnerability&rsquo;s severity according to intrinsic characteristics that stay constant over time. It covers things like whether the flaw is reachable over a network, whether the attacker needs an account, and what happens to confidentiality, integrity and availability.</p>
<p>The qualitative labels map to score ranges: Low is 0.1 to 3.9, Medium 4.0 to 6.9, High 7.0 to 8.9 and Critical 9.0 to 10.0.</p>
<p>CVSS v4.0 has three other metric groups that most published scores leave empty:</p>
<ul>
<li><strong>Threat</strong> metrics, mainly Exploit Maturity, which can be set to Attacked, Proof-of-Concept or Unreported.</li>
<li><strong>Environmental</strong> metrics, which adjust for your own controls and how much the affected system matters to you.</li>
<li><strong>Supplemental</strong> metrics, which add context without changing the score.</li>
</ul>
<p>The specification names scores by which groups were used: CVSS-B is base only, CVSS-BT adds Threat, CVSS-BE adds Environmental and CVSS-BTE uses all three. It expects vendors and analysts to supply base metrics and the consuming organization to supply Threat and Environmental ones. In practice, the number in an advisory or scanner report is almost always CVSS-B, which is the general-case severity and nothing more.</p>
<p>Two scoring versions are in circulation. Among the flaws discussed below, Cisco and Fortinet published CVSS 3.1 scores and Citrix published CVSS 4.0 scores. The two versions are calculated differently, so treat a 9.8 on one and a 9.5 on the other as &ldquo;both Critical&rdquo;, not as a ranking.</p>
<h3 id="epss-likelihood-with-a-time-window">EPSS: likelihood, with a time window</h3>
<p>FIRST&rsquo;s FAQ defines EPSS as &ldquo;a data-driven model that estimates the probability a vulnerability will be exploited in the wild within the next 30 days.&rdquo; Three details matter when you read a score:</p>
<ul>
<li><strong>It is a probability.</strong> A score of 0.02 means an estimated 2% chance of observed exploitation activity in the next 30 days.</li>
<li><strong>The percentile is a ranking, not a probability.</strong> A percentile of 0.82 means the score is higher than 82% of all scored CVEs. Because most CVEs have very low scores, a high percentile can sit on top of a small probability. When we queried the API on October 7, 2026, it reported about 383,000 scored CVEs.</li>
<li><strong>&ldquo;Exploitation activity&rdquo; means attempts.</strong> FIRST&rsquo;s model description defines it as evidence that exploitation was attempted, not that it succeeded, and says the data comes from sources such as honeypots, intrusion detection sensors and host-based detection.</li>
</ul>
<p>Scores are republished every day, typically just after 13:30 UTC according to the FAQ.</p>
<p>FIRST is direct about the limits. The FAQ says &ldquo;EPSS is not a complete risk score&rdquo;, that it does not measure how much damage exploitation would cause or whether a flaw affects your environment, and that &ldquo;multiplying EPSS by a CVSS score does not compute probability × severity and is never a good idea.&rdquo;</p>
<h3 id="kev-evidence-not-prediction">KEV: evidence, not prediction</h3>
<p>CISA adds a vulnerability to the catalog when three conditions are met: it has a CVE ID, &ldquo;there is reliable evidence that the vulnerability has been actively exploited in the wild&rdquo;, and &ldquo;there is a clear remediation action for the vulnerability, such as a vendor-provided update.&rdquo;</p>
<p>CISA&rsquo;s definition of exploitation is specific. Scanning, security research on an exploit and the existence of a proof of concept do not count. Attempted exploitation, including against a honeypot, does.</p>
<p>As of catalog version 2026.10.04, the catalog held 1,734 entries, of which 250 were added in 2026. It flags 361 entries, about 21%, as known to be used in ransomware campaigns.</p>
<p>What a listing does not tell you is how widespread exploitation is, or whether you are a likely target. And the criteria imply two gaps, which is our reading rather than CISA&rsquo;s wording: a flaw can be exploited before CISA has reliable evidence of it, and a flaw with no clear remediation does not meet the third criterion. Absence from KEV means &ldquo;no listing&rdquo;, not &ldquo;not exploited&rdquo;.</p>
<h3 id="a-fourth-signal-you-already-have-cisas-ssvc-values">A fourth signal you already have: CISA&rsquo;s SSVC values</h3>
<p>SSVC (Stakeholder-Specific Vulnerability Categorization) is a decision-tree method that CISA says was created in 2019. CISA&rsquo;s version uses five decision points, including exploitation status, technical impact and whether exploitation is automatable, and ends in one of four outcomes: Track, Track*, Attend or Act.</p>
<p>The useful part for day-to-day triage is that CISA publishes three of those values inside many public CVE records: Exploitation (none, proof of concept or active), Automatable (yes or no) and Technical Impact (partial or total). Automatable asks whether an attacker can reliably automate creating exploitation events for the vulnerability, in the wording of the SSVC project&rsquo;s documentation. A flaw marked active, automatable and total impact is the kind that gets sprayed across the internet.</p>
<h2 id="seven-exploited-flaws-three-signals">Seven exploited flaws, three signals</h2>
<p>We covered seven flaws that CISA added to KEV between September 27 and October 4, 2026. The table puts their published CVSS base scores next to their EPSS scores for October 6, 2026 and CISA&rsquo;s Automatable value.</p>
<table>
  <thead>
      <tr>
          <th>CVE</th>
          <th>Product</th>
          <th>CVSS base score (version)</th>
          <th>EPSS on Oct 6, 2026</th>
          <th>EPSS percentile</th>
          <th>KEV added</th>
          <th>Automatable (CISA)</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>CVE-2026-88771</td>
          <td>Citrix NetScaler</td>
          <td>9.5 (4.0)</td>
          <td>1.1%</td>
          <td>64th</td>
          <td>Sept 27</td>
          <td>Yes</td>
      </tr>
      <tr>
          <td>CVE-2026-88772</td>
          <td>Citrix NetScaler</td>
          <td>9.5 (4.0)</td>
          <td>1.3%</td>
          <td>70th</td>
          <td>Sept 27</td>
          <td>No</td>
      </tr>
      <tr>
          <td>CVE-2026-76504</td>
          <td>Cisco Catalyst SD-WAN Manager</td>
          <td>9.8 (3.1)</td>
          <td>1.8%</td>
          <td>78th</td>
          <td>Sept 30</td>
          <td>Yes</td>
      </tr>
      <tr>
          <td>CVE-2026-104286</td>
          <td>Fortinet FortiMail</td>
          <td>9.8 (3.1)</td>
          <td>2.2%</td>
          <td>82nd</td>
          <td>Oct 1</td>
          <td>Yes</td>
      </tr>
      <tr>
          <td>CVE-2026-102489</td>
          <td>Zammad</td>
          <td>8.7 (4.0)</td>
          <td>1.4%</td>
          <td>72nd</td>
          <td>Oct 2</td>
          <td>Yes</td>
      </tr>
      <tr>
          <td>CVE-2026-102490</td>
          <td>Zammad</td>
          <td>8.5 (4.0)</td>
          <td>0.6%</td>
          <td>48th</td>
          <td>Oct 2</td>
          <td>No</td>
      </tr>
      <tr>
          <td>CVE-2026-88779</td>
          <td>Citrix NetScaler</td>
          <td>8.7 (4.0)</td>
          <td>0.6%</td>
          <td>47th</td>
          <td>Oct 4</td>
          <td>Yes</td>
      </tr>
  </tbody>
</table>
<p>EPSS values are rounded from the API&rsquo;s figures, which run from 0.00592 to 0.02201. The Zammad records also carry a second score of 9.4 for the two flaws chained together.</p>
<p>What each signal would have done on its own:</p>
<ul>
<li><strong>EPSS alone would have missed all seven.</strong> Every score is at or below 2.2%. A team that acts on EPSS of 10% or higher, a commonly used cut-off, would have flagged none of them. Even a team working by percentile and taking the top 10% of CVEs would have flagged none, since the highest sits at the 82nd percentile.</li>
<li><strong>CVSS &ldquo;Critical only&rdquo; would have missed three.</strong> Four of the seven score 9.0 or higher. The two Zammad flaws and the NetScaler denial-of-service flaw score 8.5 to 8.7, which is High. A rule of 7.0 and above catches all seven, but that rule also puts every High and Critical flaw in your inventory into the same urgent queue, which is the backlog problem in the first place.</li>
<li><strong>KEV caught all seven, by construction.</strong> We picked these because they were listed. That is the point of the comparison, not a flaw in it: for a flaw with exploitation evidence, the evidence is the signal.</li>
</ul>
<p>This is not a criticism of EPSS, and seven flaws are not a statistical sample. EPSS is doing what FIRST says it does. Its FAQ states that the model generates scores &ldquo;without knowing if the vulnerability was recently exploited&rdquo; and that &ldquo;if direct evidence of active exploitation exists, that evidence should supersede EPSS.&rdquo; These were newly disclosed flaws: CISA added all seven to KEV within two days of their CVE records being published. That is the situation where a model built on broad observation has the least to work with, which is our interpretation, not a statement from FIRST.</p>
<p>The mistake would be on the reader&rsquo;s side: seeing 0.6% next to a CVE and lowering its priority without checking KEV.</p>
<h2 id="an-order-of-operations">An order of operations</h2>
<p>This sequence is our recommendation. It uses each signal for the question it can answer.</p>
<ol>
<li><strong>Is it in KEV, or does the vendor say it is being exploited?</strong> If yes and the affected system is reachable from untrusted networks, act now, and plan a compromise check as well as a patch. If the system is internal only, schedule it within days. Vendor statements count: Fortinet&rsquo;s advisory for CVE-2026-104286 said the flaw was being exploited on the day it was published.</li>
<li><strong>If not, what does EPSS say?</strong> A score above your chosen threshold moves the item up. Our <a href="/posts/epss-data-feed-columns-percentile-vs-probability-and-how-to-join-it-to-a-scanner-export/">EPSS data feed guide</a> shows how to add the daily scores to a scanner export. Pick the threshold from your capacity: lower it until the resulting queue is the size your team can clear.</li>
<li><strong>If EPSS is low, is it Critical and exposed?</strong> A CVSS base score of 9.0 or higher on an internet-facing system still deserves the next patch cycle, because a low EPSS score on a fresh CVE can change quickly.</li>
<li><strong>Everything else follows the routine schedule.</strong></li>
</ol>
<p>At every step, the exposure question is yours to answer. None of the three sources knows whether your FortiMail web interface faces the internet.</p>
<p>Here is that logic as a small Python function. It is an illustration of the ordering, not a finished tool. We ran it against the KEV data file and the seven flaws above; the thresholds are examples.</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:#f92672">import</span> json
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>EPSS_WATCH <span style="color:#f92672">=</span> <span style="color:#ae81ff">0.10</span>  <span style="color:#75715e"># probability, not percentile; tune to your own capacity</span>
</span></span><span style="display:flex;"><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">load_kev</span>(path: str) <span style="color:#f92672">-&gt;</span> set[str]:
</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>        <span style="color:#66d9ef">return</span> {v[<span style="color:#e6db74">&#34;cveID&#34;</span>] <span style="color:#66d9ef">for</span> v <span style="color:#f92672">in</span> json<span style="color:#f92672">.</span>load(f)[<span style="color:#e6db74">&#34;vulnerabilities&#34;</span>]}
</span></span><span style="display:flex;"><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">triage</span>(cve: str, cvss: float, epss: float, internet_facing: bool, kev: set[str]) <span style="color:#f92672">-&gt;</span> str:
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> cve <span style="color:#f92672">in</span> kev:
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#e6db74">&#34;1 - act now&#34;</span> <span style="color:#66d9ef">if</span> internet_facing <span style="color:#66d9ef">else</span> <span style="color:#e6db74">&#34;2 - this week&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> epss <span style="color:#f92672">&gt;=</span> EPSS_WATCH:
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#e6db74">&#34;2 - this week&#34;</span> <span style="color:#66d9ef">if</span> internet_facing <span style="color:#66d9ef">else</span> <span style="color:#e6db74">&#34;3 - next cycle&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> cvss <span style="color:#f92672">&gt;=</span> <span style="color:#ae81ff">9.0</span> <span style="color:#f92672">and</span> internet_facing:
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#e6db74">&#34;3 - next cycle&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#e6db74">&#34;4 - routine&#34;</span>
</span></span></code></pre></div><p><code>load_kev</code> reads the JSON file CISA publishes at <a href="https://github.com/cisagov/kev-data">github.com/cisagov/kev-data</a>. With the KEV check in place, all seven flaws returned &ldquo;1 - act now&rdquo; for an internet-facing system. With an empty KEV set, simulating a team that skips that check, four returned &ldquo;3 - next cycle&rdquo; and three returned &ldquo;4 - routine&rdquo;. Same flaws, same scores, one missing lookup.</p>
<p>Daily EPSS scores are available from FIRST&rsquo;s API at <code>https://api.first.org/data/v1/epss?cve=CVE-2026-104286</code>, which accepts a comma-separated list of CVE IDs.</p>
<h2 id="common-mistakes">Common mistakes</h2>
<ul>
<li><strong>Sorting the backlog by CVSS and working from the top.</strong> This treats a Critical flaw on an isolated test server as more urgent than a High flaw being exploited on your gateway.</li>
<li><strong>Reading a low EPSS score as &ldquo;safe&rdquo;.</strong> It is a 30-day estimate from a model. Check KEV and the vendor advisory before lowering priority.</li>
<li><strong>Confusing EPSS percentile with probability.</strong> &ldquo;82nd percentile&rdquo; sounds urgent. The probability behind it in the table above is 2.2%.</li>
<li><strong>Treating KEV as complete.</strong> It lists what CISA has evidence for and can point to a fix for.</li>
<li><strong>Multiplying the scores together.</strong> FIRST says not to. The result has no defined meaning.</li>
<li><strong>Recording &ldquo;patched&rdquo; instead of the fixed version.</strong> Citrix issued two bulletins for NetScaler within a week, and the builds that fixed the first were still affected by the second, as we covered in <a href="/posts/netscaler-cve-2026-88779-saml-denial-of-service-exploited-patched-last-weeks-zero-days/">the NetScaler CVE-2026-88779 article</a>.</li>
<li><strong>Trusting one data source for affected versions.</strong> In the <a href="/posts/fortimail-zero-day-cve-2026-104286-no-patch-yet-what-to-do/">FortiMail case</a>, the CVE record and the vendor advisory listed different version ranges.</li>
</ul>
<p>For the organizational side of this, including why backlogs grow when everything is ranked by count, see <a href="/posts/the-vulnerability-backlog-that-never-shrinks/">the vulnerability backlog that never shrinks</a>. For what to do once something lands in the top tier, see <a href="/posts/what-is-a-zero-day-vulnerability-and-how-should-you-actually-respond-to-one/">how to respond to a zero-day</a>.</p>
<h2 id="sources-and-verification">Sources and verification</h2>
<p>Checked on October 7, 2026. EPSS scores change daily and the KEV catalog grows, so the figures below describe those dates only. The seven flaws were chosen because we had already reported on them; they are examples, not a representative sample.</p>
<table>
  <thead>
      <tr>
          <th>Important claim</th>
          <th>Source</th>
          <th>Verification</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>CVSS base score reflects severity from intrinsic characteristics; metric groups; CVSS-B, -BT, -BE, -BTE naming; severity ranges; Exploit Maturity values</td>
          <td><a href="https://www.first.org/cvss/v4.0/specification-document">CVSS v4.0 specification</a></td>
          <td>Verified</td>
      </tr>
      <tr>
          <td>EPSS definition, 30-day window, not a complete risk score, do not multiply by CVSS, exploitation evidence supersedes EPSS, daily publication around 13:30 UTC, scores generated by Empirical Security</td>
          <td><a href="https://www.first.org/epss/faq">FIRST EPSS FAQ</a></td>
          <td>Verified</td>
      </tr>
      <tr>
          <td>&ldquo;Exploitation activity&rdquo; means attempted exploitation; data from honeypots, IDS/IPS sensors and host-based detection</td>
          <td><a href="https://www.first.org/epss/model">FIRST EPSS model page</a></td>
          <td>Verified</td>
      </tr>
      <tr>
          <td>KEV criteria; scanning, research and proof of concept do not count as exploitation; use KEV as an input to prioritization</td>
          <td><a href="https://www.cisa.gov/known-exploited-vulnerabilities">CISA, Known Exploited Vulnerabilities</a></td>
          <td>Verified</td>
      </tr>
      <tr>
          <td>KEV held 1,734 entries, 250 added in 2026, 361 flagged for known ransomware use</td>
          <td><a href="https://github.com/cisagov/kev-data">CISA KEV data file</a>, catalog version 2026.10.04; our count</td>
          <td>Verified for that version</td>
      </tr>
      <tr>
          <td>SSVC created in 2019; CISA decision points and four outcomes</td>
          <td><a href="https://www.cisa.gov/stakeholder-specific-vulnerability-categorization-ssvc">CISA, SSVC</a></td>
          <td>Verified</td>
      </tr>
      <tr>
          <td>Definition of Automatable</td>
          <td><a href="https://certcc.github.io/SSVC/reference/decision_points/automatable/">SSVC project documentation</a></td>
          <td>Verified</td>
      </tr>
      <tr>
          <td>CVSS base scores, KEV dates and Automatable values for the seven CVEs</td>
          <td>Public CVE records (for example <a href="https://www.cve.org/CVERecord?id=CVE-2026-104286">CVE-2026-104286</a>) and the KEV data file, retrieved October 7, 2026</td>
          <td>Verified</td>
      </tr>
      <tr>
          <td>EPSS scores and percentiles for the seven CVEs</td>
          <td><a href="https://api.first.org/data/v1/epss?cve=CVE-2026-88771,CVE-2026-88772,CVE-2026-88779,CVE-2026-76504&amp;date=2026-10-06">FIRST EPSS API</a>, scores dated October 6, 2026</td>
          <td>Verified for that date</td>
      </tr>
      <tr>
          <td>About 383,000 CVEs scored by EPSS</td>
          <td>FIRST EPSS API total, queried October 7, 2026</td>
          <td>Qualified: read from one API response</td>
      </tr>
      <tr>
          <td>10% as a commonly used EPSS cut-off</td>
          <td>Our characterization</td>
          <td>Qualified: an example threshold, not a FIRST recommendation</td>
      </tr>
      <tr>
          <td>Triage function results</td>
          <td>Run in our build environment on Python 3.13 against KEV catalog 2026.10.04</td>
          <td>Verified; thresholds are illustrative</td>
      </tr>
  </tbody>
</table>
]]></content:encoded></item><item><title>Security Champions Programs That Don't Fizzle Out</title><link>https://firewallsync.com/posts/security-champions-programs-that-dont-fizzle-out/</link><guid>https://firewallsync.com/posts/security-champions-programs-that-dont-fizzle-out/</guid><pubDate>Fri, 02 Oct 2026 09:00:00 +0000</pubDate><dc:creator>FirewallSync Editorial</dc:creator><category>Security Operations</category><description>Most security champions programs launch with energy and quietly die within two quarters. The failure pattern is predictable, and so is the fix.</description><enclosure url="https://firewallsync.com/posts/security-champions-programs-that-dont-fizzle-out/cover_hu17083360534380787993.jpg" type="image/jpeg" length="0"/><media:content url="https://firewallsync.com/posts/security-champions-programs-that-dont-fizzle-out/cover_hu17083360534380787993.jpg" medium="image" type="image/jpeg" width="1200" height="630"/><content:encoded><![CDATA[<p><img src="https://firewallsync.com/posts/security-champions-programs-that-dont-fizzle-out/cover_hu17083360534380787993.jpg" alt="Group of colleagues working together at a laptop" width="1200" height="630"></p><p>Security champions programs almost always launch well — volunteers sign up, there&rsquo;s a kickoff meeting, enthusiasm is genuinely high. Then within two quarters, champions stop showing up to the monthly sync, the security team stops preparing content for it, and the program quietly becomes a Slack channel nobody posts in. The failure pattern is consistent enough across companies that it&rsquo;s worth designing around from day one.</p>
<h2 id="give-champions-authority-not-just-information">Give champions authority, not just information</h2>
<p>Most programs are structured as a one-way information pipe: security team teaches champions about threats, champions are supposed to relay it to their teams. This makes champions a newsletter forwarding service with extra steps, and that role doesn&rsquo;t sustain motivation. Champions who instead get real authority — reviewing their own team&rsquo;s threat models, having a say in which security debt gets prioritized on their team&rsquo;s roadmap — have a reason to stay engaged, because the role does something rather than just knows something.</p>
<h2 id="make-the-time-investment-visible-to-managers">Make the time investment visible to managers</h2>
<p>Champions almost always take on the role on top of their regular workload, with no adjustment to their sprint capacity or performance goals. When crunch time hits, the champion work is the first thing to slip, invisibly, because no one but the champion notices. Get explicit buy-in from engineering managers that champion time (even just 2-3 hours a month) is accounted for in planning, not absorbed silently — this single change predicts program longevity better than almost anything else.</p>
<h2 id="rotate-the-content-not-just-the-people">Rotate the content, not just the people</h2>
<p>Programs that repeat the same &ldquo;here&rsquo;s this quarter&rsquo;s OWASP Top 10 refresher&rdquo; format burn out fast because champions stop learning anything new. Mix in things champions can&rsquo;t get elsewhere: a walkthrough of an actual internal incident, a live threat-model session on a real upcoming feature, or a chance to red-team a teammate&rsquo;s design. Content that&rsquo;s specific to your company&rsquo;s actual systems is what keeps a champions program from feeling like a recurring training module people tolerate rather than value.</p>
]]></content:encoded></item><item><title>Logging for Incidents, Not for Dashboards</title><link>https://firewallsync.com/posts/logging-for-incidents-not-for-dashboards/</link><guid>https://firewallsync.com/posts/logging-for-incidents-not-for-dashboards/</guid><pubDate>Wed, 30 Sep 2026 09:00:00 +0000</pubDate><dc:creator>FirewallSync Editorial</dc:creator><category>Security Operations</category><description>Most logging strategies optimize for building dashboards, then fail the one test that matters: can you reconstruct what happened during an actual incident?</description><enclosure url="https://firewallsync.com/posts/logging-for-incidents-not-for-dashboards/cover_hu14905193143568732667.jpg" type="image/jpeg" length="0"/><media:content url="https://firewallsync.com/posts/logging-for-incidents-not-for-dashboards/cover_hu14905193143568732667.jpg" medium="image" type="image/jpeg" width="1200" height="630"/><content:encoded><![CDATA[<p><img src="https://firewallsync.com/posts/logging-for-incidents-not-for-dashboards/cover_hu14905193143568732667.jpg" alt="Computer screen filled with data" width="1200" height="630"></p><p>Most logging strategies get built to answer &ldquo;what does normal look like&rdquo; — the questions a dashboard needs. Incident response needs the opposite: enough detail to reconstruct exactly what an attacker did, in order, across systems that don&rsquo;t share a clock or a request ID. Those are different design goals, and optimizing for one quietly starves the other.</p>
<h2 id="correlation-ids-matter-more-than-log-volume">Correlation IDs matter more than log volume</h2>
<p>Teams often respond to &ldquo;we couldn&rsquo;t reconstruct the incident&rdquo; by logging more of everything, which mostly adds noise. The actual gap is usually that a request touching five services produces five sets of logs with no shared identifier linking them. A consistent correlation ID propagated through every service call turns a volume problem into a query problem — you can pull the full story in one search instead of manually time-correlating timestamps across systems that drift by seconds.</p>
<h2 id="log-the-auth-decision-not-just-the-auth-attempt">Log the auth decision, not just the auth attempt</h2>
<p>Most systems log &ldquo;login succeeded&rdquo; or &ldquo;login failed&rdquo; but not <em>why</em> an authorization check passed or failed for a specific resource — which role, which policy, which condition matched. During an incident, &ldquo;did this account have access to this resource, and under what policy&rdquo; is one of the first questions asked, and without decision-level logging, answering it means reverse-engineering the access control logic from scratch under time pressure.</p>
<h2 id="retention-matters-more-than-most-teams-budget-for">Retention matters more than most teams budget for</h2>
<p>Attackers increasingly wait weeks or months between initial access and objective — meaning the logs you need to reconstruct the intrusion may have already rolled off retention by the time you&rsquo;re investigating. Check your actual log retention against your industry&rsquo;s typical dwell time, not against your storage budget&rsquo;s comfort zone; a shorter retention window than your realistic detection lag means some incidents are unreconstructable by design, regardless of how good your logging schema is.</p>
]]></content:encoded></item><item><title>The Tabletop Exercise Gap: Testing Incident Response for Real</title><link>https://firewallsync.com/posts/the-tabletop-exercise-gap-testing-incident-response-for-real/</link><guid>https://firewallsync.com/posts/the-tabletop-exercise-gap-testing-incident-response-for-real/</guid><pubDate>Sun, 27 Sep 2026 09:00:00 +0000</pubDate><dc:creator>FirewallSync Editorial</dc:creator><category>Security Operations</category><description>Tabletop exercises satisfy the audit requirement but rarely test whether your team can actually execute under pressure. Here's the gap and how to close it.</description><enclosure url="https://firewallsync.com/posts/the-tabletop-exercise-gap-testing-incident-response-for-real/cover_hu582100228981122759.jpg" type="image/jpeg" length="0"/><media:content url="https://firewallsync.com/posts/the-tabletop-exercise-gap-testing-incident-response-for-real/cover_hu582100228981122759.jpg" medium="image" type="image/jpeg" width="1200" height="630"/><content:encoded><![CDATA[<p><img src="https://firewallsync.com/posts/the-tabletop-exercise-gap-testing-incident-response-for-real/cover_hu582100228981122759.jpg" alt="Team with laptops watching a colleague at a whiteboard" width="1200" height="630"></p><p>A tabletop exercise where everyone sits in a conference room and narrates what they&rsquo;d do tests whether people remember the runbook. It doesn&rsquo;t test whether the runbook survives contact with a real incident — degraded communication tools, a key person on vacation, or an attacker who doesn&rsquo;t follow the scenario&rsquo;s script.</p>
<h2 id="run-at-least-one-exercise-with-a-communication-channel-down">Run at least one exercise with a communication channel down</h2>
<p>The single most common real-incident failure mode isn&rsquo;t a missing step in the runbook — it&rsquo;s that Slack, your usual coordination tool, is either compromised or unavailable (because it&rsquo;s hosted on the infrastructure that&rsquo;s on fire). If your tabletop always assumes normal tooling, you&rsquo;ve never actually tested your incident response — you&rsquo;ve tested your ability to read a document out loud. Run one exercise per year with the primary comms channel explicitly ruled out of scope.</p>
<h2 id="inject-a-wrong-turn-not-just-a-scenario">Inject a wrong turn, not just a scenario</h2>
<p>Most tabletop scripts hand the team accurate information and let them respond correctly. Real incidents include red herrings — a misleading log entry, an alert that points at the wrong service, a well-meaning engineer who changes something mid-incident without telling anyone. Build at least one deliberate wrong signal into your next exercise and see how long it takes the team to notice and correct course; that recovery time is a more useful metric than whether they reached the &ldquo;right&rdquo; answer eventually.</p>
<h2 id="test-the-decision-makers-availability-not-just-their-competence">Test the decision-maker&rsquo;s availability, not just their competence</h2>
<p>Plans routinely name a single incident commander with no tested backup. Pick a tabletop date and explicitly remove that person from the exercise without warning the rest of the team beforehand — if response quality collapses, you&rsquo;ve found a single point of failure that a real 2am incident will find for you anyway, just with worse timing.</p>
]]></content:encoded></item><item><title>Alert Fatigue Is a Security Metric, Not a Morale Problem</title><link>https://firewallsync.com/posts/alert-fatigue-is-a-security-metric-not-a-morale-problem/</link><guid>https://firewallsync.com/posts/alert-fatigue-is-a-security-metric-not-a-morale-problem/</guid><pubDate>Sat, 26 Sep 2026 09:00:00 +0000</pubDate><dc:creator>FirewallSync Editorial</dc:creator><category>Security Operations</category><description>Teams treat alert fatigue as a burnout issue to manage around. It's actually a leading indicator that your detection pipeline is broken.</description><enclosure url="https://firewallsync.com/posts/alert-fatigue-is-a-security-metric-not-a-morale-problem/cover_hu10577196946262886676.jpg" type="image/jpeg" length="0"/><media:content url="https://firewallsync.com/posts/alert-fatigue-is-a-security-metric-not-a-morale-problem/cover_hu10577196946262886676.jpg" medium="image" type="image/jpeg" width="1200" height="630"/><content:encoded><![CDATA[<p><img src="https://firewallsync.com/posts/alert-fatigue-is-a-security-metric-not-a-morale-problem/cover_hu10577196946262886676.jpg" alt="Monitoring screen showing live metrics" width="1200" height="630"></p><p>Most orgs respond to alert fatigue with rotation schedules, mental health check-ins, or hiring more analysts. All reasonable, none of them fix the actual problem: a detection pipeline generating more noise than signal, which is a tuning failure, not a staffing shortfall.</p>
<h2 id="track-signal-to-action-ratio-not-alert-volume">Track signal-to-action ratio, not alert volume</h2>
<p>Counting total alerts tells you nothing useful — it doesn&rsquo;t distinguish between 10,000 alerts that each require a decision and 10,000 that are auto-closeable duplicates. The metric that matters is what fraction of alerts result in an actual action (escalation, ticket, remediation) versus a dismiss-and-forget. If that ratio is under 5%, your rules are too broad, and adding headcount just spreads the same noise across more people instead of removing it.</p>
<h2 id="kill-rules-before-you-tune-them">Kill rules before you tune them</h2>
<p>The instinct when a rule is noisy is to tighten its thresholds. Often the faster fix is deleting it and confirming nothing important was riding on it — most legacy detection rules were written for an environment that no longer exists, and nobody owns the decision to retire them. Run a 90-day audit of which rules have never once led to a real action, and sunset them explicitly rather than letting them quietly erode trust in the rest of the pipeline.</p>
<h2 id="separate-needs-a-human-now-from-needs-a-human-eventually">Separate &ldquo;needs a human now&rdquo; from &ldquo;needs a human eventually&rdquo;</h2>
<p>A huge share of fatigue comes from routing informational alerts through the same channel as ones requiring immediate triage. If a Slack channel or pager gets both &ldquo;unusual login from a new device, self-resolved&rdquo; and &ldquo;ransomware indicator on a production host,&rdquo; analysts learn to skim everything — including the alert that actually mattered. Split your routing by required response time, not by source system, and the urgent channel becomes trustworthy again.</p>
]]></content:encoded></item><item><title>Why Your Incident Postmortems Aren't Preventing Repeats</title><link>https://firewallsync.com/posts/why-your-incident-postmortems-arent-preventing-repeats/</link><guid>https://firewallsync.com/posts/why-your-incident-postmortems-arent-preventing-repeats/</guid><pubDate>Wed, 12 Aug 2026 00:00:00 +0000</pubDate><dc:creator>FirewallSync Editorial</dc:creator><category>Security Operations</category><description>Blameless postmortems became standard practice for good reasons, but most teams stopped halfway through adopting them — and it shows in the repeat incidents.</description><enclosure url="https://firewallsync.com/posts/why-your-incident-postmortems-arent-preventing-repeats/cover_hu5203005822613407080.jpg" type="image/jpeg" length="0"/><media:content url="https://firewallsync.com/posts/why-your-incident-postmortems-arent-preventing-repeats/cover_hu5203005822613407080.jpg" medium="image" type="image/jpeg" width="1200" height="630"/><content:encoded><![CDATA[<p><img src="https://firewallsync.com/posts/why-your-incident-postmortems-arent-preventing-repeats/cover_hu5203005822613407080.jpg" alt="People taking notes around a table during a meeting" width="1200" height="630"></p><p>Blameless postmortems are now the default at most engineering orgs, which is progress. But a document being blameless doesn&rsquo;t automatically make it useful. The most common failure mode isn&rsquo;t blame creeping back in — it&rsquo;s that the postmortem produces a list of action items that never get prioritized against feature work, so the same root cause resurfaces in six months wearing a different symptom.</p>
<h2 id="the-action-item-graveyard">The action item graveyard</h2>
<p>If your postmortem template has an &ldquo;action items&rdquo; section with no owner, no deadline, and no forcing function to revisit it, you don&rsquo;t have a prevention process — you have a documentation exercise. Action items from incidents need to compete for sprint capacity the same way any other ticket does, with a visible cost to deferring them repeatedly.</p>
<h2 id="root-cause-is-rarely-singular">Root cause is rarely singular</h2>
<p>Most postmortems stop at the first plausible technical explanation: a bad deploy, a missing rate limit, an expired certificate. The more useful question is what allowed that technical failure to become a customer-facing incident — was there no monitoring on that path, no runbook, no one on call who knew the system well enough to catch it early? Fixing the technical trigger without fixing the detection gap guarantees a different trigger produces the same outcome later.</p>
<h2 id="track-recurrence-not-just-resolution">Track recurrence, not just resolution</h2>
<p>Most incident tracking measures time-to-resolution and calls it done. Add one more metric: how many incidents this quarter share a root cause category with an incident from last quarter. If that number isn&rsquo;t trending toward zero, your postmortem process is documenting problems, not solving them.</p>
<h2 id="what-a-working-process-looks-like">What a working process looks like</h2>
<ul>
<li>Every action item gets a named owner and a sprint, not a someday-list</li>
<li>Postmortems ask what let the failure surface to users, not just what failed technically</li>
<li>Recurrence rate is tracked as a metric leadership actually looks at</li>
</ul>
<p>The blameless part was never the hard part. Follow-through is.</p>
]]></content:encoded></item></channel></rss>