majorwiki/02-selfhosting/dns-networking/pihole-v6-group-management.md
majorlinux 599080bf91 Add wiki article: Pi-hole v6 Group Management — Per-Client DNS Rules
Covers creating groups, assigning clients, scoping allow rules to
specific groups via API and CLI. Includes ghost attribution gotcha
(router DNS proxy + secondary DNS causes FTL cache mis-attribution)
and the fix (Pi-hole as sole DNS, remove secondary).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-22 19:59:22 -04:00

9.1 KiB

title domain category tags status created updated
Pi-hole v6 Group Management: Per-Client DNS Rules selfhosting dns-networking
pihole
pihole-v6
dns
groups
allowlist
api
networking
runbook
published 2026-04-22 2026-04-22

Pi-hole v6 Group Management: Per-Client DNS Rules

The Problem

On a home network with mixed devices -- personal machines, work-issued laptops, IoT gadgets -- a single blocking policy does not fit. Work devices need Microsoft Intune, Zscaler, and Office 365 domains allowed so MDM and VPN stay functional. Personal devices should block those same Microsoft telemetry domains via adlists. A blanket allowlist defeats the purpose of running Pi-hole in the first place.

Pi-hole v6 solves this with groups. Assign clients to groups, then scope allow rules (and deny rules, adlists, etc.) to specific groups. Only clients in a given group see that group's rules. Everyone else stays on the default blocking policy.

Prerequisites

  • Pi-hole v6 installed (pihole -v shows Core v6.x).
  • Admin password set (needed for API auth).
  • sudo access to the Pi-hole host for CLI commands.

Concepts

Term Meaning
Group A named bucket. Clients, adlists, and domain rules are all assigned to one or more groups.
Default group Group ID 0. Every client belongs to it unless explicitly removed. All rules apply to Default by default.
Client Identified by IP address (or MAC, subnet, or interface). Added via the UI or API.
Allow rule An exact domain or regex/wildcard pattern that overrides gravity (blocklists) for clients in the assigned group(s).

Setup via the Web UI

  1. Settings > Groups -- create a new group (e.g., "Work"). Note the group ID assigned (visible in the table).
  2. Settings > Clients -- add the work device by IP address. Under "Group assignment," add it to "Work" (keep Default too, unless you want to strip all default rules).
  3. Domains > Allow -- add the domains that need to pass through. Under "Group assignment," remove Default and assign only "Work."

Result: the allow rules only apply to clients in the Work group. All other clients remain blocked by gravity.

Setup via the Pi-hole v6 API

All API calls go to http://<pihole-ip>/api/. Authenticate first to get a session token.

Authenticate

SID=$(curl -s -X POST http://192.168.50.238/api/auth \
  -H "Content-Type: application/json" \
  -d '{"password": "your-pihole-password"}' | jq -r '.session.sid')

Use $SID in subsequent requests as a bearer token.

Create a group

curl -s -X POST http://192.168.50.238/api/groups \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $SID" \
  -d '{"name": "Work", "comment": "Work-issued devices -- allow Microsoft/Zscaler"}'

The response includes the new group's id (e.g., 3). Note it for later.

Add a client and assign to groups

curl -s -X POST http://192.168.50.238/api/clients \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $SID" \
  -d '{
    "client": "192.168.50.100",
    "comment": "Work MacBook",
    "groups": [0, 3]
  }'

groups: [0, 3] means Default + Work. Omit 0 if you want the client excluded from Default group rules entirely.

Update an existing client's groups

curl -s -X PUT "http://192.168.50.238/api/clients/192.168.50.100" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $SID" \
  -d '{"groups": [0, 3]}'

Add a wildcard allow rule via CLI

sudo pihole --allow-wild login.microsoftonline.com

This creates the regex allow entry in the database. By default it is assigned to group 0 (Default), meaning every client gets the allow. To restrict it to Work only, update the rule's group assignment via the API.

Reassign an allow rule to a specific group

First, find the rule's ID. Exact allows and regex allows use separate endpoints:

# List exact allow rules
curl -s http://192.168.50.238/api/domains/allow/exact \
  -H "Authorization: Bearer $SID" | jq '.domains[] | {id, domain, groups}'

# List regex allow rules
curl -s http://192.168.50.238/api/domains/allow/regex \
  -H "Authorization: Bearer $SID" | jq '.domains[] | {id, domain, groups}'

Then update the groups on the target rule:

# For an exact allow rule (e.g., ID 5)
curl -s -X PUT "http://192.168.50.238/api/domains/allow/exact/5" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $SID" \
  -d '{"groups": [3]}'

