Skip to content

Add the origin side: nginx template and a setup script that proves itself - #6

Merged
ralyodio merged 1 commit into
mainfrom
origin-setup
Aug 1, 2026
Merged

Add the origin side: nginx template and a setup script that proves itself#6
ralyodio merged 1 commit into
mainfrom
origin-setup

Conversation

@ralyodio

@ralyodio ralyodio commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

This repo described the gateway (routes without decrypting) but nothing described the box that actually terminates the session — which is where every live Moshpit name currently goes wrong.

Diagnosed on a real one

chovy.hacker is registered with an IPv6 target pointing at a DigitalOcean droplet. Direct-to-origin resolution works — the registry hands back the address, no gateway involved. But:

$ curl -D - -H "Host: chovy.hacker" http://[2604:a880:400:d1:0:4:c3fe:1]/
HTTP/1.1 301 Moved Permanently
Location: https://chovy.hacker/

$ openssl s_client -connect [2604:...:1]:443 -servername chovy.hacker
subject=CN=dev.profullstack.com          ← wrong name
{"name":"chovy.hacker","target":"2604:a880:400:d1:0:4:c3fe:1","pins":[]}

Three independent mistakes, none exotic:

  1. No server block for the name → falls through to the default vhost, which answers with a certificate for dev.profullstack.com. Browser reports an invalid certificate; the site looks broken when what's broken is one missing server_name.
  2. :80 redirects to :443 — correct in itself, but it routes visitors straight into that error.
  3. pins: [] — nothing published, so no client would accept the name even once the certificate is fixed.

Hence a template and a script rather than prose.

What the script does

sh scripts/setup-origin.sh chovy.hacker    # as root

Key + self-signed certificate → server block → reload → connect back to 127.0.0.1:443 with the name as SNI and check what is actually presented.

That last step is the point. A clean nginx -t does not mean the name reaches the intended block, and the default-vhost fallthrough is exactly the bug this exists to prevent. So it is verified, not assumed:

==> checking what the server now presents for chovy.hacker
warning: the server answered 'chovy.hacker' with: subject=CN=dev.profullstack.com
warning: another server block is matching first — check for a default_server

Other decisions:

  • Reuses an existing key when present. The pin is over the key, so certificates can be regenerated freely without invalidating what is published.
  • Template asks for X25519MLKEM768, needing nginx against OpenSSL 3.5+. The script reads nginx -V, warns, and comments the line out rather than handing someone a failed reload that takes their live site down with it.
  • No HSTS. It is a promise about a name the public DNS does not own, and a stale entry for a Moshpit name is not something a user can easily clear.
  • Session tickets off — a second, weaker resumption path that does not go through the pin check.

Verification

The pin the script prints is byte-identical to what lib/spki.ts computes — the value moshpit-proxy and TronBrowser actually check against:

setup-origin.sh pipeline       : Ii9fAn6VsIOCr2vC9FAWzy6Re23xrpBRcUp0KBuGZSc=
lib/spki.ts (what clients use) : Ii9fAn6VsIOCr2vC9FAWzy6Re23xrpBRcUp0KBuGZSc=
MATCH ✓

sh -n clean, --dry-run exercised, 35 tests pass, tsc --noEmit clean.

Not run against a live droplet — I have no access to one. The reload-and-verify path is untested end to end and should get one real run before this is relied on.

Note on .hacker being IPv6-only

chovy.hacker's target has no A record, so IPv4-only clients cannot reach it at all regardless of certificates. Out of scope here, but worth fixing before treating it as live.

🤖 Generated with Claude Code

…self

The gateway config in this repo describes the box that routes without
decrypting. Nothing described the box that actually terminates the session,
which is where every live Moshpit name currently goes wrong.

Diagnosed on a real one. `chovy.hacker` is registered with an IPv6 target
pointing at a droplet, and direct-to-origin resolution works — no gateway
involved. But the droplet answers :443 for that name with a Let's Encrypt
certificate for `dev.profullstack.com`, because there is no server block for
`chovy.hacker` and it falls through to the default vhost. Its :80 block then
301s to https://, so a browser is redirected straight into the error. The
registry also has `pins: []`, so no client would accept the name even once
the certificate is right.

That is three separate mistakes and none of them is exotic. Hence a template
and a script rather than prose.

setup-origin.sh generates the key and self-signed certificate, writes the
server block, reloads, and then connects back to 127.0.0.1:443 with the name
as SNI to check what is actually presented. A clean `nginx -t` does not mean
the name reaches the intended block, and the default-vhost fallthrough is
precisely the bug this exists to prevent — so it is verified, not assumed.

Reuses an existing key when one is present, because the pin is over the key:
certificates can be regenerated freely without invalidating what is published.

The template pins TLS 1.3 and asks for X25519MLKEM768, which needs nginx built
against OpenSSL 3.5+. The script reads `nginx -V`, warns, and comments the line
out rather than handing someone a failed reload that takes their live site down
with it.

Verified the pin the script prints is byte-identical to what lib/spki.ts
computes — the value moshpit-proxy and TronBrowser actually check against.
Shell syntax checked, dry-run exercised. 35 tests pass, tsc clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Aug 1, 2026

Copy link
Copy Markdown

vu1nz Security Review

0 finding(s) in PR #?

No security issues found.

@ralyodio
ralyodio marked this pull request as ready for review August 1, 2026 17:58
@ralyodio
ralyodio merged commit e12fe7a into main Aug 1, 2026
3 checks passed
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