<?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>IAM | FirewallSync</title><link>https://firewallsync.com/tags/iam/</link><description>Practical cybersecurity writing for engineering teams that ship fast.</description><language>en-us</language><image><url>https://firewallsync.com/icon-512.png</url><title>FirewallSync</title><link>https://firewallsync.com/</link></image><lastBuildDate>Fri, 06 Nov 2026 09:00:00 +0000</lastBuildDate><atom:link href="https://firewallsync.com/tags/iam/index.xml" rel="self" type="application/rss+xml"/><item><title>What Is Workload Identity and Why Should You Stop Using Static Cloud Keys?</title><link>https://firewallsync.com/posts/what-is-workload-identity-and-why-should-you-stop-using-static-cloud-keys/</link><guid>https://firewallsync.com/posts/what-is-workload-identity-and-why-should-you-stop-using-static-cloud-keys/</guid><pubDate>Fri, 06 Nov 2026 09:00:00 +0000</pubDate><dc:creator>FirewallSync Editorial</dc:creator><category>Cloud Security</category><description>A static cloud key stored on a server is a credential that works until someone remembers to rotate it. Workload identity replaces that with a credential that's issued fresh, per request, and expires by design.</description><enclosure url="https://firewallsync.com/posts/what-is-workload-identity-and-why-should-you-stop-using-static-cloud-keys/cover_hu681824106042170752.jpg" type="image/jpeg" length="0"/><media:content url="https://firewallsync.com/posts/what-is-workload-identity-and-why-should-you-stop-using-static-cloud-keys/cover_hu681824106042170752.jpg" medium="image" type="image/jpeg" width="1200" height="630"/><content:encoded><![CDATA[<p><img src="https://firewallsync.com/posts/what-is-workload-identity-and-why-should-you-stop-using-static-cloud-keys/cover_hu681824106042170752.jpg" alt="Close-up of server cooling fans in a data center" width="1200" height="630"></p><p>A static cloud credential — an access key downloaded once and placed in a configuration file, an environment variable, or a secrets manager — works exactly the same way for as long as it exists, whether it&rsquo;s being used by the legitimate service it was created for or by whoever else eventually gets hold of it. Workload identity replaces this model with credentials that are issued automatically, scoped narrowly, and expire on their own, removing the long-lived static secret from the equation entirely.</p>
<h2 id="the-core-idea">The core idea</h2>
<p>Workload identity lets a workload — a VM, a container, a Kubernetes pod, a CI/CD pipeline — prove its identity to a cloud provider using an identity mechanism native to the platform it&rsquo;s already running on, rather than a separately issued static credential it has to store and protect. The cloud provider verifies that identity (often via a signed token from the underlying platform) and, if the trust relationship allows it, issues short-lived, temporary credentials scoped to what that specific workload is allowed to do. No static key is generated, stored, or needs rotating, because the credential is requested fresh each time it&rsquo;s needed and expires shortly after.</p>
<p>This is the same underlying pattern as GitHub Actions OIDC for CI/CD, generalized to any workload: an ambient, platform-verified identity replacing a portable static secret.</p>
<h2 id="where-this-shows-up-across-major-platforms">Where this shows up across major platforms</h2>
<p><strong>AWS IAM roles for EC2 instances and ECS/EKS workloads</strong> let a compute resource assume a role automatically, without ever having an access key stored on the instance — the AWS SDK, when running on that instance, retrieves temporary credentials from the instance metadata service automatically, and those credentials rotate on their own throughout the instance&rsquo;s lifetime.</p>
<p><strong>Google Cloud&rsquo;s Workload Identity Federation</strong> extends this further, allowing workloads running outside Google Cloud entirely (in another cloud, or in an on-premises environment, or in GitHub Actions) to authenticate using their own platform&rsquo;s native identity, federated into GCP&rsquo;s IAM without any GCP-specific static credential ever being generated for that external workload.</p>
<p><strong>Azure Managed Identities</strong> provide the equivalent for Azure resources — a VM, a function, an App Service instance can have an identity automatically managed by Azure, with credentials retrieved and rotated transparently by the platform.</p>
<h2 id="why-the-static-key-model-is-worse-than-it-initially-appears">Why the static-key model is worse than it initially appears</h2>
<p>A static key&rsquo;s risk isn&rsquo;t just &ldquo;it might leak.&rdquo; It&rsquo;s that the key, once created, has no natural expiration and no built-in mechanism forcing anyone to verify it&rsquo;s still needed or still appropriately scoped — it persists exactly as originally configured until someone actively intervenes. Combined with the fact that static keys are portable (they can be copied, embedded in code, committed to a repository, or exfiltrated and used from anywhere), a leaked static key gives an attacker exactly the same access the legitimate workload has, usable from anywhere, for as long as the key remains valid — which in practice is often indefinitely, since key rotation discipline tends to be inconsistent in most organizations.</p>
<p>Workload identity credentials, by contrast, are typically scoped to be usable only from the specific platform context that requested them, are short-lived by default, and require no manual rotation process to maintain that short lifespan — the security property comes from the architecture itself, not from an operational discipline that has to be maintained correctly over time.</p>
<h2 id="migrating-from-static-keys-to-workload-identity">Migrating from static keys to workload identity</h2>
<p><strong>Start with the highest-risk static credentials first</strong> — keys with broad permissions, keys used in externally reachable services, keys that have existed longest without rotation — rather than attempting a full simultaneous migration, which is often impractical across a large existing environment.</p>
<p><strong>Confirm the workload&rsquo;s actual runtime platform supports the corresponding identity mechanism</strong> before planning the migration — a workload running on a platform without native workload identity support (some older on-premises or hybrid setups) may need an intermediate solution, like a secrets manager with automated rotation, as a partial improvement rather than the full architectural change.</p>
<p><strong>Update IAM policies to trust the workload identity mechanism specifically</strong>, scoped as narrowly as the static key&rsquo;s replacement role should be — this is also a natural opportunity to right-size permissions that may have accumulated excess over the static key&rsquo;s lifetime, rather than simply replicating the old key&rsquo;s exact permission set under the new mechanism.</p>
<p><strong>Decommission the static key only after confirming the workload identity path is fully functional</strong>, running both in parallel briefly if needed to avoid an availability gap, then removing the static credential entirely rather than leaving it as an unused, still-valid fallback.</p>
<h2 id="a-practical-checklist">A practical checklist</h2>
<ul>
<li>Identify workloads still using static cloud credentials, prioritized by permission scope and exposure</li>
<li>Confirm the platform-native workload identity mechanism available for each workload&rsquo;s actual runtime environment</li>
<li>Configure IAM trust relationships scoped narrowly to the specific workload identity, not broadly</li>
<li>Migrate and verify functionality before decommissioning the static credential</li>
<li>Treat any remaining static credential, after this process, as requiring active justification for why workload identity wasn&rsquo;t a viable replacement</li>
</ul>
<p>The shift from static keys to workload identity isn&rsquo;t primarily about convenience, even though it does remove manual rotation overhead. It&rsquo;s about changing the credential&rsquo;s fundamental risk profile — from a portable secret that persists until someone acts, to an ephemeral one that expires by default.</p>
]]></content:encoded></item><item><title>Non-Human Identities: The Security Problem Behind Service Accounts and AI Agents</title><link>https://firewallsync.com/posts/non-human-identities-the-security-problem-behind-service-accounts-and-ai-agents/</link><guid>https://firewallsync.com/posts/non-human-identities-the-security-problem-behind-service-accounts-and-ai-agents/</guid><pubDate>Thu, 05 Nov 2026 09:00:00 +0000</pubDate><dc:creator>FirewallSync Editorial</dc:creator><category>Identity &amp; Access</category><description>Most identity security programs were designed around human users logging in. Service accounts, API keys, and now AI agents don't fit that model, and it shows in how little oversight they typically get.</description><enclosure url="https://firewallsync.com/posts/non-human-identities-the-security-problem-behind-service-accounts-and-ai-agents/cover_hu5579159601805468162.jpg" type="image/jpeg" length="0"/><media:content url="https://firewallsync.com/posts/non-human-identities-the-security-problem-behind-service-accounts-and-ai-agents/cover_hu5579159601805468162.jpg" medium="image" type="image/jpeg" width="1200" height="630"/><content:encoded><![CDATA[<p><img src="https://firewallsync.com/posts/non-human-identities-the-security-problem-behind-service-accounts-and-ai-agents/cover_hu5579159601805468162.jpg" alt="Small robot standing on a table" width="1200" height="630"></p><p>Most identity security programs — MFA policies, access reviews, onboarding and offboarding workflows — were designed around a mental model of a human user logging in. Service accounts, API keys, workload identities, and now AI agents acting autonomously don&rsquo;t fit that model cleanly, and in most organizations, they receive a small fraction of the governance attention human identities do, despite frequently outnumbering human users by a wide margin in any modern cloud environment.</p>
<h2 id="why-non-human-identities-are-structurally-under-governed">Why non-human identities are structurally under-governed</h2>
<p><strong>There&rsquo;s no natural offboarding trigger.</strong> A human employee leaving triggers an HR process that (ideally) disables their access. A service account has no equivalent event — it keeps functioning until someone specifically remembers it exists and decides to remove it, which, as covered in identity audits generally, tends to happen rarely and inconsistently.</p>
<p><strong>Permissions get granted broadly because there&rsquo;s no individual accountability pressure.</strong> Nobody feels personally exposed by over-provisioning a service account&rsquo;s permissions the way they might hesitate to request excessive personal access — the account is abstract, its permissions feel like an infrastructure detail rather than a decision with a name attached to it.</p>
<p><strong>They&rsquo;re numerous enough that manual review doesn&rsquo;t scale.</strong> A mid-sized cloud environment can easily have service accounts, API keys, and workload identities numbering in the thousands, dwarfing the human user count — manual, human-driven access review processes that work reasonably well for a few hundred employee accounts break down entirely at that scale.</p>
<h2 id="ai-agents-introduce-a-new-and-distinct-version-of-this-problem">AI agents introduce a new and distinct version of this problem</h2>
<p>An AI agent with tool access — the ability to call APIs, execute code, modify data, or take actions on a user&rsquo;s behalf — is a non-human identity with a property service accounts traditionally didn&rsquo;t have: its actions aren&rsquo;t fully deterministic from a static configuration. A service account does exactly what its code tells it to do, every time, in a way that&rsquo;s auditable in advance. An AI agent&rsquo;s specific actions depend on its inputs and its reasoning at runtime, which means the risk isn&rsquo;t just &ldquo;does this identity have too much permission&rdquo; (the traditional over-provisioning question) but also &ldquo;can this identity be manipulated, through its inputs, into using its legitimate permissions in unintended ways&rdquo; — a genuinely new dimension that traditional service-account governance wasn&rsquo;t built to address.</p>
<h2 id="excessive-agency-as-the-specific-failure-mode-to-watch-for">Excessive agency as the specific failure mode to watch for</h2>
<p>The OWASP Top 10 for LLM Applications identifies excessive agency — granting an AI system more autonomy, tool access, or permission than the task actually requires — as a distinct risk category. It manifests in the same pattern as traditional over-provisioning (an agent given broad database write access when it only needs to read a specific table) but with an added risk surface: if that agent can be manipulated through its inputs (a classic prompt injection scenario) into taking actions using its own legitimate, unrevoked permissions, the excessive scope of those permissions determines how much damage a successful manipulation can cause. Scoping an agent&rsquo;s tool access as narrowly as the task requires isn&rsquo;t just good practice — it&rsquo;s the primary control limiting what a successful manipulation of that agent can actually accomplish.</p>
<h2 id="what-actually-needs-to-change-in-practice">What actually needs to change in practice</h2>
<p><strong>Treat non-human identity inventory as seriously as human identity inventory</strong>, with the same expectation of knowing who or what owns each one, why it exists, and what it&rsquo;s actually used for — an unowned service account is a governance gap in exactly the way an unowned human account would be treated as one.</p>
<p><strong>Apply least privilege more aggressively to non-human identities than the current default</strong>, precisely because the absence of individual accountability pressure means the default outcome, without deliberate counter-effort, is over-provisioning.</p>
<p><strong>Build offboarding triggers for non-human identities explicitly</strong>, rather than relying on someone remembering — tying service account lifecycle to the lifecycle of the project or integration it supports, so decommissioning the project has a corresponding, enforced step for its associated identities.</p>
<p><strong>For AI agents specifically, scope tool access to the minimum the task requires</strong>, and treat any tool or API access an agent has as something that could be invoked through unintended input, not only through intended use — which means the permission scope itself, not just the intended behavior, needs to be defensible.</p>
<p><strong>Extend credential hygiene practices — short-lived tokens over static keys, rotation, monitoring for anomalous usage — to non-human identities with the same rigor applied to human credentials</strong>, rather than treating a service account&rsquo;s static, long-lived API key as an acceptable default simply because it&rsquo;s not a human logging in.</p>
<h2 id="the-scale-of-the-shift">The scale of the shift</h2>
<p>As AI agents become more common in production systems — not just answering questions but taking actions, calling tools, and operating with standing permissions — the non-human identity population in most organizations is set to grow substantially further, and faster than most identity governance processes are currently equipped to track. Treating this as a known, generalizable identity problem (apply the same governance discipline, adapted for scale and for the new manipulation risk agents introduce) is a more tractable path than waiting for the population of ungoverned non-human identities to become large enough that a comprehensive retrofit is required under incident pressure.</p>
]]></content:encoded></item><item><title>How to Audit Unused Cloud Accounts, Roles, and Service Identities</title><link>https://firewallsync.com/posts/how-to-audit-unused-cloud-accounts-roles-and-service-identities/</link><guid>https://firewallsync.com/posts/how-to-audit-unused-cloud-accounts-roles-and-service-identities/</guid><pubDate>Wed, 04 Nov 2026 09:00:00 +0000</pubDate><dc:creator>FirewallSync Editorial</dc:creator><category>Cloud Security</category><description>An unused identity with live credentials is functionally identical to a vulnerability sitting unpatched — nobody's using it, but it still works exactly as well for whoever finds it.</description><enclosure url="https://firewallsync.com/posts/how-to-audit-unused-cloud-accounts-roles-and-service-identities/cover_hu15633663302747140923.jpg" type="image/jpeg" length="0"/><media:content url="https://firewallsync.com/posts/how-to-audit-unused-cloud-accounts-roles-and-service-identities/cover_hu15633663302747140923.jpg" medium="image" type="image/jpeg" width="1200" height="630"/><content:encoded><![CDATA[<p><img src="https://firewallsync.com/posts/how-to-audit-unused-cloud-accounts-roles-and-service-identities/cover_hu15633663302747140923.jpg" alt="Rack of equipment in a dark server room" width="1200" height="630"></p><p>An unused cloud identity — an IAM user nobody logs into anymore, a service account for a decommissioned integration, a role created for a migration that finished a year ago — is functionally identical to an unpatched vulnerability. It doesn&rsquo;t matter that nobody on your team is using it. What matters is that its credentials still work, exactly as well, for anyone who happens to find them.</p>
<h2 id="why-unused-identities-accumulate">Why unused identities accumulate</h2>
<p>Identities get created deliberately and removed by nobody&rsquo;s default responsibility. Offboarding processes, when they exist, usually focus on disabling a departed employee&rsquo;s primary login — they less consistently catch the service accounts that person set up, the API keys they generated for a one-off script, or the IAM role created specifically for a project that&rsquo;s since been shelved. Each of these represents a valid credential that continues to function indefinitely unless something explicitly removes it, and nothing about normal operations naturally surfaces &ldquo;this hasn&rsquo;t been used in months&rdquo; as a signal anyone acts on.</p>
<h2 id="finding-unused-identities-with-activity-data-not-assumption">Finding unused identities with activity data, not assumption</h2>
<p><strong>AWS IAM Access Analyzer&rsquo;s unused access findings</strong> directly surface unused IAM roles, unused access keys on IAM users, and unused passwords, based on actual observed activity rather than requiring manual review of every identity. This turns &ldquo;which of our hundreds of IAM users and roles are stale&rdquo; from a guessing exercise into a query against real usage data.</p>
<p><strong>Cloud provider &ldquo;last accessed&rdquo; and &ldquo;last used&rdquo; metadata</strong> exists across most major providers for service accounts, API keys, and roles — checking this directly, even without a dedicated unused-access tool, gives a concrete date rather than relying on institutional memory about whether something is still needed.</p>
<p><strong>Cross-referencing identity lists against current employee and vendor rosters</strong> catches accounts that should have been disabled during offboarding but weren&rsquo;t — this requires connecting your identity provider&rsquo;s audit data to your HR or vendor management system, which isn&rsquo;t automatic in most setups but is one of the highest-value manual checks available.</p>
<h2 id="service-identities-deserve-more-scrutiny-than-human-ones-not-less">Service identities deserve more scrutiny than human ones, not less</h2>
<p>Service accounts and machine identities often get created with broader permissions than strictly necessary, because there&rsquo;s no individual human whose access needs feel personally accountable — nobody wants to be the reason a batch job fails from insufficient permissions, so the path of least resistance is granting broadly. Combined with the fact that service accounts don&rsquo;t have an obvious &ldquo;this person left the company&rdquo; trigger to prompt review, they tend to accumulate both excess permission and staleness at a higher rate than human accounts, while receiving less routine attention.</p>
<h2 id="what-to-do-once-youve-found-unused-identities">What to do once you&rsquo;ve found unused identities</h2>
<p><strong>Don&rsquo;t delete immediately — deactivate first, and observe.</strong> An identity that appears unused in your analysis window might back an infrequent process (a quarterly compliance export, an annual data migration script) that simply hasn&rsquo;t run recently. Deactivating (disabling the credential without deleting the identity) and monitoring for any access failures over a follow-up period is safer than immediate deletion, and still closes the exposure during the observation window.</p>
<p><strong>Extend your observation window for anything ambiguous.</strong> Thirty days of &ldquo;unused&rdquo; data is a reasonable default for continuously-running services, but insufficient for anything tied to quarterly, annual, or on-demand processes — check with the team that would own the process before treating a longer-cycle identity as confirmed unused.</p>
<p><strong>Prioritize by permission scope, not just by staleness.</strong> An unused identity with narrow, low-impact permissions is a lower-priority cleanup than an unused identity holding broad administrative access — if you can only tackle a subset immediately, start with the ones that would matter most if found and used maliciously.</p>
<h2 id="building-this-into-an-ongoing-process-not-a-one-time-cleanup">Building this into an ongoing process, not a one-time cleanup</h2>
<p>A single cleanup pass reduces the current backlog but doesn&rsquo;t stop new unused identities from accumulating going forward, since the same organizational gaps that created the current backlog — no clear ownership of removing access when it&rsquo;s no longer needed — remain in place afterward. A recurring quarterly (at minimum) review, ideally automated through a tool that surfaces unused-access findings continuously rather than requiring someone to remember to run an audit, keeps the backlog from silently rebuilding.</p>
<h2 id="a-practical-checklist">A practical checklist</h2>
<ul>
<li>Run cloud-native unused access tooling (like AWS IAM Access Analyzer) as a recurring process, not a one-time audit</li>
<li>Cross-reference identity lists against current employee and vendor rosters to catch offboarding gaps</li>
<li>Apply extra scrutiny to service and machine identities specifically, since they accumulate excess more often and get reviewed less</li>
<li>Deactivate before deleting, with an observation period for anything not obviously safe to remove immediately</li>
<li>Prioritize cleanup by permission scope and blast radius, not purely by how long an identity has been idle</li>
<li>Establish a recurring cadence for this review, not a one-time project, since the conditions that create unused identities don&rsquo;t go away after a single cleanup</li>
</ul>
<p>The goal isn&rsquo;t a one-time reduction in identity count. It&rsquo;s closing the gap between &ldquo;how many valid credentials exist&rdquo; and &ldquo;how many are actually needed,&rdquo; on an ongoing basis, since that gap is where a meaningful share of real incidents originate.</p>
]]></content:encoded></item><item><title>Cloud IAM vs Network Security: Why Identity Has Become the New Perimeter</title><link>https://firewallsync.com/posts/cloud-iam-vs-network-security-why-identity-has-become-the-new-perimeter/</link><guid>https://firewallsync.com/posts/cloud-iam-vs-network-security-why-identity-has-become-the-new-perimeter/</guid><pubDate>Tue, 03 Nov 2026 09:00:00 +0000</pubDate><dc:creator>FirewallSync Editorial</dc:creator><category>Cloud Security</category><description>Network segmentation still matters, but in a cloud environment, the thing standing between an attacker and your data is increasingly a permission check, not a firewall rule.</description><enclosure url="https://firewallsync.com/posts/cloud-iam-vs-network-security-why-identity-has-become-the-new-perimeter/cover_hu2099996365430746022.jpg" type="image/jpeg" length="0"/><media:content url="https://firewallsync.com/posts/cloud-iam-vs-network-security-why-identity-has-become-the-new-perimeter/cover_hu2099996365430746022.jpg" medium="image" type="image/jpeg" width="1200" height="630"/><content:encoded><![CDATA[<p><img src="https://firewallsync.com/posts/cloud-iam-vs-network-security-why-identity-has-become-the-new-perimeter/cover_hu2099996365430746022.jpg" alt="Close-up of network equipment with connected cables" width="1200" height="630"></p><p>Traditional network security assumed a defensible boundary: a corporate network with a firewall at the edge, internal traffic implicitly more trusted than external traffic, and access controlled largely by what network segment you could physically or logically reach. Cloud environments break this assumption structurally — most cloud resources are reachable over the public internet by design, and what actually stands between an attacker and your data is increasingly a permission check evaluated by an identity provider, not a network path blocked by a firewall.</p>
<h2 id="why-the-network-perimeter-stopped-being-the-primary-boundary">Why the network perimeter stopped being the primary boundary</h2>
<p>In a cloud environment, an S3 bucket, a database, or an API endpoint is often reachable from anywhere on the internet unless explicitly restricted — and even when network restrictions exist, they&rsquo;re frequently permissive by necessity, since legitimate access (from developers, from other cloud services, from users) also needs to traverse the same public internet. This means the network layer, while still worth hardening, isn&rsquo;t where most access decisions actually get enforced anymore. The enforcement point has moved to IAM: does this identity have permission to perform this action on this resource, evaluated per-request, regardless of what network path the request arrived on.</p>
<h2 id="what-this-changes-practically">What this changes practically</h2>
<p><strong>A compromised credential now matters more than a compromised network position.</strong> In a traditional perimeter model, gaining access to the internal network was often the hard part of an attack, after which lateral movement was comparatively easier. In a cloud environment, an attacker with a valid but overprivileged credential can often reach exactly what they need directly, without needing to compromise any network boundary at all — the credential itself is the boundary that mattered.</p>
<p><strong>Least privilege on identities does the work that network segmentation used to do.</strong> Where network segmentation limited blast radius by controlling what a compromised machine could reach on the network, IAM scoping limits blast radius by controlling what a compromised identity can do regardless of where it&rsquo;s calling from. A tightly scoped IAM role with no meaningful network restriction is often a stronger control than a broad IAM role sitting behind a well-segmented network, because the segmentation doesn&rsquo;t stop a legitimately-authenticated call from an already-compromised identity.</p>
<p><strong>Service-to-service authentication is now an identity problem, not just a network problem.</strong> Whether one internal service can call another used to be substantially a network question (can it reach the right port on the right host). It&rsquo;s increasingly an identity question — does this specific service&rsquo;s identity have a role or token that authorizes this specific call — which is why workload identity and service-to-service authentication design has become a first-class security concern rather than an implementation detail.</p>
<h2 id="where-network-controls-still-matter">Where network controls still matter</h2>
<p>This isn&rsquo;t an argument that network security has become irrelevant — it remains a meaningful additional layer, particularly for defense in depth and for reducing the attack surface an identity-layer compromise can reach. Network policies (in Kubernetes, in VPC security groups) that restrict which services can even attempt to communicate reduce the paths available to an attacker who has compromised one workload, even if IAM would separately block the attempt. The point isn&rsquo;t that network controls are obsolete — it&rsquo;s that they&rsquo;re no longer sufficient on their own, and organizations that invest heavily in network segmentation while leaving IAM broadly permissive are protecting the boundary that matters less while underinvesting in the one that matters more.</p>
<h2 id="where-teams-still-get-the-balance-wrong">Where teams still get the balance wrong</h2>
<p><strong>Treating &ldquo;inside the VPC&rdquo; as equivalent to &ldquo;trusted.&rdquo;</strong> A service reachable only from within a VPC still needs proper authentication and authorization for calls it receives — network placement isn&rsquo;t a substitute for verifying who&rsquo;s calling and what they&rsquo;re allowed to do, since anything else inside that VPC (including a compromised workload) can also reach it.</p>
<p><strong>Investing in network monitoring while IAM policies go unreviewed.</strong> Network traffic monitoring and intrusion detection get regular attention in many security programs, while IAM policy review — auditing for unused, overprivileged, or stale permissions — happens far less consistently, despite IAM misconfiguration being a more direct path to most cloud breaches than a network-layer compromise.</p>
<p><strong>Assuming a well-configured security group compensates for an overprivileged role.</strong> A database with a restrictive security group but a service account holding broad, unscoped permissions to it is still exposed to anything that compromises that service account — the network restriction only helps against threats arriving from an unexpected network path, which is a shrinking fraction of realistic cloud attack scenarios.</p>
<h2 id="the-practical-implication">The practical implication</h2>
<p>Security effort in cloud environments should be weighted toward identity: least-privilege IAM policies, short-lived credentials over static ones, strong authentication for service-to-service calls, and continuous auditing of what every identity can actually do. Network controls remain a legitimate and useful additional layer, but they&rsquo;re the secondary boundary now, not the primary one — and a security program still organized as if the network perimeter is the main line of defense is protecting against yesterday&rsquo;s most common attack path more than today&rsquo;s.</p>
]]></content:encoded></item><item><title>GitHub Actions OIDC: Stop Storing Cloud Credentials in CI/CD</title><link>https://firewallsync.com/posts/github-actions-oidc-stop-storing-cloud-credentials-in-cicd/</link><guid>https://firewallsync.com/posts/github-actions-oidc-stop-storing-cloud-credentials-in-cicd/</guid><pubDate>Mon, 02 Nov 2026 09:00:00 +0000</pubDate><dc:creator>FirewallSync Editorial</dc:creator><category>AppSec &amp; APIs</category><description>A static cloud credential stored as a CI secret is valid until someone notices it's wrong. An OIDC-issued credential is valid for one workflow run and expires whether anyone notices or not.</description><enclosure url="https://firewallsync.com/posts/github-actions-oidc-stop-storing-cloud-credentials-in-cicd/cover_hu11536215644407057025.jpg" type="image/jpeg" length="0"/><media:content url="https://firewallsync.com/posts/github-actions-oidc-stop-storing-cloud-credentials-in-cicd/cover_hu11536215644407057025.jpg" medium="image" type="image/jpeg" width="1200" height="630"/><content:encoded><![CDATA[<p><img src="https://firewallsync.com/posts/github-actions-oidc-stop-storing-cloud-credentials-in-cicd/cover_hu11536215644407057025.jpg" alt="Two monitors displaying code" width="1200" height="630"></p><p>A static cloud credential — an AWS access key, a GCP service account key — stored as a GitHub Actions secret has one fundamental property that makes it a standing risk: it remains valid until someone actively notices it shouldn&rsquo;t be and rotates it. OpenID Connect (OIDC) support in GitHub Actions removes that credential from the equation entirely, replacing it with a token that&rsquo;s short-lived by design and requires no manual rotation discipline to stay safe.</p>
<h2 id="how-the-exchange-actually-works">How the exchange actually works</h2>
<p>When a workflow configured for OIDC runs, GitHub&rsquo;s Actions runner requests a JSON Web Token (JWT) from GitHub&rsquo;s own OIDC provider (<code>token.actions.githubusercontent.com</code>). This token contains claims about the workflow run — which repository, which branch or ref, which workflow file — and is signed by GitHub. The workflow then presents this token to the cloud provider (AWS STS, for example), which has been configured ahead of time to trust GitHub&rsquo;s OIDC provider and to check specific claims in the token before issuing temporary credentials. If the claims match what the trust policy expects, the cloud provider issues short-lived, scoped credentials good only for that specific workflow run.</p>
<p>No static credential is stored anywhere in this flow. The only thing GitHub holds is a role ARN or equivalent identifier telling it which role to request — the actual authentication happens fresh, per run, through the signed token exchange.</p>
<h2 id="setting-up-the-aws-side-of-the-trust-relationship">Setting up the AWS side of the trust relationship</h2>
<p>Configuring this requires two things in AWS: registering GitHub&rsquo;s OIDC provider as a trusted identity provider in IAM, and creating an IAM role with a trust policy that specifies exactly which GitHub repositories (and optionally, which branches or environments) are allowed to assume it. A typical trust policy condition restricts the <code>sub</code> claim to a specific pattern like <code>repo:your-org/your-repo:ref:refs/heads/main</code>, meaning only workflow runs on that exact branch, in that exact repository, can successfully assume the role — a workflow run from a fork, or from a different branch, will have a token with a <code>sub</code> claim that doesn&rsquo;t match, and the assume-role call will simply fail.</p>
<p>This scoping is what makes OIDC meaningfully more secure than a static credential even beyond the &ldquo;no long-lived secret&rdquo; property — the trust relationship itself constrains which workflows can even attempt to use it, rather than relying on the credential&rsquo;s holder being trustworthy by assumption.</p>
<h2 id="the-workflow-side-configuration">The workflow-side configuration</h2>
<p>The workflow needs explicit <code>permissions: id-token: write</code> set — this is the permission that allows the job to request an OIDC token from GitHub in the first place, and it&rsquo;s not granted by default even when other broad permissions are set. Combined with an action like <code>aws-actions/configure-aws-credentials</code>, specifying the target role&rsquo;s ARN, the workflow requests the token, performs the exchange with AWS STS, and receives temporary credentials scoped to that role — all without a static <code>AWS_ACCESS_KEY_ID</code> or <code>AWS_SECRET_ACCESS_KEY</code> appearing anywhere in the workflow or in GitHub&rsquo;s secret storage.</p>
<h2 id="why-the-short-lived-property-matters-beyond-convenience">Why the &ldquo;short-lived&rdquo; property matters beyond convenience</h2>
<p>A leaked static credential is a live problem until someone notices and rotates it — which could be minutes or months, depending entirely on whether monitoring catches the exposure. An OIDC-issued credential, even if somehow captured mid-flight during a specific run, expires on its own shortly after that run completes, typically within the hour. This changes the actual risk calculus: the attacker&rsquo;s usable window is bounded by design, rather than bounded by however quickly your team happens to detect and respond to a leak.</p>
<h2 id="what-oidc-does-not-solve">What OIDC does not solve</h2>
<p>OIDC removes the specific risk of a long-lived credential leaking from CI storage. It doesn&rsquo;t address other CI/CD security concerns: a compromised workflow can still assume the role and act with whatever permissions that role grants during its valid window, so scoping the IAM role&rsquo;s own permissions narrowly still matters exactly as much as it did with static credentials. It also doesn&rsquo;t protect against a malicious pull request modifying the workflow file itself to add unauthorized steps, if the workflow&rsquo;s trigger configuration allows untrusted code to influence what runs — that&rsquo;s a separate class of risk (<code>pull_request_target</code> misuse, in particular) that OIDC doesn&rsquo;t touch.</p>
<h2 id="multi-cloud-and-cross-provider-considerations">Multi-cloud and cross-provider considerations</h2>
<p>AWS, Google Cloud, and Azure all support this pattern under slightly different names (AWS calls it &ldquo;Web Identity Federation,&rdquo; GCP calls it &ldquo;Workload Identity Federation&rdquo;) but the underlying mechanism — trusting GitHub&rsquo;s OIDC provider and issuing short-lived credentials based on token claims — is conceptually the same across all three. If your pipeline deploys to multiple clouds, this pattern is worth implementing consistently across all of them rather than only for whichever provider had the most convenient existing tutorial.</p>
<h2 id="a-practical-checklist">A practical checklist</h2>
<ul>
<li>Register GitHub&rsquo;s OIDC provider in your cloud provider&rsquo;s IAM configuration</li>
<li>Write the trust policy&rsquo;s conditions narrowly — specific repository, specific branch or environment, not a broad wildcard match</li>
<li>Set <code>permissions: id-token: write</code> explicitly on any workflow using OIDC</li>
<li>Remove any existing static cloud credentials stored as GitHub secrets once OIDC is confirmed working, rather than leaving both in place indefinitely</li>
<li>Scope the IAM role&rsquo;s own permissions narrowly — OIDC changes how the credential is obtained, not how much access the resulting credential should have</li>
</ul>
<p>Migrating from static credentials to OIDC is one of the highest-leverage changes available for CI/CD security, precisely because it eliminates an entire category of leak (a long-lived secret sitting in storage) rather than just making that category harder to exploit.</p>
]]></content:encoded></item><item><title>How to Find Overprivileged AWS IAM Users and Roles</title><link>https://firewallsync.com/posts/how-to-find-overprivileged-aws-iam-users-and-roles/</link><guid>https://firewallsync.com/posts/how-to-find-overprivileged-aws-iam-users-and-roles/</guid><pubDate>Sat, 10 Oct 2026 09:00:00 +0000</pubDate><dc:creator>FirewallSync Editorial</dc:creator><category>Cloud Security</category><description>Overprivileged IAM identities aren't usually the result of one bad decision. They're the accumulated residue of a hundred small, reasonable-seeming ones. Here's how to actually find them.</description><enclosure url="https://firewallsync.com/posts/how-to-find-overprivileged-aws-iam-users-and-roles/cover_hu18280299811781223229.jpg" type="image/jpeg" length="0"/><media:content url="https://firewallsync.com/posts/how-to-find-overprivileged-aws-iam-users-and-roles/cover_hu18280299811781223229.jpg" medium="image" type="image/jpeg" width="1200" height="630"/><content:encoded><![CDATA[<p><img src="https://firewallsync.com/posts/how-to-find-overprivileged-aws-iam-users-and-roles/cover_hu18280299811781223229.jpg" alt="Rows of server racks in a data center" width="1200" height="630"></p><p>Overprivileged IAM identities rarely come from one obviously bad decision. They come from a hundred individually reasonable ones — a broad policy attached during setup, a permission added to unblock a deploy, a role copied from a template that itself was never trimmed. The result is an environment where almost every identity has more access than it uses, and nobody can say with confidence which ones without checking.</p>
<h2 id="start-with-whats-provably-unused-not-what-looks-risky">Start with what&rsquo;s provably unused, not what looks risky</h2>
<p>Guessing which roles are overprivileged by reading policy documents doesn&rsquo;t scale and misses the point — a policy can look reasonable on paper and still grant access nothing has touched in a year. IAM Access Analyzer&rsquo;s unused access findings are built for exactly this: it surfaces unused roles, unused access keys, unused passwords, and — most usefully — the specific unused services and actions within an otherwise-active identity&rsquo;s policy, based on real activity data rather than a manual read of the JSON.</p>
<p>This reframes the question from &ldquo;does this policy look too broad&rdquo; to &ldquo;what has this identity actually never used,&rdquo; which is both more concrete and harder to argue with when you bring findings to a team that owns the resource.</p>
<h2 id="cross-reference-with-cloudtrail-for-the-identities-access-analyzer-wont-fully-cover">Cross-reference with CloudTrail for the identities Access Analyzer won&rsquo;t fully cover</h2>
<p>Access Analyzer&rsquo;s unused-access findings are strong for standing IAM users and roles but won&rsquo;t capture every nuance of activity, especially for identities used inconsistently or through cross-account access patterns. For those, CloudTrail&rsquo;s event history lets you query what a specific principal has actually called over a given window — filtering by user identity ARN and looking at the distinct set of API actions invoked is a direct way to compare &ldquo;granted&rdquo; against &ldquo;used&rdquo; for any identity you&rsquo;re specifically concerned about.</p>
<h2 id="compare-against-role-purpose-not-just-activity">Compare against role purpose, not just activity</h2>
<p>An identity can be actively using every permission it has and still be overprivileged, if the permissions it&rsquo;s using go beyond what its actual job requires. This shows up most often in service roles: a Lambda function that only needs to read from one S3 bucket but was granted broad <code>s3:*</code> access because it was faster to configure. Activity-based tooling won&rsquo;t flag this, because the access is being used — just more broadly than necessary. This requires pairing the automated findings with a manual pass asking &ldquo;does this identity&rsquo;s actual function justify this scope,&rdquo; particularly for any role with wildcard resources or actions.</p>
<h2 id="prioritize-by-blast-radius-not-by-count">Prioritize by blast radius, not by count</h2>
<p>Once you have a list of overprivileged identities, tackling them in the order you found them wastes effort on low-stakes fixes while high-risk ones wait. Rank by what the identity could do if compromised, not by how many unused permissions it has — a role with three unused but highly sensitive permissions (like <code>iam:CreateUser</code> or <code>kms:Decrypt</code> on a broad key set) deserves attention before a role with twenty unused but low-impact ones.</p>
<h2 id="a-practical-checklist">A practical checklist</h2>
<ul>
<li>Run IAM Access Analyzer&rsquo;s unused access analysis across every account, not just production</li>
<li>Query CloudTrail directly for any identity Access Analyzer&rsquo;s findings don&rsquo;t fully cover</li>
<li>Cross-check active-but-broad permissions against the identity&rsquo;s actual functional purpose, not just usage data</li>
<li>Rank remediation by potential blast radius, not by finding volume</li>
<li>Treat any identity with wildcard actions or resources as a priority review, regardless of what usage data shows</li>
</ul>
<p>Finding overprivileged identities is a data problem before it&rsquo;s a policy-writing problem. The tooling to answer &ldquo;what&rsquo;s actually being used&rdquo; already exists in most AWS accounts — the gap is usually that nobody&rsquo;s running it on a recurring basis, not that the answer is hard to get.</p>
]]></content:encoded></item><item><title>AWS IAM Least Privilege: A Practical Guide to Finding Excess Permissions</title><link>https://firewallsync.com/posts/aws-iam-least-privilege-a-practical-guide-to-finding-excess-permissions/</link><guid>https://firewallsync.com/posts/aws-iam-least-privilege-a-practical-guide-to-finding-excess-permissions/</guid><pubDate>Fri, 09 Oct 2026 05:00:00 +0000</pubDate><dc:creator>FirewallSync Editorial</dc:creator><category>Cloud Security</category><description>Least privilege fails in practice because most teams have no visibility into which granted permissions are actually used. Here's how to find the gap and close it without breaking production.</description><enclosure url="https://firewallsync.com/posts/aws-iam-least-privilege-a-practical-guide-to-finding-excess-permissions/cover_hu925019013742310432.jpg" type="image/jpeg" length="0"/><media:content url="https://firewallsync.com/posts/aws-iam-least-privilege-a-practical-guide-to-finding-excess-permissions/cover_hu925019013742310432.jpg" medium="image" type="image/jpeg" width="1200" height="630"/><content:encoded><![CDATA[<p><img src="https://firewallsync.com/posts/aws-iam-least-privilege-a-practical-guide-to-finding-excess-permissions/cover_hu925019013742310432.jpg" alt="Server racks behind mesh doors with orange and teal network cables and green status lights" width="1200" height="630"></p><p>Least privilege fails in practice for a simple reason: granting permissions is a one-time decision made under uncertainty, and revoking them requires confidence that nothing will break. Most teams grant broadly upfront because they don&rsquo;t know exactly what a role will need, and then never revisit it, because finding out what&rsquo;s actually being used requires visibility that doesn&rsquo;t exist by default.</p>
<h2 id="why-permissions-accumulate">Why permissions accumulate</h2>
<p>Roles and users rarely lose permissions once granted. Common patterns:</p>
<ul>
<li>A role gets <code>AdministratorAccess</code> or a broad managed policy during initial setup &ldquo;to unblock things,&rdquo; with a plan to tighten it later that never happens</li>
<li>A permission was needed for a one-time migration or debugging session and was never removed afterward</li>
<li>A service&rsquo;s responsibilities changed, but its IAM policy was never updated to match — it still holds access relevant to what it used to do</li>
<li>Permissions get copied from an existing role as a starting template, inheriting whatever excess that role had accumulated</li>
</ul>
<p>None of these are unusual mistakes. They&rsquo;re the predictable result of granting being cheap and auditing being expensive, unless you have tooling that makes the audit cheap too.</p>
<h2 id="finding-unused-access-with-iam-access-analyzer">Finding unused access with IAM Access Analyzer</h2>
<p>AWS IAM Access Analyzer&rsquo;s unused access analysis is built specifically for this problem. Once enabled, it continuously analyzes accounts and surfaces:</p>
<ul>
<li>Unused IAM roles</li>
<li>Unused access keys on IAM users</li>
<li>Unused passwords on IAM users</li>
<li>For active roles and users, specific unused services and actions within their attached policies</li>
</ul>
<p>This last category is the one that matters most for least privilege work — it doesn&rsquo;t just tell you a role exists and might be over-permissioned in theory, it tells you which specific granted permissions haven&rsquo;t actually been exercised, based on real access activity. Findings consolidate into a centralized dashboard, so security teams can prioritize accounts by finding volume instead of manually reviewing every policy by hand.</p>
<p>Two details from AWS&rsquo;s documentation that change how you plan this work. First, the unused access analyzer has a configurable tracking period of 1 to 365 days, and it only evaluates entities that have existed for the whole period (<a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/access-analyzer-create-unused.html">AWS docs: create an unused access analyzer</a>). Second, unused access analysis is a paid feature billed per IAM role or user analyzed per month, unlike external access findings, which are free (<a href="https://aws.amazon.com/iam/access-analyzer/pricing">AWS pricing page</a>). Check the pricing page for the current rate in your region.</p>
<h2 id="turning-findings-into-safe-policy-changes">Turning findings into safe policy changes</h2>
<p>Finding unused permissions is the easy part. Removing them without breaking something is where teams get cautious, and reasonably so — an unused-in-the-analysis-window permission isn&rsquo;t necessarily an unused-forever permission if it backs an infrequent process like a quarterly batch job or an annual compliance export.</p>
<p>A safer sequence:</p>
<ol>
<li><strong>Extend the observation window</strong> before acting on any single finding — a permission unused in a 30-day window might still be legitimate for something that runs monthly or quarterly</li>
<li><strong>Cross-reference against known infrequent processes</strong> — check with the team that owns the role about anything that runs on a schedule longer than your analysis window before removing access tied to it</li>
<li><strong>Use policy generation, not just manual editing</strong> — Access Analyzer policy generation builds a policy template from the actions an entity performed in your CloudTrail logs over a period of up to 90 days, which is a better starting point than hand-editing an existing broad policy. It has limits: it needs a CloudTrail trail, it does not identify action-level activity for data events such as S3 object reads, it does not include <code>iam:PassRole</code> (CloudTrail does not track it), and it counts denied actions as activity. Review the generated template rather than attaching it as-is (<a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/access-analyzer-policy-generation.html">AWS docs: policy generation</a>)</li>
<li><strong>Apply changes in a way that&rsquo;s reversible</strong> — deploy the tightened policy, monitor for access denials over a follow-up period, and keep the prior policy version available to roll back quickly if something legitimate breaks</li>
</ol>
<h2 id="where-least-privilege-breaks-down-structurally">Where least privilege breaks down structurally</h2>
<p>Two patterns undermine least privilege even with good tooling:</p>
<p><strong>Wildcard actions and resources.</strong> A policy using <code>&quot;Action&quot;: &quot;s3:*&quot;</code> or <code>&quot;Resource&quot;: &quot;*&quot;</code> defeats granular analysis because it&rsquo;s technically &ldquo;using&rdquo; a category of permission broadly enough that unused-access tooling has less to work with. Scoping policies to specific actions and specific resource ARNs, rather than wildcards, is what makes the analysis actually useful later.</p>
<p><strong>Shared roles across multiple purposes.</strong> A single role used by several different services or functions accumulates the union of everything any of them needs, and removing anything requires confirming none of the other consumers depend on it. Splitting roles by function, even when it means more roles to manage, keeps each one&rsquo;s actual permission needs legible.</p>
<h2 id="a-practical-checklist">A practical checklist</h2>
<ul>
<li>Enable IAM Access Analyzer&rsquo;s unused access analysis across your accounts, not just external access analysis</li>
<li>Prioritize review by finding volume, starting with roles holding the broadest permissions</li>
<li>Extend your observation window before removing access tied to infrequent processes</li>
<li>Replace wildcard actions and resources with specific ARNs and actions where feasible</li>
<li>Split multi-purpose shared roles so permission needs stay attributable to a single function</li>
<li>Treat policy tightening as a monitored, reversible change, not a one-way cutover</li>
</ul>
<p>Least privilege isn&rsquo;t a policy you write once. It&rsquo;s a permissions posture that decays by default unless something is actively pulling it back toward minimal, which is what continuous unused-access analysis is actually for.</p>
]]></content:encoded></item><item><title>S3 Block Public Access: What the Four Settings Do and How to Enforce Them</title><link>https://firewallsync.com/posts/s3-block-public-access-what-the-four-settings-do-and-how-to-enforce-them/</link><guid>https://firewallsync.com/posts/s3-block-public-access-what-the-four-settings-do-and-how-to-enforce-them/</guid><pubDate>Thu, 08 Oct 2026 08:20:00 +0000</pubDate><dc:creator>FirewallSync Editorial</dc:creator><category>Cloud Security</category><description>New S3 buckets have blocked public access by default since April 2023, but older buckets, account settings and new accounts can still be open. Here is what each setting does and how to enforce all four across an account or an AWS organization.</description><enclosure url="https://firewallsync.com/posts/s3-block-public-access-what-the-four-settings-do-and-how-to-enforce-them/cover_hu11756217249689777234.jpg" type="image/jpeg" length="0"/><media:content url="https://firewallsync.com/posts/s3-block-public-access-what-the-four-settings-do-and-how-to-enforce-them/cover_hu11756217249689777234.jpg" medium="image" type="image/jpeg" width="1200" height="630"/><content:encoded><![CDATA[<p><img src="https://firewallsync.com/posts/s3-block-public-access-what-the-four-settings-do-and-how-to-enforce-them/cover_hu11756217249689777234.jpg" alt="Rows of white archive boxes on wooden shelves" width="1200" height="630"></p><p>Turn on all four S3 Block Public Access settings at the account level in every AWS account, or, if you use AWS Organizations, enforce them with an organization policy so no account can switch them off. Then look for buckets created before April 2023, because AWS&rsquo;s private-by-default change only applied to new buckets. The rest of this article explains what each setting does, because the names are similar and the differences matter when something stops working.</p>
<h2 id="the-four-settings">The four settings</h2>
<p>AWS describes four independent settings. Two act when someone tries to make something public; two act on what is already public.</p>
<table>
  <thead>
      <tr>
          <th>Setting</th>
          <th>What it does</th>
          <th>Acts on</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>BlockPublicAcls</code></td>
          <td>Makes calls that set a public ACL fail: <code>PutBucketAcl</code>, <code>PutObjectAcl</code>, and <code>PutObject</code> requests that include a public ACL</td>
          <td>New ACLs</td>
      </tr>
      <tr>
          <td><code>IgnorePublicAcls</code></td>
          <td>Makes S3 ignore all public ACLs on the bucket and its objects. Uploads that include a public ACL still succeed, but the ACL has no effect</td>
          <td>Existing and new ACLs</td>
      </tr>
      <tr>
          <td><code>BlockPublicPolicy</code></td>
          <td>Makes S3 reject a <code>PutBucketPolicy</code> call if the policy allows public access, and does the same for the bucket&rsquo;s same-account access point policies</td>
          <td>New policies</td>
      </tr>
      <tr>
          <td><code>RestrictPublicBuckets</code></td>
          <td>Limits a bucket or access point that has a public policy to AWS service principals and authorized users in the owner&rsquo;s account, blocking all other cross-account access</td>
          <td>Existing policies</td>
      </tr>
  </tbody>
