| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Name | Name | Last commit date | ||
|---|---|---|---|---|
Registry-pinned TLS for Moshpit names. The origin's key is checked against what the registry published for that name — not against a certificate authority, because no certificate authority will ever vouch for .moshpit.
You can't, and it isn't a matter of persuasion. The CA/Browser Forum Baseline Requirements banned certificates for non-IANA names: issuance stopped in November 2015 and existing ones were revoked by October 2016. A CA in a browser root store that issued for .moshpit would be distrusted for doing it. The rule is the penalty.
So the trust has to come from somewhere else. It comes from the registry, which already knows who owns each name — a stronger claim than domain validation ever made, since it proves registry ownership rather than momentary control of a socket.
| Gateway sees | ||
|---|---|---|
| A. Gateway terminates TLS | everything | simplest, but the gateway is a designed-in MITM |
| B. Gateway SNI-passthrough | hostname only | implemented here |
| C. Registry returns origin IP | nothing | leaks origin IPs, breaks NAT'd sites |
A central terminating gateway and end-to-end encryption are mutually exclusive. This is topology B: nginx routes on the SNI without a private key, so the session that terminates at the origin is genuinely end-to-end.
tronbrowser.dev has built-in Moshpit support. It resolves names and verifies registry pins natively, so it needs no DNS configuration, no local proxy, and no root certificate. Nothing in this package is required to browse Moshpit from TronBrowser.
Everything below exists for stock browsers, which cannot be taught to check a registry pin — there is no certificate-validation API in any browser extension model.
browser --TLS--> proxy --TLS--> gateway (ssl_preread) --TCP--> origin
local CA registry-pinned key
The right-hand session is the real one. The left-hand session exists only to say the result in the one language a browser accepts.
npm start # or: node bin/moshpit-proxy.tsPair it with moshpit-dns, which points .moshpit names at loopback. The deployed unit is systemd/moshpit-dns.service below.
| Variable | Default | |
|---|---|---|
| MOSHPIT_PROXY_PORT | 8443 | 443 needs privileges; the installer moves it |
| MOSHPIT_PROXY_HOST | 127.0.0.1 | |
| MOSHPIT_GATEWAY_HOST | pit.moshcode.sh | |
| MOSHPIT_REGISTRY_BASE | https://pit.moshcode.sh | |
| MOSHPIT_PROXY_TLDS | moshpit | comma-separated |
| MOSHPIT_PROXY_PINS | ~/.moshpit/pins.json | overrides, win over the registry |
| MOSHPIT_PROXY_TOFU | off | see below |
| MOSHPIT_PROXY_DIR | ~/.moshpit |
curl -fsSL https://raw.githubusercontent.com/profullstack/moshpit-proxy/main/install.sh | shInstalls under ~/.local, needs no root for the code, and finishes by setting up your browsers — asking once, in plain language, before it changes anything:
Moshpit needs to add a security key to this computer so your browser trusts .moshpit sites. It only works for .moshpit and cannot affect any other website. Continue? [Y/n]
--no-trust installs the code only. --uninstall removes both.
Already have the code? moshpit-trust does the browser setup on its own, and moshpit-trust --status says what is set up without changing anything. It is idempotent — running it twice is a no-op, not a duplicate.
Nothing in this section applies to TronBrowser, which verifies registry pins natively. No setup, no proxy, no local root.
moshpit-trust automates everything below; this section is what it does and why, for anyone who would rather do it by hand or wants to know what changed.
The proxy generates a root on first run at ~/.moshpit/ca/ca.crt and prints its fingerprint. Two things make installing it a much smaller ask than a shared CA:
That second property is tested, not asserted — tests/ca.test.ts signs a www.google.com certificate with the root and checks that openssl rejects it with permitted subtree violation.
# Linux (NSS: Chrome, Chromium, Edge)
certutil -d sql:$HOME/.pki/nssdb -A -t "C,," -n "Moshpit Local CA" -i ~/.moshpit/ca/ca.crt
# macOS
sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain ~/.moshpit/ca/ca.crtC,, is server-trust only — not code signing, not mail.
Firefox is not covered by either command. It carries its own NSS database per profile on every platform, including macOS, so the keychain does nothing for it:
certutil -d sql:~/.mozilla/firefox/<profile> -A -t "C,," -n "Moshpit Local CA" -i ~/.moshpit/ca/ca.crtOn current Ubuntu the default Firefox is the snap, whose profiles live under ~/snap/firefox/common/.mozilla/firefox/ instead. moshpit-trust covers the snap and flatpak paths as well, which is most of why doing this by hand tends to half-work.
certutil itself is not installed by default on Debian or Ubuntu — the command above fails on a machine that does have the store it points at. Install libnss3-tools (Debian/Ubuntu), nss-tools (Fedora/RHEL) or nss (Arch, brew).
Node, Python and Java keep their own trust stores — NODE_EXTRA_CA_CERTS, certifi, cacerts respectively. Installing into the OS store does not cover them, and neither does moshpit-trust.
On the box that will host the name — a VPS, a droplet, anything with a public address:
sudo sh scripts/setup-origin.sh chovy.hackerGenerates a key and self-signed certificate, writes an nginx server block from nginx/moshpit-origin.conf, reloads, connects back to itself to prove the name now answers with the key it just made, and prints the pin to publish. --dry-run shows what it would do.
Self-signed is the design, not a shortcut: no CA will issue for a Moshpit TLD, so identity comes from the registry rather than an issuer. Nothing about the certificate is checked except the key, so an expensive one and this one are worth exactly the same here.
The failure this is built to prevent: a name with no matching server block falls through to nginx's default vhost, which answers with a certificate for some other name entirely. The browser reports an invalid certificate and the site looks broken, when what is broken is one missing server_name. The script connects back and checks, rather than trusting that a clean nginx -t means the name resolves to the right block.
setup-origin.sh stops at printing the pin because publishing it needs an account. With an API key it does not stop:
MOSHPIT_API_KEY=... sh scripts/setup-origin.sh chovy.hacker \
--target dev.profullstack.comKey, certificate, nginx block, reload, verify, publish the pin, set the target, and read it back from the registry — the whole of "serve this name". Get a key at app.moshcode.sh/settings.
This works because /api/moshpit accepts a bearer token as well as a cookie session, so nothing here needs a browser. Without a key the script behaves exactly as before and prints the pin for you to paste.
The read-back at the end is the point. A 201 means the write was accepted; only a read proves the endpoint clients actually query returns it, and a pin that was accepted but is not served leaves the name refused with nothing to show for it.
--target will not take an IPv4 literal, because the registry refuses one by design — an A record on a small host is leased, NATed or shared, and a name pointed at one goes stale silently. Use a hostname: the address behind it is resolved normally, which is also how a name reaches IPv4-only clients.
Nothing is proxied on the server side. moshpit-proxy runs on the visitor's machine — it is the thing that checks the pin on behalf of a browser that cannot.
npx moshpit-pin ./origin.crt # from a file
npx moshpit-pin scrambled.eggs:443 # from what a live server presentssetup-origin.sh prints this for you. Until the pin is published every client refuses the name — there is no TOFU and no unauthenticated mode, because the pin stands exactly where a certificate authority would.
Publish the result at app.moshcode.sh/pit — sign in, open the Yours tab, and use Key pins on the TLD.
Not /pit/dns, which this file used to say. That page is the instructions for pointing a machine's resolver at Moshpit — it has no pin form on it, so the old link sent operators somewhere they could never finish the job.
The same page is where a name's points at target lives, which is the other half of making a name reachable.
Keep the previous pin listed alongside the new one while rotating — the client accepts any pin in the list, so a key can change without a flag day.
The pin is SHA-256(SubjectPublicKeyInfo), base64 — the RFC 7469 format, so openssl x509 -pubkey | openssl pkey -pubin -outform der | openssl dgst -sha256 prints the same string. tests/spki.test.ts asserts we agree with openssl.
nginx/moshpit-gateway.conf — there is no ssl_certificate directive anywhere in it, which is the point. Requires ngx_stream_module with ssl_preread (already compiled into stock Debian/Ubuntu nginx).
node scripts/gen-upstreams.ts # writes the SNI->origin map, reloads nginxThe map is regenerated on a timer rather than looked up per connection, so the registry is not on the path of every TCP handshake.
The gateway above is for reaching other people's names. caddy/Caddyfile is for the opposite case: services running on this machine that you want reachable under a suffix you own. A moshpit wildcard DNS record (*.chovy.hacker, set alongside your pin at app.moshcode.sh/pit/dns) points every subdomain at this machine's address; Caddy answers TLS for each of them and reverse-proxies to a local port. Every subdomain defaults to localhost:8080; individual names are overridden to other ports with a two-line handle block, either in the template or as a drop-in under /etc/caddy/moshpit.d/.
The certificate comes from Caddy's internal CA, for the same reason the proxy mints its own root: the CA/Browser Forum ban leaves no public option, so the trust anchor is a key that never leaves the machine. Trust it in your browser once, exactly as with ~/.moshpit/ca/ca.crt.
sudo cp caddy/Caddyfile /etc/caddy/Caddyfile # or merge the block into yours
sudo mkdir -p /etc/caddy/moshpit.d # per-service overrides live here
sudo systemctl reload caddy # or: caddy reloadTo host another suffix, copy the site block and replace the name — the template marks the spots. To route one subdomain elsewhere, see caddy/examples/app.chovy.hacker.caddy. The nginx config is untouched by any of this: it terminates nothing and never sees these connections.
Because both ends of the trust decision are ours, server keys can move to ML-DSA whenever we like — no CA, no root program, no CA/B ballot. Browsers don't support PQ signatures in TLS yet, but the proxy terminates for the browser, so the proxy↔origin leg can run ML-DSA before the public web can.
The proxy configures no groups at all — the origin leg uses Node's defaults. On Node 24 against OpenSSL 3.5 those defaults already put the hybrid first and send a real ML-KEM key share in the first flight. Read off the wire from the ClientHello the proxy actually sends:
supported_groups: X25519MLKEM768, x25519, secp256r1, x448, secp384r1, ... key_share sent : X25519MLKEM768(1216B), x25519(32B)
An origin on OpenSSL 3.0–3.4 — what Ubuntu 22.04 and 24.04 still ship — has no ML-KEM, so the handshake silently falls back to x25519 and succeeds. The session is fine against every adversary that exists today and decryptable by one that doesn't yet. Nothing said so, which made the guarantee unobservable.
Every upstream leg is now classified and counted:
[proxy] ok scrambled.eggs (registry) TLSv1.3 hybrid-pq [proxy] warn old.eggs: origin has no ML-KEM, fell back to classical/X25519 ... [proxy] 41 post-quantum, 3 classical
MOSHPIT_PROXY_REQUIRE_PQ=1 turns the fallback into a refusal. Leave it off until the counters say the grid is ready — switching it on today takes every pre-3.5 origin offline.
Node exposes no binding for SSL_get_negotiated_group(), so the group is read through getEphemeralKeyInfo(), which cannot represent a hybrid KEM and returns {} for one while naming any classical group. That is an inference from an absence, so it is never trusted on faith: two loopback handshakes at startup prove both halves of the mapping, the result is printed, and enforcement refuses to engage if the proof fails. See lib/pq.ts.
The rest of the Moshpit stack is Bun. This is not, and the reason is specific rather than aesthetic. Bun 1.3.14 cannot do dynamic SNI:
A proxy that mints a certificate per name needs all of the above. Node 24 supports every part of it and runs the TypeScript directly via type stripping, so there is no build step. If Bun's node:tls catches up, this moves back.
npm test # node --test tests/*.test.ts27 tests. The ones that matter are in tests/proxy.test.ts: they stand up a real origin over real TLS and check that a vouched-for key gets through and a substituted one is refused before any request byte leaves the machine.
Apache-2.0. See LICENSE and NOTICE.
Apache rather than MIT for a specific reason: this is protocol code meant to be reimplemented — TronBrowser performs the same pin verification independently — and Apache-2.0 carries an explicit patent grant where MIT is silent. Implement the pin scheme however you like; the NOTICE covers naming.
Security policy and the trust model behind the local CA: SECURITY.md.
systemd/moshpit-dns.service and systemd/00-moshpit.conf are the deployed resolver setup, kept here because they were previously configured by hand on each box and existed in no repository.
cp systemd/moshpit-dns.service /etc/systemd/system/
cp systemd/00-moshpit.conf /etc/systemd/resolved.conf.d/
systemctl daemon-reload && systemctl enable --now moshpit-dns
systemctl restart systemd-resolvedThe bridge runs as this machine's primary resolver, with no list of Moshpit endings anywhere. That is not a shortcut — a per-TLD list cannot be made to work:
Sending every query to the bridge removes both problems. It answers Moshpit names from the registry and forwards everything else upstream (mode=clearnet: the ordinary internet owns any name it can answer; the registry is a backfill).
The drop-in is named 00- so it is read first: systemd-resolved appends DNS= in filename order and uses the first server, rotating only on failure — never on NXDOMAIN. So any pre-existing resolvers stay listed as a genuine fallback for if the bridge stops, rather than shadowing it.
Do not set MOSHPIT_DNS_CATCHALL=1 here. It parks every unresolved name on the registry's parking address, which on a machine's own resolver means github.com resolves to a parking page. Correct startup logs say mode=clearnet. If clearnet names start resolving to a 69.46.46.x address, check for a stray moshcode dns start bound to 127.0.0.1:5354 — a loopback bind beats the service's 0.0.0.0 bind and wins every local query.
| Back | FazBrowse Home | New Git URL |