Every device on your network asks a question before it opens a connection, which makes the resolver the cheapest enforcement point you own and the one with the least context. This page is the mechanics: pulling the daily CSV, diffing it with the added and removed changelog, emitting an RPZ zone, validating before reload, and being honest about what a name-only decision cannot see.
A name has to be resolved before a socket can be opened. That single fact is why protective DNS is cheap to deploy and why it will never be a complete control.
The reason operators keep coming back to the resolver is that it is the one component every other component depends on. A proxy sees the traffic that was configured to traverse it. An endpoint agent sees the devices it was installed on. A mail gateway sees the messages it was in the path of. The resolver sees the question that precedes all three, asked by the laptop, the phone that joined the guest network an hour ago, the printer, the badge reader, the contractor's tablet and the appliance nobody has logged into since it was racked. There is no agent to install and no protocol to terminate. If a lure arrives by SMS, in a chat client, from a QR code photographed off a poster, or inside a document that was never scanned, the name still has to be looked up.
A resolver decides on a name. It does not see the path, the query string, the certificate, the page body or the form the user is about to fill in. If a phishing kit is hosted on a shared platform under a hostname thousands of legitimate customers also use, a resolver cannot separate the malicious tenant from the rest of them, because from where it stands they are indistinguishable. Blocking that name blocks everybody. This is not a defect in any particular blocklist; it is the resolution granularity of the protocol, and every honest protective-DNS deployment is designed around it rather than in denial of it.
What follows from that boundary is a design rule about what belongs in the zone. Entries should be hostnames whose entire reason for existing is hostile — a domain registered to imitate a bank's login, a throwaway host serving a credential form, infrastructure spun up for one campaign. Those can be blocked at the name with no collateral cost because there is no legitimate content behind them to lose. Content that lives under a shared or otherwise legitimate name belongs to a different layer: a proxy that can act on the path, an email gateway that can act on the message, or an endpoint agent that can act on what actually executed.
A blocked name is not blocked for a client that already holds a positive answer with time left on it, and a name you allow after a false positive will keep failing for whatever holds the negative. Every operational decision on this page — the TTL you publish on the zone, how often you reload, whether you flush after a change — exists because caching turns a list update into something that propagates rather than something that happens.
The resolver has to hold the whole thing in memory and answer under load, which makes list quality an operational rather than an aesthetic concern. A list that grows without ever shrinking becomes a zone that is slower to load, larger to hold and longer to validate, while protecting against hosts that stopped answering months ago. The value of a blocklist is not the count on the marketing page, it is the proportion of the count that is still live.
Nothing here is exotic. What matters is that each stage can fail loudly rather than quietly, because a silently stale zone is the realistic failure mode.
One cron entry, one temporary file, one atomic swap. The whole thing is a shell script you can read in a minute.
The daily feed is the complete database as CSV on a separate subscription, published alongside a changelog of the domains added and removed since the previous build. Download to a temporary path, never straight over the file the resolver reads. A truncated download that lands directly on the live zone is the classic way to turn a routine update into an outage.
The changelog exists so you do not have to infer the delta, but computing it locally as a cross-check costs nothing and tells you whether your copy matches what you think it is. A day where the additions are zero, or where the removals are a quarter of the zone, is a day something went wrong upstream or in your own transfer. Both should page somebody rather than being applied.
Each domain becomes an RPZ record, and in almost every deployment a wildcard record for its subdomains too. A phishing operator who has control of a name has control of everything under it, so blocking the apex without *.name leaves the obvious evasion open. Emit into a new file with a fresh serial.
Run the zone through the checker your resolver ships with before it is allowed near production, and assert on size: if the generated file is dramatically smaller than yesterday's, abort and keep the old one. A zone that parses but contains a fifth of the records is worse than a stale zone, because it looks healthy.
Swap the file atomically, reload the zone rather than restarting the daemon, and write one line somewhere durable: build serial, record count, added, removed, and the timestamp. That line answers “when did we last actually update” six months later, and it is what an alert can watch for staleness.
The choice is usually made by default and rarely revisited, which is a shame, because it determines what your users see and what your logs are worth.
The cleanest thing a resolver can do. The client gives up and no connection is attempted in any direction. It costs nothing to serve, works identically for every protocol, and leaves no infrastructure of yours in the path. The drawback is experiential: the user gets a browser error that looks like a network fault, so they retry, switch to mobile data, or open a ticket saying the internet is broken. You also lose the second signal — you know the query was made, but you learn nothing about whether the client then tried to connect anyway, which is a useful indicator that something automated rather than human is driving it.
The client connects, and now you can log the connection, the source, the port and often the intended hostname from the request. That second observation is worth a great deal during an incident, because the difference between one query from a machine and forty connection attempts a minute from the same machine is the difference between a user who clicked a link and a host that is running something. The cost is that you have to operate the sinkhole and accept traffic at it, including traffic from clients you would rather not have connecting to anything of yours. A sinkhole that stops answering becomes a quiet outage for whatever was pointed at it.
The only option that produces a decent outcome for a human. Done properly it says what happened, does not accuse the user of anything, and offers a way to report a mistake. Done badly — a bare “access denied” on a corporate template — it generates support load and teaches people to route around the filter. The awkwardness is technical rather than editorial: browsers demand HTTPS for anything that looked like an HTTPS destination, so serving a page for a blocked name means presenting a certificate the client will reject unless you have engineered around it. Most deployments show the page for plain HTTP, accept a certificate error for the rest, and treat the page as a courtesy rather than a guarantee.
In practice the useful configuration is mixed, and it is worth being deliberate about it. Corporate or campus networks with managed devices and a helpdesk tend to want the block page, because the support conversation it starts is cheaper than the ticket a bare error produces. Guest networks and residential subscribers are usually better served by NXDOMAIN, because you have no relationship to explain anything with and no appetite for terminating their traffic. Server segments should almost always get NXDOMAIN and a loud log entry, since nobody is reading a page there and a resolution attempt from a database host is far more interesting than a resolution attempt from a laptop. Operators serving schools or public libraries generally have a specific policy obligation about what the user is told, which settles the question for them.
Download, sanity-check, diff, emit, validate, reload — with every stage able to abort without touching what is already running.
#!/bin/bash
set -euo pipefail
NEW=/var/tmp/pda-new.csv # today's download
CUR=/var/lib/pda/current.csv # what the running zone was built from
ZONE=/etc/bind/db.rpz.phishing
# 1. pull the daily CSV to a temporary path, never over the live file
curl -fsS --retry 3 "$FEED_URL" -o "$NEW"
# 2. refuse anything implausible before it can do damage
lines=$(wc -l < "$NEW")
[ "$lines" -gt 300000 ] || { echo "feed too small ($lines) - aborting"; exit 1; }
# 3. diff against yesterday; cross-check the published changelog
comm -13 <(cut -d, -f1 "$CUR" | sort) <(cut -d, -f1 "$NEW" | sort) > /var/tmp/added.txt
comm -23 <(cut -d, -f1 "$CUR" | sort) <(cut -d, -f1 "$NEW" | sort) > /var/tmp/removed.txt
# 4. emit the RPZ zone: apex plus wildcard, allowlist removed at build time
{
printf '$TTL 60\n@ IN SOA localhost. root.localhost. %s 3600 900 604800 60\n' "$(date -u +%Y%m%d%H)"
printf '@ IN NS localhost.\n'
grep -vxF -f /etc/pda/allowlist.txt < <(cut -d, -f1 "$NEW" | tail -n +2) |
while read -r d; do printf '%s CNAME .\n*.%s CNAME .\n' "$d" "$d"; done
} > "$ZONE.tmp"
# 5. validate, swap atomically, reload the zone only
named-checkzone rpz.phishing "$ZONE.tmp" >/dev/null
mv "$ZONE.tmp" "$ZONE" && mv "$NEW" "$CUR"
rndc reload rpz.phishing
logger -t pda-rpz "records=$lines added=$(wc -l < /var/tmp/added.txt) removed=$(wc -l < /var/tmp/removed.txt)"
A CNAME . target is the RPZ idiom for NXDOMAIN. Swap it for a sinkhole address to log the follow-on connection instead.
They are not competing products. They answer the same question at different points, with different failure modes and completely different cost curves.
The cost curves diverge immediately and that is usually what settles it. A resolver serving a mid-sized organisation answers millions of queries a day; pricing thper lookup is not a budget conversation anybody wants to have, and the latency of an external call in the resolution path is not a design anybody should want either. The zone model has no per-query cost at all, which means the price of protecting a network does not move with how busy it gets, how many branches you add or how many devices join the guest SSID this week. That property is what makes the model workable for an ISP or a large campus.
The per-query endpoint earns its place elsewhere. The most common use is retrospective: pull yesterday's resolver logs, extract the distinct names, and run them through POST /api/v1/batch a hundred at a time, one lookup each. Names that were not in the zone at query time but are on the list today identify clients that reached something before you were enforcing against it — which is a hunting output, not a blocking one, and it belongs in whatever SIEM workflow you already run. The same batch call is how a support team checks a name a user reported without touching the zone.
Reload cadence is the knob between the two. A daily rebuild pulled shortly after publication puts your worst-case staleness at roughly a day. Pulling more often does not make the list fresher than its build cadence, but it does shorten your exposure to a failed download, which is a different and more common problem. Most operators settle on a daily pull with a retry, an alert on age, and the batch endpoint for anything urgent that cannot wait for the next build.
Protective DNS is frequently sold as a replacement for the layers around it. It is a floor underneath them, and the differences are structural rather than a matter of quality.
| Question | Protective DNS | Web proxy / TLS inspection | Mail gateway | Endpoint agent |
|---|---|---|---|---|
| Covers devices with no agent | Yes, anything using the resolver | Only if routed through it | Mail only | Agent required |
| Works for SMS, chat and QR-code lures | Yes, the name is still resolved | If the traffic traverses it | No | Depends on the module |
| Can act on the URL path | No — name granularity only | Yes | Yes, for links in messages | Yes, in the browser |
| Blocks before any packet reaches the attacker | Yes, at resolution | Connection is made first | Yes, at delivery | Usually after launch |
| Cost scales with traffic volume | No, with a local zone | Sizing follows throughput | Usually per mailbox | Per seat |
| Survives an upstream outage | Yes, matching is local | Depends on the cloud path | Queues, then degrades | Cached policy |
| Survives encrypted DNS to a third party | Bypassed unless enforced | Unaffected | Unaffected | Unaffected |
Read down the DNS column and the shape of the layer is obvious: it is broad, early, cheap and shallow. It reaches devices nothing else reaches, it decides before a packet leaves, its cost does not follow your traffic, and it knows nothing beyond the name. Every deployment that goes wrong does so by treating one of those four properties as if it were five.
Requiring an active DNS record for inclusion inverts that. Domains that have stopped resolving are dropped rather than retained, so the zone is a description of what is answering today rather than an archive. In a resolver context that has three concrete effects: the file stays a size that reloads quickly, the false-positive surface shrinks because most blocklist false positives come from stale entries on names that have since been recovered by legitimate owners, and the hit-rate numbers you show your own management mean something, because a match is against something that was live when it was matched. Operators combining this with their own threat intelligence sources usually keep the verified feed as the base layer precisely for that reason.
None of these are reasons to skip protective DNS. They are the things to know before you tell anyone the network is covered.
A browser or an application that speaks DoH or DoT to a public resolver never asks your resolver anything, and your zone becomes irrelevant for that process. Some clients do this by default, some fall back to it when the configured resolver misbehaves, and users who find the block page inconvenient learn to turn it on deliberately. The mitigations are real but each has a cost: block the well-known encrypted-DNS endpoints at the firewall, use the canary name that lets a network signal it filters, push resolver policy through device management on managed fleets, and accept that unmanaged devices on a guest network will do as they please. What you should not do is assume the zone is enforcing when a meaningful share of clients have stopped consulting it — measure the split rather than assuming it.
A new entry does not take effect for a client already holding a positive answer, and a removal does not take effect for anything holding the block. Stub resolvers, browser caches and downstream forwarders all keep their own copies with their own ideas about lifetime. Short TTLs on the policy zone narrow the window; nothing closes it. When you allowlist a name after a false positive, expect to flush, and expect the complaint to continue for a few minutes from whoever cached first.
A phishing page hosted under a legitimate shared hostname is invisible to this layer, and blocking that hostname is usually worse than the thing you are preventing. The same applies to compromised sites, to link shorteners resolving to a name used by everything, and to anything served from a platform whose customers include your own users. This is the boundary described earlier, and it is the single most common source of unrealistic expectations about protective DNS. Pair the resolver with a layer that can see the path — the firewall and proxy patterns cover that side.
Inclusion requires observation, attribution and verification that the host is resolving, and each of those takes time. A domain stood up this morning for a campaign that runs until lunch may never appear in any list before it is finished. That gap is real, it is shared by every blocklist in existence, and a clean answer from the resolver means “not on the list,” never “safe.” What makes the layer worth operating anyway is that most of what reaches your users is not bespoke: it is infrastructure already used against somebody else, still answering, and reused until it is taken down. Removing that traffic cheaply is what leaves your expensive layers with a workload they can actually handle.
An MSP resolver has the same pipeline with one extra requirement: the base list is shared, the policy around it never is.
A managed provider running resolvers for a few hundred customers is operating one ingest pipeline and many policies. The efficient shape is to build the phishing zone once, then apply per-tenant overlays: a tenant allowlist that takes precedence over the shared zone, a tenant choice of response action, and a tenant-scoped log stream. Rebuilding the full zone per customer is the mistake to avoid — it multiplies memory and load time by the customer count for data that is identical in every copy. Most resolver software supports zone ordering or policy precedence well enough to express this without duplication.
When a tenant allows a name that is on the shared list, the tenant should win, immediately and without a support ticket, because the alternative is a customer whose business is broken by a list they did not choose. That is defensible provided two things are true: the override is scoped strictly to that tenant, and it is recorded with who added it and why. An override nobody can explain a year later is how a permanent hole ends up in a shared control. Providers running this at scale give tenants a self-service allowlist and a periodic report of what they have accumulated — the wider pattern is on the managed security page.
False positives, when they happen, are almost always about a name that was hostile and has since changed hands or been recovered. The daily rebuild resolves those without anyone filing anything: a domain that stops resolving, or stops being classified as phishing infrastructure, leaves on the next build. The tenant-level allowlist is the immediate valve. What you want to avoid is an allowlist that only ever grows, because it slowly becomes an unreviewed exception list nobody understands — expire entries after a fixed period and make somebody re-affirm them.
A protective-DNS deployment produces a complete record of every name every client asked for, which is more sensitive than most of what the rest of the security stack collects and is frequently subject to specific obligations depending on the sector. Decide the retention period deliberately, scope tenant access to that tenant, and note that the bulk-feed model means query data never leaves your infrastructure in the first place — a materially easier position to defend than one where every lookup is sent to a third party.
Seven questions that come up in every evaluation, answered without hedging.
Around 390,000 apex records, doubled if you emit the wildcard companion for each. That is a size every mainstream recursive resolver handles as an ordinary policy zone; the constraint is load and validation time on reload rather than steady-state query performance, which is why the pipeline validates a temporary file and swaps rather than reloading the daemon.
Not for enforcement. Once the zone is loaded, every decision is made locally with no external call, no per-lookup cost and no dependency on us being reachable. That is the right model for anything in the resolution path.
Nothing, if the pipeline is built as described. The download lands on a temporary path, a line-count assertion rejects anything implausibly small, the zone checker has to pass, and only then is the file swapped. A failed run leaves yesterday's zone enforcing, which is the correct behaviour.
NXDOMAIN if you want the cheapest, most protocol-neutral answer and have no need to observe what happens next. Sinkhole if the follow-on connection is worth logging — and on any segment where you will later have to explain to somebody what their machine did, it is worth logging, because a query alone does not distinguish a person clicking from a client prefetching.
Fix it locally first, because that is the only fix that is immediate. Keep an allowlist file that the build script subtracts while emitting the zone, add the name, rebuild, reload, and flush caches so clients holding the block pick up the change. In a multi-tenant deployment the allowlist should be tenant-scoped and take precedence over the shared zone.
It makes it incomplete, which is a different thing. A client that speaks DoH to a public resolver bypasses your policy entirely, so the honest position is to measure how much of your traffic that represents rather than assume it is negligible or fatal.
Yes, and they answer different questions. A category product classifies subject matter and is built around a taxonomy that changes slowly. This list is about hostnames that exist to deceive, which appear and disappear inside days, so the two have different update rhythms and different standards of proof.
390,000+ DNS-verified phishing domains as a daily CSV, with the added and removed changelog that makes a diff-and-reload pipeline trivial. Per-query lookups for the workflows around it.