<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator>
  <link href="https://bizzal70.github.io/itsalreadywhen/rtfm-feed.xml" rel="self" type="application/atom+xml" />
  <link href="https://bizzal70.github.io/itsalreadywhen/rtfm/" rel="alternate" type="text/html" />
  <updated>2026-07-20T14:45:34+00:00</updated>
  <id>https://bizzal70.github.io/itsalreadywhen/rtfm-feed.xml</id>
  <title type="html">It’s Already When. — RTFM</title>
  <subtitle>Long-form, technical, reference-grounded security best practices.</subtitle>
  <author>
    <name>The Analyst</name>
  </author>
  
  
  <entry>
    <title type="html">Logging Without Anyone Reading the Logs</title>
    <link href="https://bizzal70.github.io/itsalreadywhen/rtfm/2026/07/15/logging-without-anyone-reading-the-logs/" rel="alternate" type="text/html" title="Logging Without Anyone Reading the Logs" />
    <published>2026-07-15T00:00:00+00:00</published>
    <updated>2026-07-15T00:00:00+00:00</updated>
    <id>https://bizzal70.github.io/itsalreadywhen/rtfm/2026/07/15/logging-without-anyone-reading-the-logs/</id>
    <content type="html" xml:base="https://bizzal70.github.io/itsalreadywhen/rtfm/2026/07/15/logging-without-anyone-reading-the-logs/">&lt;p&gt;Every organization logs. Almost none of them read the logs. This is the oldest open secret in the industry: a SIEM humming in the corner, ingesting a few terabytes a day, generating dashboards nobody opens, firing alerts nobody triages, and satisfying an auditor who checked a box that said “audit logging: enabled.” The comforting fiction is that collection equals visibility. It doesn’t. Collection is storage. Detection is work. And the distance between the two is measured in the number of days an attacker gets to operate before anyone notices, which industry data has kept pinned in the triple digits for the better part of a decade.&lt;/p&gt;

&lt;h2 id=&quot;the-standard&quot;&gt;The Standard&lt;/h2&gt;

&lt;p&gt;CIS Critical Security Control 8, Audit Log Management, exists precisely because “we have logs” and “we can detect anything” are unrelated claims. The control breaks into eight safeguards, and it is worth reading them in order because the order is the point.&lt;/p&gt;

&lt;p&gt;It starts with 8.1, establishing an audit log management process, and 8.2, collecting audit logs. Fine, table stakes. But 8.3 requires you to &lt;em&gt;ensure adequate audit log storage&lt;/em&gt;, because logs that roll over in three days are logs you can’t investigate an incident with. 8.4 mandates &lt;em&gt;time synchronization&lt;/em&gt;, because correlation across sources is impossible when your firewall, your domain controller, and your web tier disagree about what time it is. 8.5 pushes for &lt;em&gt;detailed audit logging&lt;/em&gt;, capturing source, destination, timestamp, user, and event outcome, not just “something happened.” 8.6 and 8.7 extend collection to DNS and URL/command-line activity. And 8.9 through 8.11 get to the part everyone skips: &lt;em&gt;centralize&lt;/em&gt; the logs, &lt;em&gt;retain&lt;/em&gt; them for a defined period (CIS suggests a minimum of 90 days, ideally longer), and &lt;em&gt;conduct reviews&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Read that last one again. The control does not say “collect logs.” It says review them. The entire framework is constructed to funnel you toward the act of looking. Most implementations stop at 8.2 and declare victory.&lt;/p&gt;

&lt;h2 id=&quot;where-it-breaks-down&quot;&gt;Where It Breaks Down&lt;/h2&gt;

