fix(moshpit): put the pinned-TLS proxy on the path, and stop trusting CAs as leaves - #343
Merged
Conversation
`dns trust <name>` installed whatever the socket served, provided the registry published a matching pin. The pin proves the registry vouches for that *key*; it says nothing about whether trusting it is bounded. A certificate installed here is installed as a trust anchor, and an anchor marked CA:TRUE may issue for any name. The code claimed the opposite — "its SAN limits it to this one name" — but a SAN describes what a certificate speaks for, not what a key trusted as an authority may sign. This is the same hole requireNameConstraints closes on the root path, arriving by the other door. It went unnoticed because openssl's `req -x509` defaults to CA:TRUE, so every origin created by setup-origin.sh serves exactly the shape that must be refused, and it is indistinguishable from a correct one until someone trusts it. The refusal names a remedy that costs nothing: re-issue as CA:FALSE reusing the key, and the published pin does not move. A gate with no way forward is a gate people route around. An unreadable certificate is treated as a CA — the safe direction to fail in. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ThreatCrush Security Scan92 finding(s) HIGH/CRITICAL: 2 | MEDIUM: 42 | LOW: 48
…and 42 more. Full results in the Security tab. Snippets are redacted; ThreatCrush never prints matched credential material. |
… works `addressAnswer` could already point every live name at the local pinned-TLS proxy, `dns start --proxy` could already switch it on, and `dns enable` already installed the root the proxy signs with. Nothing ever connected them. So names resolved straight to their origin, a stock client got a certificate no CA had signed, and the trust store was populated for a proxy that was never on the path — which reads, correctly, as "this is still broken". `dns enable` now probes for the proxy and starts the bridge in proxy mode when it finds one. The probe is a TLS handshake, not a connect. `proxyReachable` answers "is something listening", and on one common class of machine the two answers differ in the worst way: an origin runs nginx on 0.0.0.0:443, which covers loopback, so a connect succeeds and proxy mode would point every live Moshpit name on the machine at a web server that has never heard of them. That is not a certificate problem, it is every name serving the wrong site at once. So `proxyServes` completes a handshake and checks who issued the certificate. The proxy mints a leaf per name from the root it generated here; nginx serves the origin's own self-signed certificate, issued by itself. Nothing is trusted in the process — the peer certificate is read, not verified, and only the issuer name is taken from it. Refusing is the default in every uncertain case. Proxy mode with nothing behind it resolves every name and then refuses every connection, which looks like the sites are down while `dig` stays healthy. A bridge this run did not start keeps its own mode, so the probe is skipped rather than run and then discarded — announcing a proxy and retracting it two lines later is worse than not looking. `--no-proxy` opts out. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
| host: address, | ||
| port, | ||
| servername: name, | ||
| rejectUnauthorized: false, |
Merged
ralyodio
added a commit
that referenced
this pull request
Aug 9, 2026
Bump to v0.33.0, releasing the herd (#342) — agent sessions that outlive the terminal that started them — along with the PRD behind it (#341) and the moshpit pinned-TLS proxy fix (#343), all of which have been sitting on main unreleased. Minor rather than patch: #342 adds six commands (herd, ps, attach, kill, wait, restore) and six moshscript verbs, and changes none of the existing ones. `moshcode start claude` with no -d behaves exactly as it did. This release is what makes any of it reachable. install.sh serves the latest release tarball rather than main, so until a release carries it every installed machine answers `unknown command "ps"` — and the npm channel only moves when publish.yml sees a published GitHub release. No plugin bumps: stocks and crypto are untouched, and neither names a command that moved. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
Two commits. Together they are why
curl https://chovy.hackernever verified.1.
dns enablenever turned proxy mode onEverything needed was already built and none of it was connected:
moshpit-proxyverifies an origin against the registry pin and re-signs with a root it generates locally.addressAnswerknows how to point every live name at that proxy (proxied: true).dns start --proxycan switch it on.dns enablealready installs the root the proxy signs with.Nothing ever passed
--proxy. So names resolved straight to their origin, a stock client got a certificate no CA had signed, and the trust store was populated for a proxy that was never on the path.dns enablenow probes for the proxy and starts the bridge in proxy mode when it finds one.The probe is a TLS handshake, not a connect. This is the part that matters.
proxyReachableanswers "is something listening", and on one common class of machine the two answers differ in the worst possible way: an origin runs nginx on0.0.0.0:443, which covers loopback — so a connect succeeds, proxy mode goes on, and every live Moshpit name on the machine is pointed at a web server that has never heard of them. That is not a certificate problem, it is every name serving the wrong site at once.So
proxyServes()completes a handshake and checks who issued the certificate. The proxy mints a leaf per name from the root it generated here; nginx serves the origin's own self-signed certificate, issued by itself. Nothing is trusted in the process — the peer certificate is read rather than verified, and the only thing taken from it is the issuer name (verifying properly would need the root already installed, which has not happened yet at that point).Refusing is the default in every uncertain case, because proxy mode with nothing behind it resolves every name and then refuses every connection — the sites look down while
digstays healthy. A bridge this run did not start keeps its own mode, so the probe is skipped rather than run and discarded; announcing a proxy and retracting it two lines later is worse than not looking.--no-proxyopts out.2.
dns trustwould install a CA as a trust anchormoshcode dns trust <name>installed whatever the socket served, provided the registry published a matching pin. The pin proves the registry vouches for that key; it says nothing about whether trusting it is bounded.A certificate installed by this path becomes a trust anchor, and an anchor marked
CA:TRUEmay issue for any name. The code claimed the opposite — "its SAN limits it to this one name" — but a SAN describes what a certificate speaks for, not what a key trusted as an authority may sign. Same holerequireNameConstraintscloses on the root path, arriving by the other door.It went unnoticed because openssl's
req -x509defaults toCA:TRUE, so every origin created bysetup-origin.shserves exactly the shape that must be refused, and it is indistinguishable from a correct one until someone trusts it. All three names on the dev box are CA:TRUE today.The refusal names a remedy that costs nothing — re-issuing from the same key leaves the published pin untouched — because a gate with no way forward is a gate people route around.
Pairs with profullstack/moshpit-proxy#18, which changes what is issued.
Tests
13 new (10 proxy mode, 3 trust). The proxy probe tests use real openssl chains and a real TLS server, since what separates the two cases is the issuer on a completed handshake and a stub would only prove the stub sets the field the code reads. Full suite 1073 pass / 0 fail.
🤖 Generated with Claude Code