The tailnet ACL only permits the gateway to open connections to nodes tagged tag:notplants-test-server. Map an untagged node and the gateway accepts the mapping and then never connects — no error anywhere, and it presents as a broken gateway rather than as a missing tag on your own box. Nothing said so, and it is not discoverable from the failure. The skill now leads with the requirement and the one-liner to check it, and names it as the first thing to look at when traffic does not flow. The tool also warns when the node it is about to map does not carry the tag. It cannot check a backend given explicitly on the command line — it only sees its own tags — so that case stays documented rather than enforced. Found by mapping this orchestrator box (tag:orchestrator, tag:server) as a smoke test: the mapping was written and looked entirely healthy.
150 lines
7.2 KiB
Markdown
150 lines
7.2 KiB
Markdown
---
|
|
name: gateway-domain
|
|
description: Give a tailnet box a real public HTTPS domain (<name>.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 two things that silently break it: your box must carry the tag:notplants-test-server tailnet tag or the ACL blocks the gateway from reaching it, and your box serves the TLS cert rather than the gateway.
|
|
---
|
|
|
|
# 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.
|
|
|
|
## Your box must carry the `notplants-test-server` tag
|
|
|
|
The tailnet ACL only permits the gateway to open connections to nodes tagged
|
|
**`tag:notplants-test-server`**. Without it the gateway accepts your mapping and then simply
|
|
never connects — which looks like a broken gateway and is not one. Check before you start:
|
|
|
|
```bash
|
|
tailscale status --json | jq -r '.Self.Tags[]?'
|
|
```
|
|
|
|
If `tag:notplants-test-server` is not listed, add it to that node in the Tailscale admin (a
|
|
node's tags are set when it is authenticated, so this may mean re-authenticating it), or map a
|
|
backend that already has the tag. `gateway-domain.py` warns when the node it is about to map
|
|
lacks the tag, but it cannot see the tags of a backend you name explicitly — that one is on you.
|
|
|
|
The gateway itself is tagged `tag:testing-gateway`; that is the other half of the same ACL rule.
|
|
|
|
## The commands
|
|
|
|
```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 `<label>.gtest.commoninternet.net`. Anything containing a dot is
|
|
used verbatim, so you can map a domain you control elsewhere — but then **you** must point its
|
|
DNS at the gateway (`49.13.156.72`); only `*.gtest.commoninternet.net` is pre-pointed.
|
|
|
|
## The credential
|
|
|
|
The admin password is in the orchestrator secret store, **not** in any repo:
|
|
|
|
```bash
|
|
python3 engine/secrets.py get gateway.admin_password
|
|
```
|
|
|
|
| | |
|
|
|---|---|
|
|
| store | `/secrets/store.yaml` (sops+age, `0600`, outside every git tree) |
|
|
| keys | `gateway.admin_password`, `gateway.fqdn`, `gateway.admin_user` |
|
|
| age key | `~/.config/sops/age/keys.txt` |
|
|
| add/edit | `sops /secrets/store.yaml` |
|
|
|
|
`gateway-domain.py` reads it itself — you should never need to handle the value. Do not copy it
|
|
into a config file, an env file, or a repo. If you need it in a subprocess, use
|
|
`engine/secrets.py exec-env` or `with-file` rather than writing a second copy.
|
|
|
|
## The one thing that surprises people: your box serves the certificate
|
|
|
|
The gateway does **not** terminate TLS. It reads the SNI name from the TLS handshake and proxies
|
|
the still-encrypted bytes onward:
|
|
|
|
```
|
|
browser --TLS--> gateway :443 --reads SNI, proxies encrypted--> your box (tailnet)
|
|
```
|
|
|
|
So after mapping `myapp.gtest.commoninternet.net`, **your box** must serve a certificate valid
|
|
for that exact name, on the backend port (443 unless you set one). The gateway has a cert for
|
|
its own name only; it never sees your plaintext.
|
|
|
|
Getting a cert on your box works: the gateway passes HTTP-01 challenges through on `:80` for
|
|
mapped names, so ACME can complete normally. Add the domain here **first**, then request the
|
|
cert — issuance needs the mapping to already exist.
|
|
|
|
If you only need plain HTTP for a quick test, that also passes through on `:80`.
|
|
|
|
## Traps
|
|
|
|
**Backends must be a literal IPv4 address** (optionally `IP:port`). The tool refuses hostnames,
|
|
and it is protecting you: the gateway's `validate_ip` accepts a hostname, but its
|
|
`put_domain`/`remove_domain` only match lines whose backend is numeric. A hostname mapping can
|
|
be written once and then **never updated or removed** through the admin UI — it becomes an
|
|
orphan that only a hand-edit of `/var/lib/tunnel-gateway/tunnel_map.conf` on the box can clear.
|
|
|
|
**Only ports 22/80/443 are open at the edge.** Mapping `IP:8443` changes which port on *your
|
|
box* the gateway connects to; it does not open 8443 to the internet. Exposing a different
|
|
*gateway* port is a config change in `nix/hosts/hetzner-test.nix`
|
|
(`services.tunnelGateway.openTCPPorts`) **and** the Hetzner firewall in `nix/terraform/` — not
|
|
something this tool can do.
|
|
|
|
**Names are shared.** Anyone with the password can list, overwrite, or delete any mapping.
|
|
Prefix yours with something recognisable and remove it when you are finished.
|
|
|
|
**This is the test gateway.** Never point this tool at the production one. The box is long-lived
|
|
(it is also the e2e and CI target), but its map is not sacred.
|
|
|
|
## When the password stops working
|
|
|
|
The e2e suite reseeds `.htpasswd` with a throwaway credential for the duration of a run and
|
|
restores the previous file afterwards. If a run was killed part-way, the real one is still on
|
|
the box at `/var/lib/tunnel-gateway/.htpasswd.e2e-backup`:
|
|
|
|
```bash
|
|
ssh -i /srv/gateway-coop/.secrets/id_admin root@49.13.156.72 \
|
|
'mv -f /var/lib/tunnel-gateway/.htpasswd.e2e-backup /var/lib/tunnel-gateway/.htpasswd'
|
|
```
|
|
|
|
To reseed it from the store instead — piping so the value never lands on disk:
|
|
|
|
```bash
|
|
python3 engine/secrets.py get gateway.admin_password | \
|
|
ssh -i /srv/gateway-coop/.secrets/id_admin root@49.13.156.72 \
|
|
'htpasswd -ci /var/lib/tunnel-gateway/.htpasswd admin \
|
|
&& chgrp nginx /var/lib/tunnel-gateway/.htpasswd \
|
|
&& chmod 640 /var/lib/tunnel-gateway/.htpasswd'
|
|
```
|
|
|
|
## If it still does not work
|
|
|
|
Check in this order — most failures are the last two.
|
|
|
|
1. `gateway-domain.py list` — is the mapping actually there?
|
|
2. `getent hosts myapp.gtest.commoninternet.net` — should be `49.13.156.72`.
|
|
3. **Is your node tagged `tag:notplants-test-server`?** (`tailscale status --json | jq -r
|
|
'.Self.Tags[]?'`) This is the single most common cause. The gateway is `gateway-test-1`
|
|
(`100.91.44.90`), tagged `tag:testing-gateway`; the ACL pairs those two tags, so an
|
|
untagged backend is unreachable no matter how correct the mapping looks.
|
|
4. **Is your service actually serving TLS for that name on the backend port?** A backend that
|
|
speaks plain HTTP on 443, or serves a cert for a different name, fails here and nowhere else.
|
|
|
|
## The gateway itself
|
|
|
|
`gtest.commoninternet.net` / `49.13.156.72` — a Hetzner `cx23` running NixOS, configured in the
|
|
`tunnel-gateway-server` repo (`nix/hosts/hetzner-test.nix`), which `notplants-nix` pins as a
|
|
submodule under `external/`. It also accepts reverse-SSH tunnels, for boxes not on the tailnet;
|
|
that path is separate from this one and is not covered here.
|