&lt;p&gt;The failures are specific and depressingly consistent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Logging the wrong things, verbosely.&lt;/strong&gt; Teams turn on everything the vendor offers and then drown. A domain controller with full Kerberos and NTLM auditing enabled but no tuning will bury a golden-ticket anomaly under a million routine 4624 and 4634 logon events. Windows Event ID 4688 (process creation) is enormously valuable, but only if you also enabled the command-line auditing GPO, which is off by default. Most environments log that a process started and never capture &lt;em&gt;what it was invoked with&lt;/em&gt;, which is the one field that turns “powershell.exe ran” into “powershell.exe ran a base64-encoded download cradle.”&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The endpoints that never talk.&lt;/strong&gt; Domain controllers ship logs. The Linux jump host someone stood up in 2019 does not. Cloud control planes are their own dark continent: CloudTrail exists, but management events and data events are separately configured and separately billed, so data events (the S3 object-level reads that actually reveal exfiltration) are frequently disabled to save money. Kubernetes audit logging is off unless you explicitly wrote an audit policy and wired it into the API server flags. Every one of these gaps is a place where an attacker operates with zero telemetry.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ingestion without parsing.&lt;/strong&gt; A log that arrives as an unparsed blob is a log that cannot be queried, correlated, or alerted on. Syslog dumped into an index with no field extraction is a haystack with no magnet. If your &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;user&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;src_ip&lt;/code&gt;, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;action&lt;/code&gt; fields aren’t normalized to a schema (ECS, OCSF, whatever you’ve chosen), then a query for “all failed authentications by this account across all sources” returns nothing useful, and nobody writes the detection because writing it is miserable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Alerts with no owner and no triage.&lt;/strong&gt; The SIEM ships with a thousand correlation rules enabled out of the box. They fire constantly. Within a month the analysts have muted the noisy ones, and within a quarter they’ve stopped looking at the channel entirely. This is alert fatigue, and it is not a personal failing; it is the predictable outcome of deploying detection content nobody tuned against a baseline nobody established.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Retention that quietly lies.&lt;/strong&gt; Someone configured 90 days of hot retention, then the ingestion volume tripled, the storage tier filled, and the index started aging out at eleven days. Nobody noticed, because nobody was querying anything older than yesterday. The 90-day number still lives in the compliance document.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No time discipline.&lt;/strong&gt; NTP is “probably fine.” Then an incident happens, you pull logs from four systems, and their timestamps span a nine-minute spread with two different time zones and one host stuck in local time with no offset recorded. Correlation collapses. This is Safeguard 8.4, ignored, and it poisons everything downstream.&lt;/p&gt;

&lt;h2 id=&quot;doing-it-right&quot;&gt;Doing It Right&lt;/h2&gt;

&lt;p&gt;Start from the question, not the source. Detection engineering means deciding &lt;em&gt;what you want to catch&lt;/em&gt; and then working backward to the telemetry that would reveal it. Map your desired detections to a framework like MITRE ATT&amp;amp;CK, identify the data sources each technique requires, and only then decide what to collect. This inverts the usual order and immediately tells you that you don’t need debug-level logs from everything; you need specific, high-fidelity events from the right places.&lt;/p&gt;

&lt;p&gt;Fix the plumbing before the analytics:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Time first.&lt;/strong&gt; Enforce NTP from a common, trusted source across every host. Log in UTC everywhere and record the offset. Nothing else works until this does.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Centralize with structure.&lt;/strong&gt; Ship to a central store (a SIEM, or a log pipeline built on something like the Elastic stack, OpenSearch, Loki, or a managed platform) and normalize to a schema at ingest. Use a pipeline layer (Cribl, Logstash, Vector, Fluent Bit) to parse, enrich, and drop noise &lt;em&gt;before&lt;/em&gt; indexing, which also controls the cost that drives the retention lies.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Log the fields that matter.&lt;/strong&gt; Enable command-line process auditing (Event ID 4688 with the GPO, or Sysmon Event ID 1 with a curated config such as the widely used community baseline). Turn on PowerShell script block logging (Event ID 4104). Enable CloudTrail data events for sensitive buckets, and a Kubernetes audit policy scoped to writes and exec.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then do the part the control actually asks for: review. Build a small set of high-signal detections you trust and tune them against your real baseline until the false-positive rate is livable. A dozen reliable alerts beat a thousand ignored ones. Write them as code, version them, test them, and treat them like software (detection-as-code). Run periodic threat hunts against the centralized data for the things too subtle to alert on: anomalous service-account logons, first-time parent/child process pairs, outbound connections from servers that should never initiate them.&lt;/p&gt;

&lt;p&gt;And validate. Run atomic tests, generate the event you claim to detect, and confirm it (a) was collected, (b) was parsed, and (c) fired. An untested detection is a hypothesis, not a control. The number of “enabled” alerts that silently broke when a log source changed format would embarrass the entire profession if anyone measured it.&lt;/p&gt;

&lt;h2 id=&quot;the-bottom-line&quot;&gt;The Bottom Line&lt;/h2&gt;

