Zum Inhalt springen
Farbschema wählenSprache wählen

Warum App-Hosting auf einem separaten Origin

SupaCloud kann gehostete App-Frontends ausliefern — einfache, React- oder Svelte-Quellordner, die vom Vite-Worker gebaut werden. Diese Seite erklärt die Sicherheitsgrenze zwischen diesen gehosteten Frontends und dem SupaCloud Control Plane und warum sie auf einer separaten registrierbaren Domain leben müssen.

Das SupaCloud Control Plane ist eine Cookie-authentifizierte API. Das Session-Cookie wird mit SameSite=Lax, host-only (kein Domain-Attribut), HttpOnly und Path=/ gesetzt. Solange eine gehostete App und die API denselben Origin teilen, kann eine bösartige (oder kompromittierte) App /api/* aufrufen, und der Browser hängt stillschweigend das Session-Cookie des besuchenden Operators an. Die App wird dann mit den vollen Session-Rechten desjenigen ausgeführt, der sie geöffnet hat.

Dieses Risiko besteht auch bei einer strikten Default-Deny-CSP: script-src 'self' und connect-src 'self' erlauben trotzdem Same-Origin-fetch-Aufrufe an /api, und SameSite=Lax hält Cookies nur bei Cross-Site-Anfragen zurück — nicht bei Same-Origin- oder Same-Site-Anfragen.

Eine naheliegende erste Antwort lautet: „Apps auf apps.supacloud.run platzieren.“ Das genügt jedoch nicht. Eine Browser-Site entspricht der registrierbaren Domain (eTLD+1), nicht dem vollständigen Origin. app.supacloud.run und api.supacloud.run sind dieselbe Site — supacloud.run — daher leitet SameSite=Lax das Session-Cookie bei Anfragen von einer zur anderen weiterhin weiter. Außerdem kann eine Subdomain unter derselben eTLD+1 ein Domain=supacloud.run-Cookie setzen, das für alle Geschwister-Subdomains sichtbar ist (Cookie-Tossing / Shadow-Cookie-Angriff). Weder SameSite noch host-only-Cookies schließen diese Angriffspfade; der einzige Mechanismus, der das unter einer gemeinsamen eTLD+1 tut, ist ein CSP- sandbox-Attribut, das den Origin der App vollständig annulliert — ein brüchiger Ansatz mit eigenen Fehlerszenarien.

SupaCloud liefert gehostete App-Frontends auf supacloud.net aus — einer Domain mit einer anderen eTLD+1 als die Control-Plane-Domain (supacloud.run). Dadurch ist jede gehostete App cross-site gegenüber dem Control Plane. Mit der bestehenden Cookie-Konfiguration schließt sich das Session-Riding automatisch:

  • SameSite=Lax hält das Session-Cookie bei Cross-Site-Anfragen von der App an /api zurück.
  • Cross-eTLD+1-Regeln verbieten ein Domain-Attribut, das beide Domains überspannen würde, sodass Cookie-Tossing strukturell unmöglich ist.
  • Die exakte Allowlist-CORS-Policy des Control Plane schließt die Apps-Domain natürlicherweise aus; eine CORS-Lockerung ist nicht erforderlich.

SUPACLOUD_APPS_BASE_DOMAIN — auf dem gehosteten Deployment auf supacloud.net gesetzt. Das ist die einzige Konfiguration, die auf SupaCloud- Seite erforderlich ist.

Boot-Guard (secret_hydration.rs) — SupaCloud verweigert den Start, wenn die Apps-Domain dieselbe eTLD+1 wie der Control-Plane-Host hat. Das psl-Crate vergleicht registrierbare Domains; Gleichheit, Teilmengen- oder Geschwister-Subdomain-Beziehungen werden alle abgelehnt.

SUPACLOUD_APPS_HOSTED_FRONTEND=true setzt zusätzlich voraus, dass SUPACLOUD_APPS_BASE_DOMAIN gesetzt ist; der Guard erzwingt dies beim Start, sodass der gehostete Pfad niemals ohne die Isolationsgrenze aktiviert werden kann.

Opaques Subdomain pro Deployment (Migration 172) — jede bereitgestellte gehostete App erhält eine stabile 80-Bit-Hex-Subdomain (z. B. a3f8b2….supacloud.net), die einmalig vergeben wird und über Redeploys hinweg stabil bleibt. Nur gehostete Frontends (React, Svelte, Plain-with-Source) erhalten eine Subdomain; Form-Renderer-Apps werden ausschließlich auf dem Control-Plane-Pfad ausgeliefert und sind von dieser Änderung nicht betroffen.

Host-Dispatch-Middleware (interfaces/http/apps_host.rs) — als äußerste Schicht des axum-Routers eingehängt. Eine Anfrage, deren Host mit supacloud.net oder einer Subdomain davon übereinstimmt, wird als App-Inhalt ausgeliefert und erreicht den Control Plane niemals — keine /api-Routen, keine SPA, keine Anmeldeseite. Nur GET und HEAD werden akzeptiert; alle anderen Methoden werden mit 405 Method Not Allowed abgelehnt.

Pfad-Routen-Isolation (services/app_deployments/serve.rs) — wenn eine Apps-Domain konfiguriert ist, verweigert die Control-Plane-Pfad-Route (/a/{workspace_slug}/{route_path}) gehostete Frontends. Eine gehostete App ist ausschließlich über ihre Subdomain auf dem separaten Origin erreichbar. Die #113-Form-Renderer-App (Manifest-only-Plain-App) ist kein gehostetes Frontend und wird weiterhin auf der Pfad-Route ausgeliefert.

Per-App-Callback-Token (Migration 173, Präfix scwa_) — ein serverseitiger, vom Eigentümer ausgestellter Bearer-Token für App-zu-/api-Callbacks. Da das JS der App das Operator-Session-Cookie nicht Cross-Origin mitführen kann, verwenden App-Backends, die die SupaCloud-API aufrufen müssen, stattdessen diesen Token. Er wird über POST /api/apps/{id}/deployment/token ausgestellt, ist workspace-scoped und wird beim Undeploy automatisch widerrufen. Der Token wird serverseitig verwendet und nie in das öffentliche Frontend eingebettet.

Maßnahme Schließt Hinweise
Separate registrierbare Domain (supacloud.net) Session-Riding über SameSite=Lax Primäre Isolation
Cross-eTLD+1 Cookie-Tossing / Shadow-Cookie-Angriff Strukturell; kein Code erforderlich
Boot-Guard (eTLD+1-Prüfung) Fehlkonfigurierte Same-Site-Apps-Domain Fail-Closed beim Start
Host-Dispatch-Middleware Gehostete App erreicht /api-Routen Nur GET/HEAD
Pfad-Routen-Verweigerung Gehostete App auf Control-Plane-Origin Form-Renderer ausgenommen
Per-App-Callback-Token App-zu-API-Identität ohne Session Vom Eigentümer ausgestellt, nur Hash

Für die operative Einrichtung siehe App-Hosting konfigurieren und die Umgebungsvariablen-Referenz. Die vollständige Design-Begründung findet sich in ADR 0041.