resources background

Blog

How to Block Newly Registered Domains in Pi-hole

Written By Sameer Asad, WhoisFreaks Team Published: June 12, 2026, Last Updated: June 15, 2026

Quick answer: A newly registered domain (NRD) feed is a daily list of every domain added to the global DNS in the last N days. Loading it into Pi-hole as a rolling blocklist means any domain registered in your chosen window (commonly 5 to 30 days) returns NXDOMAIN on your network. Because most phishing, malware, and scam infrastructure runs on domains registered hours to days before a campaign launches, blocking by freshness stops threats that curated blocklists have not flagged yet.


Curated blocklists arrive late. A phishing domain gets reported, a security team triages it, eventually it lands in PhishTank or URLhaus. By then the campaign has burned through it and registered five more. Reactive feeds are doing the right thing in the wrong order.

Newly Registered Domains (NRDs) flip that order. Instead of waiting for evidence of malice, you treat freshness itself as the risk signal. The data backing this approach is hard to argue with.

In Unit 42's analysis of newly registered domains, Palo Alto Networks' threat team studied registration activity across 1,530 TLDs and found that more than 70% of domains registered in the previous 32 days were classified as malicious, suspicious, or not safe for work. Their published guidance is direct: block NRDs outright with URL filtering. They acknowledge it is aggressive but conclude the risk outweighs the false-positive cost.

This guide walks through wiring the WhoisFreaks NRD feed into Pi-hole as a true rolling-window blocklist, configurable for 5, 10, 15, or 30 days, refreshed daily, with auto-subscription and automatic gravity reload. Twenty minutes of setup, zero manual maintenance afterward.

Why a rolling window matters

Plenty of NRD tutorials show you how to subscribe to a single static list. That's better than nothing, but it misses the point. Threat actors keep registering. Yesterday's NRD list doesn't cover this morning's freshly-spun phishing kit.

What you actually want is a sliding window, say the last 10 days of registrations, that updates every night. New domains in the front, expired ones falling off the back. Pi-hole sees a steady-size blocklist of "everything registered in the last N days," automatically.

The window length is a knob worth tuning to your tolerance:

  • 5 days. Minimum viable. Catches the immediate burn-and-rotate campaigns. Unit 42's Smishing Triad investigation found that more than 70% of that campaign's domains were active for less than a week. Lowest false-positive rate.
  • 10 days. A good default. Catches most malware C2 staging. Unit 42's newly observed domains research puts the average gap between registration and first malicious traffic at 5.57 days, which also covers phishing campaigns that take a few days to warm up.
  • 14 days. A reasonable starting point if you are protecting non-technical users.
  • 30 days. Unit 42's recommendation. Maximum coverage, maximum false positives. Best for hardened environments where the security team can absorb the friction.

You set the number in one environment variable. The system handles the rest.

What you'll build

Three Docker containers in one compose file:

  1. pihole – the DNS resolver and admin UI.
  2. feed-server – a tiny nginx that serves the combined NRD blocklist over the internal Docker network.
  3. feed-fetcher, the brains. Runs cron internally, pulls each day's NRD list from WhoisFreaks, caches per-day files, rebuilds the combined blocklist, subscribes Pi-hole to it automatically, and triggers gravity reload.

The cron job lives inside the fetcher container, not on your host. When you stop the stack, cron stops with it. Nothing to clean up on the host. The feed gets added to Pi-hole's Lists on first run, with no manual clicks in the admin UI.

Prerequisites

  • A machine with Docker installed (Linux, macOS, or Windows).
  • A WhoisFreaks account and an NRD API key. The WhoisFreaks NRD feed delivers daily gzipped lists of newly registered gTLD and ccTLD domains across 1,528+ TLDs, with previous-day ccTLDs typically published around 3:00 AM UTC and current-day gTLDs around 5:00 PM UTC. That is exactly what Pi-hole needs. If you want to weigh free community lists against a paid feed before committing, see our guide to free and paid NRD access.
  • About 20 minutes.

