DEV Community

Bala Paranj
Bala Paranj

Posted on

The Bucket Is Public. Was It Supposed to Be?

You've done this triage many times.

The scanner flags a public S3 bucket. You look at the name: acme-marketing-assets. You know that bucket serves the company website's images. It's supposed to be public. You mark the finding accepted, add a note — "static website assets, intentionally public" — and move on.

You just supplied a judgment the tool couldn't make. You looked at the finding, reached into your head for context the tool doesn't have, and decided this particular public bucket is fine. The tool detected a state. You decided whether that state was a problem.

That small, familiar act is the gap. Every triage like it is a human manually bridging the distance between what a tool can see (the bucket is public) and what it can't (whether it was supposed to be). The question isn't whether the tool found the right state. It did. The question is why, in 2026, the tool still can't answer "was this intended?" without asking a human.

How the field decides today

Three approaches exist. They're not equally naive, and being precise about their differences matters.

Scanners — Prowler, ScoutSuite, AWS Config Rules, CIS Benchmarks — evaluate a predicate over a resource's own attributes and emit a finding. The bucket is public: that's the finding. The scanner detected the state and delegated the decision to you. It has no mechanism to distinguish "public because it serves website assets" from "public because someone misconfigured it." Both are the same predicate result. Both produce the same alert. The human sorts them.

Graph-based CNAPPs — Wiz, Orca, and similar platforms — deserve honest credit. They don't stop at "the bucket is public." They ask what the bucket is connected to: does it hold sensitive data? Is it reachable from the internet through a specific network path? Is the IAM principal that accesses it overprivileged? They build a graph of relationships and prioritize based on blast radius and data sensitivity. This is genuinely more useful than a flat predicate check. A public bucket holding marketing PNGs gets deprioritized. A public bucket holding database backups gets escalated.

But notice how they determine materiality: by reaching into the data plane to infer it. They scan the bucket's contents, classify the data, and decide whether the public exposure matters based on what they find inside. This is probabilistic inference — the classifier might miscategorize, the scan might miss a file, and the approach requires read access to every resource it evaluates. It infers intent from evidence rather than reading a declaration of it.

The move neither approach makes: enforce a declared prior intent as a contract. The owner states what the bucket is for — before the scanner runs — and the system checks whether the actual state matches the declared intent. Not "is this bucket public?" but "is this bucket public and was it declared as public?" Not inference. Not delegation. A contract between the owner and the system, checked deterministically.

Three modes are:

Detect and delegate:     Scanner finds state → human decides if it's a problem
Infer from data:         CNAPP scans contents → classifier decides materiality
Enforce declared intent: Owner declares intent → system checks state against declaration
Enter fullscreen mode Exit fullscreen mode

The first two are well-established. The third is the gap.

The dog that didn't bark

There's a Sherlock Holmes story where the key evidence is a dog that didn't bark — the absence of an expected signal, meaningful only to someone who knew the dog should have barked.

Cloud security has the same structure. The meaningful signal isn't always a misconfiguration that's present. Sometimes it's an intent that's absent.

Consider three buckets:

An untagged bucket with public access. Is it a website asset? A misconfigured data store? A test bucket someone forgot to delete? The scanner can't distinguish these. The CNAPP can scan the contents and infer — but if the bucket is empty or contains ambiguous data, the inference fails. The absence of a declared intent makes the question unanswerable from present state alone.

A bucket tagged data-classification: internal with public access. This is a contradiction — a declared intent that conflicts with the actual state. Both the scanner and the CNAPP can see the public access. But the contradiction is only visible if someone reads the tag as a commitment and checks it against the state. A tag that nobody enforces is a comment, not a contract.

A bucket tagged data-classification: public with public access. This passes. Not because public access is inherently safe, but because the owner declared their intent and the actual state matches it. The human triage step — "I looked at the name and decided it was fine" — has been replaced by a machine-checkable declaration.

The untagged bucket is the dog that didn't bark. Its silence is meaningful, but only to a system that expects the declaration. A scanner that checks resource attributes will never notice a missing tag, because it's checking what's present, not what's absent. An inference system might flag the bucket based on its contents, but it can't flag the governance gap — the fact that nobody declared the purpose of this bucket.

Intent as data

A tag is a key-value pair attached to a cloud resource. Tags already exist in every cloud provider. Most teams use them for cost allocation and environment labeling. The move is to treat a specific tag — say, data-classification — as a state variable that an invariant can check deterministically.

The invariant: if a resource is publicly accessible and its data-classification tag is not public, that's a violation. Not of a best practice. Of a contract the resource owner wrote themselves.

The contract framing matters. The owner declares intent by setting the tag. The system enforces the declaration by checking it against the actual state. The human judgment that used to happen during triage — "I know this bucket is fine because it serves website assets" — now happens during provisioning, as a declaration, before the scanner ever runs. The judgment is the same. The timing and the enforcement are different.

Lamport's work on state vectors and temporal logic gives this a formal foundation: a system's correctness is defined as the set of states it should never reach. A public bucket tagged pii is one of those states. The tag makes the "should never" machine-checkable rather than human-interpretable.

