Assert every permitted name reaches the set its class names #356

Merged
branderson merged 3 commits from verify-entries into development 2026-09-15 19:47:09 -07:00
Collaborator

Summary generated by LLM agent

Closes the gap that four review rounds kept falling into, by asserting the property in a running container instead of inferring it from reading.

What was only checked for one destination

Check 4 asserted that a permitted name resolves and its address reaches the address set its class names. It did that for exactly one host: the one in the GitHub metadata URL, which is the destination hardest to get wrong. The operator's own forge, the Loki host, a self-served backend and every approved domain were never subjected to it.

That is where the defects have been. The class an entry was recorded with, the resolver line built from it, and the set the ports are keyed to are three separate statements, and each can be right about a different set. Reading the code cannot tell them apart, which is why every defect of this shape reached a green suite:

  • a forge name recorded in the open class, so its answers fed the port-unrestricted set
  • a forge name skipped out of every class, so internal_domains could not find it
  • a name that could not resolve at all, because the resolver will not forward a dotless query

All three present at boot in the same way: a name that answers, and an address that is not where the ports expect it. One check catches all three, in the real container, before the session starts.

The change

The sweep that already resolves every permitted name now reads each answer back out of its class's set.

It is fatal, unlike a name that returns no address. That distinction is deliberate and kept: an unresolvable name is the world being unavailable, and refusing to start a session over a host that happens to be down would cost more than it prevents. A name that resolves to an address the policy did not permit in the class it was recorded under is the policy disagreeing with itself, and the session it produces reaches a configured destination only by accident, or not at all.

Which set a class feeds now has one definition. The config that fills a set and the check that reads it back asking separately is the same shape as the bug: they could name different sets and the check would pass while the class meant nothing.

One name, one class

Writing the check found that this repository's own allowlist would fail it.

A name can reach both class lists: release-assets.githubusercontent.com is an open-class entry of the component's own and also sits under a forge suffix, so the approved-domains loop adds it to the forge class as well. Any harness or backend domain under a forge suffix would do the same. That produced two entries for one name, two resolver lines naming different sets, and one of them honoured - so nothing could say which class was in force, and the new check would look in the set that lost and stop the launch.

Entries are now deduplicated by name, keeping the first, and forge is recorded first. A name that is forge class by any route keeps the port restriction rather than inheriting the open class from a second mention. That also makes fixed-domains.sh's claim that the forge class wins wherever the two overlap true of the entries file, which it was not.

This is the item recorded as the third finding on #350.

Testing

Full confined gate, all ten suites, green.

The new assertions were each run against their own removal: dropping the membership check lets a stranded name pass, giving the two places their own set name defeats the single-definition test, and removing the deduplication produces two entries for the one name.

The stranded case is covered in both shapes - an address in no set at all, and a forge-class name whose address is in the open class's set, which is the one that actually happened and which a check asking only "is it permitted at all" would miss.

The harness for these stubs nft as well as dig. A sandbox's own nft answers for a table that is not there, so without it every name would look stranded and the failure would be about the test environment rather than the code.

runtime.bats was not run locally. It is where this earns its keep: every container that suite boots now asserts the property for every permitted entry, so a future class or wiring mistake fails the privileged job instead of shipping.

Refs #350

**Summary generated by LLM agent** Closes the gap that four review rounds kept falling into, by asserting the property in a running container instead of inferring it from reading. ## What was only checked for one destination Check 4 asserted that a permitted name resolves *and* its address reaches the address set its class names. It did that for exactly one host: the one in the GitHub metadata URL, which is the destination hardest to get wrong. The operator's own forge, the Loki host, a self-served backend and every approved domain were never subjected to it. That is where the defects have been. The class an entry was recorded with, the resolver line built from it, and the set the ports are keyed to are three separate statements, and each can be right about a different set. Reading the code cannot tell them apart, which is why every defect of this shape reached a green suite: - a forge name recorded in the open class, so its answers fed the port-unrestricted set - a forge name skipped out of every class, so `internal_domains` could not find it - a name that could not resolve at all, because the resolver will not forward a dotless query All three present at boot in the same way: a name that answers, and an address that is not where the ports expect it. One check catches all three, in the real container, before the session starts. ## The change The sweep that already resolves every permitted name now reads each answer back out of its class's set. It is fatal, unlike a name that returns no address. That distinction is deliberate and kept: an unresolvable name is the world being unavailable, and refusing to start a session over a host that happens to be down would cost more than it prevents. A name that resolves to an address the policy did not permit in the class it was recorded under is the policy disagreeing with itself, and the session it produces reaches a configured destination only by accident, or not at all. Which set a class feeds now has **one** definition. The config that fills a set and the check that reads it back asking separately is the same shape as the bug: they could name different sets and the check would pass while the class meant nothing. ## One name, one class Writing the check found that this repository's own allowlist would fail it. A name can reach both class lists: `release-assets.githubusercontent.com` is an open-class entry of the component's own and also sits under a forge suffix, so the approved-domains loop adds it to the forge class as well. Any harness or backend domain under a forge suffix would do the same. That produced two entries for one name, two resolver lines naming different sets, and one of them honoured - so nothing could say which class was in force, and the new check would look in the set that lost and stop the launch. Entries are now deduplicated by name, keeping the first, and forge is recorded first. A name that is forge class by any route keeps the port restriction rather than inheriting the open class from a second mention. That also makes `fixed-domains.sh`'s claim that the forge class wins wherever the two overlap true of the entries file, which it was not. This is the item recorded as the third finding on #350. ## Testing Full confined gate, all ten suites, green. The new assertions were each run against their own removal: dropping the membership check lets a stranded name pass, giving the two places their own set name defeats the single-definition test, and removing the deduplication produces two entries for the one name. The stranded case is covered in both shapes - an address in no set at all, and a forge-class name whose address is in the open class's set, which is the one that actually happened and which a check asking only "is it permitted at all" would miss. The harness for these stubs `nft` as well as `dig`. A sandbox's own `nft` answers for a table that is not there, so without it every name would look stranded and the failure would be about the test environment rather than the code. `runtime.bats` was not run locally. It is where this earns its keep: every container that suite boots now asserts the property for every permitted entry, so a future class or wiring mistake fails the privileged job instead of shipping. Refs #350
dejima-agent added 2 commits 2026-09-14 23:14:32 -07:00
Check 4 asserted this for one destination: the host of the GitHub metadata
URL, which is the one hardest to get wrong. The operator's own forge, Loki, a
self-served backend and every approved domain were never subjected to it.