&lt;p&gt;Nobody gets fired for buying a SIEM. Plenty of people get breached behind one. The logs were there the whole time, dutifully collected, faithfully retained, gloriously unread, and the forensics team will find the smoking gun three weeks after the attacker left, in an index everyone had access to and no one had opened. Collection is a purchase order. Detection is a discipline, and disciplines require someone whose job is to actually look. Until then you don’t have a security control, you have a very expensive write-only database and a clean audit report to bury with it.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Your logs already saw it. That was never the problem.&lt;/em&gt;&lt;/p&gt;
</content>
    <summary type="html">Everyone collects logs; almost nobody reads them, and the gap between compliance logging and operational detection is where breaches live undetected for months.</summary>
    <author>
      <name>The Analyst</name>
    </author>
  </entry>
  
  <entry>
    <title type="html">Patch Cadence vs. Patch Theater</title>
    <link href="https://bizzal70.github.io/itsalreadywhen/rtfm/2026/07/08/patch-cadence-vs-patch-theater/" rel="alternate" type="text/html" title="Patch Cadence vs. Patch Theater" />
    <published>2026-07-08T00:00:00+00:00</published>
    <updated>2026-07-08T00:00:00+00:00</updated>
    <id>https://bizzal70.github.io/itsalreadywhen/rtfm/2026/07/08/patch-cadence-vs-patch-theater/</id>
    <content type="html" xml:base="https://bizzal70.github.io/itsalreadywhen/rtfm/2026/07/08/patch-cadence-vs-patch-theater/">&lt;p&gt;Every organization has a patching policy. Almost none of them can tell you, with a straight face and real data, what percentage of their exploitable assets got patched within the window that policy promises. These are not the same claim, and the distance between them is where breaches actually happen. “We have a patching policy” is a statement about a PDF. “We patch” is a statement about the world, and the world is considerably less cooperative than a PDF.&lt;/p&gt;

&lt;h2 id=&quot;the-standard&quot;&gt;The Standard&lt;/h2&gt;

&lt;p&gt;CIS Critical Security Control 7, Continuous Vulnerability Management, is refreshingly unromantic about what it wants. It asks you to do four things and to keep doing them: establish and maintain a documented vulnerability management process (7.1), establish and maintain a remediation process with defined timelines by risk (7.2), perform automated operating system patch management (7.3) and automated application patch management (7.4), and perform authenticated vulnerability scanning on both internal and external-facing assets (7.5 through 7.7).&lt;/p&gt;

&lt;p&gt;The word doing the heavy lifting is “continuous.” Control 7 does not describe a project. It describes a loop: discover assets, scan them with credentials, prioritize what you find against actual risk, remediate within a defined SLA, and then verify the fix landed by scanning again. The verification step is not decorative. A vulnerability you believe you remediated but never confirmed is still an open vulnerability, plus a false sense of security, which is strictly worse.&lt;/p&gt;

&lt;p&gt;Note also that Control 7 leans on Controls 1 and 2. You cannot patch what you do not know you own. The control assumes you have an asset inventory and a software inventory, because remediation timelines are meaningless if the denominator, the total set of things that need patching, is unknown. Most “patch coverage” numbers are lies of omission: 98% of the assets we know about, and we do not talk about the ones we do not.&lt;/p&gt;

&lt;h2 id=&quot;where-it-breaks-down&quot;&gt;Where It Breaks Down&lt;/h2&gt;

&lt;p&gt;The failures are boringly consistent across organizations of every size.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Unauthenticated scanning masquerading as vulnerability management.&lt;/strong&gt; A network scan that fingerprints a service banner over the wire tells you what a port looks like from outside. It does not tell you the patch level of the installed package, the version of a statically linked library buried in an application, or whether a kernel is running a version different from the one on disk because nobody rebooted. Control 7.5 says &lt;em&gt;authenticated&lt;/em&gt; for a reason. Credentialed scans read the actual package database, the registry, the installed build numbers. If your scanner is not authenticating, you are measuring the paint, not the structure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Patch coverage metrics that hide the reboot gap.&lt;/strong&gt; Package managers are happy to report a patch as “installed” the moment the new binary is on disk. But a running process is still executing the old code loaded into memory. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;apt&lt;/code&gt; will happily update &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;glibc&lt;/code&gt; while every long-running daemon on the box keeps the vulnerable version mapped until restart. The kernel is the obvious case, but shared libraries are the silent one. Tools like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;needrestart&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;checkrestart&lt;/code&gt; exist precisely because “patched” and “no longer running the vulnerable code” are different states, and most patch dashboards conflate them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SLA timelines measured from the wrong clock.&lt;/strong&gt; A policy that says “critical vulnerabilities patched within 7 days” sounds firm until you ask: seven days from when? From vendor disclosure? From when your scanner detected it? From when a ticket was opened? Each choice can differ by weeks. Organizations quietly reset the clock to the moment of internal detection, which conveniently excludes the discovery latency that is often the largest part of exposure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The long tail nobody owns.&lt;/strong&gt; OS patching gets automated because Windows Update, WSUS, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dnf-automatic&lt;/code&gt;, and MDM profiles make it tractable. Control 7.4, application patching, is where it collapses. The Java runtime embedded in a vendor appliance. The OpenSSL statically compiled into an internal Go binary from three teams ago. The npm dependency tree of a service whose owner left the company. Container base images pinned to a tag and never rebuilt. These do not show up in the OS patch report, and no automated updater touches them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prioritization by CVSS base score alone.&lt;/strong&gt; Control 7.2 asks you to prioritize by risk. A CVSS base score is not risk. It ignores whether the vulnerability is on an internet-facing asset, whether a public exploit exists, whether the affected component is actually reachable or loaded, and whether compensating controls apply. Treating a 9.8 on an isolated internal test box the same as a 7.5 on an exposed VPN concentrator is how remediation queues become undifferentiated backlogs of tens of thousands of “criticals” that no human triages.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ephemeral and cloud assets outrunning the scan cycle.&lt;/strong&gt; A weekly scan cadence assumes assets live longer than a week. Autoscaling groups, spot instances, and short-lived containers appear and vanish between scans, so they are either never assessed or assessed after they no longer exist. Meanwhile the &lt;em&gt;image&lt;/em&gt; they were built from, the actual unit of remediation, sits unscanned in a registry.&lt;/p&gt;

