Skip to content

Configuration

The Gateway has two configuration surfaces:

  1. 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.

  2. Runtime settings — the kv_settings table in SQLite. Mutable through the web UI, the REST API, or gwctl. 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.

Reference: configs/config.example.yaml in the source tree.

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 5
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 files
tls:
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:
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.

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/*

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.

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.

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:

Terminal window
gwctl backup export > /tmp/gateway-$(date +%F).json

The 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.