Importing configuration files
When you set up purge-warden for the first time — or migrate from Pi-hole or AdGuard Home — you usually have several sources of truth to bring together: a list of devices on your LAN, the curated blocklists you want to subscribe to, the filtering profiles you want to apply, and the groups or schedules that tie them together. Rather than pasting everything into one file, purge-warden supports a modular include layout that lets each kind of input live in its own file (or per-cohort group of files). This page walks through how to populate each one.
The conceptual model and field reference for every entity below live in ./CONFIG_GUIDE.public.md. This page is operational: where do files go, what do they look like, which command brings them in.
Overview
A purge-warden install is driven by a single master file (/etc/purge-warden/config.toml) plus optional includes — sub-directories of small TOML files that the master pulls in by glob:
schema_version = 1
[server]
listen = "0.0.0.0:53"
default_profile = "default"
allow_from = ["192.168.1.0/24", "127.0.0.0/8"]
includes = [
"devices.d/*.toml",
"subnets.d/*.toml",
"profiles.d/*.toml",
"groups.d/*.toml",
"schedules.d/*.toml",
"blocklists.d/*.toml",
]- Monolithic — everything in
config.toml. Fine for very small setups (under ~20 devices). - Split — master +
*.d/subdirectories. Recommended once you have multiple cohorts, multiple operators, or want clean git diffs.
warden config lint, and reload — or you can let warden <verb> ... --into <file> write the fragment for you. Both paths produce the same TOML.Importing a clients list
A “client” in purge-warden is a [[devices]] entry: a host on your LAN identified by IP and/or MAC, optionally bundled into one or more groups. To import many of them, drop one file per cohort under devices.d/.
TOML — devices.d/family.toml:
[[devices]]
id = "edo-laptop"
display_name = "Edoardo's laptop"
ip = "192.168.1.10"
mac = "02:00:00:11:22:33"
profile = "default"
owner = "Edoardo"
[[devices]]
id = "daughter-iphone"
display_name = "Daughter's iPhone"
mac = "02:00:00:44:55:66"
mac_aliases = ["02:00:00:77:88:99"]
groups = ["kids"]--into):warden device add edo-laptop \
--ip 192.168.1.10 --mac "02:00:00:11:22:33" \
--profile default --display-name "Edoardo's laptop" \
--owner Edoardo \
--into devices.d/family.toml
warden device add daughter-iphone \
--mac "02:00:00:44:55:66" \
--mac-aliases "02:00:00:77:88:99" \
--groups kids --display-name "Daughter's iPhone" \
--into devices.d/family.toml
After hand-editing the file, validate and apply:
warden config lint # exit 0 if valid
sudo systemctl reload purge-warden # SIGHUP — no downtime
mac_aliases so the device stays recognised.Field reference
Most setups only need a handful of fields per device — typically id, display_name, mac (or ip), and either profile or groups. The table below covers every field the daemon recognises, grouped by purpose so you know where to reach when a more specific need comes up.
Identification
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
id | string | yes | — | Unique identifier referenced by groups, subnets, schedules, and CLI commands. Stable — don’t rename once it’s in use. |
display_name | string | yes | — | Human-readable label shown in the TUI, CLI output, and audit log. |
ip | IPv4 / IPv6 | no | — | Static IP pin. Skip if you only identify the device by MAC. |
mac | string XX:XX:XX:XX:XX:XX | no | — | Primary MAC address; format validated at load. |
mac_aliases | list of MAC strings | no | [] | Additional MACs for devices that rotate (iOS / Android private wifi). Any match identifies the device. |
Profile assignment
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
profile | profile id | no | — | Direct profile assignment. Wins over groups and subnets in the resolver chain. |
groups | list of group ids | no | [] | Group memberships. Used only when no direct profile is set; the highest-priority group wins. |
profile or groups, not both. The resolver always prefers profile if it’s set, so a groups entry alongside it is dead weight.Per-device rule overlays
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
allow_rules | list of admin-rule ids | no | [] | Per-device allow overlay. Checked before profile rules. Combined cap with deny_rules: 64 soft / 128 hard. |
deny_rules | list of admin-rule ids | no | [] | Per-device deny overlay. Additive over profile-level allows. Same cap as allow_rules. |
override_profile_deny | bool | no | false | When true, a per-device allow_rules entry can override a profile-level deny on the same domain. Use sparingly — every override surfaces in the audit log. |
Metadata (informational only, no effect on filtering)
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
tags | list of strings | no | [] | Free-form labels for filtering in the TUI/CLI (e.g. ["wifi", "guest"]). |
owner | string | no | — | Who’s responsible for the device. |
device_type | string | no | — | Free-form category (e.g. "iPhone", "Smart TV"). Legacy alias device is still accepted. |
department | string | no | — | Free-form organisational unit, useful in office deployments. |
notes | string | no | — | Free-form notes — anything you’d otherwise put in a comment. |
Importing blocklists
Blocklists are the domain sources that profiles consume. purge.cc maintains a curated catalogue at https://lists.purge.cc/ (plain-domain lists) and https://rules.purge.cc/ (AdGuard-syntax rule lists). You can also point at any third-party URL you trust.
TOML — blocklists.d/curated.toml:
[[blocklists]]
id = "privacy-ads"
display_name = "Privacy: ads"
url = "https://lists.purge.cc/privacy/ads.txt"
format = "domains"
update_interval_hours = 12
[[blocklists]]
id = "oisd-big"
display_name = "OISD Big"
url = "https://big.oisd.nl/"
format = "adguard"
update_interval_hours = 6warden blocklist add privacy-ads \
--url "https://lists.purge.cc/privacy/ads.txt" \
--format domains \
--update-interval-hours 12
warden blocklist add oisd-big \
--url "https://big.oisd.nl/" \
--format adguard \
--update-interval-hours 6
format is one of domains (one hostname per line — typical for Pi-hole adlists), adguard (AdGuard rule syntax with || / @@ / $important), or hosts (/etc/hosts-style with leading IPs).
Force a fresh download right after adding:
sudo warden update
Authorization: Bearer … header, store the secret in /etc/purge-warden/secrets.toml (mode 0600) and reference it via auth_token_ref = "<key>". The daemon refuses to start if secrets.toml is world-readable.The first refresh shows one progress line per list, plus an aggregate count of unique domains parsed:
Importing profiles and groups
Profiles are the filtering bundles; groups apply a profile to a named set of devices.
TOML — profiles.d/kids.toml and groups.d/family.toml:
[profiles.kids]
display_name = "Kids — daytime"
blocklists = ["privacy-ads", "security-malicious", "categories-social"]
block_response = "nxdomain"[[groups]]
id = "kids"
display_name = "Kids in the household"
profile = "kids"
priority = 10
devices = ["daughter-iphone", "son-switch"]warden profile set kids \
--display-name "Kids — daytime" \
--blocklists "privacy-ads,security-malicious,categories-social" \
--block-response nxdomain \
--into profiles.d/kids.toml
warden group add kids \
--profile kids --priority 10 \
--devices "daughter-iphone,son-switch" \
--display-name "Kids in the household" \
--into groups.d/family.toml
blocklists id you reference must exist in some [[blocklists]] entry first. The validator rejects dangling references at lint time — import the blocklist files before you import a profile that uses them.To inspect a profile after import — useful for confirming the lists, deny and allow rules resolved as you expected:
Putting it together
A realistic split layout that pulls all the above into one server:
/etc/purge-warden/
├── config.toml # master with [server] + includes
├── secrets.toml # mode 0600
├── blocklists.d/
│ └── curated.toml # privacy-ads, oisd-big
├── profiles.d/
│ ├── default.toml # [profiles.default]
│ └── kids.toml # [profiles.kids]
├── groups.d/
│ └── family.toml # [[groups]] kids
└── devices.d/
└── family.toml # [[devices]] edo-laptop, daughter-iphone, …
The master only declares globals and the include globs:
schema_version = 1
[server]
listen = "0.0.0.0:53"
default_profile = "default"
allow_from = ["192.168.1.0/24", "127.0.0.0/8"]
includes = [
"blocklists.d/*.toml",
"profiles.d/*.toml",
"groups.d/*.toml",
"devices.d/*.toml",
]warden config lint # validate everything that will be loaded
sudo systemctl reload purge-warden # hot reload, no downtime
sudo warden update # download every blocklist now
After the reload, warden config show prints the merged view — every include is flattened into a single TOML preview, which is what the daemon actually runs:
Verifying everything imported correctly
After an import, run this checklist top-to-bottom. Each command shows you a different angle on the merged config.
warden config lint
# 2. Show the resolved view (defaults expanded, references followed)
warden config show --resolved
# 3. Are all the blocklists present and downloaded?
warden blocklist list
# 4. Are all the devices visible to the daemon?
warden device list
warden device list --live # add per-device live query counters
# 5. Do groups, subnets, profiles look right?
warden group list
warden subnet list
warden profile list
# 6. For a given client IP, what profile does the daemon pick?
warden resolve 192.168.1.10
# 7. Audit trail of every recent mutation, boot, and reload
warden audit tail -n 20warden device list), you should see one line per configured device, with the IP, MAC, and the profile the daemon assigned to it:A few sanity probes against live DNS confirm the rules are actually in effect:
dig @127.0.0.1 google.com # → normal A record
dig @127.0.0.1 doubleclick.net # → 0.0.0.0 if 'privacy-ads' is loaded
warden resolve <ip> returns the wrong profile, read the level: line in its output. It tells you which resolver tier matched (device direct, schedule override, group, subnet longest-prefix, or default). Mismatches almost always come from an unexpected device entry or a subnet priority collision — both surfaced verbatim in that output.Where to go next
- Field reference for every entity in the examples above: ./CONFIG_GUIDE.public.md.
- Migrating a Pi-hole or AdGuard Home install: (TODO: link migration-from-pihole)
- Curated blocklist catalogue:
https://lists.purge.cc/index.jsonandhttps://rules.purge.cc/index.json. - Quick start and troubleshooting: ./README.md.