&lt;h2 id=&quot;doing-it-right&quot;&gt;Doing It Right&lt;/h2&gt;

&lt;p&gt;Start by refusing to report a coverage percentage without stating the denominator. Reconcile your vulnerability scanner’s asset list against your CMDB, your cloud provider’s inventory APIs, your EDR console, and your DHCP or NAC logs. The assets that appear in one source and not another are your actual attack surface. This reconciliation is tedious and permanent. Automate it or it will not happen.&lt;/p&gt;

&lt;p&gt;Run authenticated scans. Provision least-privilege scan credentials (read-only, no interactive login, source-IP restricted, rotated) and confirm authentication success rates per scan, not just aggregate findings. A credentialed scan that silently fell back to unauthenticated is a common and dangerous failure mode; alert on the auth-success rate dropping.&lt;/p&gt;

&lt;p&gt;Instrument the reboot gap explicitly. On Linux, incorporate &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;needrestart&lt;/code&gt; output or check &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/var/run/reboot-required&lt;/code&gt; into your compliance reporting. On Windows, track pending-reboot registry keys. Report two numbers: “patch applied” and “patch active.” The delta is your real exposure and the metric your leadership has never seen.&lt;/p&gt;

&lt;p&gt;Measure your SLA from detection, and separately measure detection latency itself (time from public availability to when your program first saw it). If discovery latency dominates your exposure window, no amount of fast remediation fixes it, and you need more frequent or continuous scanning rather than a faster ticket queue.&lt;/p&gt;

&lt;p&gt;Prioritize with exploitability context, not base score. Enrich findings with exploit-availability feeds (something like a known-exploited catalog), internet-exposure data from your own external scanning, and asset criticality from your inventory. Feed these into remediation SLAs so that an exposed, actively exploited flaw gets a 48-hour clock while an internal, unreachable one gets a routine cycle. This is how you make the backlog triageable by humans.&lt;/p&gt;

&lt;p&gt;For the application long tail, shift the unit of remediation upstream. Scan container images in the registry and fail builds on unremediated criticals rather than scanning running containers after deployment. Adopt software composition analysis (SCA) to enumerate embedded and transitive dependencies that OS package managers cannot see. Generate and retain an SBOM per build so that when a component turns out to be vulnerable, you can answer “where do we run this” in minutes rather than in an all-hands archaeology project.&lt;/p&gt;

&lt;p&gt;Finally, close the loop with verification. Every remediation ticket should be closed by a subsequent authenticated scan confirming the finding is gone, not by a human asserting the patch was pushed. If your ticketing system lets an engineer close a vuln without rescan evidence, your process has a hole shaped exactly like human optimism.&lt;/p&gt;

&lt;h2 id=&quot;the-bottom-line&quot;&gt;The Bottom Line&lt;/h2&gt;

&lt;p&gt;Nobody gets compromised through the vulnerabilities in their policy document. They get compromised through the ones in the gap: the asset that was not in the inventory, the library that was patched on disk but running in memory, the appliance nobody owned, the scan that quietly fell back to unauthenticated in March and has been lying ever since. A patching policy is a promise you make to an auditor. Patching is a promise you keep to an attacker, and the attacker is the only one checking your work.&lt;/p&gt;

