Files
audi-app/homeassistant/REVERSE_PROXY.md
T
tobias 5274df8909 Korrektur: Asset-Caching ist eingeschaltet und bleibt es
Der eben ergaenzte Abschnitt empfahl "aus" und behauptete, das sei entschieden.
Beides war falsch: der Schalter ist eingeschaltet, und es laeuft. Die Empfehlung
beantwortete die Frage "soll er an sein" - gefragt war "soll ich ihn umlegen",
und die ist anders, wenn der laufende Zustand traegt.

Die Messung bleibt gueltig und ist der Grund, ihn NICHT auszuschalten: /local/
liefert HA schon mit 31 Tagen Cache-Control, der Rest der Freigabeliste ist
dynamisch. Auf der Habenseite steht also nichts, und ein laufender Proxy wird
nicht ohne Gewinn umkonfiguriert.

Die Regex-Konkurrenz zwischen einem Asset-Block und dem handgesetzten
/local/bilder/-Block ist damit empirisch widerlegt - sie steht als Merkposten
fuer spaetere Aenderungen an der Freigabeliste weiter drin, zusammen mit den
zwei Symptomen, die diese Einschaetzung kippen wuerden.
2026-09-07 13:15:37 +02:00

299 lines
16 KiB
Markdown

# Reverse-Proxy: Pfad-Freigabeliste für DataMetric360
**Vorbereitet, noch nicht in Betrieb.** Gehört zu `../COMPANION_APP_ARCHITECTURE.md` §4. Reverse Proxy
ist entschieden: **Nginx Proxy Manager** (Add-on), auf demselben Host wie Home Assistant selbst. Die
konkreten Einrichtungsschritte (Cloudflare-Konto, Nameserver-Umstellung, Add-ons, ...) stehen in
[`INTERNET_ZUGRIFF_EINRICHTEN.md`](INTERNET_ZUGRIFF_EINRICHTEN.md) - dieses Dokument ist nur die
technische Pfad-Freigabeliste selbst, die dort im letzten Schritt eingetragen wird.
Ziel: Die App erreicht Home Assistant von unterwegs, **ohne dass Home Assistant selbst im Internet
steht**. Dazu ein Cloudflare-Tunnel (baut nur nach außen auf, kein Port am Router) auf einen Reverse
Proxy, der ausschließlich die unten aufgeführten Pfade durchlässt.
> **2026-08-28 aktualisiert:** die ursprüngliche Fassung dieses Dokuments ging noch von den
> pyscript-Zeiten aus (`pyscript.audi_dashboard_*`, `pyscript.reifen_*`, `/api/services/pyscript/...`).
> Seit der Umstellung auf eine native Integration (2026-08-23, siehe `AGENTS.md` Abschnitt H) heißen
> Entitäten `sensor.audi_dashboard_*` und Dienste `audi_dashboard.<name>` - keine doppelten Präfixe
> mehr. Die Regeln unten sind auf den aktuellen Stand korrigiert und um zwei seither neu hinzugekommene
> Bedarfe ergänzt (`homeassistant.restart` fürs Selbst-Update, sowie das OTA-Bündel unter
> `/audi_dashboard_static/app/*` - beide fehlten in der alten Fassung ersatzlos, ohne sie hätte weder
> der Update-Neustart-Knopf noch ein OTA-Update über den Tunnel funktioniert).
>
> **Zweite Korrektur, selber Tag:** die erste Überarbeitung übernahm noch ungeprüft die alte Annahme
> aus der frühen Planungsphase (2026-08-11), die App würde als eigener Web-Build unter
> `/config/www/dm360/` bzw. `/local/dm360/` ausgeliefert. Das wurde nie gebaut - die Distributionsart
> ist seit der Architekturentscheidung "sideload only" (siehe `COMPANION_APP_ARCHITECTURE.md` §1) eine
> **native Capacitor-App**, direkt per Xcode aufs Gerät geladen, nicht über HA im Browser aufgerufen.
> Ein `/local/dm360/*`-Pfad existiert auf dieser Instanz nicht und wird nie gebraucht. Was die native
> App stattdessen tatsächlich extern erreichen muss, ist ihr eigener Oberflächen-Nachlieferweg (OTA,
> `@capgo/capacitor-updater`, siehe `AGENTS.md` Abschnitt "Deployed-parity"-Unterabschnitt): das Bündel
> liegt unter `/audi_dashboard_static/app/bundle.zip`, ausgeliefert von der Integration selbst
> (`custom_components/audi_dashboard/frontend/app/`, `const.py`'s `BUENDEL_URL`) - dieselbe
> `basisUrl`, die die App auch für ihre normalen API-Aufrufe verwendet (`ota.ts`'s
> `buendelAnwenden()`), also derselbe Tunnel-Hostname, kein zweiter. Die Regel unten ist entsprechend
> ersetzt, nicht nur umbenannt.
## Was die App wirklich braucht
Aus dem tatsächlichen Code der Datenschicht (`companion-app/src/api/rest.ts`, `index.ts`) abgeleitet,
nicht geschätzt - dort gibt es genau drei REST-Aufrufmuster plus WebSocket:
| Methode | Pfad | Wofür |
|---|---|---|
| `GET` | `/api/` | Verbindungsprüfung bei der Ersteinrichtung |
| `GET` | `/api/states/sensor.audi_dashboard_*` | Profil, Fahrten, Tankvorgänge, Status, Batterieverlauf, Belegergebnis, Importstatus, App-Version, Reifen-Kilometerstände |
| `POST` | `/api/services/audi_dashboard/*` | alle schreibenden Vorgänge (Fahrt/Tankvorgang anlegen/ändern, CSV-/Historien-Import, Update prüfen/installieren, ...) |
| `POST` | `/api/services/homeassistant/restart` | genau ein Dienst außerhalb der eigenen Domain - der "Jetzt neu starten"-Knopf nach einem Integrations-Update (siehe `AGENTS.md` Abschnitt L) |
| `GET` (Upgrade) | `/api/websocket` | Live-Aktualisierung |
| `GET` | `/audi_dashboard_static/app/*` | das OTA-Oberflächen-Bündel (`bundle.zip`) für die native App - ohne diesen Pfad findet ein Update aus der Ferne nicht statt (die App selbst läuft nativ auf dem Gerät, wird nicht über diesen Pfad "geladen") |
| `GET` | `/local/bilder/*.{webp,png,jpg,svg}` | Fahrzeugfotos, die die App anzeigt |
Alles andere wird geblockt, insbesondere `/auth/*`, `/lovelace*`, `/config*`, `/api/config`,
`/api/history*`, `/developer-tools*` und die Home-Assistant-Oberfläche selbst.
## Ehrliche Einschränkung
**Der WebSocket lässt sich nicht pfadgenau beschneiden.** Nach `auth_ok` kann über `/api/websocket`
grundsätzlich jeder Zustand gelesen werden, nicht nur die `audi_dashboard`-Entitäten. Die Freigabeliste
ist an dieser Stelle also grobkörniger als beim REST-Zugriff.
Was bleibt: Ohne gültigen Token kommt gar keine Verbindung zustande, und ein verlorenes Gerät wird durch
Zurückziehen genau seines Tokens ausgesperrt. Das ist die im Architekturdokument bewusst akzeptierte
Abwägung - sie sollte nur nicht in Vergessenheit geraten.
Wer sie nicht eingehen will, hat eine Alternative: den WebSocket weglassen und die App auf regelmäßiges
Abfragen umstellen. Kostet Akku und Datenvolumen, verkleinert die Angriffsfläche aber auf die exakt
aufgeführten REST-Pfade.
`homeassistant.restart` verdient eine eigene Erwähnung: wer den Token besitzt, kann damit die gesamte
Home-Assistant-Instanz neu starten, nicht nur etwas im Rahmen dieser App. Dasselbe gilt lokal schon
heute (derselbe Token, dieselbe Berechtigung) - der Tunnel ändert am Berechtigungsumfang nichts, nur am
Netzwerkpfad. Wer das nicht mit ausliefern will: den Block für `homeassistant/restart` unten weglassen -
der Update-Knopf zeigt dann nach einem Update lediglich "bitte manuell neu starten" statt selbst zu
handeln (kein Absturz, siehe `AGENTS.md` Abschnitt L).
## Nginx Proxy Manager
Im Add-on unter *Hosts → Proxy Hosts → Edit → (Zahnrad-Tab) "Custom Nginx Configuration"* eintragen.
Ziel ist der interne Name der Home-Assistant-Instanz (im Supervisor-Netz `homeassistant:8123`).
> **Live gefundener Fehler (2026-08-28), deshalb ohne `include conf.d/include/proxy.conf;`:** die
> ursprüngliche Fassung dieses Blocks nutzte diese Zeile pro Block (Standard-Praxis der vanilla
> jc21-nginx-proxy-manager-Doku, um die üblichen `X-Forwarded-*`-Header zu setzen). Beim hier
> verwendeten NPM-Add-on (`homeassistant-apps/addon-nginx-proxy-manager`, Frenck) enthält diese
> geteilte Datei selbst bereits ein `proxy_pass` - zusammen mit dem eigenen `proxy_pass` im Block
> ergibt das `nginx: [emerg] "proxy_pass" directive is duplicate in
> /etc/nginx/conf.d/include/proxy.conf:7`, nginx startet dann gar nicht mehr (dauerhaft, ein bloßer
> Neustart des Add-ons behebt es nicht - nötig war eine komplette Deinstallation + Neuinstallation).
> Die Header stehen deshalb unten direkt ausgeschrieben statt über das `include`.
```nginx
# Reihenfolge zählt: die erlaubenden Blöcke stehen vor dem pauschalen Verbot.
location = /api/ {
proxy_pass http://homeassistant:8123;
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;
}
location ~ ^/api/states/sensor\.audi_dashboard_[a-z0-9_]+$ {
limit_except GET { deny all; }
proxy_pass http://homeassistant:8123;
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;
}
location ~ ^/api/services/audi_dashboard/[a-z_]+$ {
limit_except POST { deny all; }
proxy_pass http://homeassistant:8123;
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;
}
# Genau ein Dienst außerhalb der eigenen Domain - siehe "Ehrliche
# Einschränkung" oben, wer ihn nicht mit ausliefern will, lässt diesen
# Block weg.
location = /api/services/homeassistant/restart {
limit_except POST { deny all; }
proxy_pass http://homeassistant:8123;
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;
}
location = /api/websocket {
proxy_pass http://homeassistant:8123;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 3600s;
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;
}
# Das OTA-Oberflächen-Bündel für die native App (siehe Hinweis oben - das ist
# NICHT die App selbst, die läuft nativ; nur ihr nachladbares HTML/CSS/JS).
location ^~ /audi_dashboard_static/app/ {
limit_except GET { deny all; }
proxy_pass http://homeassistant:8123;
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;
}
# Die Fotos der Fahrzeuge, die die App anzeigt. Nur Lesen, nur Bilder.
location ~ ^/local/bilder/[a-z0-9_-]+\.(webp|png|jpg|svg)$ {
limit_except GET { deny all; }
proxy_pass http://homeassistant:8123;
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;
}
# Alles Übrige: nicht durchlassen.
location / {
return 404;
}
```
> **Wichtig, live erlebt (2026-08-28):** Wird dieser letzte Block beim Testen/Debuggen mal entfernt
> (z. B. um eine andere Theorie zu prüfen), unbedingt danach wieder einfügen und speichern - ohne ihn
> landet jede nicht explizit erlaubte Anfrage (auch die reine Root-Adresse `/`) direkt bei Home
> Assistant und zeigt dessen echte Anmeldemaske, was genau die Einschränkung aufhebt, die dieser
> ganze Aufbau eigentlich herstellen soll. Test: `https://<domain>/` (ohne Pfad) muss `404` liefern,
> niemals die HA-Anmeldemaske.
## „Cache Assets" im Nginx Proxy Manager: **an, und das bleibt so**
Stand 06.09.2026: der Schalter **ist eingeschaltet**, und es läuft. Damit ist die
Frage beantwortet - nicht durch Abwägen, sondern durch den Betrieb.
**Warum nicht ausschalten?** Weil auf der Habenseite nichts steht. Gemessen an
der laufenden Instanz, nicht angenommen:
| Pfad | was Home Assistant selbst schickt |
|---|---|
| `/local/bilder/…webp` | `Cache-Control: public, max-age=2678400` (31 Tage) |
| `/audi_dashboard_static/app/bundle.zip` | kein `Cache-Control`, nur `ETag` und `Last-Modified` |
Die Fotos sind die einzige nennenswerte statische Last durch den Proxy, und der
Browser behält sie ohnehin schon einen Monat. Der Rest der Freigabeliste ist
`/api/…` und die WebSocket-Verbindung - von Asset-Caching gar nicht berührt. Ein
laufender Proxy wird nicht für einen Schalter umkonfiguriert, der weder etwas
einbringt noch nachweislich schadet.
**Warum es hätte schiefgehen können - und offenbar nicht tut.** Die Freigabeliste
oben besteht aus handgesetzten `location`-Blöcken, darunter
```nginx
location ~ ^/local/bilder/[a-z0-9_-]+\.(webp|png|jpg|svg)$ { }
```
Ein Asset-Caching-Block ist seinerseits ein Regex-Block auf genau diese
Endungen. Bei nginx gewinnt unter Regex-Blöcken **der erste passende in der
Reihenfolge der Konfiguration**; stünde der erzeugte vor dem eigenen, griffe er
statt seiner, und ohne eigenes `proxy_pass` fielen die Fahrzeugfotos auf das
Dateisystem zurück - also 404. Dass die Fotos ankommen, heißt: dieser Fall tritt
hier nicht ein.
**Was das für spätere Änderungen bedeutet.** Die Konkurrenz besteht weiter. Wer
der Freigabeliste einen neuen Pfad mit einer Bild-, `.js`- oder `.css`-Endung
hinzufügt, prüft danach, ob er wirklich durchkommt - eine Verschattung zeigt
sich sofort als 404 und nicht als schleichender Fehler.
**Ein Sicherheitsproblem wäre es ohnehin nicht.** Alles, was ein Zwischenspeicher
festhalten könnte, trägt einen Cache-Brecher: die Fotos `?v=<mtime der Datei>`,
das OTA-Bündel `?v=<Version>` und obendrein eine SHA-256-Prüfung auf dem Gerät.
Der Kommentar in `companion-app/src/daten/ota.ts` nennt genau diesen Fall - „der
des Geräts wie einer im Weg".
**Wodurch diese Einschätzung fiele:** ein Fahrzeugfoto, das trotz neuem
Zeitstempel das alte bleibt, oder ein OTA-Update, das mit einem Prüfsummenfehler
abbricht. Beides wäre ein Grund, hier zuerst nachzusehen - beides ist bisher
nicht aufgetreten.
## Home Assistant muss dem Proxy vertrauen (nötig, nicht optional)
**Bestätigt korrekt, Stand 2026-08-28.** Beim ersten Live-Test kamen alle Anfragen (egal ob `/api/`,
`/lovelace`, oder sogar zufällige Bot-Scan-Pfade wie `/.env`) mit exakt derselben, byte-identischen
Fehlerantwort zurück (NPMs eigene generische Fehlerseite, nicht Home Assistants). Das lag daran, dass
Home Assistant die Anfragen grundsätzlich ablehnte, weil der Proxy nicht als vertrauenswürdig
eingetragen war - NPM fing Home Assistants Fehlerantwort dabei ab und zeigte seine eigene Seite, was
die eigentliche Ursache verdeckte.
Wichtig: seit einer HA-Version wird die `http:`-Konfiguration aus `configuration.yaml` **automatisch
in einen UI-verwalteten Config-Entry migriert und danach ignoriert** (Reparatur-Hinweis "HTTP-YAML-
Konfiguration wird nach Migration ignoriert"). Der YAML-Block unten dient nur noch der
Dokumentation/als Referenz - tatsächlich gesetzt wird es unter **Einstellungen → System → Netzwerk →
Reverse-Proxy**: "X-Forwarded-For vertrauen" aktivieren, `172.30.33.0/24` unter "Vertrauenswürdige
Proxys" eintragen, speichern (löst automatisch einen HA-Neustart aus, mit Bestätigungs-Dialog).
```yaml
# Nur noch als Referenz - wird bei aktueller HA-Version über die UI gesetzt, siehe oben.
http:
use_x_forwarded_for: true
trusted_proxies:
- 172.30.33.0/24
```
(`172.30.33.0/24` ist das Supervisor-interne Docker-Netz, in dem auch Nginx Proxy Manager läuft.)
**Live verifiziert:** `https://datametric360.de/api/` liefert jetzt `401 Unauthorized` (korrekt, ohne
Token) statt der generischen NPM-Fehlerseite - die Anfrage kommt vollständig bis Home Assistant durch.
**Ein einziger Hostname genügt.** Die frühere Überlegung, App und Schnittstelle auf zwei getrennte
Hostnamen zu legen (`datametric360.de` für die App, `api.datametric360.de` für die Schnittstelle),
stammte aus der Annahme, die App würde selbst als Web-Build unter einer eigenen Adresse ausgeliefert
(siehe Korrektur-Hinweis oben) - das entfällt mit der nativen Distribution ersatzlos. Es gibt nur noch
*eine* Adresse, die überhaupt gebraucht wird: die, die die App beim Einrichten als "Server-Adresse"
bekommt, und über die sie sowohl die API-Aufrufe als auch das OTA-Bündel erreicht. Ein zweiter Hostname
wäre zusätzlicher Aufwand ohne Gegenwert.
## Nach der Einrichtung prüfen
Von einem Netz ohne VPN, etwa über Mobilfunk (Hostnamen unten sind Platzhalter - eintragen, was bei der
Einrichtung tatsächlich gewählt wurde):
```bash
API=https://datametric360.de
T=<Zugriffstoken>
# muss gehen
curl -s -o /dev/null -w '%{http_code}\n' -H "Authorization: Bearer $T" "$API/api/"
curl -s -o /dev/null -w '%{http_code}\n' -H "Authorization: Bearer $T" \
"$API/api/states/sensor.audi_dashboard_profil"
# kein Token nötig - statische Auslieferung, kein API-Aufruf
curl -s -o /dev/null -w '%{http_code}\n' "$API/audi_dashboard_static/app/bundle.zip"
# muss 404 oder 403 liefern
curl -s -o /dev/null -w '%{http_code}\n' "$API/auth/authorize"
curl -s -o /dev/null -w '%{http_code}\n' -H "Authorization: Bearer $T" "$API/api/config"
curl -s -o /dev/null -w '%{http_code}\n' -H "Authorization: Bearer $T" \
"$API/api/states/sensor.audi_rs_4_avant_mileage"
# ohne Token: 401, nicht 200
curl -s -o /dev/null -w '%{http_code}\n' "$API/api/states/sensor.audi_dashboard_profil"
```
Zusätzlich: Port 8123 darf von außen **gar nicht** antworten.
## Noch offen
Siehe [`INTERNET_ZUGRIFF_EINRICHTEN.md`](INTERNET_ZUGRIFF_EINRICHTEN.md) für den vollständigen,
geordneten Ablauf - kurz zusammengefasst bleibt vor der ersten echten Nutzung dieser Liste offen:
- Cloudflare-Konto anlegen, `datametric360.de`-Zone hinzufügen
- Nameserver von `datametric360.de` bei all-inkl auf Cloudflare umstellen („Full setup")
- Cloudflared- und Nginx-Proxy-Manager-Add-ons installieren, Tunnel + öffentlichen Hostnamen einrichten
- `https://datametric360.de` als Server-Adresse in der App eintragen (jedes Gerät, beim Einrichten)