Security by Design
Warden treats security as a property of the daemon, not a feature list. The threat model names every in-scope threat, every control, and every deliberate omission. This page is the other half: how the posture is built, and the layers that page does not cover.
Defense in depth
Filtering is one layer. The named threats sit on the threat model.
A query is checked at the name, at the IPs in the answer, and at the host. The fourteen classes and the eight switches of the Operator Configuration Contract live there. Out of scope items belong to a different layer.
One extra layer that page does not list: [ip_blocklists]. Every A/AAAA in an upstream answer is checked against IP feeds. Fast-flux and hijacked CDN hosts are caught even when the domain is clean. Off until you enable it.
Devices can’t escape to a public resolver
A hardcoded 8.8.8.8 or a DoH/DoT endpoint makes the filter optional. Warden owns the name. The router owns the packet.
Warden ships no resolver list. You name the domains, or you subscribe a list. Encrypted DNS sent straight to an IP is out of scope at the DNS layer.
| Escape | Control | You do | Limit |
|---|---|---|---|
| DoH/DoT hostname you name | [anti_bypass].extra_domains | list the domains | REFUSED before cache and filter. No allow rule can override. The client hears “unavailable”, not a forged address. |
| DoH/DoT hostname via catalogue | services/resolvers | subscribe the list | Goes through the filter engine. Your allows still win. The list arrives at base = "deny", so it blocks for every profile that has not overridden it to allow or ignore. |
| Hardcoded IP on port 53 | warden firewall-rules | apply the printed redirect on the router | iptables, ip6tables, and nftables. A redirect on a host that does not route the LAN catches nothing. |
| DoH/DoT straight to an IP | same command, ports 443 / 853 | apply the printed drop/reject rules | Never hits port 53. Warden prints. The firewall enforces. |
External lists are sandboxed
A subscribed list can add block rules. That is all.
@@, $important, and regex are stripped at parse. They only take effect in your config.toml. The downloader is HTTPS-only and rejects private, CGNAT, and loopback IPs. The full split is the supply-chain and SSRF rows of the in-scope table.
Other DNS filters do not enforce this. A compromised list there can unblock a domain with @@. Here it cannot.
Safe by default
A config that would expose the network does not boot. An unsafe rule does not get written.
The open-resolver check and the refuse-unknown-clients posture are in the Operator Configuration Contract. These are the checks that page does not list:
| Mistake | When | Effect |
|---|---|---|
secrets.toml mode wider than 0600 | boot | refuse to start; error prints the chmod |
Homoglyph in a rule you write (gооgle.com), reserved-IP local DNS, public-suffix wildcard (*.it, *.co.uk), device allow vs profile deny | write | CLI reject; frozen English error |
The write-time homoglyph check is for rules you type. Matching lookalikes in subscribed lists is out of scope.
Append-only audit log
What changed, who did it, when.
| Fact | Value |
|---|---|
| Path | /var/lib/purge-warden/audit/audit.log |
| Format | one JSON line per event, append-only |
| Actor | kernel SO_PEERCRED UID for daemon events; UID of the warden process for CLI mutations |
| Lifecycle extra | SHA-256 of the config tree before and after (boot, shutdown, reload, restore) |
| Per-rule CLI extra | action, scope, target_id, domain, rule_id, override_used — no config hashes |
| Mode | 0640, group purge-warden |
Read it without root:
# last 20 events
warden audit tail -n 20
Daemon runs sandboxed, not as root
A compromised resolver is still not root, and still cannot see the rest of the host.
Compromise of the Warden host is out of scope. The unit still shrinks the blast radius. It is systemd/purge-warden.service. The installer wires it. The daemon binds port 53 with AmbientCapabilities=CAP_NET_BIND_SERVICE as user purge-warden.
| Surface | Directives |
|---|---|
| Identity | user purge-warden, CAP_NET_BIND_SERVICE, never root |
| Filesystem | ProtectSystem=strict, PrivateTmp |
| Privileges | NoNewPrivileges, SystemCallFilter |
| Memory / IPC | MemoryDenyWriteExecute, LockPersonality, RestrictNamespaces, RemoveIPC |
Verify it in the code
Controls unique to this page. The fourteen named threats are on the threat model.
src/filterip_filter.rs → IpFilter::check_response()src/securityanti_bypass.rs → AntiBypass::is_bypass_domain()src/cli/commandsfirewall_rules.rssrc/config/schemavalidator.rs → validate()src/configsecrets.rs → load_secrets()src/configaudit.rs → AuditLog::append()src/ipcsocket_server.rssystemdpurge-warden.service