&lt;p&gt;You will not close the gap entirely. You will only ever make it smaller, and knowable, and honest. That is the whole job. The organizations that survive are not the ones that patch fastest. They are the ones that know, to the asset, what they have not patched yet.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;File under: things you already knew, filed anyway.&lt;/em&gt;&lt;/p&gt;
</content>
    <summary type="html">A patching policy is a document; patching is a measurable outcome, and the gap between the two is where most organizations quietly live.</summary>
    <author>
      <name>The Analyst</name>
    </author>
  </entry>
  
  <entry>
    <title type="html">Least Privilege, Actually Enforced</title>
    <link href="https://bizzal70.github.io/itsalreadywhen/rtfm/2026/07/01/least-privilege-actually-enforced/" rel="alternate" type="text/html" title="Least Privilege, Actually Enforced" />
    <published>2026-07-01T00:00:00+00:00</published>
    <updated>2026-07-01T00:00:00+00:00</updated>
    <id>https://bizzal70.github.io/itsalreadywhen/rtfm/2026/07/01/least-privilege-actually-enforced/</id>
    <content type="html" xml:base="https://bizzal70.github.io/itsalreadywhen/rtfm/2026/07/01/least-privilege-actually-enforced/">&lt;p&gt;Everyone has a least-privilege policy. It lives in a Confluence page nobody has opened since the last audit, it uses the word “shall” a lot, and it has approximately zero effect on who can read the payroll database at 2 a.m. Least privilege is the security principle most universally endorsed and least frequently enforced, because writing the policy costs a meeting and enforcing it costs you the goodwill of every engineer who has ever been granted &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sudo&lt;/code&gt; “just to unblock a deploy.” The uncomfortable truth is that access accretes. It only ever accretes. And the access review that was supposed to prune it back is, in most shops, a spreadsheet that gets rubber-stamped once a year by a manager who has no idea what any of the entitlements mean.&lt;/p&gt;

&lt;h2 id=&quot;the-standard&quot;&gt;The Standard&lt;/h2&gt;

&lt;p&gt;CIS Critical Security Control 6, Access Control Management, is not subtle about what it wants. The control covers the full lifecycle of access: granting, using, revoking, and reviewing. The parts practitioners skip are Safeguards 6.1 and 6.2 (a documented process to grant and to revoke access), 6.7 (centralizing access control through a directory or SSO where feasible), and above all 6.8, which asks you to “define and maintain role-based access control” by determining and documenting the access rights necessary for each role and reviewing them.&lt;/p&gt;

&lt;p&gt;The word doing the heavy lifting there is &lt;em&gt;maintain&lt;/em&gt;. Anyone can define an access model on a whiteboard. CIS is asking for the boring, recurring, adversarial work of confirming that the access people actually hold still matches the access they are supposed to hold. Control 5, Account Management, is the sibling you cannot ignore: an inventory of accounts, disabling dormant ones, and managing service accounts. Least privilege is meaningless if you do not know the accounts exist, and Control 6 quietly assumes you have already done Control 5. Most organizations have not.&lt;/p&gt;

&lt;p&gt;Read together, the two controls describe a closed loop: you know every identity, you grant each identity the minimum entitlements its role requires, you have a mechanism that revokes those entitlements when the role changes, and you periodically verify that reality matches intent. It is a loop. The failure is almost always that it is not closed.&lt;/p&gt;

&lt;h2 id=&quot;where-it-breaks-down&quot;&gt;Where It Breaks Down&lt;/h2&gt;

&lt;p&gt;The first break is that “role-based” access control is aspirational fiction in most environments. What you actually have is entitlement-based access control with a role-shaped label glued on top. Someone requests access to a specific S3 bucket, a specific Jira project, a specific &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pg_hba.conf&lt;/code&gt; line, and it gets granted directly to their principal. The “Developer” role in your IdP maps to three group memberships; the other forty things a developer can touch were granted one ticket at a time. When you go looking for “what does this role grant,” there is no answer, because the role was never the unit of provisioning.&lt;/p&gt;

&lt;p&gt;The second break is standing access to everything, all the time. AWS IAM policies with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&quot;Action&quot;: &quot;*&quot;&lt;/code&gt; on &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&quot;Resource&quot;: &quot;*&quot;&lt;/code&gt; because scoping them was hard and the deadline was Friday. Kubernetes &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ClusterRoleBinding&lt;/code&gt; to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cluster-admin&lt;/code&gt; because someone could not figure out which &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;verbs&lt;/code&gt; on which &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;resources&lt;/code&gt; the CI runner needed. Domain accounts in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Domain Admins&lt;/code&gt; because a vendor’s installer asked for it in 2019. Every one of these is a permanent grant for an intermittent need, and permanence is the enemy.&lt;/p&gt;

&lt;p&gt;The third break is the machines. Service accounts, CI/CD tokens, OAuth client credentials, and cloud instance roles now outnumber human identities by a wide margin, and they are almost never reviewed. A human at least leaves the company and triggers an offboarding checklist. A service account lives forever, its secret rotated never, its permissions granted by whoever stood up the integration and understood by no one. The static AWS access key checked into a Terraform state file five years ago still works, and it can still assume a role that can delete production.&lt;/p&gt;

