Server globals
The handful of knobs that apply to the whole resolver — listening interfaces, upstream servers, defaults.
What this is
Server globals are the top-level [server] and [upstream] sections of the
master config.toml. They control how the daemon listens (which addresses
and ports answer DNS), who is allowed to ask (allow_from), where Warden
forwards queries it cannot answer locally (upstream.servers), and which
profile applies when nothing else matches (default_profile).
Unlike devices, profiles, or blocklists — which Warden exposes through
per-entity CLI verbs — these globals have no flag-based setter. There
is no warden config set server.allow_from "...". The supported way to
change them is warden config edit, which opens the master TOML in
$EDITOR, validates at save, and (after a follow-up warden reload)
applies them to the running daemon.
When you reach for it
- First boot, after
warden init. Pick the listening interfaces, set the upstream resolvers, setallow_fromto your LAN’s CIDR. Everything else can wait. - Restricting access. You moved Warden to a host that’s reachable
from more than one network and you want to refuse queries from the WAN.
Tighten
allow_from. - Changing upstreams. Switching from your ISP’s resolver to a privacy
resolver (Quad9, Mullvad, your own DoT) — change
upstream.servers, optionallyupstream.protocol, reload. - Setting a global fallback. Devices that don’t match any
[[devices]],[[groups]], or[[subnets]]entry hit[server].default_profile. If left unset, unmapped queries getREFUSED.
You don’t edit globals to add a single device exception, schedule a
block window, or import a new blocklist — those are entity-level changes
with their own CLI verbs (warden device add, warden schedule add,
warden blocklist add).
Schema
The two relevant sections of the master config.toml:
[server]
listen = ["0.0.0.0:53", "[::]:53"] # interfaces + ports answering DNS
allow_from = ["192.168.1.0/24"] # CIDRs allowed to query
default_profile = "default" # profile for unmapped clients
enforce_client_mac = true # downgrade on MAC mismatch (see Devices)
log_level = "info" # error|warn|info|debug|trace
[upstream]
servers = ["9.9.9.9:53", "149.112.112.112:53"] # ordered fallback list
protocol = "udp" # udp|tcp|dot|doh
timeout_ms = 1500 # per-server timeout
retries = 2 # attempts before NXDOMAIN[server] field reference
| Field | Type | Required | Default | Purpose |
|---|---|---|---|---|
listen | list of host:port | yes | ["0.0.0.0:53", "[::]:53"] | Sockets the daemon binds. IPv4 and IPv6 must be specified explicitly. |
allow_from | list of CIDRs | no | ["0.0.0.0/0", "::/0"] (open) | Source addresses allowed to query. Empty list refuses every client. |
default_profile | profile id | no | unset (REFUSED) | Profile applied to clients not matched by device/group/subnet. |
enforce_client_mac | bool | no | true | If the live ARP MAC doesn’t match a device’s mac / mac_aliases, the device is downgraded to subnet-level matching. |
log_level | enum | no | info | Verbosity of the daemon log. |
[upstream] field reference
| Field | Type | Required | Default | Purpose |
|---|---|---|---|---|
servers | list of host:port | yes | — | Recursive resolvers Warden forwards to when it can’t answer locally. Tried in order. |
protocol | udp / tcp / dot / doh | no | udp | Transport for upstream queries. dot and doh require host:port plus a hostname for SNI/SANs. |
timeout_ms | u32 | no | 1500 | Per-server timeout in milliseconds. |
retries | u32 | no | 2 | How many servers to try before returning NXDOMAIN. |
For the canonical schema with all constraints and defaults, see the TOML reference.
Editing the master — the warden config edit flow
Because there is no flag-based setter for [server] and [upstream],
the canonical flow is:
- Take a backup first. A bad
[server]edit can leave the LAN without DNS —warden config backuptakes seconds and gives you a one-command rollback. warden config editopens the master in$EDITOR. On save, the handler runs the full v1 validator. If it rejects the change, the live file is unchanged and the validator prints offending file:line.warden reloadsignals the daemon to swap to the new config. Unlike per-entity CRUD verbs,warden config editdoes not trigger a reload automatically — you reload deliberately, after re-reading the diff.- Verify. A quick
warden config show --resolvedconfirms the live value, andwarden resolve <client-ip>confirms the resolver still picks the expected profile.
sudo warden config backup
sudo warden config edit
sudo warden reload
warden config show --resolved --section server
warden device add apply one well-defined change,
so reloading immediately is safe. warden config edit lets you change
anything — possibly several things at once — and the operator usually
wants to confirm the diff before the daemon picks it up. The two-step
flow exists so you can lint, diff, or back out before reloading.allow_from — restrict who can query
A common first edit. Open the master, then change the value:
[server]
allow_from = ["192.168.1.0/24"]Multiple ranges are listed as TOML array entries. Both v4 and v6 CIDRs
are accepted; an empty list (allow_from = []) refuses every client and
is normally only useful in test environments.
# preview, edit, reload
sudo warden config diff /etc/purge-warden/backups/config-20260505T091322Z.tar.gz
sudo warden config edit
sudo warden reload
upstream.servers — change the upstream resolvers
Same flow, in the [upstream] section:
[upstream]
servers = ["9.9.9.9:53", "149.112.112.112:53"]
protocol = "udp"For DoT / DoH, the entries take the form hostname:port, and the
validator enforces protocol-specific constraints — see the
TOML reference.
default_profile — the global fallback
A short edit, but think before saving: it’s the profile that any unidentified client gets, including a phone joining the guest VLAN for the first time.
[server]
default_profile = "guest"If you leave it unset and a client doesn’t match any subnet either,
Warden answers REFUSED — quiet, but it tends to confuse non-technical
users.
CLI
Globals don’t have entity-level CRUD. The relevant verbs are all under
warden config:
| Command | What it does |
|---|---|
warden config show [--section server] | Print the live [server] (or any other section). |
warden config show --resolved | Show the effective config after include resolution and defaults. |
warden config edit | Open the master in $EDITOR; validate at save. Does not auto-reload. |
warden config lint | Validate the current on-disk tree. Exit 0 clean, 1 errors, 2 warnings. |
warden config diff <other> | Compare the live master against another file or extracted backup. |
warden reload | Tell the daemon to re-read from disk after edit. |
For backups and restore around risky edits, see
backup & restore. For the full
warden config surface, see the CLI reference.
Decision precedence
[server].default_profile is level 5 — the lowest in the resolver
chain. Any device, schedule, group, or subnet match wins over it.
That’s intentional: globals are the floor, not the ceiling.
[server].listen and [upstream].* are not part of the resolver chain
at all — they govern how queries are received and forwarded, not
which profile is applied. Changing them affects every client equally.
For the full chain, see Devices → Decision precedence.
Security
allow_fromis enforced at the listener level. Refused clients receive nothing — no REFUSED response, just a dropped packet — and appear in the audit log underLISTENER_REFUSED.- Upstream credentials. If you use a DoH endpoint that requires a
bearer token, store the token in a
*.secretfile and reference it from the TOML — backup archives never include*.secret. enforce_client_macis ergonomic, not cryptographic. A determined attacker with physical access can spoof any MAC. The flag’s job is to stop a curious teenager from changing their static IP to escape the kids profile. See the threat model for the precise guarantee.
Troubleshooting
warden config edit exits 0 but the daemon still serves the old
upstream.
You skipped warden reload. Per-entity CRUD verbs reload automatically;
warden config edit does not.
Validation rejects the new TOML and points at a CIDR you didn’t
touch.
Check allow_from and upstream.servers are TOML arrays (square
brackets, comma-separated), not bare strings. The validator is strict
about types.
Clients on the LAN suddenly get REFUSED for everything.
You probably tightened allow_from and forgot to include the LAN CIDR.
Run warden config show --section server to see the live value;
restore from the pre-edit backup if needed.
The daemon refuses to start with bind: address already in use.
Another process is on port 53. Either stop it (systemd-resolved is
the usual culprit on Debian/Ubuntu — see systemd
service) or change [server].listen.
For more, see troubleshooting.
See also
warden configreference — every config verb.- Backup & restore — rollback for risky edits.
- TOML reference — every field, every constraint.
- Devices — per-device profile, the layer above the global default.
- Threat model — what
enforce_client_macactually guarantees.