That is the gap four review rounds kept falling into. The class an entry was
recorded with, the resolver line built from it, and the set the ports are
keyed to are three separate statements, and each can be right about a
different set. Reading the code cannot tell them apart, and every defect of
that shape here reached a green suite: a forge name recorded in the open
class, a name skipped out of every class, a name that could not resolve at
all. All three present at boot as a name that answers and an address that is
not where the ports expect it.

The sweep that already resolves every permitted name now reads each answer
back out of its class's set. Fatal, unlike a name that did not answer: that
one is the world being unavailable and deliberately costs nothing, while this
one is the policy disagreeing with itself.

Which set a class feeds now has one definition. The config that fills a set
and the check that reads it back asking separately is the same shape as the
bug, and would have let the check pass while the class meant nothing.

The harness for these tests stubs nft as well as dig, because a sandbox's own
nft answers for a table that is not there - every name would look stranded
and the failure would be about the test environment.
Record a name once, in the narrower class, so its entry can be checked
tests / confined (conversation) (pull_request_target) Successful in 1m5s
tests / confined (proxy) (pull_request_target) Successful in 1m12s
tests / privileged (pull_request_target) Failing after 1m25s
tests / runner-env (privileged) (pull_request_target) Successful in 45s
tests / confined (providers) (pull_request_target) Successful in 1m59s
tests / confined (launch) (pull_request_target) Successful in 2m17s
tests / confined (share) (pull_request_target) Successful in 1m58s
tests / runner-env (confined) (pull_request_target) Successful in 51s
tests / confined (remote) (pull_request_target) Successful in 2m53s
tests / confined (shellcheck) (pull_request_target) Successful in 1m19s
tests / confined (project) (pull_request_target) Successful in 3m19s
tests / confined (audit) (pull_request_target) Successful in 3m39s
tests / confined (static) (pull_request_target) Successful in 3m4s
tests / confined (config) (pull_request_target) Successful in 5m3s
9764ddd4fc
A name can reach both class lists. release-assets.githubusercontent.com is an
open-class entry of this component's own and also sits under a forge suffix,
and any harness or backend domain under one would do the same - so this
repository's own allowlist produces it.

Two lines meant two resolver lines naming different sets for one name, of
which the resolver honours one. The entries file and `status` could not say
which class was in force, and the check added alongside this - which reads an
answer back out of its class's set - would look in the set that lost and stop
the launch. That is how this was found: the check refused the project's own
configuration.

Forge is recorded first and the deduplication keeps the first occurrence, so
a name that is forge class by any route keeps the port restriction rather
than inheriting the open class from a second mention. That also makes
fixed-domains.sh's claim that the forge class wins wherever the two overlap
true of the entries, which it was not.
dejima-agent added 1 commit 2026-09-15 00:08:09 -07:00
Handle the empty answer the policy's own set -e turns into an abort
tests / confined (conversation) (pull_request_target) Successful in 1m0s
tests / confined (launch) (pull_request_target) Successful in 2m13s
tests / confined (share) (pull_request_target) Successful in 1m59s
tests / runner-env (confined) (pull_request_target) Successful in 52s
tests / confined (remote) (pull_request_target) Successful in 2m50s
tests / confined (shellcheck) (pull_request_target) Successful in 1m19s
tests / confined (project) (pull_request_target) Successful in 3m15s
tests / privileged (pull_request_target) Successful in 6m21s
tests / confined (proxy) (pull_request_target) Successful in 1m8s
tests / confined (providers) (pull_request_target) Successful in 1m56s
tests / confined (audit) (pull_request_target) Successful in 3m37s
tests / confined (static) (pull_request_target) Successful in 3m3s
tests / confined (config) (pull_request_target) Successful in 4m51s
tests / runner-env (privileged) (pull_request_target) Successful in 47s
c805208fac
resolve_one returns non-zero for a name that answered with no address, which
is an ordinary outcome. The sweep assigned from it bare, and under
`set -Eeuo pipefail` that aborts - so a permitted name whose host is
unreachable took the container down, which is the opposite of what the note
two lines below it exists to do. The previous form tested the substitution
inside `[ -n ... ]`, where a failure is part of a condition and exempt.

The harness could not see it. bats runs a test with errexit off, and `set -e`
inside a subshell of that shell does not restore it - measured rather than
assumed, because the first attempt at this fix was exactly that subshell and
it reported the regression as passing. The extracted functions now run in a
real child bash with the options egress-policy sets, which reproduces the
failure and then the fix.

It also wanted a table name the harness had never supplied, which the
stricter shell surfaced immediately as an unbound variable. Read out of the
script, like the exemption hint beside it.

Covered by the case that reaches it: a lookup that succeeds and carries no A
record. The existing empty-answer test stubs a dig that fails, and resolve_one
returns zero on that path, so it never reached the abort.
branderson approved these changes 2026-09-15 19:46:55 -07:00
branderson merged commit 09d1881633 into development 2026-09-15 19:47:09 -07:00
Sign in to join this conversation.