Configuration
The Gateway has two configuration surfaces:
-
Bootstrap config —
/var/lib/gateway/config.yaml. Read once at daemon startup. Holds anything the daemon needs before SQLite is reachable: listen address, database path, network interfaces, logging, the very first tunnel. -
Runtime settings — the
kv_settingstable in SQLite. Mutable through the web UI, the REST API, orgwctl. Holds everything the operator changes day-to-day: policies, devices, alert rules, privacy classes, DNS rules, backup schedules.
The rule of thumb: if you’d change it from the UI, it’s runtime. If you’d change it during disaster recovery, it’s bootstrap.
Bootstrap config
Section titled “Bootstrap config”Reference: configs/config.example.yaml in the source tree.
Minimal embedded Pi config
Section titled “Minimal embedded Pi config”version: "1"
listen: api_addr: "0.0.0.0:8080" unix_socket: "/var/run/gateway.sock"
log: level: "info" # debug | info | warn | error format: "json" # json | text retention_days: 30
store: db_path: "/var/lib/gateway/gateway.db"
network: mode: "embedded" # primary | embedded | portable wan_iface: "eth0" lan_iface: "eth1"
dns: listen_addr: "0.0.0.0:53" backend: "integrated" # "integrated" or "pihole" upstream_servers: ["1.1.1.1", "9.9.9.9"] filtering_enabled: true
reconcile: interval_sec: 60 # how often to scan for drift; default 60, min 5Sections reference
Section titled “Sections reference”| Section | What goes here |
|---|---|
listen |
API bind address, unix socket path, TLS toggle |
log |
Level, format (json/text), retention |
store |
SQLite path. Use a path on persistent storage (not tmpfs). |
network |
Mode + WAN/LAN interface names |
dns |
Listener, upstreams, backend (integrated / pihole), filtering toggle |
dhcp |
Optional DHCP server (only meaningful in primary mode) |
auth |
Whether auth is enforced, local-network CIDRs that auto-elevate to admin |
tls |
Certificate paths (cert_file + key_file) or ACME settings (acme_email + acme_dir + domains) |
tunnels |
Initial tunnels to bring up before SQLite is reachable |
mesh |
Tailscale / Headscale bootstrap (server URL, auth key) |
reconcile |
Drift-scan interval |
Two options — pick one:
# Option A: manual cert / key filestls: enabled: true cert_file: "/etc/gateway/cert.pem" key_file: "/etc/gateway/key.pem"
# Option B: automatic ACME (Let's Encrypt)tls: enabled: true acme_email: "admin@example.invalid" acme_dir: "/var/lib/gateway/acme" domains: - "gateway.example.invalid"The installer ships a self-signed ECDSA P-256 cert. Swap for an ACME-issued cert in production by switching to Option B and reloading the daemon.
Auth bypass for local network
Section titled “Auth bypass for local network”auth: enabled: true local_net: "127.0.0.0/8"Clients from this CIDR get auto-elevated to admin for read-only methods only (GET, HEAD, OPTIONS). State-changing verbs (POST, PUT, PATCH, DELETE) still require a bearer token or API key — a compromised IoT device on the LAN can’t POST /api/v1/system/wipe just by being on the right subnet. The field is a single CIDR string (default 127.0.0.0/8 — loopback only). Widen it only for management ranges you fully trust, and remember LAN devices still can’t mutate state from it. When auth.enabled: false (the install default) the whole middleware is bypassed and every request is admin regardless of source.
API keys
Section titled “API keys”auth.api_keys accepts both a legacy plain-string list (all admin) and per-key role objects:
auth: enabled: true api_keys: - "legacy-key-defaults-to-admin" - { key: "ops-key", role: "operator" } - { key: "readonly-key", role: "viewer" } - { key: "diag-key", role: "diagnostics" }Roles:
| Role | Allowed |
|---|---|
admin |
Everything. Default for legacy string keys. Clients in auth.local_net also get admin for reads only (see below). |
operator |
Every method on every path except /auth/*, /config/*, /secrets/*, /storage/encrypted/*, and the sensitive /system/* endpoints (wipe, restart, reboot, poweroff, deadman) |
viewer |
Read-only — GET and HEAD only, across the whole API |
diagnostics |
All GET/HEAD, plus POST to a curated list: /observer, /drift, /verify, /validate, /render, /policy/evaluate, /policy/conflicts, /enforce/reconcile, /captive/check, /dns/pihole/* |
Runtime settings
Section titled “Runtime settings”Everything below is stored in SQLite and mutable from the UI / API. You generally do not edit these by hand.
| What | Where to manage | Persists across restarts? |
|---|---|---|
| Policies, devices | UI → Routing policies / Devices | Yes |
| Privacy classes | UI → Privacy classes | Yes |
| DNS rules, zones | UI → DNS | Yes |
| Alert rules | UI → Alerts | Yes |
| Bridge nodes | UI → Bridges | Yes |
| Mesh peers (cached) | Auto-populated from Headscale / Tailscale | Yes (cache only) |
| Tunnel runtime state | UI → Tunnels | Yes (state) / runtime (handshakes) |
| Splitting weights | UI → Splitting | Yes |
See the settings reference for the complete key list.
Migrations
Section titled “Migrations”gatewayd runs goose migrations against gateway.db at startup. Migrations are embedded into the binary — no separate migration step. A failed migration aborts startup with a non-zero exit and leaves the database untouched.
Backups
Section titled “Backups”Take a backup before any destructive change. gwctl backup export writes the sanitized plaintext JSON (secrets stripped); for the AES-256-GCM-encrypted variant with secrets, use POST /api/v1/backup/export-encrypted with a passphrase:
gwctl backup export > /tmp/gateway-$(date +%F).jsonThe export is a self-contained JSON that round-trips every runtime setting, policy, tunnel definition (without keys), and device record. See backup.md for the encrypted-export path and the asymmetry on restore.
Related
Section titled “Related”- Installation — the installer writes the initial bootstrap config.
- Settings reference — every runtime
kv_settingskey. - Backup — encrypted export / restore mechanics.
- Troubleshooting — how to recover when a bad config breaks the dataplane.