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

16 KiB

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 - 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.

# 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

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).

# 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):

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 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)