View as markdown

# Configure the gateway

Getting Started gets you running with the example config, untouched. This page covers the operational tuning you reach for as soon as you take the gateway past "kick-the-tyres" — different control plane, dashboard auth, where to bind the dashboard, systemd, state-dir hardening, and the rest.

# Transports: WireGuard, Tailscale, or both

Block presence inside the gateway {} block selects the transport. The example config uses WireGuard:

gateway {
  public_url = "https://gw.example.com"
  state_dir  = "/opt/clawpatrol"

  wireguard {
    subnet_cidr = "10.55.0.0/24"
  }
}

If your fleet already lives on a tailnet, swap (or add) embedded tsnet:

gateway {
  public_url = "https://gw.example.com"
  state_dir  = "/opt/clawpatrol"

  tailscale {
    authkey             = "{{secret:TS_AUTHKEY}}"
    funnel              = true
    tags                = ["tag:clawpatrol"]
    oauth_client_id     = "<tailscale oauth client id>"
    oauth_client_secret = "<tailscale oauth client secret>"
  }
}

Embedded tsnet joins the tailnet in-process — no UDP port to open, no iptables rule, no host Tailscale daemon. funnel = true lets non-tailnet devices reach the gateway over Tailscale Funnel.

Both blocks can coexist — peers from either transport land in the same MITM handler. Drop either block to disable that transport.

# Required tailnet ACL

The gateway routes client traffic by acting as their Tailscale exit node. Clients call EditPrefs(ExitNodeIP=<gateway>) once they join; routing only works if the tailnet ACL auto-approves the gateway as an exit node for the client tag. Without this, clients set the pref locally but every outbound dial silently times out — the gateway never sees the traffic.

In your tailnet's ACL JSON, add (or extend) autoApprovers:

{
  "autoApprovers": {
    "exitNode": ["tag:clawpatrol"]   // matches tailscale.tags above
  },
  "tagOwners": {
    "tag:clawpatrol": ["autogroup:admin"]
  }
}

The tag must be the one you set in tailscale.tags on the gateway config. If you skip this step, the daemon logs a tsnet probe: gateway unreachable via exit-node routing — check autoApprovers.exitNode in your tailnet ACL warning on first boot, and every clawpatrol run hangs at "joining tailnet".

# WireGuard endpoint

The default WireGuard listener is :51820 (set wireguard.listen_port to override). Clients dial host(public_url):port, so you only set wireguard.endpoint when you need a non-default port advertised to clients or a different host for WG than for the dashboard:

wireguard {
  subnet_cidr = "10.55.0.0/24"
  listen_port = 41820                     # server binds this UDP port
  endpoint    = "wg.example.com:51820"    # advertised in client wg.conf
}

# Single-host (loopback) WireGuard

Running the gateway and clawpatrol run on the same machine — the gateway under one user account, agents launched from another — is a supported deployment, not a debug mode. Pin the advertised endpoint to loopback so onboarded clients dial the gateway over the loopback interface:

gateway {
  state_dir = "/opt/clawpatrol"

  wireguard {
    subnet_cidr = "10.55.0.0/24"
    endpoint    = "127.0.0.1:51820"
  }
}

No public_url, no public UDP port. Useful for tightly-scoped agent sandboxes that share a host with the gateway.

# Dashboard auth

The dashboard is how operators connect endpoint credentials and inspect live traffic, so it requires a password on every request.

The first time you open the dashboard you set a root password. It lives bcrypt-hashed in clawpatrol.db and is checked on every subsequent request. You can also manage the password from the CLI:

clawpatrol gateway --set-dashboard-password '<password>' gateway.hcl
clawpatrol gateway --reset-dashboard-password gateway.hcl

# Where to bind the dashboard

gateway.dashboard_listen is the dashboard's host-side HTTP bind. The shapes that make sense for it — and the auth shortcuts available on top of the root password — depend on which transport blocks you declared, because each transport automatically exposes the dashboard on its own overlay network as well.

# With wireguard {} declared

The in-tunnel forwarder routes any connection to the dashboard port on the gateway's WG IP to the dashboard, so joined devices reach http://<gateway-wg-ip>:8080 with nothing extra configured. dashboard_listen only controls who can reach the dashboard from outside the tunnel:

Without a tailscale {} block there's no tsnet whois identity to match against, so the root password is the only auth.

# With tailscale {} declared

The embedded tsnet node always serves the dashboard on the gateway's tailnet IP at the dashboard port, so tailnet peers reach http://<gateway-tailnet-ip>:8080 with no extra configuration. dashboard_listen controls the host-side bind:

Operators can be allowlisted by tailnet identity email so they skip the root-password prompt:

tailscale {
  authkey   = "{{secret:TS_AUTHKEY}}"
  operators = [
    "alice@example.com",
    "*@example.com",
  ]
}

Each entry is matched against the requesting peer's tsnet whois login on every dashboard request. Tagged devices (your agents) have a tag-name login, not a user email, so they never match a wildcard entry — agent peers can never inherit operator powers through this path.

funnel = true exposes a small allowlist of public-bootstrap routes (/api/onboard/{start,poll,claim}, /api/cred/*) on <node>.ts.net:443 so off-tailnet devices can join and OAuth callbacks can land. The dashboard itself is not Funnel-reachable — Funnel does not replace the tailnet (or SSH) path for operator access.

# Run under systemd

For anything beyond a quick test, run the gateway as a dedicated service user so its state directory isn't readable by any non-root user on the box:

useradd --system --home /opt/clawpatrol --shell /usr/sbin/nologin clawpatrol
chown -R clawpatrol:clawpatrol /opt/clawpatrol
chmod 700 /opt/clawpatrol

Drop the following at /etc/systemd/system/clawpatrol-gateway.service, adjusting the three paths to wherever you put the binary and config:

[Unit]
Description=clawpatrol gateway
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=clawpatrol
Group=clawpatrol
WorkingDirectory=/opt/clawpatrol
ExecStart=/usr/local/bin/clawpatrol gateway /opt/clawpatrol/gateway.hcl
Restart=on-failure
RestartSec=2
LimitNOFILE=65536

[Install]
WantedBy=multi-user.target

Then:

systemctl daemon-reload
systemctl enable --now clawpatrol-gateway
journalctl -u clawpatrol-gateway -f       # tail the gateway log

If you skip the dedicated-user step, the gateway logs a warning at startup when state_dir or clawpatrol.db is readable beyond owner.

# Security notes

A few footguns worth knowing about before you point an agent at a production Claw Patrol gateway:

# Build from source

Released binaries are the supported path. To build from source instead — for example to track an unreleased branch — set CLAWPATROL_FROM_SOURCE=1 on the installer (requires Go):

curl -fsSL https://clawpatrol.dev/install.sh | CLAWPATROL_FROM_SOURCE=1 sh

Set CLAWPATROL_REF to install a non-main ref.

# What's next