gateway-domain: tag:orchestrator is now allowed by the ACL too

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.
This commit is contained in:
2026-08-20 18:22:58 +00:00
parent c7cbac6fb2
commit 23391cef2b
2 changed files with 26 additions and 17 deletions
+21 -13
View File
@@ -1,6 +1,6 @@
---
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.
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
@@ -18,20 +18,26 @@ python3 engine/tools/gateway-domain.py add myapp
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
## Your box needs a tailnet tag the ACL allows
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:
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 `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.
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.
@@ -134,10 +140,12 @@ 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.
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.
+5 -4
View File
@@ -97,10 +97,10 @@ _ERR = re.compile(r'<p style="color:red">(.*?)</p>', re.S)
# /var/lib/tunnel-gateway/tunnel_map.conf can clear. Refuse to create one.
_BACKEND = re.compile(r"^(\d{1,3}(?:\.\d{1,3}){3})(?::(\d{1,5}))?$")
# The tailnet ACL only lets the gateway open connections to nodes carrying this tag.
# The tailnet ACL only lets the gateway open connections to nodes carrying one of these.
# A mapping to an untagged node is accepted by the gateway and then simply never
# connects, which looks like a gateway fault and is not one.
REQUIRED_TAG = "tag:notplants-test-server"
ALLOWED_TAGS = ("tag:notplants-test-server", "tag:orchestrator")
def _validate_backend(backend):
@@ -144,9 +144,10 @@ def _warn_untagged(exe):
tags = _self_tags(exe)
if tags is None:
return
if REQUIRED_TAG not in tags:
if not any(t in tags for t in ALLOWED_TAGS):
print(
f"warning: this node is not tagged {REQUIRED_TAG} (tags: {', '.join(tags) or 'none'}).\n"
f"warning: this node carries none of {' / '.join(ALLOWED_TAGS)} "
f"(tags: {', '.join(tags) or 'none'}).\n"
" The gateway will accept the mapping but the tailnet ACL will not let it\n"
" reach this box, so no traffic will flow. Add the tag in the Tailscale\n"
" admin, or map a backend that already has it.",