From 22bd897a86b175574dda486ec757ba422cbdb776 Mon Sep 17 00:00:00 2001 From: notplants-bot Date: Thu, 20 Aug 2026 17:10:28 +0000 Subject: [PATCH] gateway-domain: give a tailnet box a real public domain MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An agent on a box with no public IP frequently needs a reachable HTTPS URL — an OAuth callback, a webhook receiver, a demo link. The testing gateway already holds a wildcard record for *.gtest.commoninternet.net and forwards by SNI, but nothing here knew that, so every agent had to be told by hand. tools/gateway-domain.py add myapp # myapp.gtest.commoninternet.net -> 100.84.190.30 The backend defaults to the running box's own tailscale IP, which is the case that comes up almost every time. The admin password comes from gateway.admin_password in the secret store; the tool reads it itself, so no caller handles the value and there is no second copy to drift or get committed. Two things the tool refuses to do, both learned by doing them: Backends must be a literal IPv4 address. The gateway's validate_ip accepts a hostname, but put_domain/remove_domain only match lines whose backend is numeric ([\d.:]+). A hostname mapping can therefore be written once and never updated or removed through the admin UI — it becomes an orphan that only a hand-edit of tunnel_map.conf clears. One got created while testing this. Verification re-reads the mapping table instead of trusting the POST body. The admin app mutates its in-memory dict and renders that, so a delete that silently failed still renders as success. Checking the response alone reported "removed" for an entry that was still on disk. skills/gateway-domain/ carries the rest: that the gateway does NOT terminate TLS (your box serves the cert for that name), how ACME still works through it, that only 22/80/443 are open at the edge, and how to recover if an interrupted e2e run leaves the admin password reseeded. --- README.md | 16 +++ skills/gateway-domain/SKILL.md | 129 ++++++++++++++++++++ tools/gateway-domain.py | 216 +++++++++++++++++++++++++++++++++ 3 files changed, 361 insertions(+) create mode 100644 skills/gateway-domain/SKILL.md create mode 100755 tools/gateway-domain.py diff --git a/README.md b/README.md index 68e21ac..68b22fa 100644 --- a/README.md +++ b/README.md @@ -379,6 +379,22 @@ so reviewers read the pre-fixup code and no interdiff exists — with no warning push itself succeeded. Always: push, resubmit, then reply with the printed interdiff URL. See `machine-docs/PR-WORKFLOW.md`, "A push does NOT advance the round". +## The testing gateway — public domains for tailnet boxes + +A box on the tailnet with no public IP can still have a real HTTPS domain: the shared testing +gateway holds a wildcard record for `*.gtest.commoninternet.net` and forwards by SNI. + +```bash +python3 engine/tools/gateway-domain.py add myapp # -> myapp.gtest.commoninternet.net -> this box +python3 engine/tools/gateway-domain.py list +python3 engine/tools/gateway-domain.py remove myapp +``` + +The admin password is `gateway.admin_password` in the secret store below; the tool reads it +itself. **The gateway does not terminate TLS** — it proxies the encrypted stream, so your box +serves the certificate for that name. Full detail, including how to get a cert and why hostname +backends are refused, is in the `gateway-domain` skill (`skills/gateway-domain/SKILL.md`). + ## Secrets — one encrypted store, never in git **Every credential on an orchestrator host lives in one sops+age encrypted file. Do not put a diff --git a/skills/gateway-domain/SKILL.md b/skills/gateway-domain/SKILL.md new file mode 100644 index 0000000..e58eb52 --- /dev/null +++ b/skills/gateway-domain/SKILL.md @@ -0,0 +1,129 @@ +--- +name: gateway-domain +description: Give a tailnet box a real public HTTPS domain (.gtest.commoninternet.net) by mapping it on the shared testing gateway. Use when an agent needs a publicly reachable URL for a box with no public IP — an OAuth callback, a webhook receiver, a demo link, an ACME challenge. Covers the add/remove tool, where the admin password lives, and the traps (your box serves the TLS cert, not the gateway; hostname backends are unremovable). +--- + +# Giving your box a public domain + +Your machine is on the tailnet with no public IP. You need a real HTTPS URL for it. The +**testing gateway** already owns a wildcard DNS record, so every name under +`*.gtest.commoninternet.net` resolves to it. Map your name to your tailnet IP and it forwards +matching traffic to you. + +```bash +python3 engine/tools/gateway-domain.py add myapp +# myapp.gtest.commoninternet.net -> 100.84.190.30 +``` + +That is the whole happy path. The backend defaults to **this box's own tailscale IP**, so run +it on the machine that will serve the domain. + +```bash +python3 engine/tools/gateway-domain.py list +python3 engine/tools/gateway-domain.py add myapp # this box, port 443 +python3 engine/tools/gateway-domain.py add myapp 100.64.1.5 # another box +python3 engine/tools/gateway-domain.py add myapp 100.64.1.5:8443 # backend not on 443 +python3 engine/tools/gateway-domain.py remove myapp +``` + +A bare label is expanded to `