&lt;p&gt;The fourth break is the review itself, which is theater. The classic failure mode is the annual “user access review” where a system exports a list of usernames and entitlement codes to a spreadsheet, and line managers are asked to certify them. The manager sees &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;APP_FIN_RW_PROD&lt;/code&gt; next to an employee’s name, has no idea what it grants, knows that clicking “revoke” might break something and generate a support ticket with their name on it, and clicks “approve all.” Certification rates of 99 percent are not a sign of clean access. They are a sign that nobody read anything. A review that never revokes anything is not a control. It is a compliance artifact.&lt;/p&gt;

&lt;p&gt;The fifth break is that even when someone wants to revoke, the revocation path does not exist. Access was granted directly in the resource, out of band from the IdP, so there is no central place to pull it. Nested group memberships mean removing someone from one group does nothing because they inherit the same rights through two others. Access granted through a break-glass account has no owner to notify. You end up with orphaned entitlements that no automated process will ever clean up because no automated process knows they are there.&lt;/p&gt;

&lt;h2 id=&quot;doing-it-right&quot;&gt;Doing It Right&lt;/h2&gt;

&lt;p&gt;Start by making the identity provider the source of truth, per Safeguard 6.7. If access can be granted outside your SSO/IdP, you cannot review it and you cannot revoke it. Federate everything that speaks SAML or OIDC. For the systems that do not, put them behind SCIM provisioning or, at minimum, a documented provisioning pipeline that flows through the IdP. The goal is a single point where deprovisioning actually deprovisions.&lt;/p&gt;

&lt;p&gt;Then attack standing privilege directly. The single highest-leverage change is moving from persistent grants to just-in-time access. Human access to production should be requested, time-boxed, and expired automatically. This is what PAM tooling and cloud-native mechanisms like AWS IAM Identity Center permission sets with session policies, or Azure PIM eligible assignments, exist to do. An entitlement that expires in eight hours does not need an annual review. It reviews itself.&lt;/p&gt;

&lt;p&gt;For what cannot be made ephemeral, scope it. Replace wildcard IAM policies with explicit actions and resource ARNs, and use conditions (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;aws:SourceIp&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;aws:PrincipalTag&lt;/code&gt;) to constrain further. Replace &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cluster-admin&lt;/code&gt; bindings with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Roles&lt;/code&gt; scoped to a namespace and the specific &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;verbs&lt;/code&gt; the workload needs. Read your cloud provider’s access analyzer output, which tells you which granted permissions were never used, and delete them.&lt;/p&gt;

&lt;p&gt;Treat service accounts as first-class review targets. Every non-human identity needs a named human owner, a documented purpose, an expiry or rotation schedule, and a place in the same review cycle as humans. Prefer short-lived, workload-identity-federated credentials (OIDC-based, IRSA, workload identity) over long-lived static keys so there is nothing to leak and nothing to review.&lt;/p&gt;

&lt;p&gt;Finally, fix the review so it can say no. Do not certify entitlement codes; certify against a defined role model, so the reviewer’s question is “should this person be a Developer” instead of “what is &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;APP_FIN_RW_PROD&lt;/code&gt;.” Feed the review with usage data: an entitlement not exercised in 90 days is a revocation candidate by default, and the burden is on the owner to justify keeping it. Make revocation the cheap path and retention the one that requires effort. Track your revocation rate as the actual metric. A review cycle that revokes nothing is broken by definition, regardless of how many boxes got ticked.&lt;/p&gt;

&lt;h2 id=&quot;the-bottom-line&quot;&gt;The Bottom Line&lt;/h2&gt;