Step 1 – Folder layout

mkdir -p ~/pihole/etc-pihole ~/pihole/etc-dnsmasq.d ~/pihole/feed
cd ~/pihole

If you're on Linux, free up port 53 first. Ubuntu and most Debian-family distros run systemd-resolved, which holds it by default:

sudo ss -tulpn | grep ':53 '

If systemd-resolved shows up, disable it:

sudo systemctl disable --now systemd-resolved
sudo rm /etc/resolv.conf
echo "nameserver 1.1.1.1" | sudo tee /etc/resolv.conf

Step 2 – Store the WhoisFreaks API key safely

The key never goes in the compose file. The key never goes in a script. The key lives in one file on the host, mounted read-only into the fetcher:

sudo mkdir -p /etc/whoisfreaks
echo "YOUR_WHOISFREAKS_API_KEY_HERE" | sudo tee /etc/whoisfreaks/apikey > /dev/null
sudo chmod 600 /etc/whoisfreaks/apikey

If the key ever leaks, you rotate it in one place. If you commit your compose folder to git, nothing sensitive ships with it. This matters more than people think – NRD API keys typically come with daily download quotas, and a leaked key turns into a billing event fast.

Step 3 – The fetcher script

Save this as ~/pihole/feed/fetch-nrd.sh. The version below is abridged for readability; the full script with retry logic, schema-mismatch handling, and extended logging is on GitHub (link at the bottom).

#!/bin/bash
set -euo pipefail

# Configuration via environment variables (defaults shown)
API_KEY_FILE="${API_KEY_FILE:-/etc/whoisfreaks/apikey}"
CACHE_DIR="${CACHE_DIR:-/var/feed/cache}"
OUTPUT="${OUTPUT:-/var/feed/nrd.txt}"
WINDOW_DAYS="${WINDOW_DAYS:-10}"
FEED_TYPES="${FEED_TYPES:-gtld cctld}"
PIHOLE_CONTAINER="${PIHOLE_CONTAINER:-pihole}"
AUTO_SUBSCRIBE="${AUTO_SUBSCRIBE:-true}"
FEED_URL="${FEED_URL:-http://feed-server/nrd.txt}"
TRIGGER_GRAVITY="${TRIGGER_GRAVITY:-true}"

API_KEY="$(cat "$API_KEY_FILE")"
mkdir -p "$CACHE_DIR"
log() { echo "[$(date -u +%FT%TZ)] $*"; }

# Build the list of dates we need (yesterday back through WINDOW_DAYS days)
WANTED_DATES=()
for ((i=1; i<=WINDOW_DAYS; i++)); do
  WANTED_DATES+=("$(date -u -d "${i} days ago" +%Y-%m-%d)")
done

# Fetch only missing days - idempotent, safe to re-run mid-day
for date in "${WANTED_DATES[@]}"; do
  for feed in $FEED_TYPES; do
    cache_file="${CACHE_DIR}/${date}_${feed}.txt"
    [[ -s "$cache_file" ]] && continue

    url="https://files.whoisfreaks.com/v3.1/download/domainer/${feed}?apiKey=${API_KEY}&date=${date}&whois=false"
    tmp_gz="$(mktemp)" tmp_txt="$(mktemp)"

    if curl -sS --fail --max-time 300 -o "$tmp_gz" "$url"; then
      zcat "$tmp_gz" | tr -d '\r' | tr '[:upper:]' '[:lower:]' \
        | grep -E '^[a-z0-9][a-z0-9.-]*\.[a-z]{2,}$' \
        | sort -u > "$tmp_txt"
      [[ -s "$tmp_txt" ]] && mv "$tmp_txt" "$cache_file"
    fi
    rm -f "$tmp_gz" "$tmp_txt"
  done
done

