From db2c6c70a02626ae69b98906e53d97aa2fef816e Mon Sep 17 00:00:00 2001 From: Oliver G Date: Mon, 3 Aug 2026 06:55:32 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20Admin-Dienste=20hinter=20dem=20VPN=20wie?= =?UTF-8?q?der=20korrekt=20pr=C3=BCfen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Seit der VPN-Absicherung (Phase 2) antworten n8n, die beiden Nginx Proxy Manager, Grafana und CloudPanel auf ihren öffentlichen Domains nur noch mit 403. Der Checker wertete das als "down" — die Statusseite stand deshalb dauerhaft auf "down", ohne dass ein Dienst tatsächlich gestört war. Der Checker läuft per Cron auf hetzner, das selbst WireGuard-Peer 10.10.0.14 ist. Diese Dienste werden jetzt über das neue Feld "check_url" intern geprüft, Grafana und CloudPanel laufen auf hetzner selbst und werden über localhost angesprochen. "url" bleibt die öffentliche Adresse für die Anzeige; "check_url" wird vor dem Schreiben aus status.json entfernt, damit die interne Netztopologie nicht auf einer öffentlichen Seite landet. Uptime Kuma zeigte bisher nur einen 302 auf der gesperrten Root-URL. Geprüft wird jetzt die öffentliche Status-Page, die tatsächlich beweist, dass Kuma antwortet. Nebenbei korrigiert: ng.vanityontour.de war als "CloudPanel" gefuehrt, ist aber laut NPM-Datenbank der Nginx Proxy Manager auf VoTServer. CloudPanel selbst (cp.blog.vanityontour.de) wurde dadurch nie geprüft und ist jetzt ergaenzt. Der GitHub-Actions-Workflow entfaellt: er lief ohnehin nur noch manuell, von GitHub aus sind die internen Adressen unerreichbar, und ein Start haette ein falsches "alles down" committet und per FTP über die Live-Seite geschoben. README beschreibt jetzt den tatsaechlichen Weg (Cron auf hetzner + SCP). --- .github/workflows/update-status.yml | 43 ---------------------- README.md | 56 +++++++++++++++++++++++------ scripts/check_status.py | 30 +++++++++++----- 3 files changed, 67 insertions(+), 62 deletions(-) delete mode 100644 .github/workflows/update-status.yml diff --git a/.github/workflows/update-status.yml b/.github/workflows/update-status.yml deleted file mode 100644 index 5c190c5..0000000 --- a/.github/workflows/update-status.yml +++ /dev/null @@ -1,43 +0,0 @@ -name: Update Status Page - -on: - workflow_dispatch: # manual trigger only — status updates run via Hetzner cron + SCP - -permissions: - contents: write - -jobs: - update: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: "3.12" - - - name: Run status checks - run: python3 scripts/check_status.py - - - name: Commit updated status.json - run: | - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - git add public/status.json - git diff --cached --quiet && echo "No changes" && exit 0 - git commit -m "chore: status update $(date -u '+%Y-%m-%d %H:%M UTC')" - git push - - - name: Deploy to Hostinger via FTP - uses: SamKirkland/FTP-Deploy-Action@v4.3.5 - with: - server: ${{ secrets.FTP_SERVER }} - username: ${{ secrets.FTP_USERNAME }} - password: ${{ secrets.FTP_PASSWORD }} - local-dir: ./public/ - server-dir: /public_html/ - exclude: | - **/.git* - **/.git*/** diff --git a/README.md b/README.md index 83c3b34..2f43568 100644 --- a/README.md +++ b/README.md @@ -12,20 +12,51 @@ Automated status dashboard for all VanityOnTour services, hosted on Hostinger at ## How it works -GitHub Actions runs every 5 minutes: -1. `scripts/check_status.py` checks all services and writes `public/status.json` -2. Commits the updated `status.json` to the repo -3. Deploys `public/` to Hostinger via FTP +Ein Cron auf **hetzner** (`88.99.209.207`) läuft alle 5 Minuten: -## Setup: GitHub Secrets required +``` +*/5 * * * * /opt/run_status_check.sh >> /var/log/status_check.log 2>&1 +``` -Go to **Settings → Secrets → Actions** and add: +1. `/opt/check_status.py` prüft alle Dienste und schreibt `/opt/public/status.json` +2. `run_status_check.sh` kopiert die Datei per SCP (Port 65002) nach + `/home/u982551092/domains/status.vanityontour.de/public_html/status.json` -| Secret | Value | -|--------|-------| -| `FTP_SERVER` | FTP hostname from Hostinger hPanel | -| `FTP_USERNAME` | `u982551092` | -| `FTP_PASSWORD` | FTP password from Hostinger hPanel | +Die statischen Dateien unter `public/` (HTML, CSS, Icons) liegen unverändert auf +Hostinger; nur `status.json` wird zyklisch überschrieben. + +> Der frühere Weg über GitHub Actions + FTP-Deploy wird **nicht** mehr benutzt. +> Das committete `public/status.json` ist deshalb nur ein Platzhalter — der +> Live-Stand steht ausschließlich auf Hostinger. + +### Deployment einer Skript-Änderung + +`scripts/check_status.py` wird **nicht** automatisch ausgerollt. Nach einer +Änderung: + +```bash +scp scripts/check_status.py hetzner:/opt/check_status.py +ssh hetzner '/opt/run_status_check.sh' # einmal testweise ausführen +``` + +## Dienste hinter dem Management-VPN + +Seit der VPN-Absicherung antworten die Admin-Oberflächen öffentlich nur noch mit +`403`. Weil der Checker auf hetzner läuft und hetzner WireGuard-Peer +`10.10.0.14` ist, werden diese Dienste über das Feld `check_url` intern geprüft: + +| Dienst | Anzeige (`url`) | Prüfung (`check_url`) | +|---|---|---| +| N8N Automation | `n8n.vanityontour.de` | `http://10.10.0.13:5678` | +| Nginx Proxy Manager | `nginx.vanityontour.de` | `http://10.10.0.13:81` | +| Nginx Proxy Mgr (VoT) | `ng.vanityontour.de` | `http://10.10.0.12:81` | +| Statistiken (Grafana) | `stats.vanityontour.de` | `http://127.0.0.1:3000` | +| CloudPanel | `cp.blog.vanityontour.de` | `https://127.0.0.1:8443` | +| Uptime Kuma | `server.vanityontour.de` | `…/status/vanity` (öffentlich) | + +`check_url` wird vor dem Schreiben aus `status.json` entfernt — die interne +Netztopologie gehört nicht auf eine öffentliche Seite. Die Statusseite zeigt +weiterhin nur den Hostnamen aus `url` an. ## Local test @@ -33,3 +64,6 @@ Go to **Settings → Secrets → Actions** and add: python3 scripts/check_status.py # → writes public/status.json ``` + +Achtung: Lokal (ohne VPN-Route zu `10.10.0.0/24`) melden die intern geprüften +Dienste zwangsläufig „down". Aussagekräftig ist der Lauf nur auf hetzner. diff --git a/scripts/check_status.py b/scripts/check_status.py index 272fd10..c498cb3 100644 --- a/scripts/check_status.py +++ b/scripts/check_status.py @@ -14,18 +14,28 @@ from datetime import datetime, timezone OUTPUT_FILE = "public/status.json" +# Seit der VPN-Absicherung (Phase 2) antworten die Admin-Oberflächen auf ihren +# öffentlichen Domains nur noch mit 403. Der Checker läuft per Cron auf hetzner, +# das selbst WireGuard-Peer 10.10.0.14 ist — diese Dienste werden deshalb über +# "check_url" intern geprüft. "url" bleibt die öffentliche Adresse: die Statusseite +# zeigt daraus nur den Hostnamen an, interne Adressen gehören nicht ins status.json. WEBSITES = [ {"name": "VanityOnTour", "url": "https://vanityontour.de", "group": "websites", "expect": [200, 301, 302]}, {"name": "News Portal", "url": "https://news.vanityontour.de", "group": "websites", "expect": [200, 301, 302]}, {"name": "Wiki", "url": "https://wiki.vanityontour.de", "group": "websites", "expect": [200, 301, 302]}, {"name": "StaySense", "url": "https://staysense.vanityontour.de", "group": "websites", "expect": [200, 301, 302]}, {"name": "StaySense Landing", "url": "https://landing.staysense.vanityontour.de", "group": "websites", "expect": [200, 301, 302]}, - {"name": "N8N Automation", "url": "https://n8n.vanityontour.de", "group": "tools", "expect": [200, 301, 302]}, - {"name": "Nginx Proxy Manager", "url": "https://nginx.vanityontour.de", "group": "tools", "expect": [200, 301, 302]}, - {"name": "Uptime Kuma", "url": "https://server.vanityontour.de", "group": "tools", "expect": [200, 301, 302]}, - {"name": "Statistiken", "url": "https://stats.vanityontour.de", "group": "tools", "expect": [200, 301, 302]}, + {"name": "N8N Automation", "url": "https://n8n.vanityontour.de", "group": "tools", "expect": [200, 301, 302], "check_url": "http://10.10.0.13:5678"}, + {"name": "Nginx Proxy Manager", "url": "https://nginx.vanityontour.de", "group": "tools", "expect": [200, 301, 302], "check_url": "http://10.10.0.13:81"}, + {"name": "Nginx Proxy Mgr (VoT)", "url": "https://ng.vanityontour.de", "group": "tools", "expect": [200, 301, 302], "check_url": "http://10.10.0.12:81"}, + # Root liefert öffentlich zwar 302, aber /dashboard ist gesperrt — die echte + # öffentliche Status-Page beweist dagegen, dass Kuma wirklich antwortet. + {"name": "Uptime Kuma", "url": "https://server.vanityontour.de", "group": "tools", "expect": [200], "check_url": "https://server.vanityontour.de/status/vanity"}, + {"name": "Statistiken", "url": "https://stats.vanityontour.de", "group": "tools", "expect": [200, 301, 302], "check_url": "http://127.0.0.1:3000"}, {"name": "App Backend", "url": "https://app.vanityontour.de", "group": "tools", "expect": [200, 301, 302]}, - {"name": "CloudPanel", "url": "https://ng.vanityontour.de", "group": "tools", "expect": [200, 301, 302]}, + # Grafana und CloudPanel laufen auf hetzner selbst — localhost statt 10.10.0.14, + # damit die Prüfung auch bei liegendem Tunnel noch stimmt. + {"name": "CloudPanel", "url": "https://cp.blog.vanityontour.de", "group": "tools", "expect": [200, 301, 302], "check_url": "https://127.0.0.1:8443"}, {"name": "RSS News API", "url": "https://news.vanityontour.de/health", "group": "apis", "expect": [200]}, {"name": "StaySense API", "url": "https://staysense.vanityontour.de/api/health", "group": "apis", "expect": [200]}, ] @@ -119,10 +129,14 @@ def main(): results = [] for site in WEBSITES: - r = check_http(site["url"], site["expect"]) - results.append({**site, **r}) + target = site.get("check_url", site["url"]) + r = check_http(target, site["expect"]) + # check_url beschreibt die interne Netztopologie und wird nicht veröffentlicht + public = {k: v for k, v in site.items() if k != "check_url"} + results.append({**public, **r}) sym = "✓" if r["status"] == "up" else "✗" - print(f" {sym} {site['name']:30s} {r['status']:8s} {r.get('status_code') or '---'} {r.get('response_time_ms') or '---'}ms") + via = " via " + target if "check_url" in site else "" + print(f" {sym} {site['name']:30s} {r['status']:8s} {r.get('status_code') or '---'} {r.get('response_time_ms') or '---'}ms{via}") print("Checking SSL certificates...") ssl_results = {}