Three buckets

The system sees:

Bucket                Tag                    Public?   Verdict
─────────────         ──────────────         ───────   ───────
marketing-assets      data-class: public     yes       PASS — intent matches state
customer-exports      data-class: pii        yes       VIOLATION — contract broken
analytics-staging     (no tag)               yes       GOVERNANCE GAP — no intent declared
Enter fullscreen mode Exit fullscreen mode

The first bucket passes silently. No alert. No triage. No human in the loop. The owner said "public," the state is public, the contract holds.

The second bucket is a violation of a contract the owner wrote themselves. The finding isn't "this bucket is public" — every scanner produces that. The finding is "this bucket is public and the owner declared it shouldn't be." The remediation is specific: either make the bucket private (fix the state) or reclassify it (fix the declaration). Either way, the owner decides — but they decide by updating a versioned, auditable declaration, not by clicking "accept risk" in a dashboard.

The third bucket is the governance gap. It's not a misconfiguration — the bucket might be perfectly fine. It's a missing declaration. The system can't evaluate intent because no intent was stated. The finding is: "this resource has no declared classification, and it's publicly accessible." The remediation isn't to fix the bucket. It's to classify it.

What the system does and doesn't do

The system is a deterministic evaluator. It checks whether the declared intent matches the observed state. That's the scope. Two properties follow directly, and neither is a limitation — they're the definition of the tool.

The system evaluates declarations, not truth. A bucket tagged data-classification: public that actually contains PII will pass. The declaration says public, the state is public, the contract holds. If the declaration is wrong, the evaluation reflects that. This is how every deterministic system works. A compiler doesn't verify that your logic is correct — it verifies that your syntax is valid. A type checker doesn't verify that your function does the right thing — it verifies that the types align. Stave doesn't verify that your classification is accurate — it verifies that your classification and your resource state don't contradict each other. Garbage in, garbage out. The system is not a fortune teller.

Declarations are inputs, and inputs need protection. Tags are data. Data can be changed. If an attacker compromises an IAM principal and relabels a pii bucket as public, the evaluation will pass — because the declaration now matches the state. The response isn't to make the evaluator smarter. It's to protect the input. Tag-protection policies exist in AWS (SCPs restricting TagResource/UntagResource, tag policies in Organizations). The invariant checks the tag. The tag policy controls who can write the tag. Protecting the input is the owner's responsibility, the same way protecting your source code is your responsibility before the compiler runs.

From Declaration to Ground Truth

Declared-intent enforcement is deterministic, air-gapped, credential-free, and auditable. It runs against a snapshot. It doesn't need to read bucket contents. It doesn't need network access. It produces the same result every time on the same input.

Content inference is probabilistic, needs data-plane access, and can validate declarations. A CNAPP that scans bucket contents can confirm whether a public tag accurately reflects the data inside — something a declaration evaluator doesn't do, because that's not its job. Content scanning requires credentials, network access, and a classifier with its own error rates. But it answers a different question: "is the declaration accurate?" rather than "does the declaration match the state?"

These are different bets.

Declared intent wins when: you need determinism, you're operating air-gapped or pre-deployment, you want an auditable trail of who declared what and when, or you're checking thousands of resources at CI speed without data-plane access.

Content inference wins when: you need to validate that declarations are accurate, you're doing initial classification before declarations exist, or you're auditing an environment where tagging discipline is immature.

They compose. Declared intent as the cheap, deterministic front line — runs in CI, catches contract violations at the speed of code generation. Content scanning as the validation layer — runs periodically, confirms that declarations match reality. The front line handles 95% of the volume. The validation layer audits the declarations themselves.

They answer different questions about the same resources. One asks "does the state match the intent?" The other asks "is the intent accurate?" Neither replaces the other. Together, they cover the full chain from declaration to ground truth.

Intent debt

The cost of leaving intent undeclared isn't that one finding gets missed. It's that every triage re-derives the same judgment by hand, forever.

The engineer who triaged the marketing-assets bucket today will triage it again next quarter when the scanner runs again. A different engineer will triage it next year and reach the same conclusion — or a different one, because they don't have the context. The judgment that "this bucket is fine" lives in a dashboard annotation, a Jira comment, a Slack thread, or nowhere at all. It's not versioned. It's not enforced. It's not auditable. It decays.

Meanwhile, the absences stay structurally invisible. The untagged bucket never appears in any report because no system is checking for what's missing. The governance gap compounds silently — more resources, more missing declarations, more triage cycles spent re-deriving judgments that could have been declared once and enforced permanently.

Intent belongs in the system as a first-class, version-controlled, enforced input — not as folklore in an engineer's head or a comment in a ticketing system. The bucket is public. The question isn't whether your tool can see that. Every tool can see that. The question is whether your system knows it was supposed to be.


The Stave project explores declared-intent enforcement as one layer in a cloud security architecture. It's open source at github.com/sufield/stave.

Top comments (0)