EN

Profiles

Work in progress — content may be incomplete.

A profile is the policy. Devices, groups, and subnets are how Warden decides which policy to apply.

What this is

A profile is a named bundle of filter behaviour: which subject areas to filter, which admin rules to layer on top, what shape the canned answer takes when a query is denied, and — optionally — static DNS records, name rewrites, SafeSearch, and an EDNS Client Subnet policy. Every entity that resolves to a filter decision — devices, groups, subnets, and the global default_profile — points at a profile by id.

A profile does not enumerate blocklists. It declares tags, and a list applies when its own tags intersect the tag set the query resolved to. That indirection is the single most important thing on this page; everything else is detail.

This page covers when to add a [profiles.<id>] entry, every dimension you can tune on it, the CLI / TUI / API surfaces that read and write profiles, and how a profile’s contents interact with the per-device overlay. For the bigger picture of how a query gets matched to a profile in the first place, see filtering process and decision precedence below.

When you reach for it

Create one profile per filter behaviour you want to express, not one per device. A typical setup ends up with three to five:

  • A default profile for anything on the LAN, tagged for the privacy and security subject areas.
  • A kids profile that adds the adult-content tag, denies a few specific domains, turns on SafeSearch, and switches the block response to nxdomain so iOS apps stop retrying.
  • A kids-night profile (block_all = true plus a narrow admin allowlist), bound by a schedule between 21:00 and 07:00.
  • An iot profile tagged for manufacturer telemetry, with a few local_records pinned so the smart-TV apps stop calling home.
  • A work profile carved out of the kids profile with classroom domains added back.

You don’t need a separate profile for one-off exceptions. A single domain that should be blocked or allowed for one person belongs in [[devices]].allow_rules / deny_rules — see devices.

You do need a separate profile whenever an exception must not bleed. Profiles are flat — there is no extends, no inheritance, no merging of two profiles. warden group allow and warden group deny write to the group’s underlying profile, so the rule reaches everything else bound to that profile too.

Schema

A profile is a TOML map keyed by id — unlike [[devices]], [[blocklists]], or [[subnets]], profiles use [profiles.<id>], not array-of-tables. The id (the part after the dot) is what every other entity references.

toml
[profiles.kids]
display_name     = "Kids profile"
tags             = ["ads", "tracking", "malicious", "adult-content"]
block_response   = "nxdomain"
blocked_ttl_secs = 300
admin_rules      = ["kids-allow-classroom"]
block_all        = false
safe_search      = true

How lists attach: tag intersection

Both sides declare subject areas. The engine intersects them.

text
effective_tags(device) = device.tags ∪ profile.tags ∪ group.tags   (source has a [[devices]] row)
                       = profile.tags ∪ subnet.tags                (anonymous source, no device row)

group.tags = union across every group the device belongs to

a blocklist applies  <=>  blocklist.tags ∩ effective_tags  is non-empty

Read the two branches carefully — they are not cumulative. A source with an explicit [[devices]] row takes device.tags ∪ profile.tags ∪ group.tags and the subnet contributes nothing, even when the IP falls inside a configured CIDR. Subnet tags exist for sources Warden has never seen before.

Three consequences worth internalising:

  • Either side empty means the list never applies. A profile with tags = [] filters no lists at all; so does a blocklist with no tags. The intersection short-circuits on emptiness before anything else.
  • Adding a list is a zero-touch operation on profiles. Tag a new feed ads and every profile already asking for ads picks it up on the next reload. You never edit a profile to subscribe it.
  • A typo is not an error. Tags have no registry — they exist by being used. tags = ["addult-content"] is a perfectly valid slug that intersects nothing. Warden emits a WARN and keeps running; see troubleshooting.

List state gates the intersection as a second step. A list whose fetch failed keeps protecting from its cached copy; a list with no cache contributes nothing. Source: src/profiles/profile.rs:448-478 (list_applies).

Setting profile tags
warden profile tag add|remove <id> <tag> writes profile.tags — idempotent, the slug is validated at the CLI boundary, and a real change triggers a hot reload. The IPC layer backs it: ProfileUpdatePatch carries a tags field, so the TUI profile editor’s tags chip picker sets them too. The supply side has warden blocklist tag add|remove, and the other demand sides have warden device tag, warden group tag, and warden subnet tag. Use warden tags list to see the live tag landscape before you type a slug.

Full tag reference: tags. Slug grammar is ^[a-z][a-z0-9-]{0,31}$ — lowercase-letter-led, no trailing dash, 32 bytes max.