</table>
<p>An ACL (access control list) is the older per-bucket and per-object permission mechanism in S3; a bucket policy is the JSON resource policy attached to a bucket.</p>
<p>The pairs matter. <code>BlockPublicPolicy</code> alone stops a new public policy but leaves an existing one working. <code>RestrictPublicBuckets</code> alone neutralizes an existing public policy but lets someone attach a new one. AWS recommends turning on all four.</p>
<p>One warning from AWS&rsquo;s documentation is worth repeating: &ldquo;Block public access settings don&rsquo;t alter existing policies or ACLs. Therefore, removing a block public access setting causes a bucket or object with a public policy or ACL to again be publicly accessible.&rdquo; The settings are a shield over your policies, not a cleanup of them.</p>
<h2 id="what-s3-counts-as-public">What S3 counts as &ldquo;public&rdquo;</h2>
<p>S3 uses its own definition, and it is stricter than &ldquo;allows everyone&rdquo;.</p>
<ul>
<li><strong>ACLs.</strong> An ACL is public if it grants any permission to the <code>AllUsers</code> or <code>AuthenticatedUsers</code> groups. <code>AuthenticatedUsers</code> means any AWS account holder, not users in your account.</li>
<li><strong>Bucket policies.</strong> S3 starts by assuming a policy is public, then checks whether it qualifies as non-public. To be non-public, a policy must grant access only to fixed values, with no wildcard and no IAM policy variable, for things such as specific AWS principals, <code>aws:SourceIp</code> address ranges, or conditions like <code>aws:SourceVpc</code>, <code>aws:SourceArn</code> and <code>aws:SourceAccount</code>.</li>
</ul>
<p>AWS&rsquo;s documentation gives an example: a policy granting <code>&quot;Principal&quot;: &quot;*&quot;</code> with the condition <code>&quot;StringEquals&quot;: {&quot;aws:SourceVpc&quot;: &quot;vpc-91237329&quot;}</code> is non-public, because the VPC ID is fixed. The same policy using <code>StringLike</code> with a wildcard VPC value is public.</p>
<h2 id="where-the-settings-apply-and-which-wins">Where the settings apply, and which wins</h2>
<p>You can set Block Public Access on an access point, a bucket, an AWS account, and, since November 2025, across an organization. AWS&rsquo;s rule for overlapping settings is simple: S3 &ldquo;applies the most restrictive combination&rdquo;. If the account blocks public policies, no bucket in that account can be made public through a policy, whatever the bucket&rsquo;s own setting says.</p>
<p>This is why the account level is where to start. One command covers every existing and future bucket in the account:</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>aws s3control put-public-access-block <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    --account-id <span style="color:#ae81ff">123456789012</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    --public-access-block-configuration <span style="color:#e6db74">&#39;{&#34;BlockPublicAcls&#34;: true, &#34;IgnorePublicAcls&#34;: true, &#34;BlockPublicPolicy&#34;: true, &#34;RestrictPublicBuckets&#34;: true}&#39;</span>
</span></span></code></pre></div><p>The command needs the <code>s3:PutAccountPublicAccessBlock</code> permission. AWS&rsquo;s CLI reference notes that it can return an access-denied error when the account is governed by an organization-level policy, because that policy takes over account-level settings.</p>
<p>To check a single bucket&rsquo;s own settings:</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>aws s3api get-public-access-block --bucket amzn-s3-demo-bucket
</span></span></code></pre></div><p>AWS&rsquo;s API reference cautions that this returns the bucket-level configuration only; the effective behavior also depends on the account and organization settings.</p>
<h3 id="access-points-behave-differently">Access points behave differently</h3>
<p>AWS notes two details for S3 access points. Their Block Public Access settings can only be set when the access point is created and cannot be changed afterward. And an access point whose network origin is a VPC is always treated as non-public, whatever its policy says.</p>
<h2 id="enforcing-it-across-an-organization">Enforcing it across an organization</h2>
<p>On November 26, 2025, AWS announced organization-level enforcement for S3 Block Public Access. You attach an AWS Organizations S3 policy at the root, at an organizational unit or to specific accounts, and new member accounts inherit it. AWS states there is no additional charge. AWS&rsquo;s policy syntax documentation shows the whole policy:</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 style="color:#f92672">&#34;s3_attributes&#34;</span>: {
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">&#34;public_access_block_configuration&#34;</span>: {
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">&#34;@@assign&#34;</span>: <span style="color:#e6db74">&#34;all&#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>The value is <code>&quot;all&quot;</code> or <code>&quot;none&quot;</code>. The organization level is all or nothing: it turns the four settings on or off together. AWS&rsquo;s documentation adds that if you want to manage the settings per account, you should disable the S3 policy type at the organization level.</p>
<p>For a multi-account estate, this is the strongest form of the control, because an administrator inside a member account cannot turn it off. That is our reading of the design rather than an AWS claim. Before attaching it, find any account that legitimately serves public content from S3 and move that content behind CloudFront first.</p>
<h2 id="the-april-2023-default-and-what-it-did-not-change">The April 2023 default, and what it did not change</h2>
<p>In December 2022, AWS announced two changes that took effect from April 2023: newly created buckets would have Block Public Access enabled, and ACLs disabled through the &ldquo;bucket owner enforced&rdquo; Object Ownership setting. AWS said the console already used both defaults, and the change extended them to buckets created through the S3 API, CLI, SDKs and CloudFormation. An update dated April 27, 2023 says the settings then applied to all new buckets in all AWS Regions.</p>
<p>The announcement describes newly created buckets only. A bucket created in 2021 with a public policy keeps it until someone acts. The places to look first:</p>
<ul>
<li>AWS accounts older than 2023, especially ones nobody owns any more.</li>
<li>Buckets created by older infrastructure-as-code modules that set the bucket ACL or policy explicitly.</li>
<li>Accounts where someone turned the account-level setting off to make one website work.</li>
</ul>
<h2 id="auditing-an-account">Auditing an account</h2>
<p>AWS Security Hub has four controls that cover this, if you use it:</p>
<table>
  <thead>
      <tr>
          <th>Control</th>
          <th>What it checks</th>
          <th>Severity</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>S3.1</td>
          <td>All four settings enabled at the account level</td>
          <td>Medium</td>
      </tr>
      <tr>
          <td>S3.8</td>
          <td>All four settings enabled at the bucket level</td>
          <td>High</td>
      </tr>
      <tr>
          <td>S3.2</td>
          <td>Bucket permits public read access, considering settings, policy and ACL</td>
          <td>Critical</td>
      </tr>
      <tr>
          <td>S3.3</td>
          <td>Bucket permits public write access, considering settings, policy and ACL</td>
          <td>Critical</td>
      </tr>
  </tbody>
