Skip to content

fix(api): the auth-off startup arm now reports instance_exposed (BACKLOG #1013, ASVS 6.3.3) - #256

Closed
wshallwshall wants to merge 3 commits into
mainfrom
fix-1013-authoff-startup-arm
Closed

fix(api): the auth-off startup arm now reports instance_exposed (BACKLOG #1013, ASVS 6.3.3)#256
wshallwshall wants to merge 3 commits into
mainfrom
fix-1013-authoff-startup-arm

Conversation

@wshallwshall

Copy link
Copy Markdown
Collaborator

Fixes BACKLOG #1013. Plus 5 doc-accuracy files.

The auth-off startup arm did not surface instance_exposed, so an instance running with
authentication disabled did not announce that fact at the point an operator would see it. ASVS 6.3.3.

⚠️ ADJACENCY (no conflict, shared file): docs/DEPLOYMENT.md is also touched by #329. Verified
merge-tree clean against it -- they touch different regions.

Claimed FROM the lane worktree; claim gate passed honestly. Verified: merge-tree clean against
current main, merged-tree heading count 142 matching main.

…ACKLOG #1013)

The `[auth] enabled=false` startup arm keyed on the bind alone
(`not settings.api.is_loopback`), so it did not fire for a loopback bind
behind a declared upstream TLS terminator. A PHI instance with
authentication entirely off behind a declared terminator would have
started with no refusal and no warning on first deployment, while the same
topology with auth on but MFA off is already refused by the gate #326
fixed. The two arms disagreed about what "exposed" means in the same file.

Hoist the single `instance_exposed` definition (#326: an off-loopback bind
OR a declared upstream TLS terminator) above the auth-off arm and widen the
arm to consult it, so it refuses on a non-loopback bind OR a declared
terminator. The existing loopback refusal is kept; the condition is
widened, not replaced.

Load order verified: `instance_exposed`'s inputs -- `settings.api.host`
(through `is_loopback`) and `settings.api.tls_terminated_upstream` -- are
read straight off the loaded config, and the only in-place mutation of
`settings.api.*` between the hoisted definition and the former site is
`serve_ui` (twice), which the predicate does not read. The definition
remains defined exactly once.

Tests (tests/test_cli.py): auth off + declared terminator on a loopback
bind refuses (positive); auth off + true loopback with no terminator still
starts (negative control); auth on + declared terminator is unaffected by
the arm. Each was falsified -- reverting the arm to the bare bind check
reds the positive test, firing on any auth-off reds the negative control,
and dropping the auth check reds the auth-on test; each was restored.

Docs updated so the contract travels with the code (CLAUDE.md 11):
DEPLOYMENT.md, SYSTEM-REQUIREMENTS.md, SECURITY.md, REMOTE-CONSOLE.md and
SECURITY-LOOSENING.md now describe the auth-off refusal as firing on an
exposed instance (a non-loopback bind OR a declared terminator), not on the
bind alone.

BACKLOG #1013
…ion (BACKLOG #1013)

Flip the #1013 banner from filed/open to fixed and record, in the AMENDED
blockquote, that the load-order prerequisite the item flagged as unproven
holds: `instance_exposed`'s inputs resolve where the auth-off arm runs.
Name the single-definition pointer comment rather than pin its line number,
since the hoist shifts that line.

Banner flip only: the ranked table and the four census distribution lines
were NOT recomputed.

BACKLOG #1013
@wshallwshall
wshallwshall enabled auto-merge (squash) August 6, 2026 16:15
wshallwshall added a commit that referenced this pull request Aug 7, 2026
…256 #259 #264) (#275)

* feat(api): report-only TLS key-exchange groups posture field; correct stale "pinned" doc claims (BACKLOG #338)

The engine's TLS key-exchange (KEX) groups are INHERITED from OpenSSL's
default group list, not pinned to the approved set. harden_kex_groups pins
nothing until SSLContext.set_groups lands in Python 3.15, so on every
interpreter this project currently runs on the approved pin is inert. This
is documentation accuracy plus observability -- it changes no live TLS
behaviour (the TLS 1.2+ floor is the enforced control), and on a
NOT-DEPLOYED beta there is no exposure today; the pin is a future 3.15
hardening.

Two parts:

1. Report-only surfacing. New pure helper config/tls_policy.kex_groups_report()
   builds a throwaway probe context and asks the ONE authority,
   harden_kex_groups, what it manages to pin -- so the read-out can never
   drift from what the connectors actually do. It returns "inherited (...)"
   on a pre-3.15 interpreter and "pinned: ..." on 3.15+. Surfaced as an
   additive SecurityPosture.kex_groups field (str | None, default None),
   wired in create_app beside fips_attestation(), rendered as a status-page
   row in the web console beside the FIPS/OpenSSL rows. Report-only: it
   reflects, and changes, no TLS behaviour.

2. Three doc-accuracy edits correcting restatements that still read as
   "pinned": CONTAINER-EXPOSURE-EVALUATION.md (verification table),
   ASVS-L2-PHASE0-CHANGES.md (PQC roadmap row), and #200's Closes line in
   docs/archive/backlog/BACKLOG-CLOSED.md (11.6.2 annotated PARTIAL). Each
   links to PHI.md's data-in-transit section, the single source of record
   for the measured accepted set, rather than restating it.

The two Python-3.15 tripwire tests in test_tls_policy.py that fire when
set_groups/get_groups land are left untouched -- they are the signal to
actually set the pin.

Engine UI seam bumped 17 -> 18: the golden seam snapshot introspects
SecurityPosture's field set, so a purely additive field trips the
handshake; SUPPORTED_ENGINE_SEAMS and the golden snapshot updated to match.

Tests: test_tls_policy.test_kex_groups_report_reports_inherited_today
(the helper reports inherited + names the approved list, never "pinned:");
test_api_auth.test_security_posture_reports_kex_groups (the field flows
through the MONITORING_READ-gated, audited posture route and matches the
helper); a status-builder assertion that the console renders the row. All
three falsified: blanking the helper reddens the two report tests
(assert 'inherited' in ''); removing the console row reddens the
status-builder test (assert 'key-exchange' in html).

* docs(backlog): flip #338 banner to shipped (BACKLOG #338)

Flip the #338 status banner from filed/not-started to shipped, now that the
report-only kex_groups posture field and the three doc-accuracy corrections
have landed. The banner records that the KEX groups are documented as
inherited (the pin is inert until Python 3.15) plus the report-only
surfacing behind engine seam v18.

Banner line only, under the #338 heading, verified by number. The ranked
table, the four census distribution lines, and every other item's banner
are untouched. The census was NOT recomputed.

* docs(research): OpenFlow step-attribute vocabulary gap-map (BACKLOG #238)

A findings note comparing Windmill's seven OpenFlow step-attributes
(retry, timeout, stop_after_if, skip_if, continue_on_error, mock,
cache_ttl) against MessageFoundry's engine/handler vocabulary. Per
attribute: what it is, the engine analogue (grounded in a real
symbol/file), and the gap (covered-different-locus / partial / absent).

This is a review, not a feature. OpenFlow is explicitly NOT a
compatibility target; the note is an informational vocabulary map, not a
gap-to-close list. Adopting a declarative artifact stays declined by
ADR 0076 section 7 and BACKLOG #26. Framing is conditional throughout
(not-deployed beta).

Lands at docs/research/openflow-step-attributes.md, mirroring the
existing docs/research/ review-note convention (config-ux-review.md,
message-model-eval.md, ide-low-code-options.md).

The mock row cites config/db_lookup.py / config/fhir_lookup.py for the
pure-dry-run raise guard (config/db_lookup.py raise DbLookupError /
config/fhir_lookup.py raise FhirLookupError), not config/wiring.py whose
raise text is about the router phase.

* docs(backlog): flip #238 banner to CLOSED (BACKLOG #238)

The findings note (docs/research/openflow-step-attributes.md) is the
item's expected output and is now delivered, so #238's banner flips from
open to closed. One banner line only, under the #238 heading.

Census NOT recomputed: this commit changes only #238's banner line and
does not touch the ranked table or the four census distribution lines.

* fix(serve): auth-off startup arm now refuses a declared terminator (BACKLOG #1013)

The `[auth] enabled=false` startup arm keyed on the bind alone
(`not settings.api.is_loopback`), so it did not fire for a loopback bind
behind a declared upstream TLS terminator. A PHI instance with
authentication entirely off behind a declared terminator would have
started with no refusal and no warning on first deployment, while the same
topology with auth on but MFA off is already refused by the gate #326
fixed. The two arms disagreed about what "exposed" means in the same file.

Hoist the single `instance_exposed` definition (#326: an off-loopback bind
OR a declared upstream TLS terminator) above the auth-off arm and widen the
arm to consult it, so it refuses on a non-loopback bind OR a declared
terminator. The existing loopback refusal is kept; the condition is
widened, not replaced.

Load order verified: `instance_exposed`'s inputs -- `settings.api.host`
(through `is_loopback`) and `settings.api.tls_terminated_upstream` -- are
read straight off the loaded config, and the only in-place mutation of
`settings.api.*` between the hoisted definition and the former site is
`serve_ui` (twice), which the predicate does not read. The definition
remains defined exactly once.

Tests (tests/test_cli.py): auth off + declared terminator on a loopback
bind refuses (positive); auth off + true loopback with no terminator still
starts (negative control); auth on + declared terminator is unaffected by
the arm. Each was falsified -- reverting the arm to the bare bind check
reds the positive test, firing on any auth-off reds the negative control,
and dropping the auth check reds the auth-on test; each was restored.

Docs updated so the contract travels with the code (CLAUDE.md 11):
DEPLOYMENT.md, SYSTEM-REQUIREMENTS.md, SECURITY.md, REMOTE-CONSOLE.md and
SECURITY-LOOSENING.md now describe the auth-off refusal as firing on an
exposed instance (a non-loopback bind OR a declared terminator), not on the
bind alone.

BACKLOG #1013

* docs(backlog): mark #1013 fixed; record the resolved load-order question (BACKLOG #1013)

Flip the #1013 banner from filed/open to fixed and record, in the AMENDED
blockquote, that the load-order prerequisite the item flagged as unproven
holds: `instance_exposed`'s inputs resolve where the auth-off arm runs.
Name the single-definition pointer comment rather than pin its line number,
since the hoist shifts that line.

Banner flip only: the ranked table and the four census distribution lines
were NOT recomputed.

BACKLOG #1013

* docs(coord): the session record has no branch, and two rosters disagree about one

A session was told a coordinator "might not be there" on the strength of the
session-list MCP tool's `isRunning: false`, and separately the two rosters
reported different branches for one checkout. Both readings were wrong the same
way -- a field answering a question adjacent to the one asked -- and neither trap
was written down anywhere a reader would look.

MEASURED 2026-08-06. A session record holds exactly cwd, entrypoint, kind, name,
nameSource, peerProtocol, pid, procStart, sessionId, startedAt, version. There is
NO branch field. So any branch printed beside a session came from elsewhere, and
the two sources answer different questions while both being labelled "branch":

    presence.ps1 / occupancy.ps1   the WORKTREE's branch, live from
                                   `git worktree list --porcelain`. Current.
    session-list MCP tool          a SESSION attribute captured at registration.
                                   Does not follow a later `git switch`.

For one checkout they reported two different names -- the live roster the branch
that checkout had been switched onto, the session list the one it registered
with. Neither was wrong. A disagreement is not evidence that either roster is
broken, and a branch from the session list must never be quoted as a checkout's
current branch.

ALSO RECORDED, same family: `isRunning` means "currently EXECUTING A TURN", not
"alive". An idle session between turns reads false while being perfectly
reachable. It is not a liveness fence and must not be used as one --
Get-SessionLiveness is, subject to the rule already stated directly above it that
ONLY THE POSITIVE ANSWER IS SAFE TO ACT ON. That rule is why the original
inference was doubly wrong: it drew a negative conclusion from a signal that
cannot support one, using a field that answers a different question.

The concrete branch names are deliberately NOT quoted -- the leak gate refused
the first attempt because a real worktree slug is an internal project name, and
the lesson does not need them.

Documentation only; no behaviour change. Both files parse, presence.ps1 still
runs, 272 tests pass across the coord/presence/occupancy suites.

* docs(supply-chain): correct at least two claims the shipped v0.3.2 release assets do not support

Verified against the actual release assets, not the prose: `gh release download v0.3.2` gives an
SBOM with licenses on 40/40 components and hashes on 0/40, and a VEX with `"statements": []`.

Two claims did not survive that check.

VEX contents. `docs/SUPPLY-CHAIN.md` described the OpenVEX asset as "our per-CVE exploitability
assessments" and told the reader it "records, per CVE, whether the vulnerable code is reachable" --
a statement about the contents of a published artifact, false in any tense. It sat immediately after
"Do not demand a zero-CVE clean scan", so a procurement reader who applied the VEX, saw no
suppressions, and read that as an assessed all-clear would have had no assessment behind it: a
compensating control resting on a false premise (CLAUDE.md section 11). The page now names the
artifact, says what a statement carries when one exists, and says plainly that where we have not
assessed a CVE the document is silent and the scanner's finding stands. The empty-state rule stays
stated once, at security/vex/README.md:17-18 and ADR 0149, and is linked rather than re-copied --
the removed sentence was itself the divergent third copy.

Component hashes. The inventory sentence enumerated "components, versions, PackageURLs, hashes, and
licenses". Backfilling hashes was investigated and rejected on semantics rather than effort:
CycloneDX `component.hashes` means the hash of THE file, while requirements-core.lock carries 301
`--hash=sha256:` lines over 41 packages and no package with exactly one (cryptography alone has 40).
cyclonedx-py deliberately routes lock hashes to `externalReferences` for that reason, the 1.6 schema
imposes no uniqueness constraint so a multi-entry set would validate clean as a silent false claim,
and `pip` is an inventoried component with no lock line at all. So the sentence is corrected instead:
"at least" replaces the closed enumeration, the reason given is the verified one, and no substitute
integrity control is offered -- Sigstore and SLSA attest the SBOM document and our own release files,
which is an adjacent question, and the lock is not a released artifact.

The same false enumeration sat in scripts/security/sbom_finalize.py's docstring, one hop from the
corrected page, which names that script by path in its "for auditors" section. Fixed there too
rather than leaving the repo self-contradictory on the fact this commit is about.

Nothing is deployed, so nobody has been misled; the defect is that the shipped page WOULD mislead a
first reader who tried to verify components against hashes the SBOM does not carry.

Deliberately unchanged: the true "hash-locked" phrases at :16 and :86 refer to the lock the inventory
is built from, and a blanket scrub of the word would have deleted accurate claims -- the new text
disambiguates them instead. No VEX statement is written here; see the notes handed to the coordinator.
@wshallwshall

Copy link
Copy Markdown
Collaborator Author

Superseded by merge train #275, which merged as 58b67b88. Closing -- this work LANDED, it was not dropped.

Your commits went to main inside the train with history intact, batching five already-green PRs into one CI cycle instead of five.

Verified before closing. #275 was squash-merged, so git merge-base --is-ancestor reports no for this PR's head -- the documented squash-merge lie, not evidence of loss. A file-level git diff origin/main is also the wrong instrument, because main has moved forward on shared files and differs in the other direction.

The question is "is this PR's own change present in main", and the test is whether its patch reverse-applies:

git diff <merge-base>..<head> | git apply --check -R   (against origin/main @ 58b67b88)
-> ALREADY APPLIED

Run for all five train B members; all five already-applied.

On this PR's red test (windows-2025) leg specifically: that failure was an assertion in tests/test_connscale_cpu_probe.py / tests/test_connscale_smoke.py -- a process-table walk that never succeeded in 30s -- and the test's own comment already records this leg failing twice in one job on 2026-07-30. It was never related to this change, and the train carrying this content passed the same leg. Recorded rather than dismissed: if it recurs it deserves a number.

auto-merge was automatically disabled August 7, 2026 20:43

Pull request was closed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant