Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
4a63123
docs: specify secure maintenance web interface
tcbselmar Sep 7, 2026
d265cbd
docs: plan maintenance web implementation
tcbselmar Sep 7, 2026
8a78fb3
feat: add privileged maintenance job runner
tcbselmar Sep 7, 2026
73effc7
fix: harden maintenance runner execution
tcbselmar Sep 7, 2026
926cf78
feat: add authenticated maintenance web panel
tcbselmar Sep 7, 2026
91a1b9c
fix: serialize maintenance web operations
tcbselmar Sep 7, 2026
3367983
feat: install hardened maintenance web services
tcbselmar Sep 8, 2026
e2cccf2
docs: document maintenance web operations
tcbselmar Sep 8, 2026
7dc4344
fix: enforce maintenance runtime bounds
tcbselmar Sep 8, 2026
1d02ec0
fix: harden suite upgrade migration
tcbselmar Sep 8, 2026
2f8dd60
docs: normalize maintenance web planning files
tcbselmar Sep 8, 2026
ae4b7da
fix: force hardened suite migration
tcbselmar Sep 8, 2026
7f4cb1e
fix: replace packaged Caddy sample
tcbselmar Sep 8, 2026
29d5404
fix: prevent Caddy HTTP redirect listener
tcbselmar Sep 8, 2026
50fd49b
fix: wait for suite listener during activation
tcbselmar Sep 8, 2026
3c4884f
fix: allow maintenance sudo transition
tcbselmar Sep 8, 2026
f8ae314
fix: grant minimal sudo transition capabilities
tcbselmar Sep 8, 2026
bc9f895
feat: add maintenance web dashboard
tcbselmar Sep 10, 2026
84e6a45
Integrate maintenance interface with DNS dashboard
Oct 27, 2026
3ce409d
Guard maintenance upgrades and support Python 3.10
Oct 27, 2026
c544ee5
Constrain installer paths and remove dynamic test execution
Oct 27, 2026
3353653
Test isolated upgrades and constrain provisioning paths
Oct 27, 2026
f12cf09
Expose guarded upgrade for existing installations
Oct 27, 2026
ee81b91
Expose API routes and report unavailable metrics honestly
Oct 27, 2026
58f03c6
fix: bound DNS log parsing and avoid persistent dashboard key
Oct 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,6 @@ jobs:
- name: Run tests
run: |
PYTHONPATH=. pytest tests/
bash tests/test_install_maintenance_web.sh
node tests/test_dashboard_dom.js
bash tests/test_upgrade_container.sh
25 changes: 25 additions & 0 deletions DESIGN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Wartungs-Flugplan

## Oberfläche

Lokaler Betriebsarbeitsplatz, kein Marketing-Dashboard. Eine breite linke Zone zeigt den letzten Auftrag, reale Systemwerte und die Prüfliste. Rechts stehen drei getrennte Wartungsaktionen. Unter 720 px folgt eine einzelne Spalte. Backups und geschlossene JSON-Details schließen die Ansicht ab.

## Gestaltungswerte

- Systemschrift, Zeilenhöhe 1,5; Haupttitel 1,65–2,35 rem; maximal 1380 px Arbeitsbreite.
- Hintergrund `#0d1419`, Statusfläche `#151f26`, Trennlinien `#354650`.
- Primärtext `#edf3f5`, Sekundärtext `#a9bbc4`.
- Erfolg `#77e0c6`, Aufmerksamkeit `#f3c578`, Fehler `#ffaaa7` – stets zusätzlich ausgeschrieben.
- Eingabebegrenzungen `#607681`, 3,89:1 gegenüber dem Hintergrund.
- Statusfläche mit 12-px-Radius, Bedienelemente mit 6 px. Mindestens 44 px Zielhöhe, einschließlich Markenlink und Checkboxlabel.
- Sichtbarer 3-px-Fokus mit Abstand. Nur kurze Farbwechsel bei erlaubter Bewegung; keine Animation bei reduzierter Bewegung.

## Verhalten

Deutsch/Englisch aus statischen Wörterbüchern; nur `de` oder `en` werden gespeichert. Serverwerte landen ausschließlich in Textknoten. Fehlende Messwerte heißen „Nicht verfügbar“. Ein laufender Auftrag zeigt keine geschätzten Prozente; der bestehende Runner liefert die einzelnen Ergebnisse erst nach Abschluss.

Anmeldung mit dem bestehenden `SUITE_API_KEY` und Sitzungserneuerung führen ins Dashboard, Abmeldung und Ablauf zurück zum API-Key-Feld. Startende und laufende Aufträge sperren konkurrierende Aktionen. Verbindungsfehler verlangen eine manuelle Statusabfrage; kein dauerhaftes Hintergrundpolling.

## Prüfung

Browsermatrix: 1440, 1063, 390 und 320 px, DE/EN, 200-%-CSS-Zoom, echte Tab-Fokusnavigation und reduzierte Bewegung. Die Prüfbilder verwenden ausdrücklich Testdaten einschließlich fehlender optionaler Werte und eines als Text dargestellten XSS-Testhosts. Unabhängiger Review: zwei Befunde zu Feldkontrast und Markenlink-Zielhöhe behoben und bestätigt. Der zusätzliche Design-Detector lief nur im eingeschränkten Regex-Modus; keine vollständige automatische Barrierefreiheitszertifizierung. Keine Rasterbilder oder externen Assets im Produkt.
7 changes: 7 additions & 0 deletions PRODUCT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Pi-hole Wartungs-Flugplan

Lokaler Betriebsarbeitsplatz für Tim und andere Self-Hoster: Pi-hole und Unbound prüfen, sichern und kontrolliert aktualisieren. Ergänzt Pi-hole ohne Änderungen an dessen Dateien oder Navigation.

Deutsch und Englisch, Desktop und Smartphone, ausschließlich reale Runnerdaten. Drei feste Wartungsaktionen, bestätigte Änderungen und verifizierte Backups. Kein Restore, kein Internetzugang, keine generischen Root-Aktionen. Die Anmeldung verwendet unverändert den bestehenden `SUITE_API_KEY`.

Dunkler Preflight-Arbeitsplatz mit breiter Statuszone und schmalerer Aktionszone. Türkis, Amber und Rot ergänzen ausgeschriebene Zustände. Systemfonts, keine externen Assets oder Bibliotheken. Technisches JSON bleibt geschlossen abrufbar.
18 changes: 17 additions & 1 deletion README.de.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ sudo ./install.sh

> Als **normaler Nutzer** klonen (z.B. `pi`), nicht als root. Der Installer wird via `sudo ./install.sh` ausgeführt.

Für eine bereits laufende Suite mit Wartungsoberfläche aktualisiert `sudo ./install.sh --upgrade-only` ausschließlich Suite- und Webdateien. DNS-Konfiguration und Python-Environment bleiben erhalten; Voraussetzungen und Rollback stehen in [docs/MAINTENANCE_WEB.md](docs/MAINTENANCE_WEB.md).

---

## ✅ Voraussetzungen
Expand Down Expand Up @@ -252,11 +254,25 @@ pip install -r requirements.txt

# Starten
python3 start_suite.py
# API-Doku: http://127.0.0.1:8090/docs
# Die API bleibt auf 127.0.0.1:8090; die Produktivdokumentation ist deaktiviert.
```

**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`:

```bash
sudo ./install.sh --with-maintenance-web
```

Siehe [docs/MAINTENANCE_WEB.md](docs/MAINTENANCE_WEB.md) für Voraussetzungen,
Zertifikatvertrauen, Sicherheitsgrenze und Rollback.

---

## 🌐 Optional: NetAlertX
Expand Down
18 changes: 17 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ sudo ./install.sh

> Clone as a **normal user** (`pi`), not root. The installer requires `sudo ./install.sh`.

For an existing suite with the maintenance interface, `sudo ./install.sh --upgrade-only` updates the suite and web files without changing DNS configuration or the Python environment. See [docs/MAINTENANCE_WEB.md](docs/MAINTENANCE_WEB.md) for prerequisites and rollback.

---

## ✅ Requirements
Expand Down Expand Up @@ -254,11 +256,25 @@ pip install -r requirements.txt

# Start
python3 start_suite.py
# API docs: http://127.0.0.1:8090/docs
# The API stays on 127.0.0.1:8090; production API documentation is disabled.
```

**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`:

```bash
sudo ./install.sh --with-maintenance-web
```

See [docs/MAINTENANCE_WEB.md](docs/MAINTENANCE_WEB.md) for prerequisites,
certificate trust, security boundaries and rollback.

---

## 🌐 Optional: NetAlertX
Expand Down
32 changes: 16 additions & 16 deletions dashboard/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,13 @@ document.addEventListener('DOMContentLoaded', () => {
const lastUpdateEl = document.getElementById('last-update');

// API Key management
let apiKey = sessionStorage.getItem('suite_api_key');
// Do not persist the permanent API key in browser storage. It is held only
// for this page lifetime and must be entered again after a reload.
sessionStorage.removeItem('suite_api_key');
let apiKey = null;

// Initialization
if (apiKey) {
showDashboard();
fetchData();
} else {
showAuth();
}
showAuth();

// Event Listeners
authForm.addEventListener('submit', async (e) => {
Expand All @@ -33,7 +31,7 @@ document.addEventListener('DOMContentLoaded', () => {

if (res.ok) {
apiKey = key;
sessionStorage.setItem('suite_api_key', key);
apiKeyInput.value = '';
authError.classList.add('hidden');
showDashboard();
fetchData();
Expand All @@ -48,7 +46,6 @@ document.addEventListener('DOMContentLoaded', () => {
});

logoutBtn.addEventListener('click', () => {
sessionStorage.removeItem('suite_api_key');
apiKey = null;
apiKeyInput.value = '';
showAuth();
Expand All @@ -67,13 +64,13 @@ document.addEventListener('DOMContentLoaded', () => {
}

function updateStatusIndicator(elementId, status) {
const el = document.getElementById(elementId);
const el = document.querySelector('#' + elementId);
if (!el) return;
el.className = 'status-indicator ' + status;
}

function updateElementText(elementId, text) {
const el = document.getElementById(elementId);
const el = document.querySelector('#' + elementId);
if (el) el.textContent = text;
}

Expand All @@ -99,7 +96,7 @@ document.addEventListener('DOMContentLoaded', () => {
const now = new Date();
updateElementText('last-update', `Letzte Aktualisierung: ${now.toLocaleTimeString()}`);

updateStatusIndicator('overall-status', 'loading');
updateStatusIndicator('overall-status .status-indicator', 'loading');
updateElementText('overall-status .status-text', 'Wird geladen...');

const [health, urls, pihole, unbound, netalertx, stats] = await Promise.all([
Expand Down Expand Up @@ -144,7 +141,7 @@ document.addEventListener('DOMContentLoaded', () => {
}
updateElementText('pihole-version', pihole.pihole_version);

let upstreamsStr = 'Keine / Unbekannt';
let upstreamsStr = pihole.upstreams === null ? 'Unbekannt (nicht lesbar)' : 'Keine konfiguriert';
if (pihole.upstreams && pihole.upstreams.length > 0) {
upstreamsStr = pihole.upstreams.join(', ');

Expand All @@ -153,10 +150,13 @@ document.addEventListener('DOMContentLoaded', () => {
const expectedUpstream = `127.0.0.1#${unbound.port}`;
if (!pihole.upstreams.includes(expectedUpstream)) {
upstreamsStr += ' (Warnung: Unbound nicht konfiguriert!)';
updateStatusIndicator('pihole-status-indicator', 'warning');
if (pihole.ftl_active) updateStatusIndicator('pihole-status-indicator', 'warning');
allHealthy = false;
}
}
} else {
if (pihole.ftl_active) updateStatusIndicator('pihole-status-indicator', 'warning');
allHealthy = false;
}
updateElementText('pihole-upstreams', upstreamsStr);

Expand All @@ -167,7 +167,7 @@ document.addEventListener('DOMContentLoaded', () => {
}

if (stats) {
updateElementText('pihole-recent-queries', stats.recent_queries);
updateElementText('pihole-recent-queries', stats.recent_queries ?? 'Nicht verfügbar');
}

// Process Unbound
Expand Down Expand Up @@ -236,4 +236,4 @@ document.addEventListener('DOMContentLoaded', () => {
res.push(`${m}m`);
return res.join(' ');
}
});
});
15 changes: 15 additions & 0 deletions deploy/Caddyfile.maintenance
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
pi.hole:8443, 192.168.178.2:8443 {
tls internal
@lan remote_ip 192.168.178.0/24 127.0.0.1 ::1
handle @lan {
request_body {
max_size 8KB
}
@maintenance path / /maintenance.css /maintenance.js /api/session /api/session/logout /api/maintenance/* /dashboard /dashboard/* /health /urls /pihole /unbound /netalertx /stats /version /dns /devices /leases
handle @maintenance {
reverse_proxy 127.0.0.1:8090
}
respond "Not found" 404
}
respond "Forbidden" 403
}
2 changes: 2 additions & 0 deletions deploy/maintenance-web.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[Service]
ReadWritePaths=/var/backups/pihole-suite /var/lib/pihole-suite/exports /run/lock
17 changes: 17 additions & 0 deletions deploy/pihole-maintenance-backup.service
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
[Unit]
Description=Pi-hole maintenance verified backup
After=network-online.target pihole-FTL.service unbound.service
Wants=network-online.target

[Service]
Type=oneshot
User=root
Group=root
UMask=0077
Environment=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
ExecStart=/usr/local/libexec/pihole-maintenance-runner backup
TimeoutStartSec=15min
NoNewPrivileges=true
PrivateTmp=true
ProtectHome=true
ReadWritePaths=/var/backups/pihole-suite /var/lib/pihole-suite/jobs /run/lock
16 changes: 16 additions & 0 deletions deploy/pihole-maintenance-check.service
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
[Unit]
Description=Pi-hole maintenance system check
After=network-online.target pihole-FTL.service unbound.service
Wants=network-online.target

[Service]
Type=oneshot
User=root
Group=root
UMask=0077
Environment=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
ExecStart=/usr/local/libexec/pihole-maintenance-runner check
TimeoutStartSec=15min
NoNewPrivileges=true
PrivateTmp=true
ProtectHome=true
17 changes: 17 additions & 0 deletions deploy/pihole-maintenance-update.service
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
[Unit]
Description=Pi-hole maintenance controlled update
After=network-online.target pihole-FTL.service unbound.service
Wants=network-online.target

[Service]
Type=oneshot
User=root
Group=root
UMask=0077
Environment=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
ExecStart=/usr/local/libexec/pihole-maintenance-runner update
TimeoutStartSec=120min
NoNewPrivileges=true
PrivateTmp=true
ProtectHome=true
ReadWritePaths=/var/backups/pihole-suite /var/lib/pihole-suite/jobs /run/lock
5 changes: 5 additions & 0 deletions deploy/pihole-maintenance-web.sudoers
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
pihole-suite ALL=(root) NOPASSWD: /usr/bin/systemctl start --no-block pihole-maintenance-check.service
pihole-suite ALL=(root) NOPASSWD: /usr/bin/systemctl start --no-block pihole-maintenance-backup.service
pihole-suite ALL=(root) NOPASSWD: /usr/bin/systemctl start --no-block pihole-maintenance-update.service
pihole-suite ALL=(root) NOPASSWD: /usr/local/libexec/pihole-maintenance-backups ^delete [0-9]{8}T[0-9]{6}Z-[0-9]{8}T[0-9]{12}Z-[a-f0-9]{32}$
pihole-suite ALL=(root) NOPASSWD: /usr/local/libexec/pihole-suite-upstreams ""
9 changes: 9 additions & 0 deletions docs/CONSOLE_MENU.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,15 @@ The suite ships two interactive menus:

Both menus use `scripts/lib/ui.sh` for consistent output formatting.

## Web Maintenance Panel

The optional HTTPS panel is separate from both menus. After explicit
installation with `sudo ./install.sh --with-maintenance-web`, it provides only
system check, verified backup and controlled update at
`https://pi.hole:8443/`. It does not offer restore, backup download or deletion.
Use the existing Rescue menu for recovery operations. Operational details and
rollback are documented in [MAINTENANCE_WEB.md](MAINTENANCE_WEB.md).

---

## Console Menu
Expand Down
Loading
Loading