</table>
<p>IAM Access Analyzer for S3 also reports buckets that grant access to anyone on the internet or to other AWS accounts. AWS notes that in rare cases Access Analyzer and the Block Public Access evaluation can disagree on whether a bucket is public.</p>
<p>If you do not use either service, this Python script with boto3 reports the gaps in one account. It only reads configuration. It is illustrative: we tested its logic with botocore&rsquo;s Stubber against simulated responses, not against a live AWS account.</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:#e6db74">&#34;&#34;&#34;Report S3 Block Public Access gaps in one AWS account (read-only).&#34;&#34;&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> boto3
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> botocore.exceptions <span style="color:#f92672">import</span> ClientError
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>SETTINGS <span style="color:#f92672">=</span> (<span style="color:#e6db74">&#34;BlockPublicAcls&#34;</span>, <span style="color:#e6db74">&#34;IgnorePublicAcls&#34;</span>, <span style="color:#e6db74">&#34;BlockPublicPolicy&#34;</span>, <span style="color:#e6db74">&#34;RestrictPublicBuckets&#34;</span>)
</span></span><span style="display:flex;"><span>MISSING <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;NoSuchPublicAccessBlockConfiguration&#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">block_config</span>(fetch) <span style="color:#f92672">-&gt;</span> dict:
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;&#34;&#34;Return the four settings, treating &#39;not configured&#39; as all False.&#34;&#34;&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">try</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> fetch()[<span style="color:#e6db74">&#34;PublicAccessBlockConfiguration&#34;</span>]
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">except</span> ClientError <span style="color:#66d9ef">as</span> e:
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> e<span style="color:#f92672">.</span>response[<span style="color:#e6db74">&#34;Error&#34;</span>][<span style="color:#e6db74">&#34;Code&#34;</span>] <span style="color:#f92672">==</span> MISSING:
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">return</span> {k: <span style="color:#66d9ef">False</span> <span style="color:#66d9ef">for</span> k <span style="color:#f92672">in</span> SETTINGS}
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">raise</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">audit</span>(session<span style="color:#f92672">=</span><span style="color:#66d9ef">None</span>) <span style="color:#f92672">-&gt;</span> list[str]:
</span></span><span style="display:flex;"><span>    session <span style="color:#f92672">=</span> session <span style="color:#f92672">or</span> boto3<span style="color:#f92672">.</span>Session()
</span></span><span style="display:flex;"><span>    account_id <span style="color:#f92672">=</span> session<span style="color:#f92672">.</span>client(<span style="color:#e6db74">&#34;sts&#34;</span>)<span style="color:#f92672">.</span>get_caller_identity()[<span style="color:#e6db74">&#34;Account&#34;</span>]
</span></span><span style="display:flex;"><span>    s3control <span style="color:#f92672">=</span> session<span style="color:#f92672">.</span>client(<span style="color:#e6db74">&#34;s3control&#34;</span>)
</span></span><span style="display:flex;"><span>    s3 <span style="color:#f92672">=</span> session<span style="color:#f92672">.</span>client(<span style="color:#e6db74">&#34;s3&#34;</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    findings <span style="color:#f92672">=</span> []
</span></span><span style="display:flex;"><span>    account <span style="color:#f92672">=</span> block_config(<span style="color:#66d9ef">lambda</span>: s3control<span style="color:#f92672">.</span>get_public_access_block(AccountId<span style="color:#f92672">=</span>account_id))
</span></span><span style="display:flex;"><span>    off <span style="color:#f92672">=</span> [k <span style="color:#66d9ef">for</span> k <span style="color:#f92672">in</span> SETTINGS <span style="color:#66d9ef">if</span> <span style="color:#f92672">not</span> account<span style="color:#f92672">.</span>get(k)]
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> off:
</span></span><span style="display:flex;"><span>        findings<span style="color:#f92672">.</span>append(<span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;account </span><span style="color:#e6db74">{</span>account_id<span style="color:#e6db74">}</span><span style="color:#e6db74">: off at account level: </span><span style="color:#e6db74">{</span><span style="color:#e6db74">&#39;, &#39;</span><span style="color:#f92672">.</span>join(off)<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> bucket <span style="color:#f92672">in</span> s3<span style="color:#f92672">.</span>list_buckets()[<span style="color:#e6db74">&#34;Buckets&#34;</span>]:
</span></span><span style="display:flex;"><span>        name <span style="color:#f92672">=</span> bucket[<span style="color:#e6db74">&#34;Name&#34;</span>]
</span></span><span style="display:flex;"><span>        cfg <span style="color:#f92672">=</span> block_config(<span style="color:#66d9ef">lambda</span>: s3<span style="color:#f92672">.</span>get_public_access_block(Bucket<span style="color:#f92672">=</span>name))
</span></span><span style="display:flex;"><span>        <span style="color:#75715e"># Effective setting is the most restrictive of account and bucket.</span>
</span></span><span style="display:flex;"><span>        effective_off <span style="color:#f92672">=</span> [k <span style="color:#66d9ef">for</span> k <span style="color:#f92672">in</span> SETTINGS <span style="color:#66d9ef">if</span> <span style="color:#f92672">not</span> (cfg<span style="color:#f92672">.</span>get(k) <span style="color:#f92672">or</span> account<span style="color:#f92672">.</span>get(k))]
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">try</span>:
</span></span><span style="display:flex;"><span>            is_public <span style="color:#f92672">=</span> s3<span style="color:#f92672">.</span>get_bucket_policy_status(Bucket<span style="color:#f92672">=</span>name)[<span style="color:#e6db74">&#34;PolicyStatus&#34;</span>][<span style="color:#e6db74">&#34;IsPublic&#34;</span>]
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">except</span> ClientError <span style="color:#66d9ef">as</span> e:
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">if</span> e<span style="color:#f92672">.</span>response[<span style="color:#e6db74">&#34;Error&#34;</span>][<span style="color:#e6db74">&#34;Code&#34;</span>] <span style="color:#f92672">!=</span> <span style="color:#e6db74">&#34;NoSuchBucketPolicy&#34;</span>:
</span></span><span style="display:flex;"><span>                <span style="color:#66d9ef">raise</span>
</span></span><span style="display:flex;"><span>            is_public <span style="color:#f92672">=</span> <span style="color:#66d9ef">False</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> is_public <span style="color:#f92672">or</span> effective_off:
</span></span><span style="display:flex;"><span>            findings<span style="color:#f92672">.</span>append(<span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;bucket </span><span style="color:#e6db74">{</span>name<span style="color:#e6db74">}</span><span style="color:#e6db74">: policy public=</span><span style="color:#e6db74">{</span>is_public<span style="color:#e6db74">}</span><span style="color:#e6db74">; &#34;</span>
</span></span><span style="display:flex;"><span>                            <span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;effectively off: </span><span style="color:#e6db74">{</span><span style="color:#e6db74">&#39;, &#39;</span><span style="color:#f92672">.</span>join(effective_off) <span style="color:#f92672">or</span> <span style="color:#e6db74">&#39;none&#39;</span><span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> findings
</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">if</span> __name__ <span style="color:#f92672">==</span> <span style="color:#e6db74">&#34;__main__&#34;</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> line <span style="color:#f92672">in</span> audit():
</span></span><span style="display:flex;"><span>        print(line)
</span></span></code></pre></div><p><code>GetBucketPolicyStatus</code> returns <code>IsPublic: true</code> when S3 considers the bucket&rsquo;s policy public. Known limits of the script:</p>
<ul>
<li>It does not read organization-level policies, so an account protected only by an organization policy may be reported as &ldquo;off&rdquo;.</li>
<li>It does not paginate the bucket list or handle buckets in Regions that need a regional client, which matters for large accounts.</li>
<li>The missing-configuration error code is taken from the AWS SDK&rsquo;s model for the account-level API; we did not confirm the bucket-level code against a live account.</li>
<li>It checks policies and settings, not ACLs on individual objects.</li>
</ul>
<h2 id="if-you-need-public-content">If you need public content</h2>
<p>Serving a website straight from a public bucket is the main reason people switch these settings off. AWS&rsquo;s recommended alternative is CloudFront with origin access control (OAC): the bucket stays private and a bucket policy grants read access only to your CloudFront distribution. AWS recommends OAC over the older origin access identity (OAI), citing support for all Regions including those launched after December 2022, for SSE-KMS encryption, and for PUT and DELETE requests. The bucket policy looks like this, with your own bucket name, account ID and distribution ID:</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 style="color:#f92672">&#34;Version&#34;</span>: <span style="color:#e6db74">&#34;2012-10-17&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;Statement&#34;</span>: [
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&#34;Sid&#34;</span>: <span style="color:#e6db74">&#34;AllowCloudFrontServicePrincipalReadOnly&#34;</span>,
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&#34;Effect&#34;</span>: <span style="color:#e6db74">&#34;Allow&#34;</span>,
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&#34;Principal&#34;</span>: {<span style="color:#f92672">&#34;Service&#34;</span>: <span style="color:#e6db74">&#34;cloudfront.amazonaws.com&#34;</span>},
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&#34;Action&#34;</span>: <span style="color:#e6db74">&#34;s3:GetObject&#34;</span>,
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&#34;Resource&#34;</span>: <span style="color:#e6db74">&#34;arn:aws:s3:::amzn-s3-demo-bucket/*&#34;</span>,
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&#34;Condition&#34;</span>: {
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">&#34;StringEquals&#34;</span>: {
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">&#34;AWS:SourceArn&#34;</span>: <span style="color:#e6db74">&#34;arn:aws:cloudfront::111122223333:distribution/EDFDVBD6EXAMPLE&#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><span style="display:flex;"><span>  ]
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Because the condition uses a fixed <code>AWS:SourceArn</code> value, S3 treats this policy as non-public under the definition above, so it works with all four settings on.</p>
<p>Block Public Access does not cover everything. AWS&rsquo;s documentation advises also checking identity-based policies on IAM roles and resource-based policies on related resources such as KMS keys. For why per-bucket checklists rarely keep up across many accounts, see <a href="/posts/cloud-misconfiguration-checklists-dont-work-heres-what-does/">why cloud misconfiguration checklists don&rsquo;t work</a>.</p>
<h2 id="sources-and-verification">Sources and verification</h2>
<p>Checked on October 8, 2026. We did not run the CLI commands or the audit script against a live AWS account.</p>
<table>
  <thead>
      <tr>
          <th>Important claim</th>
          <th>Source</th>
          <th>Verification</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Behavior of the four settings; settings do not alter existing policies or ACLs; most restrictive combination applies; definition of public for ACLs and policies; access point rules; recommendation to enable all four; check identity and KMS policies too; Access Analyzer can disagree</td>
          <td><a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/access-control-block-public-access.html">AWS, Blocking public access to your Amazon S3 storage</a></td>
          <td>Verified</td>
      </tr>
      <tr>
          <td>Account-level CLI command, required permission, access denied under organization policy</td>
          <td><a href="https://docs.aws.amazon.com/cli/latest/reference/s3control/put-public-access-block.html">AWS CLI reference, s3control put-public-access-block</a></td>
          <td>Verified</td>
      </tr>
      <tr>
          <td>Bucket-level GET returns bucket configuration only</td>
          <td><a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetPublicAccessBlock.html">AWS API reference, GetPublicAccessBlock</a></td>
          <td>Verified</td>
      </tr>
      <tr>
          <td><code>IsPublic</code> meaning</td>
          <td><a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetBucketPolicyStatus.html">AWS API reference, GetBucketPolicyStatus</a></td>
          <td>Verified</td>
      </tr>
      <tr>
          <td>Organization-level enforcement announced November 26, 2025; inheritance; no additional charge</td>
          <td><a href="https://aws.amazon.com/about-aws/whats-new/2025/11/amazon-s3-block-public-access-organization-level-enforcement/">AWS What&rsquo;s New, November 26, 2025</a></td>
          <td>Verified</td>
      </tr>
      <tr>
          <td>Organization policy JSON; &ldquo;all&rdquo; and &ldquo;none&rdquo;; disable the policy type to manage per account</td>
          <td><a href="https://docs.aws.amazon.com/organizations/latest/userguide/orgs_manage_policies_s3_syntax.html">AWS Organizations, S3 policy syntax</a></td>
          <td>Verified</td>
      </tr>
      <tr>
          <td>April 2023 defaults for new buckets; creation methods covered; April 27, 2023 update</td>
          <td><a href="https://aws.amazon.com/blogs/aws/heads-up-amazon-s3-security-changes-are-coming-in-april-of-2023/">AWS News Blog, December 13, 2022</a></td>
          <td>Verified</td>
      </tr>
      <tr>
          <td>Older buckets keep their settings</td>
          <td>Same post describes only newly created buckets</td>
          <td>Qualified: inferred from the announcement&rsquo;s scope</td>
      </tr>
      <tr>
          <td>Security Hub controls S3.1, S3.8, S3.2, S3.3 and severities</td>
          <td><a href="https://docs.aws.amazon.com/securityhub/latest/userguide/s3-controls.html">AWS Security Hub, Amazon S3 controls</a></td>
          <td>Verified</td>
      </tr>
      <tr>
          <td>OAC recommended over OAI and reasons; example bucket policy</td>
          <td><a href="https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-restricting-access-to-s3.html">Amazon CloudFront, Restrict access to an Amazon S3 origin</a></td>
          <td>Verified</td>
      </tr>
      <tr>
          <td>Audit script logic</td>
          <td>Tested with botocore Stubber, boto3 1.43, Python 3.13</td>
          <td>Qualified: not run against a live account</td>
      </tr>
      <tr>
          <td>Missing-configuration error code</td>
          <td>AWS SDK model for the s3control API</td>
          <td>Qualified: bucket-level code not confirmed</td>
      </tr>
  </tbody>
</table>
]]></content:encoded></item><item><title>Passkeys vs Passwords: What Actually Changes for Security Teams?</title><link>https://firewallsync.com/posts/passkeys-vs-passwords-what-actually-changes-for-security-teams/</link><guid>https://firewallsync.com/posts/passkeys-vs-passwords-what-actually-changes-for-security-teams/</guid><pubDate>Tue, 06 Oct 2026 09:00:00 +0000</pubDate><dc:creator>FirewallSync Editorial</dc:creator><category>Identity &amp; Access</category><description>Passkeys aren't just a more convenient password. They remove an entire attack category at the protocol level, but only if recovery and enrollment are governed correctly.</description><enclosure url="https://firewallsync.com/posts/passkeys-vs-passwords-what-actually-changes-for-security-teams/cover_hu5782018246401639556.jpg" type="image/jpeg" length="0"/><media:content url="https://firewallsync.com/posts/passkeys-vs-passwords-what-actually-changes-for-security-teams/cover_hu5782018246401639556.jpg" medium="image" type="image/jpeg" width="1200" height="630"/><content:encoded><![CDATA[<p><img src="https://firewallsync.com/posts/passkeys-vs-passwords-what-actually-changes-for-security-teams/cover_hu5782018246401639556.jpg" alt="Smartphone showing a security lock icon on a desk" width="1200" height="630"></p><p>Passwords and passkeys get compared as if they&rsquo;re two options on the same axis, usually framed as &ldquo;convenience versus security.&rdquo; That framing undersells what actually changes. A password is a shared secret: both the user and the server know a value derived from it, and anything either party can be tricked into revealing can be replayed. A passkey, built on the FIDO2/WebAuthn standard, removes the shared secret entirely and replaces it with a public-private key pair, where the private key never leaves the user&rsquo;s device.</p>
<h2 id="why-this-isnt-just-a-stronger-password">Why this isn&rsquo;t just &ldquo;a stronger password&rdquo;</h2>
<p>The practical difference shows up during a phishing attempt. A password (or a one-time code from SMS or an authenticator app) can be typed into a convincing fake login page, and the attacker relays it to the real site in real time — this is how adversary-in-the-middle phishing kits defeat traditional MFA. A passkey can&rsquo;t be phished this way, because the cryptographic challenge generated during login is bound to the actual origin (the domain) the browser is talking to. If a user lands on a lookalike domain, the browser and authenticator simply won&rsquo;t produce a valid signed assertion for it — there&rsquo;s no code or value for the user to accidentally hand over, because nothing reusable is ever transmitted.</p>
<p>This is why NIST&rsquo;s SP 800-63B digital identity guidelines specifically describe WebAuthn as phishing-resistant through this verifier-origin binding, rather than relying on the user noticing something looks wrong.</p>
<h2 id="where-the-protection-actually-breaks">Where the protection actually breaks</h2>
<p>Passkeys only preserve this property end to end. Two places commonly undo it:</p>
<p><strong>Recovery paths.</strong> If losing a passkey means falling back to an email link or an SMS code to regain access, the account&rsquo;s real security level is whatever that fallback provides, not what the passkey provides. Attackers target the weakest link in the chain, and a phishing-resistant primary factor with a phishable recovery path is still a phishable account.</p>
<p><strong>Enrollment.</strong> A passkey is only as trustworthy as the process that registered it. If an attacker can register their own passkey on a compromised account (through a stolen session, a support-desk social engineering call, or a weak initial verification step), the passkey itself does nothing to stop them going forward — it just becomes their credential now, not the legitimate user&rsquo;s.</p>
<h2 id="what-actually-changes-operationally-for-a-security-team">What actually changes operationally for a security team</h2>
<ul>
<li><strong>Help desk load shifts, it doesn&rsquo;t disappear.</strong> Password resets get replaced by device-loss and recovery-flow support requests. If your recovery flow is weak, this becomes a new social-engineering target rather than a solved problem.</li>
<li><strong>Device and platform coverage matters.</strong> Passkeys sync differently across Apple, Google, and Microsoft ecosystems, and cross-platform sync behavior affects how you think about account recovery and device offboarding when someone leaves.</li>
<li><strong>Not every login surface supports it yet.</strong> Legacy internal tools, some enterprise SaaS admin panels, and API-based service accounts often can&rsquo;t use WebAuthn at all, so passkeys typically roll out unevenly across an org&rsquo;s actual login surface rather than replacing passwords everywhere at once.</li>
<li><strong>It doesn&rsquo;t replace authorization controls.</strong> A phishing-resistant login says nothing about what that authenticated session can then do — least-privilege access control still matters exactly as much after passkeys as before.</li>
</ul>
<h2 id="rollout-priority-not-rollout-completeness">Rollout priority, not rollout completeness</h2>
<p>Given uneven support, the highest-value place to start is the accounts where phishing has the most consequence: administrators, finance roles, and anyone with standing access to sensitive systems — not a company-wide simultaneous cutover. Pair the rollout with hardening the recovery path (no email or SMS fallback for high-privilege accounts) and verifying enrollment requires strong initial proof of identity. Without both of those, you&rsquo;ve changed the login screen without actually closing the attack path it was meant to close.</p>
]]></content:encoded></item><item><title>ZITADEL Account-Takeover Flaws CVE-2026-105207, -105209 and -105215: Fixed Versions and What to Check</title><link>https://firewallsync.com/posts/zitadel-account-takeover-cve-2026-105207-cve-2026-105209-cve-2026-105215-fixed-versions/</link><guid>https://firewallsync.com/posts/zitadel-account-takeover-cve-2026-105207-cve-2026-105209-cve-2026-105215-fixed-versions/</guid><pubDate>Tue, 06 Oct 2026 06:35:00 +0000</pubDate><dc:creator>FirewallSync Editorial</dc:creator><category>Identity &amp; Access</category><description>Three critical ZITADEL flaws let attackers take over accounts through external identity provider linking, passkey enrollment and forged login callbacks. Fixes shipped between July and September 2026, but the CVE records only appeared on October 4, and ZITADEL 3.x has no fix for the worst one.</description><enclosure url="https://firewallsync.com/posts/zitadel-account-takeover-cve-2026-105207-cve-2026-105209-cve-2026-105215-fixed-versions/cover_hu14719302590193262893.jpg" type="image/jpeg" length="0"/><media:content url="https://firewallsync.com/posts/zitadel-account-takeover-cve-2026-105207-cve-2026-105209-cve-2026-105215-fixed-versions/cover_hu14719302590193262893.jpg" medium="image" type="image/jpeg" width="1200" height="630"/><content:encoded><![CDATA[<p><img src="https://firewallsync.com/posts/zitadel-account-takeover-cve-2026-105207-cve-2026-105209-cve-2026-105215-fixed-versions/cover_hu14719302590193262893.jpg" alt="Account settings screen showing a verification prompt" width="1200" height="630"></p><p>If you run ZITADEL, the open-source identity platform, check the version number today. Three critical flaws, each of which lets an attacker take over someone else&rsquo;s account, were fixed in releases between mid-July and early September 2026, but their CVE records were published on October 4, 2026. Anything older than 4.17.3 is affected by at least one of them, and ZITADEL 3.x has no fix for the most serious one.</p>
<p>This article lays out which versions are affected by each flaw, what each requires from an attacker, and which questions to ask of your own logs. It is based on ZITADEL&rsquo;s GitHub security advisories and the CVE records; we did not install ZITADEL or test any of the flaws.</p>
<h2 id="the-three-flaws">The three flaws</h2>
<p>All three are authorization or verification failures around how an account gets tied to a login method. The details come from ZITADEL&rsquo;s advisories, and the dates and CVSS 4.0 scores from the CVE records, which were published by VulnCheck, acting as CVE Numbering Authority (the organization authorized to issue the CVE IDs).</p>
<table>
  <thead>
      <tr>
          <th></th>
          <th>CVE-2026-105207</th>
          <th>CVE-2026-105209</th>
          <th>CVE-2026-105215</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Advisory</td>
          <td>GHSA-g8gj-gq47-xgf4</td>
          <td>GHSA-pq2q-2c6r-75c4</td>
          <td>GHSA-738m-7888-jfv8</td>
      </tr>
      <tr>
          <td>Flaw</td>
          <td>External identity provider (IdP) links are created without verifying a primary factor or the caller&rsquo;s permission</td>
          <td>Passkey or passwordless enrollment codes are issued after checking only the organization named in a request header, not the target user&rsquo;s organization</td>
          <td>Login V1&rsquo;s &ldquo;external account not found&rdquo; registration endpoint trusts client-supplied identity fields without a completed IdP callback</td>
      </tr>
      <tr>
          <td>Result</td>
          <td>Account takeover</td>
          <td>Account takeover across organizations</td>
          <td>Account pre-hijacking</td>
      </tr>
      <tr>
          <td>What the attacker needs</td>
          <td>The victim&rsquo;s login name; no account, no interaction</td>
          <td>User-write permission in any one organization on the same instance</td>
          <td>The victim&rsquo;s external user ID, known or predictable; the victim must later sign in with that IdP</td>
      </tr>
      <tr>
          <td>Affected versions</td>
          <td>4.0.0 to 4.17.2; 3.0.0 to 3.4.15</td>
          <td>4.0.0 to 4.17.0; 3.0.0 to 3.4.14</td>
          <td>4.0.0 to 4.16.1; 3.0.0 to 3.4.13</td>
      </tr>
      <tr>
          <td>Fixed in</td>
          <td>4.17.3 and later; no 3.x fix</td>
          <td>4.17.1 and later; 3.4.15 and later</td>
          <td>4.16.2 and later; 3.4.14 and later</td>
      </tr>
      <tr>
          <td>Login UI affected</td>
          <td>Login V2 and the User Service V2 API; Login V1 unaffected</td>
          <td>Not described as specific to one login UI; concerns issuing enrollment codes</td>
          <td>Login V1 only; Login V2 unaffected</td>
      </tr>
      <tr>
          <td>Advisory score</td>
          <td>9.8 Critical</td>
          <td>9.6 Critical</td>
          <td>9.1 Critical</td>
      </tr>
      <tr>
          <td>CVSS 4.0 score in CVE record</td>
          <td>9.3</td>
          <td>9.3</td>
          <td>9.3</td>
      </tr>
      <tr>
          <td>CVE record published</td>
          <td>October 4, 2026</td>
          <td>October 4, 2026</td>
          <td>October 4, 2026</td>
      </tr>
  </tbody>
</table>
<p>An IdP (identity provider) is an external service such as a company directory or social login that vouches for a user&rsquo;s identity. A passkey is a phishing-resistant sign-in credential bound to a device, covered in our comparison of <a href="/posts/passkeys-vs-passwords-what-actually-changes-for-security-teams/">passkeys and passwords</a>.</p>
<h3 id="what-each-one-means-in-practice">What each one means in practice</h3>
<p><strong>CVE-2026-105207 is the broadest.</strong> ZITADEL&rsquo;s advisory says an attacker who knows only a victim&rsquo;s login name can bind their own external identity to the victim&rsquo;s account through the Login V2 identify-only session, then sign in as the victim. The same weakness is reachable through the API endpoint that adds IdP links, so changing the login screen does not close it. The advisory says it needs no authentication, no privileges and no user interaction. If your instance allows external IdP sign-in at all, treat this as the priority.</p>
<p><strong>CVE-2026-105209 matters most for multi-tenant setups.</strong> If you use separate ZITADEL organizations for separate customers or business units on one instance, an administrator in one organization could obtain an enrollment code for a user in another and register their own authenticator. The attacker needs a legitimate foothold first, so for a single-organization company the realistic threat is an insider or a compromised administrator account. For a service that gives each customer an organization, a customer administrator is a possible attacker.</p>
<p><strong>CVE-2026-105215 is a pre-hijack.</strong> The attacker creates an account in advance, bound to the victim&rsquo;s external identity. The victim&rsquo;s first genuine sign-in through that IdP then lands in the attacker-prepared account. It needs the victim&rsquo;s external user ID, which the advisory says may be known or predictable, and only Login V1 is affected.</p>
<h2 id="when-the-fixes-actually-shipped">When the fixes actually shipped</h2>
<p>The CVE records were published on October 4, 2026; two of them (CVE-2026-105209 and CVE-2026-105215) were updated on October 5. The fixes are older. The &ldquo;public&rdquo; dates in the CVE records are July 29, August 14 and September 4, 2026 for CVE-2026-105215, CVE-2026-105209 and CVE-2026-105207, respectively. ZITADEL&rsquo;s releases page lists 3.4.15 and 4.17.1 as released August 14, 2026 and 4.17.3 as released September 4, 2026, which matches. It lists 4.16.2 and 3.4.14 as released July 29, 2026, which also matches.</p>
<p>Two consequences follow. First, an organization that kept to a routine upgrade cadence may already be on a fixed build without having seen any alert. Second, an organization that skipped those releases has been exposed for weeks to flaws whose fixes were public, and the CVE publication on October 4 makes the technical detail easier for others to find. Whether anyone used them in that window is something we found no reporting on.</p>
<h3 id="where-the-sources-disagree-or-are-unclear">Where the sources disagree or are unclear</h3>
<ul>
<li><strong>Version ranges for CVE-2026-105207.</strong> ZITADEL&rsquo;s advisory lists 3.0.0 through 3.4.15 and 4.0.0 through 4.17.2 as affected, with 4.17.3 as the fix. The CVE record&rsquo;s text agrees, but its structured data also contains a second block that lists versions up to and including 4.19.4 as affected. That contradicts the advisory, which names 4.17.3 as the fixed release, and would flag a patched 4.19.4 server in any scanner that reads that field. We treat the advisory as authoritative and note the record&rsquo;s data as inconsistent; we have not seen a correction.</li>
<li><strong>Severity numbers.</strong> A widely circulated summary lists the three flaws as 9.8, 9.6 and 9.1. Those are the CVSS 3.1 scores. On CVSS 4.0, which the CVE records also carry, all three score 9.3. The two are different scales and should not be compared against each other.</li>
<li><strong>What &ldquo;latest&rdquo; is.</strong> The newest release we found on October 6 was 4.19.4, dated October 1, 2026. The release notes for 4.19.2 (September 28, 2026) say it addresses three further vulnerabilities: two rated High, both account takeover issues (advisories GHSA-x4c7-fpcx-w9q6 and GHSA-jh92-5mrj-p2w2), and one rated Low (GHSA-w4gv-rcwj-w6r5). We did not review those advisories in detail, and they are separate from the three CVEs covered here.</li>
</ul>
<h2 id="what-to-do-in-order">What to do, in order</h2>
<p>This sequence is our recommendation, built on ZITADEL&rsquo;s advisories.</p>
<h3 id="1-find-every-instance-and-its-version">1. Find every instance and its version</h3>
<p>Include development, staging and any instance that serves a single customer. Record whether it runs 3.x or 4.x, and whether Login V1, Login V2 or both are enabled, since that decides which flaws apply.</p>
<h3 id="2-upgrade-to-4173-or-later-preferably-the-newest-4x">2. Upgrade to 4.17.3 or later, preferably the newest 4.x</h3>
<p>Fixing all three requires at least 4.17.3. Given the further fixes in 4.19.2, aim for the newest 4.x release. One upgrade requirement to plan for: the 4.19.2 release notes say Login V2 now requires session cookie signing, that the <code>ZITADEL_SESSION_COOKIE_SECRET</code> environment variable (at least 32 characters) must be set before upgrading, and that users have to sign in again afterward. If you are on 3.x, plan the move to 4.x now: the advisory for CVE-2026-105207 says the 3.x line reached end-of-life on August 31, 2026 and will not be patched, so staying on 3.x leaves that flaw open. Test the upgrade first, because a major-version move can change login behavior.</p>
<h3 id="3-if-you-must-wait-apply-the-partial-workarounds-and-limit-exposure">3. If you must wait, apply the partial workarounds and limit exposure</h3>
<p>ZITADEL lists two partial workarounds. Disabling external IdP account linking reduces the CVE-2026-105207 route through Login V2 but not the API route. Disabling manual account creation on external IdPs reduces CVE-2026-105215 but turns off legitimate self-service registration. For CVE-2026-105209 the advisory says no configuration workaround fully addresses it. Our own suggestion, not ZITADEL&rsquo;s: where an instance is only used by staff, restricting it to a VPN or an allowlist of source addresses reduces the number of people who can reach the vulnerable endpoints, though it does not help against CVE-2026-105209, where the attacker is already an authenticated administrator.</p>
<h3 id="4-look-for-takeovers-that-already-happened">4. Look for takeovers that already happened</h3>
<p>Upgrading removes the flaw, not an attacker who has already taken an account. We did not verify ZITADEL&rsquo;s log or event names, so use your own audit logs and match these questions to what they record:</p>
<ul>
<li><strong>IdP links.</strong> Have any existing accounts gained an external IdP link that the owner did not create, especially since August 2026? A takeover through CVE-2026-105207 leaves a new link on the victim&rsquo;s account.</li>
<li><strong>Passkey and passwordless enrollment.</strong> Were enrollment codes issued by an administrator in organization A for a user in organization B? Did any account register a new authenticator it did not request?</li>
<li><strong>Accounts created through external sign-in that no real person used.</strong> Pre-hijack accounts are created through the registration endpoint, with an external identity attached before the real person arrives.</li>
<li><strong>Sign-ins that follow one of those changes</strong> from an unfamiliar address or device.</li>
</ul>
<p>If you find a suspicious link or authenticator, remove it, revoke the account&rsquo;s active sessions and review what the account could reach. Our guide to <a href="/posts/how-to-detect-and-rotate-leaked-api-keys-before-attackers-use-them/">detecting leaked keys and rotating them</a> covers the follow-up for any secrets those accounts could read.</p>
<h3 id="5-keep-the-logs">5. Keep the logs</h3>
<p>Make sure identity-provider events are retained somewhere an attacker with administrator rights in one tenant cannot erase them. The reasoning is in our piece on <a href="/posts/logging-for-incidents-not-for-dashboards/">logging for incidents rather than dashboards</a>.</p>
<h2 id="what-this-says-about-identity-systems-generally">What this says about identity systems generally</h2>
<p>The three flaws share one pattern, which is our reading, not ZITADEL&rsquo;s: the system trusted a claim about who was acting without checking it where the change happened. In CVE-2026-105207 it was a link request, in CVE-2026-105209 a header naming an organization, and in CVE-2026-105215 identity fields supplied by the browser. When you review any identity platform, including a hosted one, ask where an account&rsquo;s credentials can be added or changed, and what is checked at that exact step.</p>
<p>The timing is also a lesson. These are fixed-before-disclosed flaws, and the organizations hurt are those that treat identity software as set-and-forget. Put your identity provider on the same patch watchlist as your edge devices, and rank it using the approach in our <a href="/posts/the-vulnerability-backlog-that-never-shrinks/">vulnerability backlog</a> piece.</p>
<h2 id="sources-and-verification">Sources and verification</h2>
<p>Checked on October 6, 2026. We did not install ZITADEL, test any flaw or review its source code. ZITADEL&rsquo;s advisories and releases page were read through a page-summarizing tool; the CVE records were read directly from the CVE Program API. The affected-version lists, workarounds and release dates below should be checked against the primary pages before you act on them.</p>
<table>
  <thead>
      <tr>
          <th>Important claim</th>
          <th>Source</th>
          <th>Verification</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>CVE-2026-105207: unauthenticated account takeover via IdP linking; affects 4.0.0 to 4.17.2 and 3.0.0 to 3.4.15; fixed in 4.17.3; 3.x end-of-life; Login V1 unaffected</td>
          <td><a href="https://github.com/zitadel/zitadel/security/advisories/GHSA-g8gj-gq47-xgf4">GHSA-g8gj-gq47-xgf4</a>; <a href="https://cveawg.mitre.org/api/cve/CVE-2026-105207">CVE record</a></td>
          <td>Qualified: CVE record&rsquo;s structured data contains an inconsistent second range</td>
      </tr>
      <tr>
          <td>CVE-2026-105209: cross-organization takeover via passkey enrollment; affects 4.0.0 to 4.17.0 and 3.0.0 to 3.4.14; fixed in 4.17.1 and 3.4.15; no complete workaround</td>
          <td><a href="https://github.com/zitadel/zitadel/security/advisories/GHSA-pq2q-2c6r-75c4">GHSA-pq2q-2c6r-75c4</a>; <a href="https://cveawg.mitre.org/api/cve/CVE-2026-105209">CVE record</a></td>
          <td>Verified</td>
      </tr>
      <tr>
          <td>CVE-2026-105215: pre-hijacking in Login V1; affects 4.0.0 to 4.16.1 and 3.0.0 to 3.4.13; fixed in 4.16.2 and 3.4.14; Login V2 unaffected</td>
          <td><a href="https://github.com/zitadel/zitadel/security/advisories/GHSA-738m-7888-jfv8">GHSA-738m-7888-jfv8</a>; <a href="https://cveawg.mitre.org/api/cve/CVE-2026-105215">CVE record</a></td>
          <td>Verified</td>
      </tr>
      <tr>
          <td>Advisory scores 9.8, 9.6, 9.1; CVE records carry CVSS 3.1 scores of 9.8, 9.6, 9.1 and CVSS 4.0 scores of 9.3</td>
          <td>The three advisories and CVE records above</td>
          <td>Verified</td>
      </tr>
      <tr>
          <td>CVE records published October 4, 2026 by VulnCheck; public dates July 29, August 14 and September 4, 2026</td>
          <td>CVE records above</td>
          <td>Verified</td>
      </tr>
      <tr>
          <td>4.16.2 and 3.4.14 released July 29, 2026; 3.4.15 and 4.17.1 released August 14, 2026; 4.17.3 released September 4, 2026; 4.19.2 (September 28, 2026) addresses three further vulnerabilities and requires a Login V2 session cookie secret; 4.19.4 dated October 1, 2026</td>
          <td><a href="https://github.com/zitadel/zitadel/releases">ZITADEL releases</a>; <a href="https://github.com/zitadel/zitadel/releases/tag/v4.19.2">4.19.2 release notes</a></td>
          <td>Verified: as read October 6, 2026</td>
      </tr>
      <tr>
          <td>Partial workarounds: disable external IdP linking (105207, Login V2 only); disable manual account creation on external IdPs (105215)</td>
          <td>GHSA-g8gj-gq47-xgf4; GHSA-738m-7888-jfv8</td>
          <td>Qualified: vendor advisory; not tested</td>
      </tr>
      <tr>
          <td>Not in CISA KEV (catalog 2026.10.04); CISA enrichment lists exploitation &ldquo;none&rdquo; for CVE-2026-105209 and CVE-2026-105215 as of October 5, 2026</td>
          <td><a href="https://github.com/cisagov/kev-data/blob/main/known_exploited_vulnerabilities.json">CISA KEV data</a>; CVE records</td>
          <td>Qualified: point-in-time; no CISA enrichment seen for CVE-2026-105207</td>
      </tr>
  </tbody>
</table>
]]></content:encoded></item><item><title>Vetting Third-Party SaaS Tools Before They Touch Your Data</title><link>https://firewallsync.com/posts/vetting-third-party-saas-tools-before-they-touch-your-data/</link><guid>https://firewallsync.com/posts/vetting-third-party-saas-tools-before-they-touch-your-data/</guid><pubDate>Sun, 04 Oct 2026 09:00:00 +0000</pubDate><dc:creator>FirewallSync Editorial</dc:creator><category>Privacy &amp; Third-Party Risk</category><description>Most SaaS vetting stops at a security questionnaire the vendor's sales team fills out. Here's what actually predicts third-party risk in practice.</description><enclosure url="https://firewallsync.com/posts/vetting-third-party-saas-tools-before-they-touch-your-data/cover_hu16361058726573884718.jpg" type="image/jpeg" length="0"/><media:content url="https://firewallsync.com/posts/vetting-third-party-saas-tools-before-they-touch-your-data/cover_hu16361058726573884718.jpg" medium="image" type="image/jpeg" width="1200" height="630"/><content:encoded><![CDATA[<p><img src="https://firewallsync.com/posts/vetting-third-party-saas-tools-before-they-touch-your-data/cover_hu16361058726573884718.jpg" alt="Magnifying glass next to a laptop" width="1200" height="630"></p><p>A security questionnaire filled out by a vendor&rsquo;s sales or solutions team, then filed away unread, is the default third-party risk process at most companies — and it predicts almost nothing about actual risk, because the person answering it usually isn&rsquo;t the person who built the system or knows its real failure modes.</p>
<h2 id="scope-data-access-before-you-evaluate-anything-else">Scope data access before you evaluate anything else</h2>
<p>The single most useful vetting question isn&rsquo;t &ldquo;do you have SOC 2&rdquo; — it&rsquo;s &ldquo;what data does this tool actually need, and does the integration request more than that.&rdquo; OAuth scopes and API permissions requested during setup routinely exceed what the tool&rsquo;s stated function requires, because broad defaults are easier for the vendor to build than granular ones. Reviewing the actual requested scopes against the tool&rsquo;s job description, before granting access, catches over-permissioned integrations that a compliance questionnaire never asks about.</p>
<h2 id="compliance-certifications-tell-you-about-process-not-about-your-specific-risk">Compliance certifications tell you about process, not about your specific risk</h2>
<p>SOC 2 and ISO 27001 are useful signals that a vendor has <em>a</em> security process, but they don&rsquo;t tell you whether that process covers the specific way you&rsquo;re using the tool, or whether a breach at the vendor would actually expose your data versus just their infrastructure. Ask vendors directly what a breach on their end would mean for your specific data — the quality and specificity of that answer is a better risk signal than the certification badge itself.</p>
<h2 id="offboarding-is-where-most-third-party-risk-actually-lives">Offboarding is where most third-party risk actually lives</h2>
<p>Vetting focuses entirely on the point of approval, but a large share of real incidents trace back to tools that were approved once, integrated deeply, and never revisited — accumulating access and dormant integrations for accounts nobody uses anymore. Build a recurring review (even annual) of what&rsquo;s still connected and what access it still has, not just a one-time approval gate. An unused integration with live credentials is functionally identical to a live vulnerability, and most companies have several they&rsquo;ve forgotten about.</p>
]]></content:encoded></item><item><title>MFA Fatigue Attacks: What Actually Stops Them</title><link>https://firewallsync.com/posts/mfa-fatigue-attacks-what-actually-stops-them/</link><guid>https://firewallsync.com/posts/mfa-fatigue-attacks-what-actually-stops-them/</guid><pubDate>Fri, 25 Sep 2026 09:00:00 +0000</pubDate><dc:creator>FirewallSync Editorial</dc:creator><category>Identity &amp; Access</category><description>Push-notification bombing keeps working because most MFA rollouts treat every factor as equally trustworthy. Here's what actually closes the gap.</description><enclosure url="https://firewallsync.com/posts/mfa-fatigue-attacks-what-actually-stops-them/cover_hu11399287156490859215.jpg" type="image/jpeg" length="0"/><media:content url="https://firewallsync.com/posts/mfa-fatigue-attacks-what-actually-stops-them/cover_hu11399287156490859215.jpg" medium="image" type="image/jpeg" width="1200" height="630"/><content:encoded><![CDATA[<p><img src="https://firewallsync.com/posts/mfa-fatigue-attacks-what-actually-stops-them/cover_hu11399287156490859215.jpg" alt="Person holding a smartphone showing a fingerprint prompt" width="1200" height="630"></p><p>MFA fatigue attacks — also called push bombing — don&rsquo;t exploit a technical flaw. They exploit the fact that most MFA rollouts treat &ldquo;any second factor&rdquo; as good enough, when the factor&rsquo;s design determines whether an exhausted employee at 11pm taps &ldquo;approve&rdquo; just to make the notifications stop.</p>
<h2 id="number-matching-isnt-optional-anymore">Number matching isn&rsquo;t optional anymore</h2>
<p>Plain push approval (&ldquo;tap yes or no&rdquo;) is the weakest form of MFA still in wide use, precisely because it asks the user to make zero cognitive effort. Number matching — where the user has to type a code shown on the login screen into the push prompt — adds just enough friction to break the autopilot response that makes bombing effective. If your identity provider supports it, this is a config change, not a project, and it should be the default for every account, not an opt-in.</p>
<h2 id="phishing-resistant-factors-change-the-incentive-not-just-the-defense">Phishing-resistant factors change the incentive, not just the defense</h2>
<p>Number matching reduces bombing success rates, but it doesn&rsquo;t touch attacks that pair the push spam with a live phishing page relaying real-time codes. FIDO2 security keys and passkeys remove this entire class of attack because the cryptographic challenge is bound to the origin domain — there&rsquo;s no code to relay. Rolling these out for admin and finance roles first (the accounts actually worth targeting) gets you most of the risk reduction without a company-wide hardware rollout.</p>
<h2 id="rate-limit-and-alert-on-push-volume-not-just-failed-logins">Rate-limit and alert on push volume, not just failed logins</h2>
<p>Most detection stacks are tuned to flag failed authentication attempts, but a bombing attack looks like a string of <em>successful</em> prompt deliveries with no failures at all — the attacker already has valid credentials, they&rsquo;re just waiting for a mis-tap. Add a specific alert for unusual push notification volume per user per hour, independent of your failed-login alerting, and rate-limit repeated prompts to the same device to cut off the exhaustion tactic entirely.</p>
]]></content:encoded></item><item><title>Least-Privilege Access Controls That Don't Slow Teams Down</title><link>https://firewallsync.com/posts/least-privilege-access-controls-that-dont-slow-teams-down/</link><guid>https://firewallsync.com/posts/least-privilege-access-controls-that-dont-slow-teams-down/</guid><pubDate>Sat, 01 Aug 2026 00:00:00 +0000</pubDate><dc:creator>FirewallSync Editorial</dc:creator><category>Identity &amp; Access</category><description>Most least-privilege rollouts fail because they optimize for audit checklists instead of how engineers actually work. Here's a rollout order that holds up.</description><enclosure url="https://firewallsync.com/posts/least-privilege-access-controls-that-dont-slow-teams-down/cover_hu11790370285784053108.jpg" type="image/jpeg" length="0"/><media:content url="https://firewallsync.com/posts/least-privilege-access-controls-that-dont-slow-teams-down/cover_hu11790370285784053108.jpg" medium="image" type="image/jpeg" width="1200" height="630"/><content:encoded><![CDATA[<p><img src="https://firewallsync.com/posts/least-privilege-access-controls-that-dont-slow-teams-down/cover_hu11790370285784053108.jpg" alt="Keys hanging from a door lock" width="1200" height="630"></p><p>Most least-privilege initiatives stall for the same reason: security teams design the policy around what an auditor wants to see, not around how engineers request and use access day to day. The result is a system everyone routes around within a month.</p>
<h2 id="start-with-role-mining-not-role-design">Start with role mining, not role design</h2>
<p>Before writing a single policy, pull 90 days of actual access logs and group people by what they <em>use</em>, not what their job title implies they should use. You&rsquo;ll almost always find that job-title-based roles overprovision by 3-5x. Role mining gives you a baseline that reflects reality, which means the policy you eventually ship won&rsquo;t immediately break someone&rsquo;s workflow.</p>
<h2 id="time-bound-elevated-access-by-default">Time-bound elevated access by default</h2>
<p>Standing admin access is the single biggest reason least-privilege programs get undone six months later — someone gets temporary access for an incident, and it never gets revoked because revocation isn&rsquo;t anyone&rsquo;s job. Build expiry into the request, not into a quarterly review. A 24-hour default that requires re-justification is far more durable than a 90-day access review that nobody actually reads.</p>
<h2 id="measure-friction-not-just-coverage">Measure friction, not just coverage</h2>
<p>Track how many access requests get approved without modification versus how many get pushback or take more than a day. If your approval time creeps up, engineers will start requesting broader roles &ldquo;just in case&rdquo; to avoid asking twice — which quietly reverses the entire program. Friction is a leading indicator that your policy has drifted from actual need.</p>
<h2 id="the-rollout-order-that-works">The rollout order that works</h2>
<ol>
<li>Mine current usage, not job descriptions</li>
<li>Ship time-bound elevation before you touch standing roles</li>
<li>Narrow standing roles only after elevation data shows what&rsquo;s actually needed</li>
<li>Review friction metrics monthly, formal access reviews quarterly</li>
</ol>
<p>Teams that reverse this order — writing the policy first and measuring friction never — are the ones still running the same over-permissioned setup a year later, just with a nicer-looking spreadsheet.</p>
]]></content:encoded></item></channel></rss>