Protective DNS & Resolver Operators

A resolver can only act on the name. Give it a list where every name is still answering.

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.

390,000+DNS-verified domains, all currently resolving
24 hRebuild cadence with an added/removed changelog
1 zoneGenerated from one CSV, reloaded in place
Host onlyWhat DNS sees — never the path or the payload
RPZ zone generation
Daily added/removed diff
NXDOMAIN or sinkhole
Multi-tenant policy
Allowlist precedence
Cache & TTL aware
Home / Use Cases / DNS Filtering
The control point

Everything asks the resolver first, which is both the strength and the whole limitation

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.

Definition — name granularity

The exact, unglamorous boundary of what a resolver decides

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.

The answer is cached, downstream too

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.

Every entry costs memory and load time

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.

The pipeline

Five stages between the published CSV and a resolver that is enforcing it

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.

Ingest to enforcement

Pull the daily CSV
Diff against yesterday
Emit the RPZ zone
Validate before use
Reload & record

One cron entry, one temporary file, one atomic swap. The whole thing is a shell script you can read in a minute.

01

Pull

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.

02

Diff

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.

03

Emit

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.

04

Validate

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.

05

Reload and record

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.

Response strategy

NXDOMAIN, sinkhole, or a block page — three answers with three different aftermaths

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.

01

NXDOMAIN — the name simply does not exist

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.

02

Sinkhole — answer with an address you control

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.

03

Block page — a sinkhole with an explanation attached

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.

Whatever you choose, publish a short TTL on the policy zone. The response you serve today is the response clients keep until it expires. A sixty-second TTL on blocked answers costs you almost nothing in query volume and means a correction takes a minute to reach everyone instead of an afternoon.
The daily cycle

What the cron entry actually does

Download, sanity-check, diff, emit, validate, reload — with every stage able to abort without touching what is already running.

390,000+Records in the generated zone, every one verified as resolving
DailyRebuild, with the added and removed changelog alongside
<50 msTypical per-query API lookup, for the paths that need one
100Domains per batch request, for log review and retro-hunting
#!/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.

Two models

Bulk zone or per-query lookup, and why most operators end up running both

They are not competing products. They answer the same question at different points, with different failure modes and completely different cost curves.

Bulk feed: the answer is already local

The whole database becomes a zone your resolver loads. Matching happens in memory at query time with no external dependency, no per-lookup cost and no client data leaving your infrastructure. Freshness is bounded by your reload cadence, and an upstream outage degrades tomorrow's list without touching today's enforcement.

Per-query lookup: the answer is current

A call to GET /api/v1/check reflects the database as it stands, not as it stood at the last reload. one lookup per API call, typically under 50 ms. It suits paths that are not in the hot query loop — log review, an analyst console, a filtering engine that already makes an external call per classification.

Rule of thumb: if the check sits in the resolution path of production traffic, it belongs in the zone. If it sits in a human or analytical workflow, per-query is simpler and cheaper than keeping a second copy of the data in sync.

Why the cost curves settle it

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.

Where the per-query endpoint earns its place

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, the knob between the two

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.

Layer comparison

Where each control can actually see the thing you are trying to stop

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.

QuestionProtective DNSWeb proxy / TLS inspectionMail gatewayEndpoint 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.

This is where list hygiene stops being a marketing point and becomes an operating concern. A zone is loaded, parsed, held in memory and reloaded on every update, so its size is a running cost you pay continuously. A feed that only accumulates — that keeps every domain anyone ever reported, including the ones suspended two years ago — grows without bound while the share of it that could ever match anything shrinks. You pay for that in load time, in memory on every resolver in the estate, and in the validation window during which a bad update is hardest to spot.

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.

Honest limits

Four ways a name-based block is quietly not in effect

None of these are reasons to skip protective DNS. They are the things to know before you tell anyone the network is covered.

Encrypted DNS routes around you

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.

Caches hold the old answer

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.

The name is the only thing being judged

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.

A name registered ninety seconds ago is not in the zone

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.

And a fifth item that is a warning about your own logs rather than a limitation. A blocked query tells you a name was asked for, not that a person typed it. Prefetching, link previews in chat clients, security tooling detonating a URL, and browsers speculatively resolving names on a page all generate resolutions nobody intended. Before you tell a user they visited a phishing site, check whether anything on that host also connected. This is the strongest argument for a sinkhole over NXDOMAIN in an environment where you will be having those conversations.
Multi-tenant

Serving the list to customers who each want their own exceptions

An MSP resolver has the same pipeline with one extra requirement: the base list is shared, the policy around it never is.

One zone, many policies

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.

Allowlist precedence, decided rather than defaulted

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.

The rebuild is the systemic fix

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.

Query logs are the most sensitive thing you hold

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.

Operator questions

What resolver teams ask before the first pull

Seven questions that come up in every evaluation, answered without hedging.

How large is the zone, and what will it cost my resolver?

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.

Size over timeThe number stays roughly stable rather than climbing indefinitely, because inclusion requires the domain to be currently resolving and dead ones are dropped on the next rebuild. That is the difference between a zone you can keep and a file that grows until somebody quietly removes it.
Do I need the per-query API if I have the feed?

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.

Where it does helpThe API earns its place in the workflows around enforcement: batch-checking names pulled from resolver logs, answering a user report without editing the zone, and giving an analyst console a current answer. Those are low-volume and bounded, which is exactly what a monthly plan suits.
What happens if a download fails or the file is truncated?

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.

The alert to buildThe failure that actually hurts is the one nobody notices, so record the build timestamp and record count on every successful run and alert when the timestamp is more than forty-eight hours old. That single alert covers broken cron entries, expired credentials and silent network failures.
Should I use NXDOMAIN or point blocked names at a sinkhole?

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.

Mixing by segmentThis is normal and worth planning: block page on managed corporate, NXDOMAIN on guest and residential, sinkhole with loud alerting on server networks where a resolution attempt is inherently suspicious.
How do I handle a domain we believe is wrongly listed?

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.

Then let the rebuild workBecause entries require an active DNS record and the database is rebuilt every twenty-four hours, names that stop resolving or stop being hostile leave on their own, so most local exceptions turn out to be temporary. Put an expiry on them so they do not become permanent by default.
Does encrypted DNS make this pointless?

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.

Managed versus unmanagedOn managed fleets you can push resolver configuration through device management and block the well-known encrypted endpoints at the perimeter. On unmanaged guest networks you largely cannot, and the correct response is to weight your other layers accordingly rather than to pretend the resolver is covering those devices.
Can I use this alongside the category blocklists we already load?

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.

How to load themKeep them as separate policy zones with an explicit precedence so you can tell which one produced a block. That distinction matters when somebody asks why a name was blocked, and it means you can tune or remove one without disturbing the other.

Build the zone from a list where every entry is still answering

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.