Declarative configuration
Warden’s configuration is declarative: you describe the state you want, not the steps to get there. A single config.toml is enough. As the setup grows you can split it across files, the same way a NixOS system is split across modules. Warden loads them, merges them in memory, and runs that picture. The files on disk stay as you wrote them.
Why the file is the policy: Project principles. What those entities do at query time: Filtering process.
File and folder structure
Start with one file. Split a directory out when a file gets hard to read or to share. The tree below is a suggestion, not a requirement. Name files for what is in them, so git diff devices.d/iot.toml tells the story.
The master file
The master config.toml declares which fragments to load:
schema_version = 5
includes = [
"devices.d/*.toml",
"profiles.d/*.toml",
"groups.d/*.toml",
"subnets.d/*.toml",
"schedules.d/*.toml",
"blocklists.d/*.toml",
]
[server]
listen = "0.0.0.0:53"
allow_from = ["10.0.0.0/8", "192.168.0.0/16"]
[upstream]
servers = ["9.9.9.9", "1.1.1.1"]The servers values are examples — pick your own. Warden ships no default upstream.
On load, Warden expands every glob, sorts the matches, and reads each fragment. A fragment uses the same TOML schema as the master: devices.d/family.toml is just [[devices]] entries.
- A glob that matches zero files is fine.
- A file listed by name in
includesand missing from disk is a hard error.
Merge rules
Merge happens in memory on every boot, every hot reload, and every CLI write. Disk files are not rewritten. Warden never silently picks a winner.
The rules key off the TOML shape, not the file name. A [[devices]] row in misc.toml loads the same way as one in devices.d/. Convention is still useful: singletons in the master, arrays in devices.d/*.toml, profiles in profiles.d/*.toml.
| Structure | Where you see it | Across files |
|---|---|---|
| Singleton | schema_version, includes, [server], [upstream], [dnssec], [cache], [security], [anti_bypass], [ip_blocklists], [local_dns], [tracking], [socket], [api], [lists], [custom_list_limits], [resource_budget], [backup], [cluster], [node], and [[forwarding]] | Exactly one file may define it. These stay in the master. [[forwarding]] is an array but merges as a singleton: keep every [[forwarding]] row in one file. |
| Array of tables | [[devices]], [[groups]], [[subnets]], [[schedules]], [[blocklists]], [[custom_lists]], [[labels]], [[retired]] | Concatenate. Every id must be unique. Split these. |
| Named map | [profiles.<id>] | Merge by key. The same id in two files is a conflict. Split these. Profiles are not [[profiles]]. |
A phantom device that depends on file order is worse than a load that fails on the exact line. Errors name (file, line) because a provenance map rides next to the merged config.
The CLI writes the same files
Every warden command that changes configuration writes back into this tree:
--into picks the fragment. Omit it and Warden looks next to the master (devices.d/ for warden device add, groups.d/ for warden group add). One file in that directory → that file. Several → Warden refuses and asks. None → the entry goes into config.toml.
The CLI is a typing aid. The files remain what you diff, commit, and back up (tar /etc/purge-warden/).
Hot reload — full re-read, atomic swap
Editing a file does not apply it. Validate, then reload:
warden config lint && sudo systemctl reload purge-warden
warden reload does the same via the daemon’s IPC socket.
On reload, Warden re-reads every file, validates the merge, and swaps the running config only if the new picture is valid. If validation fails, the previous config keeps running. The DNS handler never sees a half-applied state.
Some sections are read only at start. A reload that changes one of them is rejected whole — reload requires daemon restart for runtime sections: … — and the previous config keeps running. Restart the daemon after changing [server] listen, log_level or tcp_timeout_secs, [cache], [socket], [api], the runtime fields of [tracking], [ip_blocklists], [anti_bypass], the enabled flags of [security], [custom_list_limits], [resource_budget], [cluster], or the [node] identity and control listener. Devices, groups, subnets, profiles, lists, Custom Lists and schedules reload without a restart. From 0.44.5, [local_dns] also reloads without a restart, global records included. On a 0.44.0 host, a [local_dns] change still needs a restart; see Troubleshooting.