Dimensions

A profile is a vector across these independent axes. Every dimension has a sensible default and is optional except the id.

GroupDimensionTOML fieldWhat it tunes
Identityid(map key)Stable cross-reference. Every device, group, subnet, and schedule that wants this profile names this id.
Display namedisplay_nameOperator-facing label shown in the TUI and CLI listings.
List selectionSubject-area tagstagsThe only lever on which blocklists apply. Intersected against each list’s own tags.
Custom rulesAdmin-rule referencesadmin_rulesThe only path for @@ allow, $important, and regex patterns — external blocklists are sandboxed away from those powers.
Wire-level responseAnswer shape on a blockblock_responsezero (default), nxdomain, refused, soa_nodata. See the table below.
TTL on the block answerblocked_ttl_secsPer-profile override; falls back to [server].default_blocked_ttl_secs.
ModeDeny-by-default switchblock_allWhen true, every query is denied except those covered by an admin allow rule.
Static DNSPer-profile recordslocal_recordsA/AAAA/CNAME entries served before going upstream. Consulted before the global [local_dns] table.
RewritesName-to-name redirectsrewrite_rulesApplied after the block check and before the upstream forward, so a rewrite cannot bypass filtering on the original name.
SafeSearch presetsafe_searchOne bool; injects eight vendor-documented search-engine rewrites at resolve time.
UpstreamECS policy[profiles.<id>.ecs]Per-profile EDNS Client Subnet override. Gated by the [upstream.ecs].enabled master switch.
Schedule binding (external)Schedules pointing at this profile[[schedules]].profileA schedule swaps a device’s or group’s profile to this one inside its window.
Resolver bindings (external)Devices, groups, subnets, and [server].default_profile referencing this id(other entities)Profiles are referenced; they don’t reference back. Removing a referenced profile is refused.

Things that look like profile dimensions but aren’t:

  • Explicit blocklist subscriptions. Removed. blocklists and categories are no longer fields; tags replaced both.
  • Profile inheritance. Profiles are flat — there is no extends. Composition happens at the tag layer: a device-backed source unions device ∪ profile ∪ group, a device-less source unions profile ∪ subnet. Device and subnet never contribute together.
  • Cache TTL is global ([cache]) — only the block-response TTL is per-profile.
  • Upstream forwarder choice is global ([[forwarding]]) — you can’t pin “kids uses Quad9, work uses Cloudflare”. ECS is the one upstream knob that is per-profile.
  • Per-profile rate limits — RRL is global ([security]).

Field reference

FieldTypeRequiredDefaultPurpose
<id> (map key)string, [a-z0-9-], 1–64 bytesyesStable cross-reference.
display_namestringno""TUI / CLI label.
tagslist of tag slugs, ^[a-z][a-z0-9-]{0,31}$no[]Subject areas. Intersected with each blocklist’s tags to decide applicability.
admin_ruleslist of admin-rule idsno[]Operator-curated rules; the only place @@, $important, and regex are honoured.
block_responseenumnoinherits [server].default_block_responsezero | nxdomain | refused | soa_nodata.
blocked_ttl_secsu32 (seconds)noinherits [server].default_blocked_ttl_secsTTL stamped on the canned block answer.
block_allboolnofalseWhen true, denies every query except admin-allow matches.
local_recordslist of LocalDnsRecordno[]Per-profile A/AAAA/CNAME, with optional ttl_secs and match_subdomains. Consulted before the global table.
rewrite_ruleslist of RewriteRuleno[]Name-to-name rewrites, applied post-filter and pre-forward.
safe_searchboolnofalseInjects the curated search-engine rewrite set. See SafeSearch.
ecssub-tablenoinherits [upstream.ecs]mode (off | coarse | subnet), plus source_prefix_v4 / source_prefix_v6 under subnet. Each field falls back independently.

#[serde(deny_unknown_fields)] is on — typos fail at config load with unknown field. That includes the retired v1 keys: a profile carrying blocklists or categories is rejected, not migrated in place. Use warden migrate to convert a pre-tag config; it maps every kind = "deny" list to tags = ["uncategorized"] and every kind = "allow" list to tags = []. Source: src/config/schema/profile.rs:60-130.

Block response variants

