Post

🛡️ Building a Dashboard for Group/Category Based Content Filtering for Pi-hole.

Internet Managment Solution for Pi-hole

🛡️ Building a Dashboard for Group/Category Based Content Filtering for Pi-hole.

Overview

In this guide, we walk through how we created a custom Pi-hole Dashboard using Flask with features like query logs, regex blacklisting, real-time alerts, and group toggles. We also detail how we deployed the app securely using ModSecurity + OWASP CRS behind an Nginx reverse proxy in Docker.


🔧 Key Features of the IMS Dashboard (Internet Managment Solution)

✅ 1. User Authentication

  • Login system using Flask sessions.
  • Basic credentials (admin/whoami by default — replace in production).

📊 2. Live DNS Statistics

  • Total Queries
  • Blocked Queries
  • Block Percentage
  • Auto-refreshed every 5 seconds via AJAX.

🧾 3. Query Log Viewer

  • Real-time display of the latest 100 queries from Pi-hole.
  • Blocked domains highlighted using Bootstrap’s red table rows.
  • Auto-updates every 5 seconds.
  • Alerts shown if newly blocked domains are detected.

🧰 4. Pi-hole Group Management

  • Dashboard homepage shows all non-default Pi-hole groups.
  • Displays:
    • Group ID
    • Name
    • Current Status (Enabled / Disabled)
  • Enable/Disable toggle buttons for each group.
  • Calls /toggle_group/<group_id> which updates Pi-hole’s gravity.db and reloads DNS.

🚫 5. Blacklist Regex Management

  • Add domains or full URLs to the Pi-hole regex blacklist.
  • Prevents duplicates.
  • Avoids blacklisting domains already whitelisted via whitelists.yml.
  • Assign domains to selected group (one group per domain enforced).

🧹 6. Blacklist Viewer & Domain Removal

  • View blacklisted domains for each group.
  • Delete specific regex entries safely (removes from both mapping table and domainlist if unused).

🚨 7. Real-Time Alerting

  • Frontend polls /recent-blocked-domains every 10 seconds.
  • New blocked domains trigger an on-screen alert.
  • Alerts persist with cooldown using localStorage to avoid spamming.

🧱 Tech Stack

  • Backend: Flask
  • Frontend: Bootstrap 5 + Vanilla JS
  • Database: SQLite (/etc/pihole/gravity.db)
  • Security: ModSecurity + OWASP CRS
  • Reverse Proxy: Nginx (Docker)
  • System: Alpine Linux in Proxmox LXC

📦 Requirements

requirements.txt:

Flask==2.3.3
requests==2.31.0
PyYAML==6.0.1

Install with:

1
pip install -r requirements.txt

🚀 Deployment Guide

1. Start Flask on Host (Port 5000)

Optional OpenRC service file (for Alpine Linux):

1
2
3
4
5
6
7
#!/sbin/openrc-run
command="/usr/bin/python3"
command_args="/root/pihole-dash/app.py"
pidfile="/run/pihole-dashboard.pid"
name="pihole-dashboard"
description="Pi-hole Flask Dashboard"
command_background=true

Start with:

1
2
rc-update add pihole-dashboard
rc-service pihole-dashboard start

2. Dockerized Nginx with ModSecurity

Dockerfile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
FROM owasp/modsecurity-crs:4.15.0-nginx-alpine-202506050606

USER root

#COPY ./nginx.conf /etc/nginx/conf.d/default.conf

# Update apk repos and install certbot + nginx plugin + bash (optional)
RUN apk update && apk add --no-cache \
    certbot \
    certbot-nginx \
    bash

CMD ["nginx", "-g", "daemon off;"]

nginx.conf (Localhost, No HTTPS)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
server {
    listen 80;
    server_name localhost;

    # Reverse proxy to Flask app running on host system
    location / {
        proxy_pass http://192.168.1.15:5000;  # or http://<host-ip>:5000 if not on Docker Desktop
        proxy_http_version 1.1;
        proxy_set_header Host              $host;
        proxy_set_header X-Real-IP         $remote_addr;
        proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    # Security headers (optional for local testing)
    add_header X-Frame-Options "DENY" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-XSS-Protection "1; mode=block" always;
    add_header Content-Security-Policy "default-src 'self';
        style-src 'self' 'unsafe-inline' https://fonts.googleapis.com https://cdn.jsdelivr.net;
        script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net;
        font-src 'self' https://fonts.gstatic.com https://cdn.jsdelivr.net;
        connect-src 'self';
        img-src 'self';" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;
    add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
    add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate" always;
    add_header Expect-CT "max-age=86400, enforce" always;

    # Hide nginx version
    server_tokens off;

    # Custom error page (optional)
    error_page 404 /404.html;
}


4. Run Nginx Docker Container

1
2
docker build -t ims-dashboard-nginx .
docker run -d --name ims-dashboard-nginx  -p 80:80 -p 443:443 --add-host=host.docker.internal:host-gateway ims-dashboard-nginx

🔒 Security Tips

  • Replace default credentials
  • Use HTTPS (Certbot) if exposing externally
  • Use ufw or iptables to restrict access
  • Consider adding fail2ban to protect login endpoint
  • Don’t disable ModSecurity rules globally — use targeted overrides

✅ Final Thoughts

This project brings together a clean UI for Pi-hole admins, with real-time data and security-focused deployment practices. It’s ideal for home networks or small environments where simplicity and visibility are key.


Happy Pi-holing! 🧠🔒

This post is licensed under CC BY 4.0 by the author.