Common failure scenarios and how to resolve them. Each entry follows the same layout: symptom (what the operator sees), quick check (commands to confirm the diagnosis), root causes, remediation.
For the install walkthrough see deployment.md. For
the upgrade and rollback flows see upgrade.md.
Symptom. Every public request returns
HTTP/1.1 421 Misdirected Request regardless of path.
Quick check.
curl -vk -H "Host: example.your-domain.example" https://edge-1.your-domain.example/
sudo journalctl -u gateway-proxy -p info --since "10m ago" | grep -i tenant
# Look for: tenant.loaded host=example.your-domain.exampleRoot causes.
- Tenant not registered on the hub.
- Tenant registered but the edge has not received the delta yet (transient race during a fresh install).
- Client
Hostheader does not match the tenanthostfield exactly after lowercasing. - Tenant
assigned_nodesexcludes this edge.
Remediation. For (1), register the tenant — see
deployment.md. For (3), confirm the
host: field byte-matches the client Host after lowercasing. For
(4), check assigned_nodes: — the default "*" matches every edge.
Symptom. The installer's admin URL returns 404 with empty body
and no Content-Type.
Quick check.
curl -vI https://hub.your-domain.example:9080/EXAMPLE-SLUG/EXAMPLE-TOKEN-A/EXAMPLE-TOKEN-B/
grep -A5 '^admin:' /etc/gateway/config.yamlRoot causes.
admin.enabled: falseinconfig.yaml.- Slug or token in URL does not match the value in
config.yaml(constant-time match returns 404 on any mismatch). - Operator IP is in soft-backoff or hard-banned state from earlier probes (defaults: 30s soft / 1h hard).
- URL is missing the trailing slash.
- Gate values were rotated by a re-installer run.
Remediation. For (1), set admin.enabled: true and ensure all
three fields are at least 32 characters; restart. For (2) and (5),
compare the URL byte-for-byte against admin: in config.yaml. For
(3), wait for the timer to expire or restart the binary to clear the
in-memory tracker. For (4), append the trailing slash. If the URL is
genuinely lost, see I lost my admin tokens.
Symptom. Every public request returns 502 or 504. Edge logs show
socks dial failed or no live backends.
Quick check.
curl -sS http://10.0.0.1:9080/v1/health
sudo journalctl -u gateway-hub --since "5m ago" | grep -i tor
ps -ef | grep -E 'tor( |$)' | grep -v grepRoot causes.
- Tor pool not finished bootstrapping (cold-start: 30-60s for first
instance, 2-3min for
min_instances=4). - Every Tor instance crashed (misconfigured
torbinary, missingdata_dir, port already in use). - Hub's wg interface is down; edge cannot reach
10.0.0.1. - Edge
transport.hub_addris wrong. - Negative cache marked every backend cold after a real upstream incident (clears on its own; default TTL 5 minutes).
Remediation. For (1), wait — /v1/health shows bootstrapping
until ready. For (2), look at pgrep -a tor; common: tor not on
$PATH, data_dir not writable by gateway, SOCKSPort already
in use. For (3), restart wg-quick@wg0 on both sides. For (5),
toggle the tenant enabled: false then enabled: true to clear
the cache for that tenant's backends.
Symptom. Mirror verdicts flip to unknown and stay there. Hub
logs show check-host: 429 Too Many Requests on every poll cycle.
Quick check.
sudo journalctl -u gateway-hub --since "30m ago" | grep -c 'check-host: 429'
curl -sS -b cookies.txt "$HUB/$SLUG/$T1/$T2/api/settings/checkhost" | jqRoot causes.
intervaltoo aggressive (defaults to 15 minutes; 1 minute against a fleet of 20 mirrors trips check-host's per-IP limit).- Hub's egress IP is shared with a noisy neighbour at the provider.
regionslist too long (50 mirrors x 6 regions x 4/hour = 1200 requests/hour from one IP).
Remediation. Raise interval and trim regions:
curl -sS -b cookies.txt -X PUT \
-H "Content-Type: application/json" -H "X-CSRF-Token: $CSRF" \
-d '{"interval":"30m","regions":["us1","de1","jp1"],"threshold_pct":50}' \
"$HUB/$SLUG/$T1/$T2/api/settings/checkhost"For (2), the only mitigation is moving the hub to a different egress
IP. check-host's rate limit is global per IP and there is no
API-key tier the gateway uses. See checkhost.md for
the polling architecture.
Symptom. Operator drops a tenant YAML into
$HUB_DATA_DIR/runtime/tenants/; the hub does not pick it up. File
is present and readable; tenant absent from GET /api/tenants.
Quick check.
sudo -u gateway cat /var/lib/gateway/hub/runtime/tenants/example.your-domain.example.yaml | head
sudo journalctl -u gateway-hub --since "5m ago" | grep -E 'fsnotify|reload|tenant'
touch /var/lib/gateway/hub/runtime/tenants/example.your-domain.example.yamlRoot causes.
- Filesystem does not propagate inotify events. Most common: btrfs
subvolumes with
nodatacow, bind-mounts inside Docker. - NFS mount (no inotify support; hub falls back to its 60-second poll cycle).
- User inotify-watch limit exhausted (default 8192).
- Editor wrote a
.swpor.tmprather than the final*.yaml.
Remediation. For (1), move $HUB_DATA_DIR to ext4 or xfs;
confirm with inotifywait -m <dir>. For (2), accept the 60-second
latency or move state off NFS. For (3):
sudo sysctl -w fs.inotify.max_user_watches=524288
echo 'fs.inotify.max_user_watches=524288' | sudo tee /etc/sysctl.d/40-inotify.confFor (4), use install rather than the editor's tempfile flow:
sudo install -o gateway -g gateway -m 0640 \
/tmp/example.your-domain.example.yaml \
/var/lib/gateway/hub/runtime/tenants/example.your-domain.example.yamlSymptom. Edge cannot ping 10.0.0.1. wg interface is up on both
sides but no traffic flows.
Quick check.
# On the edge:
sudo wg show wg0 # latest handshake within 2 minutes; transfer > 0
ping -c 3 -W 2 10.0.0.1
# On the hub:
sudo wg show wg0 peers # edge's pubkey present with correct allowed-ipsRoot causes.
- Peer not configured on the hub (operator never ran the
wg setline printed by the edge installer). AllowedIPsmismatch (e.g.10.0.0.0/24instead of10.0.0.42/32).- UDP port 51820 blocked between edge and hub.
- Edge's wg
Endpointpoints at the wrong hub address. - Both sides have the same wg public key (copy-paste mistake).
Remediation. For (1), run the printed wg set line on the hub:
sudo wg set wg0 peer EDGE_WG_PUBLIC_KEY allowed-ips 10.0.0.42/32
sudo wg-quick save wg0For (3), switch to transport=https_tunnel (TCP :8443); requires
reinstalling both sides. For (5), regenerate the edge's wg key pair
and re-add the peer on the hub with the new pubkey.
Symptom. Browser keeps bouncing between a 302 and the admin URL; address bar shows the slug and tokens repeatedly. No admin page renders.
Quick check.
curl -sS -L -c cookies.txt -b cookies.txt -D - \
"https://hub.your-domain.example:9080/EXAMPLE-SLUG/EXAMPLE-TOKEN-A/EXAMPLE-TOKEN-B/" \
-o /dev/null
# Look for: single 302 followed by 200, with Set-Cookie: gw_adm=...
date -u
curl -sI https://hub.your-domain.example:9080/ | grep -i '^date:'Root causes.
- Admin URL is HTTP, not HTTPS. The gate sets
Secure: trueunconditionally; browsers refuseSecurecookies on plain HTTP. - Browser private window with cookies disabled for this origin.
- Clock skew larger than 60 seconds between browser and hub (cookie's absolute-TTL fails).
- Reverse proxy in front of the hub strips
Set-Cookie, theSecureflag, the path attribute, orHttpOnly.
Remediation. For (1), install behind TLS — the production path
is public HTTPS in socks5_tls mode or SSH port-forward to a
trusted localhost. Curl works without TLS for diagnostics because
curl does not enforce Secure. For (3), run chrony or
systemd-timesyncd on both sides. For (4), configure the upstream
proxy to pass Set-Cookie and Cookie verbatim with no rewriting
of Secure, the cookie name, or the path.
Symptom. Every PUT, POST, PATCH, DELETE returns 403 with body
{"error":"csrf"}. GET calls succeed.
Quick check.
CSRF=$(curl -sS -b cookies.txt -D - \
"$HUB/$SLUG/$T1/$T2/api/me" -o /dev/null \
| awk -F': *' 'tolower($1)=="x-csrf-token"{print $2}' | tr -d '\r\n')
echo "CSRF=$CSRF"
sudo tail -n 5 /var/lib/gateway/hub/audit/$(date -u +%Y-%m-%d).jsonlRoot causes.
- Client did not echo
X-CSRF-Tokenat all. - Client cached an old token after a session refresh.
- Reverse proxy stripped the
X-CSRF-Tokenrequest header. - Cookie scope mismatch after a slug rotation.
Remediation. For (1), always read X-CSRF-Token from a
safe-method response before issuing a mutation; see
admin.md for the canonical pattern. For (2),
after a 403 on a mutation, re-read /api/me and retry once. For
(3), add X-CSRF-Token to the WAF's allow-list of forwarded headers.
Symptom. systemctl start fails. Journal shows a Go panic, a
config error, or an address already in use.
Quick check.
sudo journalctl -u gateway-hub -n 100 --no-pager
sudo ss -tnlp | grep :9080Root causes.
- Config validation rejected a field (the message names it).
- Required port already bound by another process.
gatewayuser cannot read config or write data dir.- mTLS CA key file missing or wrong mode (binary refuses to load
anything other than
0600). torbinary not on$PATH(hub and local mode only).
Remediation. Common config diagnostics:
admin.slug too short— needs 32+ characters when admin is enabled.transport.kind invalid— must bewireguard,https_tunnel, orsocks5_tls.tenants directory not readable—chown -R gateway:gatewayon$HUB_DATA_DIR.
Fix permissions:
sudo chown -R gateway:gateway /etc/gateway /var/lib/gateway /var/log/gateway
sudo chmod 0750 /etc/gateway /var/lib/gateway
sudo chmod 0640 /etc/gateway/config.yaml
sudo chmod 0600 /etc/gateway/hub-ca.key /etc/gateway/wg-hub-private.keyFor (5), install tor and confirm with which tor.
Symptom. Operator no longer has the admin URL.
Quick check. sudo grep -A5 '^admin:' /etc/gateway/config.yaml.
If the slug and tokens are present, reassemble:
https://hub.your-domain.example:9080/<slug>/<token1>/<token2>/.
Remediation. If config.yaml is also lost, regenerate on the
host (requires SSH):
sudo systemctl stop gateway-hub # or gateway-proxy / gateway-door
SLUG=$(openssl rand -hex 16)
T1=$(openssl rand -hex 16)
T2=$(openssl rand -hex 16)
echo "slug: $SLUG"; echo "token1: $T1"; echo "token2: $T2"
# Edit /etc/gateway/config.yaml — replace admin.slug, admin.token1,
# admin.token2 with the values above. Save and restart.
sudo systemctl start gateway-hubThe new URL takes effect on the next visit; old session cookies are invalidated because the cookie path no longer matches. Rotation is per-binary by design — rotating on a hub leaves edges and doors unaffected. Record the new URL in the operator's secrets manager; the installer's one-time print path is intentionally narrow.
Symptom. First start of gateway-proxy fails to obtain a TLS
cert. Logs show acme: HTTP-01 challenge failed.
Quick check.
dig +short edge-1.your-domain.example
curl -v http://edge-1.your-domain.example/.well-known/acme-challenge/test
sudo ss -tnlp | grep ':80 'Root causes.
- DNS A/AAAA points at the wrong IP, or has not propagated.
- Port 80 blocked at the firewall.
- Another HTTP server already bound to port 80 (Apache or nginx default install on a fresh VPS).
- ACME rate limit hit (Let's Encrypt: 50 certs per registered domain per week).
Remediation. For (1), fix the DNS record and wait out the TTL. For (2), open port 80 in the cloud provider's firewall and any local iptables. For (3), stop the conflicting service. For (4), use Let's Encrypt's staging environment for development:
acme:
directory: https://acme-staging-v02.api.letsencrypt.org/directorySymptom. Edge installer's mTLS step fails. Logs show
POST /v1/nodes/register failed: 401 Unauthorized or
connection refused.
Quick check.
ping -c 3 10.0.0.1
curl -sS http://10.0.0.1:9080/v1/health
grep node_secret /etc/gateway/config.yamlRoot causes.
- wg tunnel is not up; registration runs over the same transport.
node_secretin edge config does not match the hub's expected value (manual edit can desync them).- Hub's CA is not initialized (hub install never completed).
- Hub already has a node with the same
node_id.
Remediation. For (1), restart wg-quick@wg0 on both sides. For
(2), rerun the edge installer's registration step from scratch. For
(3), check the hub's logs at install time and rerun
install-hub.sh. For (4), deregister the old node first:
curl -sS -b cookies.txt -X DELETE \
-H "X-CSRF-Token: $CSRF" \
"$HUB/$SLUG/$T1/$T2/api/nodes/edge-1"Then retry the edge registration.