# For a regex allow rule (e.g., ID 2)
curl -s -X PUT "http://192.168.50.238/api/domains/allow/regex/2" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $SID" \
  -d '{"groups": [3]}'

Setting "groups": [3] removes the rule from Default (group 0). Only clients in the Work group (group 3) will match it.

Practical Example: Work Devices + Microsoft/Zscaler

A work laptop running Microsoft Intune and Zscaler needs the following domains (among others) to function:

login.microsoftonline.com
*.login.microsoftonline.com
device.login.microsoftonline.com
enterpriseregistration.windows.net
manage.microsoft.com
*.manage.microsoft.com
gateway.zscaler.net
*.zscaler.net
*.zscalerone.net
*.zscalertwo.net
*.zscalerthree.net
*.zscloud.net

Meanwhile, the rest of the network blocks Microsoft telemetry via adlists (e.g., HaGeZi's Microsoft tracker list).

Step-by-step

  1. Create the "Work" group (API or UI).
  2. Add the work laptop's IP to Pi-hole as a client, assigned to groups [0, 3].
  3. Add each domain above as an allow rule -- use pihole --allow-wild for wildcard entries, or add exact entries via the API.
  4. For every allow rule created, update its group assignment to [3] only (removing Default).
  5. Verify: from the work laptop, dig +short login.microsoftonline.com @192.168.50.238 should return a real IP. From a personal device, the same query should return 0.0.0.0 (blocked by gravity).

Scripting it

#!/bin/bash
PIHOLE="http://192.168.50.238"
WORK_GROUP=3

SID=$(curl -s -X POST "$PIHOLE/api/auth" \
  -H "Content-Type: application/json" \
  -d '{"password": "your-password"}' | jq -r '.session.sid')

DOMAINS=(
  "login.microsoftonline.com"
  "device.login.microsoftonline.com"
  "enterpriseregistration.windows.net"
  "manage.microsoft.com"
)

WILDCARDS=(
  "(\\.|^)login\\.microsoftonline\\.com$"
  "(\\.|^)manage\\.microsoft\\.com$"
  "(\\.|^)zscaler\\.net$"
  "(\\.|^)zscalerone\\.net$"
  "(\\.|^)zscalertwo\\.net$"
  "(\\.|^)zscalerthree\\.net$"
  "(\\.|^)zscloud\\.net$"
)

# Add exact allows scoped to Work group
for d in "${DOMAINS[@]}"; do
  curl -s -X POST "$PIHOLE/api/domains/allow/exact" \
    -H "Content-Type: application/json" \
    -H "Authorization: Bearer $SID" \
    -d "{\"domain\": \"$d\", \"comment\": \"Work device allow\", \"groups\": [$WORK_GROUP]}"
  echo ""
done

# Add regex allows scoped to Work group
for r in "${WILDCARDS[@]}"; do
  curl -s -X POST "$PIHOLE/api/domains/allow/regex" \
    -H "Content-Type: application/json" \
    -H "Authorization: Bearer $SID" \
    -d "{\"domain\": \"$r\", \"comment\": \"Work device wildcard allow\", \"groups\": [$WORK_GROUP]}"
  echo ""
done

Ghost Attribution Gotcha

The symptom

Pi-hole's query log shows blocked domains attributed to the wrong client. A query from a personal device appears under the work laptop's IP, or vice versa. Group-scoped allow rules fire (or do not fire) for the wrong device.

The cause

When the router acts as a DNS proxy (forwarding queries from clients to Pi-hole), FTL sees all queries as coming from the router's IP. Pi-hole v6's FTL engine can also mis-attribute cached responses when the upstream source is a single forwarding IP -- it associates the cached response with whichever client triggered the cache fill, not the client making the current request.

The fix

  1. Set Pi-hole as the only DNS server in DHCP. In the router's DHCP settings, set the primary DNS to the Pi-hole IP (e.g., 192.168.50.238) and remove any secondary DNS entries.
  2. Remove the router as a DNS intermediary. Clients must query Pi-hole directly, not through the router. If the router insists on injecting itself as DNS, disable that feature (varies by router firmware).
  3. Set the router's own WAN DNS to Pi-hole. The router itself should use Pi-hole for its own lookups. This ensures the router's own queries are logged accurately and do not pollute client attribution.

After making these changes, each client's queries arrive at Pi-hole from the client's own IP. FTL attributes them correctly. Group rules apply as intended.

Verifying correct attribution

# Check the last 20 queries and their sources
curl -s "http://192.168.50.238/api/queries?length=20" \
  -H "Authorization: Bearer $SID" | jq '.queries[] | {client, domain, status}'

Every entry should show the actual client IP, not the router's IP. If you still see the router IP as the source for client queries, the DHCP or router DNS proxy configuration is not yet correct.