A rule was added so the testing gateway may also reach nodes tagged tag:orchestrator, not only tag:notplants-test-server. Both tags now appear in the skill's table, and the tool's warning fires only when a node carries neither. Verified end to end from an orchestrator box (tag:orchestrator, tag:server) rather than assumed: - the gateway can open a TCP connection to it over the tailnet - a request to https://acltest.gtest.commoninternet.net/ returns the box's own self-signed certificate, subject and issuer both CN=acltest..., which is only possible if the gateway proxied the stream instead of terminating it - the payload came back and the box's listener logged the request The troubleshooting section gains the one-liner that isolates this from the gateway itself: open a TCP connection to your backend from the gateway.
158 lines
7.5 KiB
Markdown
158 lines
7.5 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 a tailnet tag the ACL allows (tag:notplants-test-server or tag:orchestrator) or the gateway cannot reach 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 needs a tailnet tag the ACL allows
|
|
|
|
The gateway can only open connections to nodes the tailnet ACL lets it reach. Two tags qualify:
|
|
|
|
| tag | who |
|
|
|---|---|
|
|
| `tag:notplants-test-server` | test servers — the usual case |
|
|
| `tag:orchestrator` | orchestrator boxes (added 2026-08-20, verified end to end) |
|
|
|
|
Without one of them 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 neither tag is listed, add one 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 one. `gateway-domain.py` warns when the node it is about to map carries neither, 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. **Does your node carry `tag:notplants-test-server` or `tag:orchestrator`?**
|
|
(`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 that with the tags above, so a backend with neither is unreachable no matter how
|
|
correct the mapping looks. Quick check from the gateway itself:
|
|
`ssh root@49.13.156.72 'timeout 5 bash -c "echo > /dev/tcp/<your-tailnet-ip>/<port>"'`
|
|
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.
|