TOML valueWire effectWhen to use
zero (default)0.0.0.0 for A, :: for AAAA, NOERROR.Fast client give-up. Browsers stop retrying within milliseconds. The right default.
nxdomainRCODE=NXDOMAIN.Apps treat the name as missing and cache that aggressively. iOS apps especially stop retrying. Some stub resolvers fall through to a secondary DNS — bypassable in that case.
refusedRCODE=REFUSED.Stub resolvers fall through to the next configured DNS. Useful only when Warden is one of several upstreams on purpose. Always used by safety paths regardless of profile setting.
soa_nodataNOERROR + empty answer + authority SOA.RFC 2308 negative-cache friendly. Best for noisy queriers (tablets, IoT) that honour the SOA minimum TTL and stop re-asking.

All four are settable from the CLI, plus clear to fall back to the server global.

Examples

A household default — privacy and security subject areas, fast give-up:

/etc/purge-warden/profiles.d/default.toml
toml
[profiles.default]
display_name   = "Home default"
tags           = ["ads", "tracking", "malicious"]
block_response = "zero"

The lists that satisfy it live on the other side of the intersection:

/etc/purge-warden/blocklists.d/core.toml
toml
[[blocklists]]
id   = "oisd-basic"
url  = "https://big.oisd.nl/domainswild"
kind = "deny"
tags = ["ads", "tracking"]

[[blocklists]]
id   = "urlhaus"
url  = "https://urlhaus.abuse.ch/downloads/hostfile/"
kind = "deny"
tags = ["malicious"]

A kids profile — one extra tag, NXDOMAIN to short-circuit retries, SafeSearch on, one school allow on top:

/etc/purge-warden/profiles.d/kids.toml
toml
[profiles.kids]
display_name     = "Bambini"
tags             = ["ads", "tracking", "malicious", "adult-content"]
admin_rules      = ["kids-allow-classroom"]
block_response   = "nxdomain"
blocked_ttl_secs = 300
safe_search      = true

A kids-night profile bound to a schedule — deny everything except a narrow admin allowlist. The profile declares no tags at all: under block_all the list layer is irrelevant, because nothing reaches it. The local_records entry still resolves, because local records are answered before the filter runs.

/etc/purge-warden/profiles.d/kids.toml
toml
[profiles.kids-night]
display_name = "Bambini, notte"
tags         = []
block_all    = true
admin_rules  = ["kids-night-bedtime-audio"]

[[profiles.kids-night.local_records]]
name  = "homework.lan"
type  = "A"
value = "10.10.1.20"
/etc/purge-warden/schedules.d/family.toml
toml
[[schedules]]
id          = "kids-bedtime"
target_type = "group"
target_id   = "kids"
profile     = "kids-night"
days        = ["mon", "tue", "wed", "thu", "sun"]
start       = "21:00"
end         = "07:00"

An IoT profile that filters telemetry, suppresses ECS so the vendor CDN learns nothing about the LAN, and pins one local override:

/etc/purge-warden/profiles.d/iot.toml
toml
[profiles.iot]
display_name   = "IoT"
tags           = ["telemetry", "malicious"]
block_response = "soa_nodata"

[profiles.iot.ecs]
mode = "off"