&lt;p&gt;Least privilege is not a policy problem. It is a garbage-collection problem, and you have never once run the collector. The document that says “users shall have minimum necessary access” is true the moment it is written and false by the end of the week, because access is granted continuously and reclaimed approximately never. The organizations that get breached through over-permissioned accounts are not the ones without a policy. They all have a policy. They are the ones whose access review approved everything, forever, because saying no was harder than saying yes. Close the loop or stop pretending you have one.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Access granted is access forever, unless you go take it back. You won’t.&lt;/em&gt;&lt;/p&gt;
</content>
    <summary type="html">Everyone writes a least-privilege policy and almost no one enforces it; here&apos;s the gap between the document and an access review that actually revokes something.</summary>
    <author>
      <name>The Analyst</name>
    </author>
  </entry>
  
  <entry>
    <title type="html">MFA Fatigue and Push-Bombing</title>
    <link href="https://bizzal70.github.io/itsalreadywhen/rtfm/2026/06/30/mfa-fatigue-and-push-bombing/" rel="alternate" type="text/html" title="MFA Fatigue and Push-Bombing" />
    <published>2026-06-30T00:00:00+00:00</published>
    <updated>2026-06-30T00:00:00+00:00</updated>
    <id>https://bizzal70.github.io/itsalreadywhen/rtfm/2026/06/30/mfa-fatigue-and-push-bombing/</id>
    <content type="html" xml:base="https://bizzal70.github.io/itsalreadywhen/rtfm/2026/06/30/mfa-fatigue-and-push-bombing/">&lt;p&gt;We bolted push notifications onto authentication because passwords were a disaster and SMS one-time codes were worse, and for a brief, hopeful moment it looked like we’d solved something. Then attackers discovered that a button which says &lt;em&gt;Approve&lt;/em&gt; will, given enough requests at 3 a.m., eventually get pressed. The control we deployed to stop credential theft became a control that launders credential theft into account takeover, one weary thumb at a time. This is not a failure of the technology so much as a failure to read the manual that shipped with it.&lt;/p&gt;

&lt;h2 id=&quot;the-standard&quot;&gt;The Standard&lt;/h2&gt;

&lt;p&gt;NIST Special Publication 800-63B (the &lt;em&gt;Digital Identity Guidelines&lt;/em&gt;, Authentication and Lifecycle Management volume) does not actually contain a control called “push MFA.” It never did. What it defines are &lt;em&gt;authenticators&lt;/em&gt;, classed by Authenticator Assurance Level (AAL), with specific requirements around phishing resistance, verifier impersonation, and what the document calls “authentication intent.”&lt;/p&gt;

&lt;p&gt;Two concepts in 800-63B matter most here, and almost nobody implementing push notifications reads them.&lt;/p&gt;

&lt;p&gt;The first is &lt;strong&gt;phishing resistance&lt;/strong&gt;, formalized in the AAL3 requirements and discussed throughout §5. An authenticator is phishing-resistant when the authentication protocol cryptographically binds the verifier’s identity into the ceremony, so a credential proven to one party cannot be replayed against another. WebAuthn/FIDO2 does this via channel binding to the origin. A push notification does not. A push approval is a bare yes/no decision divorced from any cryptographic context about &lt;em&gt;who&lt;/em&gt; is asking and &lt;em&gt;why&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The second is &lt;strong&gt;authentication intent&lt;/strong&gt;, defined in §5.2.9. NIST is explicit: the authentication process should “establish that the claimant is the entity attempting to authenticate,” meaning the human pressing the button actually initiated the login. A naked push prompt establishes that &lt;em&gt;someone&lt;/em&gt; has the phone and is willing to tap. It establishes nothing about intent.&lt;/p&gt;

&lt;p&gt;800-63B also restricts what it calls &lt;strong&gt;out-of-band authenticators&lt;/strong&gt; (§5.1.3) and is openly skeptical of unauthenticated approval flows. The document has, across revisions, steadily downgraded the standing of anything that relies on a user making a security decision under ambiguity. The standard, in other words, told you the simple push prompt was weak. You shipped it anyway because the UX was clean.&lt;/p&gt;

&lt;h2 id=&quot;where-it-breaks-down&quot;&gt;Where It Breaks Down&lt;/h2&gt;

&lt;p&gt;The failures are specific and they repeat across organizations like a liturgy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Simple approve/deny with no context.&lt;/strong&gt; The dominant deployment pattern is a single prompt: &lt;em&gt;Sign-in request: Approve / Deny&lt;/em&gt;. No number to match, no location, no application name, no request count. The user has been trained by hundreds of legitimate prompts to treat the button as a speed bump. Attackers who have already harvested the password (via a reverse proxy phishing kit, credential stuffing, or an infostealer dump) simply replay the login and wait. This is &lt;strong&gt;push-bombing&lt;/strong&gt; (or MFA fatigue): fire the authentication attempt repeatedly until the prompt becomes an annoyance the user dismisses by approving it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No rate limiting on push generation.&lt;/strong&gt; Many IdP configurations will happily emit a push notification on every authentication attempt with no throttling, no exponential backoff, and no lockout after N rejected or unanswered prompts. The attacker controls the volume. The user controls only their patience, which is finite.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Self-service enrollment with no binding integrity.&lt;/strong&gt; 800-63B §6.1 is specific about binding authenticators to identities with appropriate assurance. In practice, enrollment frequently happens over the same weak first factor it’s supposed to strengthen: a user with a phished password enrolls the &lt;em&gt;attacker’s&lt;/em&gt; device because the enrollment flow only re-verified the password. Now the second factor belongs to the adversary, permanently, and the legitimate user never sees a prompt.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fallback paths that defeat the control entirely.&lt;/strong&gt; The strong factor is push; the fallback is SMS OTP, or worse, an email magic link, or a help-desk reset with knowledge-based verification. Attackers don’t attack your strongest authenticator. They attack the weakest one you’ll accept, and 800-63B’s whole AAL model collapses the moment a high-assurance session can be established through a low-assurance recovery flow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Conflating “we have MFA” with AAL2 or AAL3.&lt;/strong&gt; Organizations check the MFA box for compliance and assume they’ve reached an assurance level. A bare push notification is, on a good day, a marginal AAL2 implementation, and only if you ignore the verifier impersonation concerns. It is categorically not AAL3 because it is not phishing-resistant. The audit said “MFA: yes.” The threat model didn’t care.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No telemetry on prompt rejection.&lt;/strong&gt; A user who declines five prompts in two minutes is a screaming signal. Most deployments log the eventual &lt;em&gt;approval&lt;/em&gt; and discard the pattern of preceding denials. The single most valuable detection signal for push-bombing is routinely thrown on the floor.&lt;/p&gt;