# Prune cache files outside the window
KEEP_PATTERN="$(IFS='|'; echo "${WANTED_DATES[*]}")"
for f in "${CACHE_DIR}"/*.txt; do
  file_date="$(basename "$f" | cut -c1-10)"
  [[ "|${KEEP_PATTERN}|" != *"|${file_date}|"* ]] && rm -f "$f"
done

# Atomic rebuild of the combined blocklist
cat "${CACHE_DIR}"/*.txt | sort -u > "${OUTPUT}.tmp"
mv "${OUTPUT}.tmp" "$OUTPUT"
log "Rebuilt nrd.txt with $(wc -l < "$OUTPUT") unique domains"

# Auto-subscribe Pi-hole on first run, then reload gravity
if [[ "$AUTO_SUBSCRIBE" == "true" ]]; then
  EXISTS=$(docker exec "$PIHOLE_CONTAINER" pihole-FTL sqlite3 /etc/pihole/gravity.db \
    "SELECT COUNT(*) FROM adlist WHERE address = '${FEED_URL}';" 2>/dev/null || echo "0")
  if [[ "$EXISTS" == "0" ]]; then
    log "Subscribing Pi-hole to ${FEED_URL}"
    NOW=$(date +%s)
    docker exec "$PIHOLE_CONTAINER" pihole-FTL sqlite3 /etc/pihole/gravity.db \
      "INSERT INTO adlist (address, enabled, date_added, date_modified, comment, type)
       VALUES ('${FEED_URL}', 1, ${NOW}, ${NOW}, 'WhoisFreaks NRD', 0);"
  fi
fi

[[ "$TRIGGER_GRAVITY" == "true" ]] && docker exec "$PIHOLE_CONTAINER" pihole -g

A few design decisions worth flagging:

  • Per-day caching. Each day's NRDs are stored as 2026-05-21_gtld.txt. The script only downloads days it doesn't already have. Re-running mid-day is a no-op.
  • Atomic writes. The combined nrd.txt is built into nrd.txt.tmp then mv'd into place. Pi-hole never reads a half-written file during gravity rebuild.
  • Auto-subscribe is idempotent. The script checks Pi-hole's adlist table before inserting. Runs every cron tick, only acts on the first one.
  • Date arithmetic in UTC. WhoisFreaks publishes by UTC date. Local time would cause off-by-one bugs.

Make it executable:

chmod +x ~/pihole/feed/fetch-nrd.sh

Step 4 – The container entrypoint

Save this as ~/pihole/feed/entrypoint.sh:

#!/bin/bash
set -euo pipefail

CRON_SCHEDULE="${CRON_SCHEDULE:-0 3 * * *}"
SCRIPT_PATH="/var/feed/fetch-nrd.sh"
LOG_FILE="/var/log/nrd-fetcher.log"

apk add --no-cache --quiet bash curl coreutils tzdata busybox-suid docker-cli >/dev/null
chmod +x "$SCRIPT_PATH"
touch "$LOG_FILE"

# Run once at startup so you don't wait until the next cron tick
env > /etc/environment
"$SCRIPT_PATH" 2>&1 | tee -a "$LOG_FILE" || echo "WARN: initial fetch failed"

# Install crontab
cat > /etc/crontabs/root <<EOF
${CRON_SCHEDULE} . /etc/environment; ${SCRIPT_PATH} >> ${LOG_FILE} 2>&1
EOF

# Graceful shutdown: kill crond cleanly when the container stops
shutdown() {
  kill -TERM "$CROND_PID" 2>/dev/null || true
  wait "$CROND_PID" 2>/dev/null || true
  exit 0
}
trap shutdown SIGTERM SIGINT

crond -f -L "$LOG_FILE" -l 8 &
CROND_PID=$!
tail -F "$LOG_FILE" &
wait "$CROND_PID"
chmod +x ~/pihole/feed/entrypoint.sh

The two interesting choices here: cron runs inside the container as the main process, and the trap handler kills it cleanly when Docker sends SIGTERM. That answers the obvious question, "what happens to the schedule if I stop the stack?" It stops with it. No orphan cron entries on the host.

Step 5 – The compose file

Save this as ~/pihole/docker-compose.yml:

services:
  pihole:
    container_name: pihole
    image: pihole/pihole:latest
    ports:
      - "53:53/tcp"
      - "53:53/udp"
      - "8080:80/tcp"
    environment:
      TZ: 'Asia/Karachi'
      FTLCONF_webserver_api_password: 'change-this-password'
    volumes:
      - './etc-pihole:/etc/pihole'
      - './etc-dnsmasq.d:/etc/dnsmasq.d'
    restart: unless-stopped
    depends_on:
      - feed-server

  feed-server:
    container_name: nrd-feed-server
    image: nginx:alpine
    volumes:
      - './feed:/usr/share/nginx/html:ro'
    restart: unless-stopped

  feed-fetcher:
    container_name: nrd-feed-fetcher
    image: alpine:latest
    environment:
      WINDOW_DAYS: "10"              # <-- TUNE THIS: 5, 10, 14, 30
      FEED_TYPES: "gtld cctld"
      CRON_SCHEDULE: "0 3 * * *"
      AUTO_SUBSCRIBE: "true"         # auto-add the feed URL to Pi-hole's Lists
      TRIGGER_GRAVITY: "true"
      PIHOLE_CONTAINER: "pihole"
      FEED_URL: "http://feed-server/nrd.txt"
      API_KEY_FILE: "/etc/whoisfreaks/apikey"
    volumes:
      - './feed:/var/feed'
      - '/etc/whoisfreaks/apikey:/etc/whoisfreaks/apikey:ro'
      - '/var/run/docker.sock:/var/run/docker.sock'
    entrypoint: ["/bin/sh", "/var/feed/entrypoint.sh"]
    restart: unless-stopped
    stop_grace_period: 30s
    depends_on:
      - pihole

The WINDOW_DAYS line is your knob. Change "10" to "5", "14", or "30", whatever matches your risk tolerance.

Step 6 – Bring it up

docker compose up -d
docker logs -f nrd-feed-fetcher

First run takes 2 to 3 minutes. The fetcher pulls every day in your window (10 days by 2 feed types is 20 files), then subscribes Pi-hole and reloads gravity. Expected output:

[2026-05-22T08:42:11Z] Starting NRD fetch | window=10d | feeds=gtld cctld
[2026-05-22T08:42:14Z]   fetching gtld for 2026-05-21
[2026-05-22T08:42:18Z]     cached 187432 domains
... (continues for 10 days) ...
[2026-05-22T08:44:02Z] Rebuilt /var/feed/nrd.txt with 2847512 unique domains
[2026-05-22T08:44:02Z] Waiting for Pi-hole to be ready...
[2026-05-22T08:44:08Z] Pi-hole is ready
[2026-05-22T08:44:08Z] Subscribing Pi-hole to http://feed-server/nrd.txt
[2026-05-22T08:44:08Z] Subscribed successfully
[2026-05-22T08:44:08Z] Triggering gravity reload on container 'pihole'
[2026-05-22T08:44:31Z] Gravity reload complete
WF Pihole NRD Feed Docker Output

The next day, the cron tick at 3 AM fetches one new day, prunes one old day, and reloads gravity. Steady state.

Step 7 – Verify it worked

The fetcher already added the feed to Pi-hole and reloaded gravity. This step is just confirmation.

Open the admin UI: http://localhost:8080/admin

Pi-hole Admin Dashboard Login

Log in with the password you set. You'll land on the dashboard.

Pi-hole Dashboard

Click Lists in the sidebar. You should see the WhoisFreaks entry was added automatically:

Pi-hole Dashboard Lists

That is the whole integration. From here, there is nothing more to do. The fetcher refreshes the file every night, the cron tick reloads gravity automatically, and Pi-hole stays current.

What this actually does for you

Eight concrete use cases, in order of payoff.

1. Stop phishing and smishing at the DNS layer

This is the headline. SMS phishing kits and email phishing campaigns run on domains registered hours to days before the campaign goes live. With the NRD feed in Pi-hole, clicking a freshly-spun phishing link returns NXDOMAIN before TLS even negotiates. Users see "site not available" instead of a fake login page. Nothing to install on their devices. Nothing for them to remember.

2. Brand impersonation monitoring

WhoisFreaks gives you the full daily registration feed, not a curated subset. That means you can grep it for substrings that matter: your company name, your bank, executive surnames, brands your household uses. A nightly script piping new registrations through grep -i 'yourbrand\|y0urbrand\|yourbrnd' and dumping matches into Pi-hole's custom block list is, effectively, free brand-protection tooling. Pair a match with a reverse WHOIS lookup to surface every other domain tied to the same registrant.

3. Catch malware C2 before it phones home

Modern malware often registers C2 domains days before deployment, well inside the dormancy window covered earlier. Infected IoT devices on your LAN trying to beacon out hit your rolling NRD list before they ever reach the actual C2 server. That is particularly useful for devices you cannot run endpoint protection on: printers, cameras, smart TVs, the cheap router someone plugged in.

4. Block consumer scam infrastructure

Rebill scams, fake delivery-fee pages, fake stores, crypto-drainer sites: all of it runs on NRDs because the infrastructure is disposable by design. A single feed protects every less-technical person on your network from the entire category.

5. Event-driven spike protection

NRD volume explodes around major events. Unit 42 measured a 656% increase in average daily coronavirus-related registrations between February and March 2020, with malicious registrations up 569%. The same pattern repeats around the World Cup, US elections, iPhone launches, and AI hype cycles. Because WhoisFreaks delivers a filterable feed, you can write a small script during a hot event that force-blocks any NRD containing the event keyword. Five lines of bash, real protection.

6. TLD-level risk shaping

A handful of cheap, low-barrier TLDs concentrate a disproportionate share of newly registered malicious domains. Spamhaus reports that the bulk of newly registered malicious domains cluster in the lowest-priced registration tiers, with TLDs like .top appearing repeatedly in its abuse data. You can either block the worst offenders outright with a Pi-hole regex ((\.)(xyz|top|click|cfd|sbs)$), or, much more gently, block only the NRDs within them and let aged domains through. That middle ground is something static blocklists cannot offer.

7. Retroactive incident hunting

The one most people miss. Pi-hole logs every DNS query on your network. If something goes weird, say a device acting strange or a phishing email opened, you can intersect Pi-hole's query log with the WhoisFreaks NRD feed from that window and instantly surface every suspicious domain the device touched. Poor-man's DNS threat hunt, zero extra cost.

8. Shadow IT and unauthorized service detection

A device suddenly resolving an NRD that matches no known service is a strong signal. Could be malware. Could be a family member trying a sketchy new app. Could be a compromised IoT device joining a botnet. Even in log-only mode, an NRD feed gives you visibility you did not have before.

How to pick your window length

Three rough profiles based on what works in real deployments:

Conservative (5 days). Use this if your users are technical, your network has lots of legitimate new-domain traffic (developers signing up for SaaS tools constantly, marketing teams testing new vendors), and you would rather miss some threats than break workflows. Smishing Triad-style campaigns are still caught, since more than 70% of those domains stay active for less than a week.

Balanced (10 days). The default. Catches most malware C2 staging, most phishing campaigns, most consumer scams. False positive rate is manageable with a small allowlist. What I'd recommend most home users and small offices start with.

Aggressive (30 days). Unit 42's actual recommendation. Maximum coverage. Expect to spend the first week adding legitimate domains to Pi-hole's allowlist as users hit them. Best for hardened environments: security-conscious households, lab networks, or offices where the security team is willing to handle exceptions.

You can change the number any time. Edit WINDOW_DAYS in the compose file, run docker compose up -d, and the next cron tick adjusts the cache accordingly. Going from 10 to 30 fetches the missing 20 days. Going from 30 to 10 prunes the cache down. No manual cleanup, no list resubscription.

Things that will bite you

False positives are real. Legitimate businesses register new domains every day. Run NRD blocking in log-only mode for the first week (use Pi-hole's group-based filtering to apply the list only to a test device) and watch the query log. Anything that breaks for a known-good service goes onto the allowlist. Pi-hole's allowlist beats gravity, so this is cheap insurance.

Memory matters on tiny devices. A 30-day window can easily hit 5-8 million domains. On a Raspberry Pi Zero or a 512 MB VM, gravity rebuild will get painful. Pi 4 with 2 GB+ handles it fine.

WhoisFreaks publishes previous-day ccTLDs first. The script asks for "yesterday UTC" and back. Same-day data is still being consolidated when the early cron tick runs. This is fine because phishing infrastructure rarely fires on the same day it is registered, and waiting for the consolidated file is the cost of getting clean, deduplicated data.

Watch your daily quota. First run is heavy: 10 days by 2 feeds is 20 downloads. Steady state is 2 downloads per day. Check your WhoisFreaks plan limits if you are on a low tier.

Pair Pi-hole with Unbound. A local recursive resolver means you're not leaking lookups of suspicious NRDs to a third-party DNS provider. Useful if you're investigating them.

Frequently asked questions

What is the best rolling window length for blocking newly registered domains?
Ten days is the most balanced default for home and small-office networks. It catches most malware staging and phishing while keeping false positives manageable with a short allowlist. Use 5 days if your users sign up for new services often, and 30 days for hardened environments that can absorb extra exceptions.

Will blocking newly registered domains break legitimate sites?
Some, at first. Legitimate businesses register new domains every day, so expect occasional false positives. Run the feed in log-only mode on a test device for the first week, watch the query log, and add any known-good domains to Pi-hole's allowlist. The allowlist overrides the blocklist, so this is low-risk.

How is an NRD feed different from a normal Pi-hole blocklist?
A normal blocklist blocks domains already confirmed as bad. An NRD feed blocks by age instead, denying every domain registered in the last N days regardless of reputation. It catches fresh threats before any reputation system has flagged them, which is exactly when phishing and malware domains are most active.

Do I need a paid plan to use the WhoisFreaks NRD feed?
The feed requires a WhoisFreaks account and an NRD API key. Free community NRD lists exist but vary in TLD coverage and often skip ccTLDs. A paid feed provides full gTLD and ccTLD coverage across 1,528+ TLDs with WHOIS and DNS enrichment, which matters for brand monitoring and threat hunting.

Does this setup work with AdGuard Home or other resolvers?
The Docker stack in this guide targets Pi-hole specifically, but the same WhoisFreaks NRD feed works as a CSV blocklist source for AdGuard Home, Zscaler, Cisco Umbrella, Quad9, and RPZ-based resolvers. The fetcher and rolling-window logic stay the same; only the subscribe step changes per resolver.

Why this beats reactive blocklists

Curated threat feeds assume the threat-intel ecosystem can keep pace with attackers. The data from Unit 42, Spamhaus, and the Smishing Triad telemetry says it cannot, and the gap is widening. Attackers register, weaponize, and burn domains faster than human-vetted blocklists can react.

NRD-based blocking shifts the burden. Instead of trusting that someone, somewhere has already flagged a domain as bad, you make a policy decision: nothing this new is welcome on my network for the next N days. A philosophical shift that turns Pi-hole from an ad blocker into a real DNS firewall.

The WhoisFreaks NRD feed is the cleanest way to feed it. Daily updates, gzipped CSVs separated by gTLD and ccTLD, optional WHOIS enrichment for downstream registrant clustering. The integration is three containers and one cron entry. Auto-subscription on first run. Configurable to your tolerance. Twenty minutes of setup for a layer of protection no curated blocklist will ever match.

Get a WhoisFreaks NRD API key →


Source code: the full setup (fetch-nrd.sh, entrypoint.sh, and docker-compose.yml) lives on GitHub at github.com/WhoisFreaks/wf-pihole-nrd-feed. Clone it, drop in your API key, and run docker compose up -d.