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>
238 lines
9.1 KiB
Markdown
238 lines
9.1 KiB
Markdown
---
|
|
title: "Pi-hole v6 Group Management: Per-Client DNS Rules"
|
|
domain: selfhosting
|
|
category: dns-networking
|
|
tags: [pihole, pihole-v6, dns, groups, allowlist, api, networking, runbook]
|
|
status: published
|
|
created: 2026-04-22
|
|
updated: 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
|
|
|
|
```bash
|
|
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
|
|
|
|
```bash
|
|
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
|
|
|
|
```bash
|
|
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
|
|
|
|
```bash
|
|
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
|
|
|
|
```bash
|
|
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:
|
|
|
|
```bash
|
|
# 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:
|
|
|
|
```bash
|
|
# 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
|
|
|
|
```bash
|
|
#!/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
|
|
|
|
```bash
|
|
# 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.
|
|
|
|
## Related
|
|
|
|
- [[pihole-v6-adlist-management]] -- managing adlists via SQL on Pi-hole v6
|
|
- [[pihole-doh-dot-bypass-defense]] -- preventing clients from bypassing Pi-hole with DoH/DoT
|