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 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 # 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 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. 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 The gateway can only open connections to nodes the tailnet ACL lets it reach. Two tags qualify:
**`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: | 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 ```bash
tailscale status --json | jq -r '.Self.Tags[]?' 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 If neither tag is listed, add one to that node in the Tailscale admin (a node's tags are set
node's tags are set when it is authenticated, so this may mean re-authenticating it), or map a when it is authenticated, so this may mean re-authenticating it), or map a backend that already
backend that already has the tag. `gateway-domain.py` warns when the node it is about to map has one. `gateway-domain.py` warns when the node it is about to map carries neither, but it
lacks the tag, but it cannot see the tags of a backend you name explicitly — that one is on you. 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 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? 1. `gateway-domain.py list` — is the mapping actually there?
2. `getent hosts myapp.gtest.commoninternet.net` — should be `49.13.156.72`. 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 3. **Does your node carry `tag:notplants-test-server` or `tag:orchestrator`?**
'.Self.Tags[]?'`) This is the single most common cause. The gateway is `gateway-test-1` (`tailscale status --json | jq -r '.Self.Tags[]?'`) This is the single most common cause.
(`100.91.44.90`), tagged `tag:testing-gateway`; the ACL pairs those two tags, so an The gateway is `gateway-test-1` (`100.91.44.90`), tagged `tag:testing-gateway`; the ACL
untagged backend is unreachable no matter how correct the mapping looks. 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 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. 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. # /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}))?$") _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 # 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. # 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): def _validate_backend(backend):
@@ -144,9 +144,10 @@ def _warn_untagged(exe):
tags = _self_tags(exe) tags = _self_tags(exe)
if tags is None: if tags is None:
return return
if REQUIRED_TAG not in tags: if not any(t in tags for t in ALLOWED_TAGS):
print( 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" " 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" " reach this box, so no traffic will flow. Add the tag in the Tailscale\n"
" admin, or map a backend that already has it.", " admin, or map a backend that already has it.",