SafeSearch
One bool on a profile, eight strict rewrites injected at resolve time. The kids’ tablet now sees Google through forcesafesearch.google.com, not because the browser asked nicely.
What this is
SafeSearch is a per-profile switch that forces Strict-mode SafeSearch on Google, YouTube, Bing, and DuckDuckGo for every device assigned to that profile — at the DNS layer, with no client cooperation. Setting safe_search = true on a profile makes Warden inject eight preset rewrite rules into that profile’s resolved rewrite table at startup, SIGHUP, and the 60-second schedule recompute tick. Every query for www.google.com from a device on the profile resolves as if the client had asked for forcesafesearch.google.com.
The mechanism is the same ProfileRewriteRules engine that powers operator-authored [[profiles.<id>.rewrite_rules]]. SafeSearch just happens to seed it with eight curated entries that the operator never has to maintain.
When you reach for it
- You manage a
kidsprofile and want the household’s Chromebooks and tablets to stop returning explicit image results from Google Images and Bing, without installing client-side extensions. - You operate a classroom or small library Pi running Warden and want SafeSearch enforced even on guest devices that won’t honour an operator-set browser preference.
- You want YouTube’s Restricted Mode enforced for kids, recognising that the protection is Strict only (
restrict.youtube.com) — there is no Moderate tier today.
You don’t reach for SafeSearch when:
- You want to block search engines entirely. Add
google.com,bing.com,youtube.comto the profile’sdeny_rulesor to a blocklist — blocklists evaluate before SafeSearch rewrites, so the deny wins. - You want a per-device override of a profile that already has SafeSearch on. SafeSearch follows the profile; if a device is on the kids profile, SafeSearch is on for that device. To exempt a device, give it a different profile.
How it works
SafeSearch is not a separate filter stage. It plugs into the existing rewrite engine through two seams:
- At profile resolve time (startup,
warden reload, every 60s when schedules recompute),ResolvedProfile::build_v1looks atProfile.safe_search. Whentrue, it callssafesearch::populate()on the profile’s rewrite-rule table, which appends the eight presets unless an operator-authored entry already claims the samefromhost. - On the hot path, each query walks: filter (allow / deny / blocklist) → rewrite → cache lookup → upstream. SafeSearch lives in the rewrite step, after the filter, before the cache key is computed. So:
- A blocklist entry for
www.youtube.comwins over SafeSearch — the query is blocked, never rewritten. - A cache hit for
forcesafesearch.google.comis reused across every client on the same profile. - The DNS response sent back to the client still carries the original qname; only the upstream lookup is swapped. The client never sees the rewrite.
- A blocklist entry for
www.google.com in their browser and in dig answers. Warden resolves forcesafesearch.google.com upstream and stamps the answer back under the original name. This is by design — no broken TLS, no certificate mismatches, no client-visible side effects.Sources: src/profiles/safesearch.rs:1-234, src/profiles/profile.rs:486-489, src/dns/handler.rs:894-903, src/dns/rewrite.rs:1-90.
Providers covered
Every preset is an exact match (match_subdomains = false). The vendor-controlled hosts on the right perform the actual SafeSearch lock at their end.
| Original qname | Rewritten to | Tier |
|---|---|---|
google.com | forcesafesearch.google.com | Strict |
www.google.com | forcesafesearch.google.com | Strict |
www.youtube.com | restrict.youtube.com | Strict |
m.youtube.com | restrict.youtube.com | Strict |
www.youtube-nocookie.com | restrict.youtube.com | Strict |
www.bing.com | strict.bing.com | Strict |
edgeservices.bing.com | strict.bing.com | Strict |
duckduckgo.com | safe.duckduckgo.com | Strict |
What’s not covered:
- Bare apex
youtube.comandbing.com. Only thewww.hosts — plusm.youtube.com,www.youtube-nocookie.com, andedgeservices.bing.com— are rewritten. The bare apex names are deliberately left alone: Google and Microsoft document the SafeSearch lock only for those specific hostnames, and pointing an undocumented apex at the SafeSearch host risks breaking the site. Google’sgoogle.comapex is the one documented exception and stays covered. In practice this is rarely a hole — browsers and the search apps resolve thewww.host — but a client that asks for the bare apex gets a normal answer. - YouTube Moderate tier (
restrictmoderate.youtube.com). Deferred — Strict (restrict.youtube.com) is the only YouTube option today. - YouTube API surface (
youtubei.googleapis.com,youtube.googleapis.com). A child watching the official YouTube Kids app uses the in-app content filter, not a web hostname, so DNS-layer enforcement doesn’t reach it. - Regional Google domains (
google.co.uk,google.it, etc.). If a client speaksgoogle.co.ukexplicitly, the rewrite list will not match. Subdomain-walking presets were deliberately skipped to keep the table predictable; add explicit per-profile rewrites if you need country-specific coverage.
Source: src/profiles/safesearch.rs:80-113.
Schema
SafeSearch is a single boolean field on the profile:
| Field | Type | Required | Default | Purpose |
|---|---|---|---|---|
safe_search | bool | no | false | When true, the eight preset rewrites are injected at resolve time. |
There is no [safesearch] table. There are no per-provider toggles. The whole point of the feature is that it’s one switch.
Source: src/config/schema/profile.rs:123-124.
Examples
The smallest possible kids profile with SafeSearch on:
[profiles.kids]
display_name = "Kids"
safe_search = trueA full kids profile that combines SafeSearch with content blocklists — the two stack:
[profiles.kids]
display_name = "Kids"
safe_search = true
categories = ["adult-content"]
blocklists = ["security-malicious"]
block_response = "nxdomain"
blocked_ttl_secs = 300Override the preset for one specific host while keeping the other seven — operator-authored rewrites take precedence:
[profiles.kids]
display_name = "Kids"
safe_search = true
# Redirect www.google.com to the household intranet instead of forcesafesearch.
# The remaining seven SafeSearch presets still apply.
[[profiles.kids.rewrite_rules]]
from = "www.google.com"
to = "intranet.local"
match_subdomains = falsePrecedence
The order matters when SafeSearch coexists with other features.
- Blocklists and admin deny win. SafeSearch sits after the filter stage. If
www.youtube.comis on a deny list active for the profile, the query is blocked. SafeSearch never gets a chance to rewrite it. - Operator-authored rewrites win over the preset. When the same
(from, match_subdomains)pair is present in both,populate()skips the preset and keeps the operator entry. This is how the example above replaces only one of the eight rules. - Schedules swap the whole profile. A device that switches from
defaulttokidsbecause of a 21:00 schedule picks up SafeSearch with the rest of the profile; switching back at 07:00 drops it. The handover is bounded by the 60-second schedule tick. - Per-device overlay does not see SafeSearch.
[[devices]].allow_rulesanddeny_rulesare filter-stage rules, not rewrites. A device’s allow rule cannot exempt one search host from SafeSearch — the rewrite happens after the filter, and the device overlay only participates in the filter step.
Sources: src/dns/handler.rs:894-903 (hot-path order), src/profiles/safesearch.rs:119-134 (collision logic).
CLI
There is no dedicated warden safesearch subcommand. Activation is a TOML edit plus a reload:
# 1. Edit the profile
sudo $EDITOR /etc/purge-warden/profiles.d/kids.toml
# add or flip: safe_search = true
# 2. Reload
sudo warden reload
To inspect:
# Profile flag (the source of truth — shown verbatim from config)
warden profile show kids | grep -i safe
# Runtime verification — should answer with forcesafesearch.google.com's address
dig @127.0.0.1 -p 53 www.google.com
# What the rewrite engine returns for the resolved profile.
# Note: warden rewrite list shows OPERATOR-AUTHORED rules only; the eight
# SafeSearch presets are injected at resolve time and intentionally hidden
# from listings to keep the operator's mental model uncluttered.
warden rewrite list --profile kids
warden rewrite list and warden profile show deliberately omit the eight SafeSearch rewrites even when safe_search = true. The bool is the contract; the presets are an implementation detail. If you want to know what’s enforced, this page is the reference — the rule set is fixed and ships with the daemon.Sources: src/profiles/safesearch.rs:21-24.
TUI
No dedicated SafeSearch tab today. The flag does not yet appear in the Devices edit modal or the Profiles drilldown — flip it from the TOML and warden reload.
This is a known gap; the field is a primitive bool and slots into the existing profile-edit form trivially when the TUI side catches up.
REST API
Reading the flag:
| Method | Path | What |
|---|---|---|
GET | /api/config | Full merged config. The boolean appears as profiles.<id>.safe_search in the JSON payload. |
There is no dedicated POST /api/profile/<id>/safesearch endpoint and there is no write surface for arbitrary profile fields over REST — profile mutations are CLI- or hand-edit-only.
Limitations
Worth knowing before promising parents that SafeSearch is on:
- DNS-only enforcement. A device that uses DNS-over-HTTPS (DoH) to a public resolver (e.g. browser-level
https://dns.google/dns-query) bypasses Warden entirely. Pair SafeSearch with a network-level DoH block if this matters. See DoH blocking recipes (forthcoming). - DNS-over-TLS (DoT) on port 853 to a public resolver is the same story — a client that asks Quad9 directly never hits Warden.
- Strict tier only. YouTube has no Moderate option in Warden today. Some families want Restricted-Moderate; that’s a config gap to flag, not a workaround.
- No regional Google domains.
google.co.uk,google.it, etc. pass through unrewritten. Add explicit[[profiles.<id>.rewrite_rules]]entries if your household uses them. - Mobile-app SafeSearch surfaces are out of scope. Android’s Google Search app and the iOS YouTube Kids app embed their own content filter; DNS-layer SafeSearch only intercepts the web-facing hostnames listed above.
- The IP returned by the upstream is not validated. If a hostile resolver returns a non-SafeSearch IP for
forcesafesearch.google.com, Warden will hand it to the client. Use a trustworthy upstream (Cloudflare, Quad9, your ISP) and consider enabling DNSSEC on the forwarder.
Troubleshooting
A device on the kids profile still sees adult image results.
- Confirm the device is actually on the profile:
warden device show <id>and check theprofilefield. - Confirm the flag is set:
grep safe_search /etc/purge-warden/profiles.d/*.toml. - Confirm the daemon picked up the change:
journalctl -u purge-warden -n 50 | grep -i reload. - Verify the rewrite:
dig @127.0.0.1 -p 53 www.google.comshould answer with the IP thatforcesafesearch.google.comwould resolve to. - If step 4 shows the real
www.google.comIP, the device is querying upstream directly — check the router DHCP advertises Warden as the DNS server and that the device hasn’t pinned its own DoH endpoint.
SafeSearch rewrites and an operator-authored rewrite both target the same host.
The operator entry wins; the preset is silently skipped. This is intentional. If you want to confirm: edit your TOML to remove the operator rewrite, run warden reload, then dig again — the answer should swing back to the SafeSearch host.
YouTube Restricted Mode shows the warning bar but lets some content through anyway.
That’s YouTube’s behaviour, not Warden’s. restrict.youtube.com is YouTube’s Strict-mode hostname; the platform decides what falls inside Strict. Warden’s contract is “route the user to YouTube’s strictest tier”, not “guarantee the strictest tier blocks everything”.
warden reload accepted the change but dig still returns the unrewritten host.
The 60-second schedule tick is the upper bound on profile rebuilds, but warden reload should rebuild immediately. Confirm the daemon is actually running the new config:
warden status | grep -i config
journalctl -u purge-warden -n 20 --no-pager
If the timestamps don’t match the reload, the daemon may not have ack’d the SIGHUP — restart with systemctl restart purge-warden as a last resort.
For deeper diagnosis, see troubleshooting.
See also
- Profiles — the entity that holds the
safe_searchfield. - Local DNS records — the other way to swap upstream answers, but per-record rather than per-vendor preset.
- Blocklists — the deny-list stage that runs before SafeSearch in the hot path.
- Recipes — household — end-to-end kids-profile example.
- TOML reference — every field, every constraint.