β Human-authored analysis; AI used for formatting and proofreading.
Most cloud security tools work by checking individual resources against a library of known-bad patterns. Is this bucket public? Is this port open? Is this key unrotated?
These checks are necessary. They're also insufficient. Because the most dangerous violations don't exist in any single resource. They exist in the RELATIONSHIPS between resources. They're detectable from the configuration structure alone, without any user input, without any tags, without any intent declarations.
Cloud security violations divide into two structurally distinct classes:
Intent-DEPENDENT (requires user declaration):
"No public bucket with sensitive data"
β Needs the user to declare: this bucket IS sensitive
β Without the declaration, no tool can distinguish a public
website bucket from a public PII bucket
Intent-INDEPENDENT (detectable from structure alone):
"No attack path from any public principal to any internal resource"
β The PATH is the violation. No declaration needed.
β If anonymous can reach an internal resource through a chain
of role assumptions, it's exploitable β regardless of what
the resource contains.
The industry has focused almost entirely on the first class. Tools that check properties of individual resources, optionally enriched by metadata. The second class is larger, more dangerous, and requires zero user setup to detect. The configuration graph is sufficient evidence.
Policy A allows User X to update an IAM role. Policy B allows that role to pass a role to a Lambda function. Policy C allows that Lambda to delete objects in an S3 bucket.
No individual policy is wrong. Each permission makes sense in isolation. The chain creates a path from a non-admin user to a data-deletion action that nobody intended.
Detection: compute transitive closure over the permission graph. If any path exists from a low-privilege principal to a high-privilege action that isn't explicitly authorized, the path is a violation. Datalog engines are designed for this computation β six lines of relational rules, and the engine finds ALL paths, including compositions nobody cataloged.
A developer writes a complex policy with conditions, exclusions (NotAction
, NotResource
), and carefully scoped allows. The policy reads like it's restrictive. But the logic of the conditions, evaluated together, mathematically simplifies to Resource: *
or Principal: *
.
No human typed the wildcard. The logic is equivalent to it.
Detection: an SMT solver (like Z3) checks whether the set of conditions covers the entire domain of possibilities. If the allow conditions form a tautology β true for all possible inputs β the resource is effectively public, regardless of how the policy reads. The solver doesn't search for the *
character. It checks whether the MATH produces the same result.
A principal can modify its own permissions via iam:PassRole
, iam:PutRolePolicy
, or iam:AttachRolePolicy
. Self-modification is structurally dangerous regardless of the principal's intended scope. If you can change what you're allowed to do, the boundary is meaningless.
Circular trust is Role A trusts Role B, Role B trusts A. It is exploitable regardless of what either role does. The graph topology is the evidence.
Detection: search the permission graph for cycles and self-referential paths. Any principal with a path from "current permissions" to "modify own permissions" has a privilege escalation vulnerability. The topology tells you everything. No tags needed.
A developer writes a Deny statement believing it protects a resource. The Deny says: "deny all access from external accounts."
But the Allow statement in the same policy (or a resource policy on the target) is structured so that the Deny condition can never be met. The conditions don't overlap. The Deny is mathematically unreachable β a dead branch in the logic tree. The developer thinks security is enforced. The SMT solver proves it isn't.
Detection: Z3 checks for unsatisfiability. If the Deny condition can never be satisfied given the structure of the Allow statements, the Deny is shadowed. The guardrail exists in the JSON. It doesn't exist in the math.
A security group references a VPC that was deleted. A role trust policy grants access to an account that no longer exists. A bucket policy references a principal ARN that was decommissioned.
The reference is dangling. The cloud provider's behavior when evaluating a dangling reference is undefined or provider-specific. The resource APPEARS configured, but one of its dependencies was removed. The configuration is in an inconsistent state.
Detection: cross-reference every resource identifier in the snapshot. If a reference target doesn't exist in the snapshot, the reference is a ghost. No intent needed. A dangling reference is wrong by definition.
AWS's own internal tool Tiros (built on Z3) converts route tables, security groups, and network ACLs into a graph of logic formulas. The solver asks: "does there exist any packet, any combination of source IP and port that can reach this private subnet from the internet gateway?"
If the solver returns SAT (satisfiable), it provides the exact packet headers and the route path that reaches the private subnet. A private subnet reachable from 0.0.0.0/0 is wrong by definition. No intent declaration required.
Detection: model the network topology as logical constraints. Let the solver search for any satisfying assignment. If one exists, an unintentional network path exists.
A scanner searches for patterns in text. The *
character, the 0.0.0.0/0
CIDR, the "Effect": "Allow"
string. This catches the obvious cases. It misses the mathematical ones.
An SMT solver understands things pattern matching can't:
Condition logic. StringLike
and StringEquals
can overlap in ways that create unintended access. A scanner sees two separate conditions. The solver evaluates their intersection.
IP math. 10.0.0.0/24 is contained within 10.0.0.0/16. A "restrictive" security group that allows a /24 in a VPC that already allows the /16 isn't restrictive. It's redundant. The solver computes CIDR containment. A scanner compares strings.
Variable substitution. If a user with a specific name matches a condition pattern the author didn't anticipate, they bypass a restriction. The solver checks ALL possible variable assignments. A scanner checks the ones it's been told about.
Policy composition. Identity policy + resource policy + SCP + permission boundary, evaluated in deny-override order. The effective permission is a function of all four layers. No individual policy statement reveals it. The solver evaluates the composition. A scanner evaluates each policy in isolation.
Pattern matching asks "does this text look bad?" Formal verification asks "does there exist ANY input that reaches an unsafe state?"
The first catches what's been cataloged. The second catches what's mathematically possible including compositions nobody anticipated.
This creates a product adoption path with zero user setup:
Day 0 (zero configuration):
Point the tool at a configuration snapshot.
No tags. No intent declarations. No metadata. No setup.
Structural findings delivered immediately:
β Attack paths (transitive reachability through IAM graph)
β Ghost references (dangling resource identifiers)
β Privilege escalation paths (self-modification chains)
β Policy shadowing (dead deny statements)
β Implicit over-permissiveness (equivalent-to-star)
β Network reachability (unintended internet paths)
Day 30 (intent declared):
User starts tagging resources: Sensitivity: High, Framework: HIPAA.
Intent-dependent findings activate:
β Public bucket + PII tag = violation
β Unencrypted database + HIPAA tag = violation
β Broad access + sensitive-service archetype = violation
Richer analysis. Deeper findings.
But the structural findings were already there on day 0.
The user doesn't need to understand intent declarations, tagging strategies, or compliance frameworks to start getting results. The structural detections require nothing except the snapshot. The intent layer deepens the analysis when the user is ready. Both classes are valuable. Both are correct. The structural class needs zero setup.
There's a boundary this approach must acknowledge. The structural analysis is only as complete as the snapshot and in real organizations, the snapshot is never the full picture.
In large enterprises, cloud configuration arrives through many channels:
The cloud APIs reflect the CURRENT state of what's been configured through them. They don't reflect:
The analysis covers what's observable through the cloud provider's APIs at the moment the snapshot was taken. In a well-managed organization with consistent IaC practices, that's close to complete. In a large enterprise with decades of accumulated infrastructure, multiple teams, console-driven workflows, and partial IaC adoption. It's less than 100%.
This means the structural findings are LOWER BOUNDS, not exhaustive inventories. "We found 3 attack paths" means at least 3 exist. It doesn't mean only 3 exist. Unobserved resources may harbor additional paths the snapshot can't see.
The tool should state this in every output:
Analysis scope: 847 resources across 12 services observed in snapshot.
Coverage: This analysis reflects API-observable state at capture time.
Resources not visible to the collector are not evaluated.
Findings are lower bounds, not exhaustive inventories.
Transparency about coverage limits is as important as the findings themselves. A tool that says "you're safe" when it only saw 60% of the environment is more dangerous than a tool that says "we found 3 problems in the 60% we could observe." The second is honest. The first creates false confidence.
This is also why the stave readiness
and stave gaps
commands exist. They report what the observation contract covers and what it doesn't. The coverage gap is a measured, tracked metric, not a hidden assumption.
The adversarial review of the specification-first platform article asked: "What if users don't tag resources correctly? What about classification circularity? What about tagging debt?"
Every question assumes the platform requires user input to produce value. For the intent-dependent class, that's true. For the structural class, it's false. The six violation classes described here need nothing from the user. The configuration graph is the evidence. The solver asks what's mathematically possible. The user provides a snapshot. The math provides the findings.
This means the strongest findings in a security assessment β attack paths, privilege escalation chains, ghost references, policy shadowing are the ones that require the LEAST user effort. The most dangerous violations are the most automatically detectable. The gap between "install the tool" and "see your first critical finding" is one command.
This article is part of a series on the structural foundations of cloud security. Related: "Your Scanner Checks Settings. The Breach Was a Combination." (the unit-of-analysis shift), "The Next Platform Won't Track Configurations." (the specification-first architecture), and The Root Cause Nobody Names: AWS Configuration IS First-Order Logic" (why engineers misconfigure cloud resources).
The structural violation classes described here are implemented in Stave β an open-source cloud security reasoning engine. Apache 2.0.