&lt;h2 id=&quot;doing-it-right&quot;&gt;Doing It Right&lt;/h2&gt;

&lt;p&gt;You do not need to rip everything out tomorrow. You need to stop treating the bare prompt as adequate and start aligning to what 800-63B actually asks for.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Move to phishing-resistant authenticators wherever you can.&lt;/strong&gt; This means FIDO2/WebAuthn with hardware security keys or platform authenticators (passkeys backed by a TPM or secure enclave), bound to the relying party’s origin. This is the only category 800-63B treats as genuinely phishing-resistant at AAL3. For privileged accounts, administrators, and anything touching identity infrastructure itself, this is not optional. It is the point.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you must run push, run number matching, properly.&lt;/strong&gt; Number-matching forces the user to read a value from the login screen and type it into the prompt, which restores a thin layer of authentication intent (§5.2.9) and breaks pure-volume push-bombing. Pair it with &lt;strong&gt;contextual display&lt;/strong&gt;: application name, geographic location, IP, and a visible count of recent attempts. Context is what lets a tired human make the decision the standard assumes they’re making.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rate-limit and backoff on the verifier side.&lt;/strong&gt; Cap push generation per account per interval. Implement exponential backoff. After a threshold of denied or ignored prompts, lock the authentication flow and require step-up or administrative intervention, not another prompt. This is the throttling 800-63B §5.2.2 contemplates for verifier compromise resistance, applied to the push channel.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix enrollment and recovery to match the assurance you claim.&lt;/strong&gt; Authenticator binding (§6.1) should require an existing strong factor, not just a password. Recovery flows must not establish a session at a higher AAL than the recovery method’s own assurance. Kill KBA. Route account recovery through verified, supervised channels for high-value accounts. The recovery path is the real authenticator; treat it that way.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Instrument prompt rejection and surface it.&lt;/strong&gt; Feed denied-prompt patterns, impossible-travel signals, and prompt-storm bursts into your SIEM or ITDR tooling. Alert on the burst, not just the breach. The fatigue attack has a loud, recognizable signature &lt;em&gt;before&lt;/em&gt; the fatal approval, if anyone is looking.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bind sessions and shorten their reach.&lt;/strong&gt; Pair strong authentication with token binding, conditional access policies keyed to device posture, and aggressive re-authentication for sensitive operations. A phished session token is its own attack class; phishing-resistant login means nothing if the resulting bearer token roams freely for thirty days.&lt;/p&gt;

&lt;h2 id=&quot;the-bottom-line&quot;&gt;The Bottom Line&lt;/h2&gt;

&lt;p&gt;Push-based MFA didn’t fail because it was a bad idea. It failed because we deployed the convenient version, declared victory, and stopped reading at the part of 800-63B that explains why the convenient version doesn’t work. The guidance was on the table the whole time. The control became the thing it was meant to prevent precisely because we optimized for the tap and not for the intent behind it.&lt;/p&gt;

&lt;p&gt;You will read this, agree with all of it, and the bare approve/deny prompt will still be live in production next quarter because someone in another meeting decided number matching “adds friction.” It does. That’s the friction doing its job. The attackers already know your users will tap the button. The only open question is when.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Approve / Deny. You already know which one they’ll press.&lt;/em&gt;&lt;/p&gt;
</content>
    <summary type="html">Push-based MFA was supposed to kill phishing and credential reuse, but by turning authentication into a single fatigued tap, it became a new attack surface, and NIST has been quietly telling you to fix it for years.</summary>
    <author>
      <name>The Analyst</name>
    </author>
  </entry>
  
</feed>
