Item 3: upstream identity pinning - #177
Merged
Merged
Conversation
|
Warning Review limit reachedNext included review available in 48 minutes. View limit detailsLimit details: You’ve used the included review currently available. You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Review configuration: ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (12)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
SPEC-v0.10 section 4, the honest slice of ASI04 and nothing more: still deciding actions, still never inspecting a package, a model, a registry or a build. What it adds is that an action entry may say which SERVER it authorises itself against. upstream: is a ctrlrun.policy/v8 action-entry key carrying two TLS pins and a tool-schema hash. Two, because a digest cannot be a trust anchor: load_verify_locations takes PEM, so tls_cert_file feeds check 3 where the pinned certificates become the connection's only trust anchors, and tls_cert_sha256 feeds checks 1 and 2, which compare what was observed. An entry pinning by digest alone gets two checks of three, stated as a limit. The gate is the action-entry shape of _V4_ENTRY_KEYS and _V5_ENTRY_KEYS, not require_v7's, which walks authority.grants because tasks: and budgets: are grant keys and a standalone --authority document carries no action entries at all. Check 2 is the one that produces a DENY, and it sits above the approval gate on T446's argument: the pin depends on nothing a human says, so asking one about an action pinned to an unverified server leaves a granted approval behind for a call that cannot run. The observation register is per process by construction, because an observation is a fact about a connection this process made and a second process that has made none must refuse rather than inherit somebody else's. That is upstream_unverified, the fail-closed half, and it is what stops an upstream switching a pin off by never being seen. In-process there is no upstream to observe, so a pinned action refuses on every call. The ACS hook refuses at CONSTRUCTION rather than at load: ACS is advisory, the platform runs the tool, and the hook holds no connection, while one loader cannot know which surface will run an action and a load error would stop verify and scan reading a document that pins. -41016 and not -41013, which SPEC-mcp-operator already allocates to ctrlrun.not_a_human. There is one namespace and the range was walked. G27 grades check 2 and only check 2, because that is the one producing a DENY and the only one verify can grade without a network: the comparison is a pure function over two strings, so verify seeds an observation and asserts the refusal. Acceptance tests T489-T497b, including check 3 against a real TLS listener with a CA-signed leaf and VERIFY_X509_PARTIAL_CHAIN. Mutation table in the PR body: three mutations, all three caught. One flaky gate run found a real race in my own test helper: it bound an ephemeral port, closed it and rebound, twice per run. HTTPServer binds for us and server_port reports what it got, so there is no gap to lose. Gate with Postgres: 4411 passed, 0 skipped. Signed-off-by: arpan <contact@arpanghoshal.com>
CodeQL reported two HIGH security alerts on this PR, both in my own test file: ssl.SSLContext(PROTOCOL_TLS_SERVER) and PROTOCOL_TLS_CLIENT still admit TLS 1.0 and 1.1 unless a floor is set. A listener in a test for a pinning feature that negotiates a protocol the product would refuse is testing something the product does not do, so both contexts now set minimum_version explicitly. The third alert was a variable assigned twice in T489, which was sloppiness. Production had no exposure: jwt_identity and revocation both use ssl.create_default_context(), which floors at TLS 1.2. But checking that turned up a real gap in my own scope claim. The PR body said T491 proves check 3, and it proved the MECHANISM while nothing wired it: the gateway's forwarder built its client with httpx's ordinary verification whatever the policy pinned. Check 3 is the only one of the three that PREVENTS rather than attributing, so shipping section 4 with it unimplemented would have overclaimed the section. upstream.pinned_context builds the context: PARTIAL_CHAIN, because a real upstream's leaf is CA-signed and OpenSSL wants a chain terminating at a self-signed certificate unless told a trusted non-root may end it, and without the flag a pinned leaf refuses every connection including the right one. Plus the TLS floor the alert above taught me to set, here rather than inherited. httpx_forwarder takes the policy and builds one where any entry pins a certificate file. T491b asserts the wiring, the floor and the flag. Gate with Postgres: 4412 passed, 0 skipped. Signed-off-by: arpan <contact@arpanghoshal.com>
arpanghoshal
force-pushed
the
v0.10/3-upstream-pinning
branch
from
September 13, 2026 17:39
31ff1b1 to
516372a
Compare
This was referenced Sep 13, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Item 3 of v0.10,
SPEC-v0.10 §4. Stacked on #176 (which is stacked on #175's spec corrections).The honest slice of
ASI04and nothing more: still deciding actions, still never inspecting apackage, a model, a registry or a build. What it adds is that an action entry may say which
server it authorises itself against.
Two TLS keys, because round two showed one cannot work
§4.2 pinned by digest while §4.3 made the pinned certificates trust anchors. A digest cannot be a
trust anchor —
load_verify_locationstakes PEM. Sotls_cert_filefeeds check 3, where aswapped server fails the handshake;
tls_cert_sha256feeds checks 1 and 2, which compare what wasobserved. An entry pinning by digest alone gets two checks of three, stated as a limit rather than
discovered.
T491 proves check 3 against a real TLS listener with a CA-signed leaf (not self-signed — the
realistic shape, and the one that needs
VERIFY_X509_PARTIAL_CHAINto be an anchor at all): thepinned server completes the handshake, a swapped one is refused. And check 3 is wired, not only
demonstrated:
httpx_forwardertakes the policy and buildsupstream.pinned_contextfrom everycertificate any entry pins, so the gateway'"'"'s one outbound connection trusts those and nothing else.
T491b asserts the wiring, the TLS floor and the PARTIAL_CHAIN flag.
Check 2, and where it sits
Check 2 produces the
DENY, and it sits above the approval gate on T446's argument: the pindepends on nothing a human says, so asking one about an action pinned to an unverified server leaves
a granted approval behind for a call that cannot run.
The observation register is per process by construction — an observation is a fact about a
connection this process made, and a second process that has made none must refuse rather than
inherit somebody else's. That is
upstream_unverified, the fail-closed half, and it is what stopsan upstream switching a pin off by never being seen.
In-process there is no upstream to observe, so a pinned action refuses on every call. The ACS
hook refuses at construction rather than at load, which §4.4 argues at length: ACS is advisory,
the platform runs the tool, the hook holds no connection — and one loader cannot know which surface
will run an action, while a load error would stop
verifyandscanreading a document that pins.-41016, not-41013SPEC-mcp-operator§9.3 already allocates-41013toctrlrun.not_a_human, plus-41014and-41015. There is one namespace; the range was walked before a number was taken.Mutation table
All three caught on the first pass.
A flake I introduced, and what it was
One gate run failed and the next passed. The cause was mine:
_servebound an ephemeral port,closed it, and rebound — a race another process can win, run twice per test because this file
stands up two servers.
HTTPServerbinds for us andserver_portreports what it got, so there isno gap to lose. Three consecutive clean runs after the fix.
G27
a swapped upstream is denied, 28 chars. Grades §4.3's check 2 and only check 2 — the one thatproduces a
DENY, which is what the title promises; check 3 producesNotExecutedwith the effectFAILED, a different outcome under a different name. It is also the only checkverifycan gradewithout a network: the comparison is a pure function over two strings, so
verifyseeds anobservation and asserts the refusal, with no listener to stand up. §8.1 made the same move for G23.
Gate
4411 passed, 0 skipped with Postgres.
ruff,mypy --strictclean.What is not done, and it is an exit criterion
§7.3 requires a shipped example that pins an upstream, with G27 grading
PASSon it. There isnone, so G27 is
N/Aon both, and the CI pin movesAUTHORITY_NA2 → 3.The reason is a genuine tension round two flagged as a nitpick and I hit head-on: §4.4 makes a
pinned action refuse on every in-process call, and
tests/test_examples.pyruns each example ina subprocess with every socket refused. So an example that pins can only ever demonstrate the
refusal, never a working call. That is a coherent example to ship, but it is a different shape
from the others and it belongs with the release item that owns the exit criteria. Flagged here
rather than silently left.
Not merging and not tagging.