From 4b63d295d4185eb8af6b0b0aaaafb3ac052e3ae0 Mon Sep 17 00:00:00 2001 From: Tim Baumann Date: Wed, 28 Oct 2026 05:15:45 +0100 Subject: [PATCH] docs: refresh READMEs and remove stale artifacts --- .gitignore | 1 + CHANGELOG.md | 10 ++ README.de.md | 98 +++++++++++------ README.md | 101 ++++++++++++------ deploy/Caddyfile.maintenance | 4 +- docs/CONSOLE_MENU.md | 6 -- docs/MAINTENANCE_WEB.md | 7 +- docs/assets/.gitkeep | 0 docs/assets/screenshot_dns_check.png | Bin 10380 -> 0 bytes docs/assets/screenshot_rescue_menu.png | Bin 33900 -> 0 bytes docs/assets/screenshot_status_check.png | Bin 37036 -> 0 bytes .../plans/2026-09-07-maintenance-web.md | 4 +- .../2026-09-07-maintenance-web-design.md | 8 +- maintenance_web.py | 4 +- tests/test_install_maintenance_web.sh | 12 +-- tests/test_maintenance_config.py | 8 +- tests/test_maintenance_web.py | 7 ++ tests/test_upgrade_container.sh | 4 +- 18 files changed, 178 insertions(+), 96 deletions(-) delete mode 100644 docs/assets/.gitkeep delete mode 100644 docs/assets/screenshot_dns_check.png delete mode 100644 docs/assets/screenshot_rescue_menu.png delete mode 100644 docs/assets/screenshot_status_check.png diff --git a/.gitignore b/.gitignore index 2db5fd6..15c5ac3 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,7 @@ report.md # Python cache __pycache__/ +.pytest_cache/ *.pyc # VS Code diff --git a/CHANGELOG.md b/CHANGELOG.md index 2fc1a2a..9be1358 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ All notable changes to this project will be documented in this file. ## [Unreleased] ### Added +- Authenticated read-only DNS dashboard at `/dashboard/` alongside the separate maintenance panel at `/`. +- Production setup and API data-availability notes in both READMEs. - **Rescue & Backup Menu** (`scripts/rescue_menu.sh` v1.0.0) — standalone interactive recovery tool with: - System status check (services, DNS, ports, temperature) - DNS loop / upstream check with blocking test @@ -20,9 +22,17 @@ All notable changes to this project will be documented in this file. - **Unified output format**: `tools/pihole_maintenance_pro.sh` `run_step()` now uses `log_ok`/`log_warn`/`log_err` from `scripts/lib/ui.sh` ### Changed +- Existing deployments with the maintenance panel can use `--upgrade-only` without changing DNS configuration or the Python environment. +- README platform, Python, dashboard, maintenance, and repository-structure details now match the current implementation. - Console menu Exit moved from [7] to [8] to accommodate new Rescue entry - `action_check_mode()` in console menu now also validates rescue_menu availability +### Security +- Bound DNS-log line parsing and keep the browser API key in memory only. +- Grant the suite service read access to Pi-hole logs through its supplementary group. +- Restrict the test workflow token to `contents: read`. +- Removed outdated screenshots that showed failed checks or the private LAN address. + ## [0.2.0] — 2026-01-02 ### Added diff --git a/README.de.md b/README.de.md index 96653df..9e06051 100644 --- a/README.de.md +++ b/README.de.md @@ -8,7 +8,7 @@ [![Pi-hole](https://img.shields.io/badge/Pi--hole-v6.x-red?style=for-the-badge&logo=pihole)](https://pi-hole.net/) [![Unbound](https://img.shields.io/badge/Unbound-DNS-orange?style=for-the-badge)](https://nlnetlabs.nl/projects/unbound/) [![Debian](https://img.shields.io/badge/Debian-Bookworm%2FTrixie-red?style=for-the-badge&logo=debian)](https://debian.org/) -[![Python](https://img.shields.io/badge/Python-3.12+-blue?style=for-the-badge&logo=python)](https://python.org/) +[![Python](https://img.shields.io/badge/Python-3.10+-blue?style=for-the-badge&logo=python)](https://python.org/) [![Buy Me A Coffee](https://img.shields.io/badge/Buy%20Me%20a%20Coffee-support-FFDD00?logo=buymeacoffee&logoColor=000&style=for-the-badge)](https://buymeacoffee.com/timintech) Tech Stack @@ -30,6 +30,19 @@ Client → Pi-hole (Port 53) → Unbound (Port 5335) → Internet Pi-hole blockiert Werbung und Tracker. Unbound löst DNS-Anfragen rekursiv auf — ohne externen DNS-Resolver, mit vollständiger DNSSEC-Validierung. +## Projektstatus und letzte Aktualisierung + +**Dokumentation zuletzt geprüft:** 2026-10-28 + +**Letztes Projektupdate:** 2026-10-28 — [PR #32](https://github.com/TimInTech/Pi-hole-Unbound-PiAlert-Setup/pull/32), [PR #33](https://github.com/TimInTech/Pi-hole-Unbound-PiAlert-Setup/pull/33) und [PR #34](https://github.com/TimInTech/Pi-hole-Unbound-PiAlert-Setup/pull/34). + +- Das lesende DNS-Dashboard wurde mit der authentifizierten Wartungsoberfläche integriert. +- Updates wurden abgesichert, DNS-Logzeilen begrenzt, der API-Key aus dauerhaftem Browser-Speicher entfernt und der Logzugriff auf eine Leseberechtigung beschränkt. +- Der Test-Workflow verwendet nur noch schreibgeschützte Repository-Berechtigungen. +- DNS-/DNSSEC-Verhalten, beide Weboberflächen und authentifizierte API-Routen wurden produktiv geprüft. + +NetAlertX ist optional. Bei der letzten Produktionsprüfung war es nicht eingerichtet; `/devices` meldet daher HTTP 503, statt eine leere Geräteliste vorzutäuschen. Pi-hole-/Unbound-Monitoring und DNS funktionieren unabhängig davon. Die Änderungshistorie steht in [CHANGELOG.md](CHANGELOG.md). + --- ## ⚡ Schnellstart @@ -54,7 +67,7 @@ Für eine bereits laufende Suite mit Wartungsoberfläche aktualisiert `sudo ./in | **Plattform** | Raspberry Pi 3/4/5, Debian Bookworm/Trixie (64-bit) | | **Pi-hole** | v6.x (wird durch dieses Script installiert) | | **Unbound** | Wird installiert und auf Port 5335 konfiguriert | -| **Python** | 3.12+ (für optionale Suite API) | +| **Python** | 3.10+ (für optionale Suite API und Wartungsoberfläche) | | **Nutzer** | Normaler Nutzer mit sudo | Abhängigkeiten manuell installieren (optional): @@ -123,8 +136,6 @@ Eigenständiges Wiederherstellungs- und Diagnosetool. Global aufrufbar: sudo pihole-rescue ``` -![Rescue Menu](docs/assets/screenshot_rescue_menu.png) - | Option | Aktion | |--------|--------| | 1 | Systemstatus (Services, DNS, Ports, Temperatur) | @@ -140,14 +151,6 @@ sudo pihole-rescue | 11 | Letzten Report / Log anzeigen | | 0 | Beenden | -**Systemstatus:** - -![System Status](docs/assets/screenshot_status_check.png) - -**DNS-Check:** - -![DNS Check](docs/assets/screenshot_dns_check.png) - --- ### 3. Maintenance Pro (`tools/pihole_maintenance_pro.sh`) @@ -168,23 +171,29 @@ Flags: `--no-apt`, `--no-upgrade`, `--no-gravity`, `--restart-ftl`, `--backup`, ``` Pi-hole-Unbound-PiAlert-Setup/ -├── install.sh # Haupt-Installer -├── start_suite.py # Optionale REST-API (FastAPI/uvicorn) +├── install.sh # Host-/Container-Installer +├── start_suite.py # Optionale API und DNS-Dashboard +├── maintenance_web.py # Wartungsoberfläche und Session-Sicherheit +├── maintenance_runner.py # Privilegierter Wartungs-Runner +├── maintenance_backups.py # Verifizierte Backups +├── maintenance_config.py # Caddy-/LAN-Konfiguration +├── maintenance_install.py # Einrichtung der Wartungsoberfläche ├── requirements.txt # Python-Abhängigkeiten ├── .env.example # Umgebungsvariablen-Vorlage +├── dashboard/ # Lesendes DNS-Monitoring-Dashboard +├── web/ # Assets der Wartungsoberfläche +├── deploy/ # systemd-, Caddy- und sudo-Dateien ├── scripts/ +│ ├── install_maintenance_web.sh # Gestufter Installer +│ ├── rollback_maintenance_web.sh # Geprüfter Deployment-Rollback │ ├── console_menu.sh # Interaktives Verwaltungsmenü -│ ├── rescue_menu.sh # Rescue & Backup Menü (sudo pihole-rescue) -│ ├── post_install_check.sh # Post-Install-Verifikation -│ ├── nightly_test.sh # Nächtlicher DNS/Service-Test -│ ├── repo_selftest.sh # Repo-Integritäts-Selbsttest -│ └── lib/ -│ └── ui.sh # Gemeinsame UI-Bibliothek -├── tools/ -│ └── pihole_maintenance_pro.sh # Batch-Wartungsscript -└── docs/ - ├── CONSOLE_MENU.md # Vollständige Menü-Dokumentation - └── assets/ # Screenshots +│ ├── rescue_menu.sh # Rescue- und Backup-Menü +│ └── lib/ui.sh # Gemeinsame Shell-UI +├── tools/pihole_maintenance_pro.sh # Batch-Wartungsscript +├── tests/ # Python-, Shell-, Browser- und Upgrade-Tests +├── docs/ # Betrieb, Entwürfe, Berichte, Screenshots +├── CHANGELOG.md +└── README.md ``` --- @@ -239,9 +248,11 @@ Backups werden in `/home/pi/pihole-rescue-backups/` gespeichert und enthalten: `start_suite.py` ist eine **optionale** FastAPI-Anwendung für Monitoring. Es enthält auch ein modernes, responsives **Dashboard** zur Überwachung Ihres DNS-Stacks. -- **Dashboard-Zugriff**: Standardmäßig erreichbar unter `http://127.0.0.1:8090/`. -- **Fernzugriff**: Ein sicherer Fernzugriff wird über SSH-Portweiterleitung (`ssh -L 8090:127.0.0.1:8090 user@pihole-server`) oder einen Reverse-Proxy empfohlen. +- **Dashboard-Zugriff**: Auf einem eingerichteten Host über Caddy unter `https://:8443/dashboard/`. Lokal für die Entwicklung unter `http://127.0.0.1:8090/dashboard/`. +- **Wartungsoberfläche**: Der Root-Pfad `https://:8443/` zeigt die separate Wartungsoberfläche, wenn sie aktiviert ist. +- **Fernzugriff**: Die API bindet an `127.0.0.1:8090`. Diesen Port nicht direkt freigeben; Caddy-LAN-Zugriff oder SSH-Tunnel verwenden. - **Authentifizierung**: Für das Dashboard-Login und die API-Endpunkte ist der `SUITE_API_KEY` erforderlich. +- Der Schlüssel bleibt nur im Speicher der Browserseite. Nach einem Neuladen ist eine erneute Anmeldung nötig. ```bash # Konfiguration @@ -253,23 +264,45 @@ python3 -m venv .venv && source .venv/bin/activate pip install -r requirements.txt # Starten +set -a +. ./.env +set +a python3 start_suite.py # Die API bleibt auf 127.0.0.1:8090; die Produktivdokumentation ist deaktiviert. ``` +Das DNS-Dashboard zeigt Gesamtstatus, Pi-hole-FTL-Status und Version, Upstreams +und die Zahl gelesener Anfragen aus den letzten 100 Logeinträgen; Unbound-Status, +Port und DNS-Prüfung; optionale NetAlertX-Erreichbarkeit; sowie Laufzeit, Version +und Host-IP der Suite-API. Fehlende Daten erscheinen als nicht verfügbar, nicht +als Null. Die API bietet `/health`, `/version`, `/urls`, `/pihole`, `/unbound`, +`/netalertx`, `/stats`, `/dns`, `/leases` und `/devices`. Die Wartungsendpunkte +liegen unter `/api/session` und `/api/maintenance/`. Geräteerkennung antwortet +mit HTTP 503, solange keine NetAlertX-Integration eingerichtet ist. + +Die Wartungsoberfläche zeigt den letzten Auftrag, seine Prüfschritte und eine +Systemmomentaufnahme. Sie kann Systemchecks, verifizierte Backups (bis zu 10, +mit Download und Löschung) sowie kontrollierte Updates ausführen. Eine +Wiederherstellung läuft lokal nach der dokumentierten Recovery-Anleitung, nicht +im Browser. + **Nicht erforderlich** für den Kern-Stack (Pi-hole + Unbound). ### Sichere Wartungsoberfläche -Die optionale Wartungsoberfläche bietet Systemcheck, verifiziertes Backup und -kontrolliertes Update unter `https://pi.hole:8443/`. Sie bleibt standardmäßig -deaktiviert und wird nur zusammen mit Caddy bewusst freigeschaltet. Die -Browser-Anmeldung verwendet den bestehenden `SUITE_API_KEY`: +Die optionale Wartungsoberfläche bietet Systemcheck, verifizierte Backups, +kontrollierte Updates, das letzte Auftragsergebnis, eine Systemmomentaufnahme +und Prüfschritte. Sie bleibt standardmäßig deaktiviert. Bei einer neuen +Host-Installation wird sie so aktiviert: ```bash sudo ./install.sh --with-maintenance-web ``` +Eine bestehende Installation mit Wartungsoberfläche wird mit +`sudo ./install.sh --upgrade-only` aktualisiert. Die Browser-Anmeldung nutzt +den bestehenden `SUITE_API_KEY`. + Siehe [docs/MAINTENANCE_WEB.md](docs/MAINTENANCE_WEB.md) für Voraussetzungen, Zertifikatvertrauen, Sicherheitsgrenze und Rollback. @@ -283,6 +316,9 @@ Netzwerk-Gerätemonitoring (separate Installation): sudo ./install.sh --with-netalertx ``` +Ohne NetAlertX ist die Geräteerkennung nicht verfügbar. DNS-Monitoring und +Pi-hole-/Unbound-Betrieb funktionieren weiterhin. + --- ## 🔎 Manueller Update-Check diff --git a/README.md b/README.md index 033d16d..2b83b57 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ [![Pi-hole](https://img.shields.io/badge/Pi--hole-v6.x-red?style=for-the-badge&logo=pihole)](https://pi-hole.net/) [![Unbound](https://img.shields.io/badge/Unbound-DNS-orange?style=for-the-badge)](https://nlnetlabs.nl/projects/unbound/) [![Debian](https://img.shields.io/badge/Debian-Bookworm%2FTrixie-red?style=for-the-badge&logo=debian)](https://debian.org/) -[![Python](https://img.shields.io/badge/Python-3.12+-blue?style=for-the-badge&logo=python)](https://python.org/) +[![Python](https://img.shields.io/badge/Python-3.10+-blue?style=for-the-badge&logo=python)](https://python.org/) [![Buy Me A Coffee](https://img.shields.io/badge/Buy%20Me%20a%20Coffee-support-FFDD00?logo=buymeacoffee&logoColor=000&style=for-the-badge)](https://buymeacoffee.com/timintech) Tech Stack @@ -32,6 +32,19 @@ Client → Pi-hole (port 53) → Unbound (port 5335) → Internet Pi-hole handles ad/tracker blocking; Unbound handles recursive DNS resolution with full DNSSEC validation — no third-party DNS resolver required. +## Project Status and Latest Update + +**Documentation last checked:** 2026-10-28 + +**Latest project update:** 2026-10-28 — [PR #32](https://github.com/TimInTech/Pi-hole-Unbound-PiAlert-Setup/pull/32), [PR #33](https://github.com/TimInTech/Pi-hole-Unbound-PiAlert-Setup/pull/33), and [PR #34](https://github.com/TimInTech/Pi-hole-Unbound-PiAlert-Setup/pull/34). + +- Integrated the read-only DNS dashboard with the authenticated maintenance panel. +- Hardened upgrades, bounded DNS-log parsing, kept the API key out of persistent browser storage, and enabled least-privilege log access. +- Restricted the test workflow token to read-only repository contents. +- Rechecked production DNS and DNSSEC behavior, both web interfaces, and authenticated API routes. + +NetAlertX is optional. It was not configured on the production system last checked, so `/devices` reports HTTP 503 rather than inventing an empty device list. Pi-hole and Unbound monitoring and DNS are independent of this optional feature. See [CHANGELOG.md](CHANGELOG.md) for the change history. + --- ## ⚡ Quickstart @@ -56,7 +69,7 @@ For an existing suite with the maintenance interface, `sudo ./install.sh --upgra | **Platform** | Raspberry Pi 3/4/5, Debian Bookworm/Trixie (64-bit) | | **Pi-hole** | v6.x (installed by this script) | | **Unbound** | Installed and configured to port 5335 | -| **Python** | 3.12+ (for optional Suite API) | +| **Python** | 3.10+ (for the optional Suite API and maintenance panel) | | **User** | Normal user with sudo | Install prerequisites manually (optional): @@ -125,8 +138,6 @@ Standalone recovery and diagnostic tool. Accessible globally: sudo pihole-rescue ``` -![Rescue Menu](docs/assets/screenshot_rescue_menu.png) - | Option | Action | |--------|--------| | 1 | System status check (services, DNS, ports, temperature) | @@ -142,14 +153,6 @@ sudo pihole-rescue | 11 | Show last report / log | | 0 | Exit | -**System Status:** - -![System Status](docs/assets/screenshot_status_check.png) - -**DNS Check:** - -![DNS Check](docs/assets/screenshot_dns_check.png) - --- ### 3. Maintenance Pro (`tools/pihole_maintenance_pro.sh`) @@ -170,23 +173,29 @@ Available flags: `--no-apt`, `--no-upgrade`, `--no-gravity`, `--restart-ftl`, `- ``` Pi-hole-Unbound-PiAlert-Setup/ -├── install.sh # Main installer -├── start_suite.py # Optional REST API (FastAPI/uvicorn) -├── requirements.txt # Python deps for Suite API -├── .env.example # Environment variables template +├── install.sh # Main host/container installer +├── start_suite.py # Optional authenticated API and DNS dashboard +├── maintenance_web.py # Browser maintenance panel and session security +├── maintenance_runner.py # Privileged maintenance job runner +├── maintenance_backups.py # Verified backup operations +├── maintenance_config.py # Caddy and LAN configuration validation +├── maintenance_install.py # Maintenance panel provisioning +├── requirements.txt # Python API and test dependencies +├── .env.example # Environment variable template +├── dashboard/ # Read-only DNS monitoring dashboard +├── web/ # Maintenance panel assets +├── deploy/ # systemd, Caddy, and sudo policy files ├── scripts/ +│ ├── install_maintenance_web.sh # Staged/live maintenance installer +│ ├── rollback_maintenance_web.sh # Validated deployment rollback │ ├── console_menu.sh # Interactive management menu -│ ├── rescue_menu.sh # Rescue & backup menu (sudo pihole-rescue) -│ ├── post_install_check.sh # Post-install verification -│ ├── nightly_test.sh # Nightly DNS/service test -│ ├── repo_selftest.sh # Repo integrity self-test -│ └── lib/ -│ └── ui.sh # Shared UI library (colors, log helpers) -├── tools/ -│ └── pihole_maintenance_pro.sh # Batch maintenance script -└── docs/ - ├── CONSOLE_MENU.md # Full menu documentation - └── assets/ # Screenshots +│ ├── rescue_menu.sh # Rescue and backup menu +│ └── lib/ui.sh # Shared shell UI helpers +├── tools/pihole_maintenance_pro.sh # Batch maintenance script +├── tests/ # Python, shell, browser, and upgrade tests +├── docs/ # Operations, design notes, reports, screenshots +├── CHANGELOG.md +└── README.de.md ``` --- @@ -241,9 +250,11 @@ Backups are stored in `/home/pi/pihole-rescue-backups/` and include: `start_suite.py` is an **optional** FastAPI server providing a JSON API for monitoring. It also hosts a modern, responsive, read-only web **Dashboard** for monitoring your DNS stack. -- **Dashboard Access**: By default accessible at `http://127.0.0.1:8090/`. -- **Remote Access**: Secure remote access is recommended via SSH port forwarding (`ssh -L 8090:127.0.0.1:8090 user@pihole-server`) or a reverse proxy. +- **Dashboard Access**: On a configured host, open `https://:8443/dashboard/` through Caddy. For local development, use `http://127.0.0.1:8090/dashboard/`. +- **Maintenance Panel**: The root path `https://:8443/` serves the separate maintenance panel when enabled. +- **Remote Access**: The API binds to `127.0.0.1:8090`. Do not expose that port directly; use the configured Caddy LAN endpoint or an SSH tunnel. - **Authentication**: Dashboard login and API endpoints require the `SUITE_API_KEY`. +- The dashboard holds the key in page memory only. Reloading the page requires signing in again. ```bash # Setup @@ -255,23 +266,44 @@ python3 -m venv .venv && source .venv/bin/activate pip install -r requirements.txt # Start +set -a +. ./.env +set +a python3 start_suite.py # The API stays on 127.0.0.1:8090; production API documentation is disabled. ``` +The DNS dashboard reports overall health, Pi-hole FTL status and version, +configured upstreams and the count of parsed queries from the latest 100 log +entries; Unbound status, port and DNS probe; optional NetAlertX reachability; and +Suite API uptime, version and detected host IP. Missing source data is shown as +unavailable, not as zero. The API provides `/health`, `/version`, +`/urls`, `/pihole`, `/unbound`, `/netalertx`, `/stats`, `/dns`, `/leases`, and +`/devices`. Maintenance endpoints are under `/api/session` and +`/api/maintenance/`. Device discovery returns HTTP 503 until NetAlertX +integration is configured. + +The maintenance panel shows the latest job result, its checks and system +snapshot. It can run a system check, create and verify backups (up to 10, with +download and deletion), and run a controlled update. Restore is performed locally +through the documented recovery procedure, not from the browser. + **Not required** for the core Pi-hole + Unbound stack. ### Secure maintenance web panel -The optional maintenance panel provides system checks, verified backups and -controlled updates at `https://pi.hole:8443/`. It remains disabled by default -and is enabled explicitly with Caddy. Browser sign-in uses the existing -`SUITE_API_KEY`: +The optional maintenance panel provides a system check, verified backups, +controlled updates, the latest job result, system snapshot, and check steps. It +remains disabled by default. On a fresh host installation, enable it with: ```bash sudo ./install.sh --with-maintenance-web ``` +For an existing installation that already has the maintenance panel, update it +with `sudo ./install.sh --upgrade-only`. Browser sign-in uses the existing +`SUITE_API_KEY`. + See [docs/MAINTENANCE_WEB.md](docs/MAINTENANCE_WEB.md) for prerequisites, certificate trust, security boundaries and rollback. @@ -285,6 +317,9 @@ Network device monitoring (separate install): sudo ./install.sh --with-netalertx ``` +Without NetAlertX, device discovery remains unavailable; DNS monitoring and +Pi-hole/Unbound operation continue normally. + --- ## 🔎 Manual Update Check diff --git a/deploy/Caddyfile.maintenance b/deploy/Caddyfile.maintenance index 90eebc1..baa31aa 100644 --- a/deploy/Caddyfile.maintenance +++ b/deploy/Caddyfile.maintenance @@ -1,6 +1,6 @@ -pi.hole:8443, 192.168.178.2:8443 { +pi.hole:8443, 192.168.1.2:8443 { tls internal - @lan remote_ip 192.168.178.0/24 127.0.0.1 ::1 + @lan remote_ip 192.168.1.0/24 127.0.0.1 ::1 handle @lan { request_body { max_size 8KB diff --git a/docs/CONSOLE_MENU.md b/docs/CONSOLE_MENU.md index 3a8def7..b1d8828 100644 --- a/docs/CONSOLE_MENU.md +++ b/docs/CONSOLE_MENU.md @@ -85,8 +85,6 @@ sudo pihole-rescue # global command (symlink in /usr/local/bin) sudo bash scripts/rescue_menu.sh ``` -![Rescue Menu](assets/screenshot_rescue_menu.png) - ### Options #### Status & Diagnostics @@ -97,10 +95,6 @@ sudo bash scripts/rescue_menu.sh | 2 | DNS loop / upstream check | Detects DNS loops, tests blocking, verifies Unbound upstream | | 3 | Nightly / diagnostic test | Runs `scripts/nightly_test.sh` (or inline fallback) | -![System Status](assets/screenshot_status_check.png) - -![DNS Check](assets/screenshot_dns_check.png) - #### Backup & Restore | # | Option | What It Does | diff --git a/docs/MAINTENANCE_WEB.md b/docs/MAINTENANCE_WEB.md index 9411fc3..3d2337a 100644 --- a/docs/MAINTENANCE_WEB.md +++ b/docs/MAINTENANCE_WEB.md @@ -1,15 +1,14 @@ # Pi-hole-Wartungsoberfläche Die Wartungsoberfläche stellt genau drei kontrollierte Aktionen bereit: -Systemcheck, verifiziertes Backup und Update. Sie ist nach erfolgreicher -Produktionsabnahme unter `https://pi.hole:8443/` sowie -`https://192.168.178.2:8443/` erreichbar. +Systemcheck, verifiziertes Backup und Update. Der Zugriff erfolgt über den +konfigurierten Caddy-Host: `https://:8443/`. ## Voraussetzungen und Installation Der Webdienst bleibt verbindlich auf `127.0.0.1:8090`; ein abweichender `SUITE_HOST` wird beim Start abgewiesen. Caddy veröffentlicht ausschließlich -HTTPS auf Port 8443 für `192.168.178.0/24` und Loopback. Für Caddy auf Debian +HTTPS auf Port 8443 für das konfigurierte private LAN und Loopback. Für Caddy auf Debian oder Raspberry Pi OS ist das offizielle Stable-Repository zu verwenden: ```bash diff --git a/docs/assets/.gitkeep b/docs/assets/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/docs/assets/screenshot_dns_check.png b/docs/assets/screenshot_dns_check.png deleted file mode 100644 index 709d1b05e068d8e5e8ca28f7766cf4bf4066fee1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10380 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 f|CPPq-^kAClw|NEbN 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 OL-$0bMKTeZ9{&pwJTKk= diff --git a/docs/superpowers/plans/2026-09-07-maintenance-web.md b/docs/superpowers/plans/2026-09-07-maintenance-web.md index 7a9f67f..bbfb9f8 100644 --- a/docs/superpowers/plans/2026-09-07-maintenance-web.md +++ b/docs/superpowers/plans/2026-09-07-maintenance-web.md @@ -12,7 +12,7 @@ ## Global Constraints -- Public URLs are exactly https://pi.hole:8443/ and https://192.168.178.2:8443/; Uvicorn remains on 127.0.0.1:8090. +- Public URLs use the configured Caddy host on port 8443; Uvicorn remains on 127.0.0.1:8090. - The FastAPI service runs as pihole-suite, never as root, and accepts no arbitrary command, path, package, URL, or shell input. - Allowed root actions are exactly check, backup, and update; they are serialized with /run/lock/pihole-maintenance-web.lock. - Update order is verified backup, apt-get update, noninteractive apt-get -y upgrade, pihole -up, pihole -g, then full check; no autoremove and no reboot. @@ -22,7 +22,7 @@ - All mutating API calls require an authenticated session, an exact allowed Origin, and a matching CSRF token. - Production OpenAPI/Swagger is disabled and CORS is not enabled. - Every Bash file added or changed uses set -euo pipefail. -- Production deployment is not complete until all three actions have been started through the HTTPS API and independently verified on pi@192.168.178.2. +- Production deployment is not complete until all three actions have been started through the HTTPS API and independently verified on the target Raspberry Pi. --- diff --git a/docs/superpowers/specs/2026-09-07-maintenance-web-design.md b/docs/superpowers/specs/2026-09-07-maintenance-web-design.md index 391baad..22d826f 100644 --- a/docs/superpowers/specs/2026-09-07-maintenance-web-design.md +++ b/docs/superpowers/specs/2026-09-07-maintenance-web-design.md @@ -8,7 +8,7 @@ Die vorhandene lokale Pi-hole-Suite erhält eine deutschsprachige Weboberfläche über die ein Administrator den Systemzustand prüfen, ein verifiziertes Backup erstellen und einen kontrollierten Update-Lauf starten kann. Die Oberfläche ist auf dem produktiven Raspberry Pi unter `https://pi.hole:8443/` und -`https://192.168.178.2:8443/` erreichbar. +`https://:8443/` erreichbar. ## Plattform und Bestand @@ -54,14 +54,14 @@ Läufe nicht gleichzeitig Pi-hole, APT oder die Backupdaten verändern. - Cookie: `Secure`, `HttpOnly`, `SameSite=Strict`, `Path=/`. - Zustandsänderungen erfordern `POST`, einen sitzungsgebundenen CSRF-Token sowie einen erlaubten `Origin` (`https://pi.hole:8443` oder - `https://192.168.178.2:8443`). + `https://:8443`). - Maximal fünf fehlgeschlagene Anmeldungen je Client innerhalb von fünf Minuten; danach Sperre bis zum Ende des Zeitfensters. -- Kein CORS. Erlaubte Hosts: `pi.hole`, `192.168.178.2`, `127.0.0.1`, `localhost`. +- Kein CORS. Erlaubte Hosts kommen aus der lokalen Wartungskonfiguration; Loopback bleibt für Tests erlaubt. - Produktiv sind Swagger und OpenAPI deaktiviert. - Sicherheitsheader: CSP ohne Fremdquellen, `frame-ancestors 'none'`, `X-Content-Type-Options: nosniff`, `Referrer-Policy: no-referrer` und HSTS. -- Caddy akzeptiert Port 8443 nur aus `192.168.178.0/24` und von Loopback. +- Caddy akzeptiert Port 8443 nur aus dem konfigurierten LAN und von Loopback. ## Aktionen diff --git a/maintenance_web.py b/maintenance_web.py index 60cf3de..56d1204 100644 --- a/maintenance_web.py +++ b/maintenance_web.py @@ -34,8 +34,8 @@ START_RESERVATION_SECONDS = 60 MAX_STATE_BYTES = 64 * 1024 COOKIE_NAME = "pihole_maintenance_session" -ALLOWED_HOSTS = frozenset({"pi.hole", "192.168.178.2", "127.0.0.1", "localhost"}) -ALLOWED_ORIGINS = frozenset({"https://pi.hole:8443", "https://192.168.178.2:8443"}) +ALLOWED_HOSTS = frozenset({"pi.hole", "127.0.0.1", "localhost"}) +ALLOWED_ORIGINS = frozenset({"https://pi.hole:8443"}) WEB_DIRECTORY = Path(__file__).resolve().parent / "web" Dispatcher = Callable[[list[str]], None] diff --git a/tests/test_install_maintenance_web.sh b/tests/test_install_maintenance_web.sh index cf38e1a..1902157 100755 --- a/tests/test_install_maintenance_web.sh +++ b/tests/test_install_maintenance_web.sh @@ -17,14 +17,14 @@ for command in apt apt-get caddy curl service sudo systemctl; do done run_staged_install() { - PATH="$GUARD_DIR:$PATH" bash "$ROOT_DIR/scripts/install_maintenance_web.sh" --destdir "$DESTDIR" --hosts pi.hole,192.168.178.2 --lan-cidr 192.168.178.0/24 + PATH="$GUARD_DIR:$PATH" bash "$ROOT_DIR/scripts/install_maintenance_web.sh" --destdir "$DESTDIR" --hosts pi.hole,192.168.1.2 --lan-cidr 192.168.1.0/24 } run_staged_install ROOT_DIR="$ROOT_DIR" bash -c ' source "$ROOT_DIR/install.sh" - run_existing_upgrade() { test "$MAINTENANCE_HOSTS" = pi.hole; test "$MAINTENANCE_LAN_CIDR" = 192.168.178.0/24; } - main --upgrade-only --maintenance-hosts pi.hole --maintenance-lan-cidr 192.168.178.0/24 + run_existing_upgrade() { test "$MAINTENANCE_HOSTS" = pi.hole; test "$MAINTENANCE_LAN_CIDR" = 192.168.1.0/24; } + main --upgrade-only --maintenance-hosts pi.hole --maintenance-lan-cidr 192.168.1.0/24 ' if ROOT_DIR="$ROOT_DIR" bash -c ' source "$ROOT_DIR/install.sh" @@ -144,7 +144,7 @@ printf '%s\n' \ $'\troot * /usr/share/caddy' \ $'\tfile_server' \ '}' > "$PACKAGED_DESTDIR/etc/caddy/Caddyfile" -PATH="$GUARD_DIR:$PATH" bash "$ROOT_DIR/scripts/install_maintenance_web.sh" --destdir "$PACKAGED_DESTDIR" --hosts pi.hole,192.168.178.2 --lan-cidr 192.168.178.0/24 +PATH="$GUARD_DIR:$PATH" bash "$ROOT_DIR/scripts/install_maintenance_web.sh" --destdir "$PACKAGED_DESTDIR" --hosts pi.hole,192.168.1.2 --lan-cidr 192.168.1.0/24 grep -Fqx 'import Caddyfile.d/*' "$PACKAGED_DESTDIR/etc/caddy/Caddyfile" grep -Fqx ' auto_https disable_redirects' "$PACKAGED_DESTDIR/etc/caddy/Caddyfile" ! grep -Fqx ':80 {' "$PACKAGED_DESTDIR/etc/caddy/Caddyfile" @@ -272,9 +272,9 @@ grep -Fq '/health /urls /pihole /unbound /netalertx /stats' "$CADDY_SNIPPET" assert_mode "$CADDY_MAIN" 644 assert_mode "$CADDY_SNIPPET" 644 test "$(grep -Fxc 'import Caddyfile.d/*' "$CADDY_MAIN")" = 1 -grep -Fqx 'pi.hole:8443, 192.168.178.2:8443 {' "$CADDY_SNIPPET" +grep -Fqx 'pi.hole:8443, 192.168.1.2:8443 {' "$CADDY_SNIPPET" grep -Fqx ' tls internal' "$CADDY_SNIPPET" -grep -Fqx ' @lan remote_ip 192.168.178.0/24 127.0.0.1 ::1' "$CADDY_SNIPPET" +grep -Fqx ' @lan remote_ip 192.168.1.0/24 127.0.0.1 ::1' "$CADDY_SNIPPET" grep -Fqx ' handle @lan {' "$CADDY_SNIPPET" grep -Fqx ' reverse_proxy 127.0.0.1:8090' "$CADDY_SNIPPET" grep -Fqx ' respond "Not found" 404' "$CADDY_SNIPPET" diff --git a/tests/test_maintenance_config.py b/tests/test_maintenance_config.py index ea16dad..8ca1141 100644 --- a/tests/test_maintenance_config.py +++ b/tests/test_maintenance_config.py @@ -5,16 +5,16 @@ def test_staging_output_does_not_write_files(tmp_path): import sys from pathlib import Path script = Path(__file__).resolve().parents[1] / 'maintenance_install.py' - result = subprocess.run([sys.executable, str(script), '--print-config', '--hosts', 'pi.hole,192.168.178.2', '--lan-cidr', '192.168.178.0/24'], cwd=tmp_path, check=True, capture_output=True, text=True) - assert json.loads(result.stdout)['lan_cidr'] == '192.168.178.0/24' + result = subprocess.run([sys.executable, str(script), '--print-config', '--hosts', 'pi.hole,192.168.1.2', '--lan-cidr', '192.168.1.0/24'], cwd=tmp_path, check=True, capture_output=True, text=True) + assert json.loads(result.stdout)['lan_cidr'] == '192.168.1.0/24' assert list(tmp_path.iterdir()) == [] from maintenance_config import caddy_config, configuration def test_network_config_and_route_boundary(): - config = configuration('pi.hole,192.168.178.2', '192.168.178.0/24') - assert config['allowed_origins'] == ['https://pi.hole:8443', 'https://192.168.178.2:8443'] + config = configuration('pi.hole,192.168.1.2', '192.168.1.0/24') + assert config['allowed_origins'] == ['https://pi.hole:8443', 'https://192.168.1.2:8443'] caddy = caddy_config(config) assert 'path / /maintenance.css /maintenance.js /api/session /api/session/logout /api/maintenance/*' in caddy assert 'respond "Not found" 404' in caddy diff --git a/tests/test_maintenance_web.py b/tests/test_maintenance_web.py index 2a9110f..b4062dd 100644 --- a/tests/test_maintenance_web.py +++ b/tests/test_maintenance_web.py @@ -20,6 +20,13 @@ ) +def test_default_host_allowlist_does_not_embed_a_private_lan_address(): + settings = MaintenanceSettings(api_key="test-key") + + assert settings.allowed_hosts == frozenset({"pi.hole", "127.0.0.1", "localhost"}) + assert settings.allowed_origins == frozenset({"https://pi.hole:8443"}) + + class RecordingDispatcher: def __init__(self): self.calls: list[list[str]] = [] diff --git a/tests/test_upgrade_container.sh b/tests/test_upgrade_container.sh index 1f67e95..0197770 100644 --- a/tests/test_upgrade_container.sh +++ b/tests/test_upgrade_container.sh @@ -65,7 +65,7 @@ EOF chmod 0755 /usr/bin/{systemctl,systemd-analyze,caddy,dig,curl,ss,sudo} /usr/sbin/visudo touch /tmp/fail-caddy -if bash /src/install.sh --upgrade-only --maintenance-hosts pi.hole,192.168.178.2 --maintenance-lan-cidr 192.168.178.0/24; then +if bash /src/install.sh --upgrade-only --maintenance-hosts pi.hole,192.168.1.2 --maintenance-lan-cidr 192.168.1.0/24; then printf 'injected deployment failure was ignored\n' >&2 exit 1 fi @@ -78,7 +78,7 @@ grep -Fqx 'upstream config' /etc/pihole/pihole.toml grep -Fqx 'unbound config' /etc/unbound/unbound.conf.d/forward.conf test "$(sha256sum /etc/resolv.conf)" = "$resolver_checksum" -bash /src/install.sh --upgrade-only --maintenance-hosts pi.hole,192.168.178.2 --maintenance-lan-cidr 192.168.178.0/24 +bash /src/install.sh --upgrade-only --maintenance-hosts pi.hole,192.168.1.2 --maintenance-lan-cidr 192.168.1.0/24 cmp /src/start_suite.py /var/lib/pihole-suite/app/start_suite.py grep -Fqx custom-data /var/lib/pihole-suite/app/custom.txt grep -Fqx old-venv /var/lib/pihole-suite/venv/preserved