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.
scrub() ended in `[ -n "$WD" ] && { ... }`. As an EXIT trap, its status becomes the
script's status, so with WD unset every read-only command exited 1 while printing the correct
answer. Callers saw a silent false failure — tests/run.sh reported 'could not read
tailscale_authkey' having successfully read it.
On 2026-08-16 a hand-rolled sops pipeline wrote plaintext to the real secrets path and only
THEN tried to encrypt it. The encrypt failed (creation_rules keyed on a filename the temp path
did not match), sops exited non-zero after the clobber had already happened, and the plaintext
file was copied to the host world-readable before anyone noticed.
The invariant here is that plaintext never exists at the destination path: every mutation
happens on a temp file in a 0700 dir, and install_encrypted() refuses to move anything into
place that is not verified ciphertext AND does not round-trip through a decrypt. A failure at
any step leaves the original untouched — verified by reproducing the incident (break the
creation rule, attempt a set, confirm the file's hash is unchanged).
Two other traps are handled because they already bit us: --config is passed explicitly, since
sops discovers .sops.yaml from the CWD and the manual runs only worked by accident of being in
the right directory; and PATH is hardened, because a tool 'not found' merely because
/run/current-system/sw/bin was absent reads as 'decryption failed', which is the wrong
conclusion entirely.
SECRETS_HOST=b1 engine/tools/secrets.sh verify|list|get|set|unset|edit|deploy