[[profiles.iot.local_records]]
name             = "samsungcloudsolution.com"
type             = "A"
value            = "0.0.0.0"
match_subdomains = true
Tip
For more than a handful of profiles, split them across profiles.d/*.toml (one file per persona — default.toml, kids.toml, iot.toml, work.toml). The master config.toml pulls them in via includes. warden profile allow and warden profile deny take --into <file> to route the write to the right file.

CLI

The subcommand is warden profile (singular). Every mutating verb triggers a hot reload of the running daemon (atomic config swap, no restart). Read-only verbs work without the daemon.

CommandWhat it does
warden profile listList configured profiles with a one-line summary.
warden profile show <id>Full detail dump of one profile.
warden profile create <id> --display-name <n>Create a profile. Refuses if the id already exists.
warden profile update <id> --display-name <n>Change the label.
warden profile block-response <id> <variant>Set the wire-level block answer. Variants: zero | nxdomain | refused | soa_nodata | clear.
warden profile blocked-ttl <id> <secs>Set the block-answer TTL. 0 clears it (inherit the server global).
warden profile block-all <id> <true|false>Toggle deny-by-default.
warden profile admin-rule-add <id> <rule_id>Reference an existing [[admin_rules]] row. Create the row first with warden rules.
warden profile admin-rule-remove <id> <rule_id>Drop the reference. The rule row itself stays.
warden profile tag add|remove <id> <tag> [--into <file>]Add or remove a subject-area tag — the writer for profile.tags. Idempotent; the slug is validated at the CLI boundary; a real change triggers a hot reload.
warden profile allow <id> <domain> [--id <rule_id>] [--remove] [--into <file>]Synthesise an `@@
warden profile deny <id> <domain> [...same flags]Symmetric — synthesises ||domain^.
warden profile ecs <id> --mode <m> [--prefix-v4 N] [--prefix-v6 N]Set the per-profile ECS policy. Prefixes apply only under --mode subnet.
warden profile ecs-clear <id>Drop the [profiles.<id>.ecs] subtree so the profile inherits [upstream.ecs].
warden profile remove <id>Delete the profile. Refused if any device, subnet, or schedule still references it.

There is no verb for local_records or rewrite_rules — those are hand-edited TOML plus warden reload. tags, by contrast, has the dedicated warden profile tag verb above. There is no warden profile blocklists either; it was removed with the field it wrote to.

Common patterns:

bash
# Tag a list so every profile asking for that subject area picks it up
sudo warden blocklist tag add oisd-basic ads

# Check the live tag landscape before typing a new slug
warden tags list

# Block a domain on the kids profile only
sudo warden profile deny kids tiktok.com

# Switch RCODE shape so iOS stubs stop falling through to a secondary
sudo warden profile block-response kids nxdomain

# Inspect what would happen for an IP, no daemon needed
warden resolve 10.10.1.107

After every hot-reload-aware mutation, exactly one of four messages is printed — the same set used by every CLI write path:

  • daemon reloaded — change is live
  • daemon not running — change will take effect on next start
  • note: change landed on disk but no admin token is available to request a daemon reload. Run warden token generate or restart the daemon to activate.
  • warning: change landed on disk but the daemon rejected the reload (<msg>). Check journalctl -u purge-warden and consider systemctl restart purge-warden.

Source: src/cli/mod.rs:536-630, src/cli/commands/profiles.rs.

TUI

There is a dedicated Profiles tab — the fourth leaf of the Network section, an offline-backed master/detail view over [profiles].

TabProfile relevanceUseful keys
ProfilesMaster list plus detail card. Add / Edit / Delete drive the ProfileCreate / ProfileUpdate / ProfileDelete IPC verbs directly. The edit modal covers display_name, block_response, blocked_ttl_secs, block_all, admin_rules, ecs, and a tags chip picker.a add, e edit, d delete.
TagsEvery slug in use across blocklists, devices, profiles, and subnets, with usage counts. Create, rename, delete for non-system tags; uncategorized refuses both.c create, R rename, d delete unused, / search.
DevicesProfile column per row. The detail card separates the device’s own tags from those inherited via the profile, and flags UNFILTERED. Group-by profile is one of the cycle options.e edit, a add, G cycle group-by axis.
ListsEach blocklist row shows which profiles reach it through tag intersection.Enter edit, a add, B catalog picker, K toggle block/allow.
Local DNSTwo panels — the global table and the per-profile records.o switch panels, n/N cycle profile, a add.
RulesPer-profile and per-device admin rules.f cycle filter.

The Resolver inspector (s from any leaf) takes a source IP and shows the full chain — matched device, match level, active profile, active schedule. It’s the fastest way to answer “why does this client get that profile?”.

The edit modal’s tags chip picker writes profile.tags over IPC (ProfileUpdatePatch.tags), so you can change which lists a profile filters straight from the TUI — or with warden profile tag …. Source: src/tui/app.rs:117-149, src/tui/ui.rs:592-600, src/tui/profile_modal.rs:129-158, src/ipc/protocol.rs:407.

REST API

MethodPathWhat
GET/api/configFull merged config including [profiles.*] as JSON. token_hash is redacted.
GET/api/whitelistThe default profile’s allow list, reconstructed from its admin_rules references whose rule text starts with @@.
POST/api/whitelist/addAdd an allow rule to the default profile. 409 if already present.
DELETE/api/whitelist/removeSymmetric. 404 if absent.

All /api/* routes are gated by Bearer token (Authorization: Bearer ps_<64hex>), constant-time-verified, with a 10-failure / 5-minute lockout. Each authenticated route carries a 30-second timeout.

There is no REST CRUD for arbitrary profiles. The whitelist endpoints are a compatibility shape over the default profile only; everything else goes through the CLI, the TUI, or a hand-edit plus reload.

IPC

Three profile verbs exist on the Unix socket, all in the Mutating tier (admin token required):

  • ProfileCreate { id, display_name } — refuses if the id exists.
  • ProfileUpdate { id, patch } — the patch carries display_name, block_response, blocked_ttl_secs, block_all, admin_rules (add / remove deltas), ecs, and tags (a delta over Profile.tags).
  • ProfileDelete { id } — refuses if any device, subnet, or schedule still references the id.

Hand-edited fields land on disk and the daemon picks them up via IpcCommand::Reload { token }, the same path every CLI mutation uses. DeviceUpdate and DevicePromote carry an optional profile field for rebinding a device. Source: src/api/handlers.rs:863-895, src/ipc/protocol.rs:267-300,407.

Decision precedence

A query goes through two ordered passes: choose the profile, then evaluate inside it.

Choosing the profile

Five levels, first match wins. The table gives the runtime evaluation order alongside the label warden resolve and the audit log print — the two do not run in the same sequence, which trips people up.

EvaluatedLevel labelCondition
1stscheduleA schedule window is active for the matched device or one of its groups.
2nddevice-directThe [[devices]] row sets profile = "...".
3rdgroupThe device’s highest-priority group. Same-priority-different-profile is a validator error, so the choice is never ambiguous.
4thsubnetLongest-prefix match against [[subnets]]. Only for sources with no [[devices]] row — or for a device downgraded by MAC enforcement.
5thglobal-default[server].default_profile. If unset, the answer is REFUSED.

The schedule-before-direct ordering is deliberate: an operator who writes a schedule for a device that also pins a profile is clearly asking the schedule to win inside its window, otherwise the schedule would do nothing. Tests pin this choice.

enforce_device_mac = true plus an ARP mismatch downgrades the device to the subnet level for that query — schedule, direct, and group are all skipped.

This is the same chain documented under devices and groups. Source: src/profiles/resolver.rs:440-520.

Evaluating inside the profile

Before any of this, the handler probes the profile’s local_records. A hit is answered straight away and bypasses the filter, the cache, and upstream entirely — it is not “allowed past” the filter, it never reaches it. That holds under block_all too, which is what keeps homework.lan reachable in the bedtime example above. Only A, AAAA, and CNAME participate; any other qtype falls through to the global [local_dns] table and then upstream. Source: src/dns/handler.rs:940-985.

Once that misses, the filter walks these stages. The first decision wins.

  1. block_all short-circuit. If block_all = true: forward only when an admin allow rule matches (exact-domain set or a matching allow rule in the priority scan). Everything else is denied, attributed to the profile policy rather than to whichever rule was the proximate match. Allow-direction lists deliberately do not pierce block_all — a sandboxed external allowlist must not weaken an explicit operator “deny everything”.
  2. Admin-rule priority scan, four tiers, highest wins:
PriorityRule shape
3$important allow — short-circuits everything below
2$important deny
1normal allow
0normal deny
  1. Unified subdomain walk, one byte pass probing every enabled set per dot position:
    • exact-domain allow set hit → forward (outranks a tier-0 deny that already matched)
    • exact-domain deny set hit → block (only consulted when no rule produced a result)
    • list allow-direction mask hit → forward
    • list deny-direction mask hit → block
  2. Default forward — anything that matched nothing goes upstream.

After the verdict and before the forward, rewrite_rules and the SafeSearch presets apply. That ordering is what stops a rewrite from being used to escape a blocklist match on the original name.

Allow beats deny at equal strength: $important allow > $important deny > normal allow > list-level allow > list-level deny ~ normal deny. Source: src/filter/engine.rs:401-560, src/filter/evaluator.rs:22-83.

Device overlay (per-query, on top of the resolved profile)

A device may carry allow_rules and deny_rules of its own, plus override_profile_deny: bool. Across the nine combinations of (profile decision × device decision × override flag), one row is load-bearing:

Profile denies, device allows, override_profile_deny = true → the query is allowed, attributed to RuleSource::Device(<id>) [OVERRIDE].

When override_profile_deny = false the same situation falls back to block — defensive on drift; the CLI and TUI also refuse to write a contradicting allow without flipping the flag. Per-device deny is always additive: it can block what the profile would allow, but the profile cannot override it.

The device cannot override block_response, blocked_ttl_secs, block_all, local_records, or rewrite_rules — those come from the resolved profile only. What the device can contribute is tags, which union into the effective set before list selection runs. Source: src/profiles/profile.rs:629-741.

Security notes

  • Only admin_rules honour @@, $important, and regex. External blocklists are sandboxed — they can deny but cannot override or use anchored / regex patterns. This keeps a hostile or compromised list source from punching through your overrides.
  • local_records are unfiltered by construction. A profile-scoped record is served before the filter, the cache, and upstream — no blocklist, no admin rule, and no block_all applies to it. Treat the list as a small, deliberate allowlist that also happens to answer, not as a convenience for pinning names you still want checked.
  • A profile with no tags filters nothing. There is no implicit default subscription. tags = [] on a profile whose devices also carry no tags means every query is forwarded — the validator warns, it does not refuse. Check the WARN lines after any reload that touched tags.
  • block_unmapped_clients is gone. Express “deny unmapped clients” by leaving [server].default_profile unset, so the chain returns REFUSED at the global-default level. The legacy key is rejected as unknown field.
  • The query log records client_ip, client_name, queried domain, RCODE, and the resolved profile name. It does not record MAC.
  • Profile lifecycle is unaudited — tag edits through the verb are not. Create, update, remove, and ECS changes are not written to the audit log. What is audited: rule.add / rule.remove / rule.undo (which warden profile allow and deny go through, carrying scope, target_id, and the effective profile), profile.tag_add / profile.tag_remove (from warden profile tag), plus blocklist auto-promotion and the zero-intersection tag warnings. To trace “who changed the kids profile’s block mode?”, use the file’s git history; tag changes made through the verb are on the audit trail.
  • warden audit tail [-n N] prints recent audit lines as tab-aligned rows: timestamp, event tag, uid, ok / rejected, detail.
  • MAC enforcement is ergonomic, not cryptographic. A determined attacker on the LAN can spoof. See the threat model.

Troubleshooting

Nothing is being blocked, but the profile looks right. Almost always an empty intersection. The daemon warns at reload on the audit channel:

text
profile "kids" contributes no tags — devices using it rely entirely on device-level tags
profile "kids" has tags but none match any enabled deny list
blocklist "oisd-basic" tags match no device, profile, or subnet — the list applies to nothing
device "anna-iphone" has tags but none match any enabled deny list — no list filtering applies

All four are WARN-only and never block the reload — a tag staged for a list you haven’t added yet is legitimate. Read them after every tag change:

bash
journalctl -u purge-warden | grep -E 'no tags|match no|matches nothing'
warden tags list

A tag with a usage count of 1 in warden tags list is nearly always a typo.

A reload fails with unknown field: blocklists. The profile still uses the retired v1 schema. blocklists and categories were replaced by tags; deny_unknown_fields rejects them rather than ignoring them. Run warden migrate on a pre-tag config, or convert by hand — each old blocklist id becomes a tag shared by the list and the profiles that used to subscribe to it.

A reload fails with “profile X not found”. Some entity references a profile that no longer exists — usually a typo, sometimes a deleted profile. The validator refuses the reload and the daemon stays on the previous good config. Run warden config lint, or grep across the config tree:

bash
grep -RnE 'profile\s*=\s*"[^"]+"' /etc/purge-warden

warden profile remove is refused. Some device, subnet, or schedule still references it. The error names them — reassign those first. The same applies if [server].default_profile points at the profile being deleted.

[server].default_profile is unset and unmapped clients fail. The last level returns REFUSED for everything. Symptom: a guest laptop gets RCODE=REFUSED for every name. Either set default_profile to an explicit id, or accept that as the policy — it does keep an open resolver from forwarding for strangers.

A device with override_profile_deny = true keeps slipping past the kids profile. That’s what the flag is for, but check whether you meant to leave it on:

bash
warden device show anna-iphone | grep -E 'allow|override'

A device is resolving to the right profile but skipping every list. Check unfiltered. A device with unfiltered = true must have tags = [] (the validator enforces the mutex) and short-circuits to an empty effective set — DNS resolution, query logging, and stats all keep working, so the device looks healthy while filtering nothing.

For more, see troubleshooting.

See also

  • Tags — the slug layer that decides which lists a profile filters.
  • Blocklists — the other side of the intersection.
  • Devices — pin a host to a profile, layer per-device tags and overlays.
  • Groups — assign one profile to many devices at once.
  • Subnets — fallback profile and tags for sources with no device row.
  • Schedules — time-based profile swaps.
  • Admin rules — what admin_rules references; the only place @@ / $important / regex live.
  • SafeSearch — what the safe_search bool switches on.
  • Local DNS records — global table that local_records shadows per profile.
  • TOML reference — every field, every constraint.
  • CLI reference — the full warden profile … surface.