Status: Verbindliches Zielbild. Löst ARCHITEKTUR-plattform-topologie.md und
ARCHITEKTUR-kontrollebene.md vollständig ab; beide sollten gelöscht werden.
Die fachlichen Dokumente (ARCHITEKTUR-update-service-v2.md,
ARCHITEKTUR-identitaet-mandanten.md) bleiben gültig — dieses Dokument beschreibt, worauf sie
laufen, nicht was sie tun.
Geltungsbereich: Alle Projekte, Knoten, Netze, Secrets, Deployments und Betriebsabläufe. Nicht enthalten: Anwendungslogik, Datenmodelle, API-Verträge.
Adressat: Wer diesen Stack von Grund auf aufbauen oder im Notfall wiederherstellen muss.
Vier Sätze, aus denen sich der Rest ableitet. Jeder ist testbar formuliert, weil ein Prinzip ohne Test eine Absichtserklärung ist.
P1 — Die Kontrollebene baut und beobachtet, sie liefert nicht aus.
Kein Datenpfad eines Produkts macht zur Laufzeit einen Netzaufruf nach toob-ops.
Test: toob-ops vollständig abschalten; ci., ota. und fw. antworten weiter, ein
Geräte-Check-in läuft durch. Wöchentlich, automatisiert (§12.3).
P2 — Vertrauen fließt von oben nach unten.
Spokes wählen sich beim Hub ein und haben keinen eingehenden Port. toob-ops erreicht die
Spokes, nie umgekehrt — mit genau einer benannten Ausnahme (Log-Push, §5.3).
Test: Portscan gegen jedes Projektnetz von außen ergibt null offene Ports.
P3 — Kein Secret im Datenpfad hat eine TTL unter 30 Tagen.
Damit ist ein Vault-Ausfall ein Deploy-Problem und niemals ein Betriebsproblem.
Test: vault lease lookup über alle Datenpfad-Secrets; Vault 24 h abschalten.
P4 — Was ausfallen darf, wird getrennt von dem, was nicht ausfallen darf. Die Registry führt fremden Code aus und darf Stunden stillstehen. Der Update-Datenpfad führt keinen fremden Code aus und darf praktisch nicht stillstehen. Inverse Profile gehören nicht auf dieselben Maschinen.
Fünf Hetzner-Projekte. Die Projektgrenze ist die Isolationsgrenze — nicht das Netz. Getrennte
Projekte bedeuten getrennte API-Tokens, getrennte Firewall- und Ressourcennamensräume und
getrennte Kostenzuordnung. Ein geleaktes HCLOUD_TOKEN kann genau ein Projekt zerstören.
| Projekt | Rolle | Öffentlicher Ingress | Orchestrierung |
|---|---|---|---|
toob-ops |
Kontrollebene: Secrets, Beobachtung, CI | keiner | systemd |
toob-identity |
Zitadel (IdP für alle Produkte) | id. |
systemd |
toob-registry |
Paket-Registry, Firecracker-Builds | ci. |
Nomad |
toob-update |
Update Service, Fleet-Management | ota., api. |
systemd |
toob-staging |
Vorproduktion, Übungsfläche | stg. |
systemd |
toob-identity ist bewusst kein Teil der Kontrollebene, obwohl es wie sie
produktunabhängig geteilt wird. Zitadel muss aus jedem Operator-Browser erreichbar sein — ein
öffentlich exponierter Dienst mit eigenem CVE-Strom gehört nicht in dasselbe Netz wie Vault und
der WireGuard-Hub. Geteilt wie die Kontrollebene, isoliert wie ein Produkt.
| Knoten | Größe | Läuft darauf | Warum eigener Knoten |
|---|---|---|---|
ops-hub |
CX32 | WireGuard-Hub, Prometheus, Loki, Grafana, Alertmanager | RAM-Bedarf des Monitoring-Stacks; zentraler Netzknoten |
ops-ci |
CX22 | Build- und Deploy-Runner, Cosign | Ein Build zieht Abhängigkeiten Dritter. Diese dürfen nicht im selben Prozessraum wie der WireGuard-Hub und die Metriken landen |
ops-vault-1/2/3 |
CX22 ×3 | Primary Vault (Raft) | Quorum |
ops-kms |
CX22 | Unseal-Vault (Transit) | Muss außerhalb des Clusters liegen, den er entsiegelt — sonst zirkulär |
Warum drei Vault-Knoten. Raft-Quorum ist floor(n/2)+1. Bei zwei Knoten ist es 2 — der
Cluster verträgt null Ausfälle, exakt wie ein Einzelknoten, aber mit doppelter Ausfallfläche.
Zwei ist die einzige Knotenzahl, die keinen Sinn ergibt. Drei verträgt einen Ausfall.
| Knoten | Größe | Läuft darauf |
|---|---|---|
idp-1, idp-2 |
CX22 ×2 | Zitadel, Caddy, WireGuard-Peer |
Zwei Knoten hinter dem Cloudflare-Load-Balancer, jeder mit dem anderen als Caddy-Fallback.
Datenhaltung in toob-idp-db (Ubicloud).
| Knoten | Größe | Läuft darauf |
|---|---|---|
reg-api-fsn1 |
CX22 | Caddy, registry-api, Nomad Server + Client, WireGuard-Peer |
reg-api-hel1 |
CX22 | Caddy, registry-api, Nomad Client, WireGuard-Peer (Standby) |
reg-worker-N |
CCX13 | Firecracker, Jailer, Nomad Client, autoskaliert |
Diese Knoten verlieren in der Migration Vault und den Monitoring-Stack. Aus zwei Pets werden zwei austauschbare Compute-Knoten — für die Registry selbst ein Gewinn, nicht nur für die Trennung.
| Knoten | Größe | Läuft darauf |
|---|---|---|
upd-edge-fsn1, upd-edge-hel1 |
CX22 ×2 | Caddy, toob-edge, WireGuard-Peer |
upd-fleet |
CX22 | toob-fleet (Management-API + Web-UI) |
| Knoten | Größe | Läuft darauf |
|---|---|---|
stg-all |
CX22 | toob-edge + toob-fleet in einem Knoten |
Staging spiegelt die Software, nicht die Topologie. Der Zweck ist, Migrationen, Deploys und den Abschalttest zu üben — nicht Lastverhalten zu messen.
Kein Projekt bekommt einen dedizierten Gateway-Server. Der Peer läuft auf dem stabilsten Knoten des Projekts und routet dessen Subnetz; ein zweiter Knoten hält einen Standby-Peer.
Der Einwand liegt auf der Hand: Bei der Registry hält damit ein Knoten den Tunnel, in dessen
Netz Firecracker fremden Code ausführt. Der Blast Radius ist trotzdem begrenzt — AllowedIPs
am Hub beschränkt jeden Peer auf sein eigenes Subnetz, und eingehend zum Hub ist nur der
Loki-Port offen. Ein dedizierter Gateway würde daran nichts ändern und vier Knoten kosten.
Admin ──WireGuard──► ops-hub ──┬──► toob-identity (10.2.1.0/24)
├──► toob-registry (10.0.1.0/24)
├──► toob-update (10.1.1.0/24)
└──► toob-staging (10.3.1.0/24)
Spokes wählen sich ein. Jeder Projekt-Peer baut die Verbindung ausgehend zum Hub auf. Konsequenz: kein Projektnetz hat einen eingehenden Port — auch kein SSH, auch kein WireGuard- Listener. Das ist der Unterschied zum bisherigen Bastion-Modell, bei dem jedes Projekt einen öffentlichen UDP-Port brauchte.
Absicherung am Hub:
AllowedIPspro Peer auf exakt das Subnetz des Projekts. Peer-zu-Peer-Routing ist deaktiviert.- Die Hub-Firewall verwirft eingehenden Verkehr von Peers vollständig, bis auf den Loki-Ingest.
- Ein Admin-Profil erreicht alle Subnetze — ein Profil statt fünf, kein Doppel-Hop.
Interne Namensauflösung weiterhin über /etc/hosts, von Ansible gesetzt. Kein DNS-Server im
Cluster: eine Komponente weniger, die ausfallen kann, und die Namensmenge ist statisch.
| Signal | Mechanismus | Richtung | Konform zu P2 |
|---|---|---|---|
| Metriken | Prometheus scrapt über den Tunnel | ops → spoke | ✓ |
| Logs | Alloy pusht nach Loki | spoke → ops | Ausnahme |
| Alerts | Alertmanager in ops-hub |
intern | ✓ |
| Dashboards | Grafana, Ordner pro Projekt | intern | ✓ |
Der Log-Push ist die einzige erlaubte Gegenrichtung. Die Alternative — Loki pro Projekt mit
Grafana als föderierter Abfrageschicht — wäre richtungsrein, verdreifacht aber die
Betriebsfläche. Ein kompromittierter Spoke, der Müll in Loki schreibt, ist niedrigschwellig und
wird durch mTLS, Pflicht-Label project, Rate-Limit und Retention-Cap eingefangen.
Jeder öffentlich erreichbare Dienst wird doppelt überwacht:
- Interner Scrape über den Tunnel — misst den Prozess.
- Externe Blackbox-Probe über Cloudflare — misst den vollständigen Pfad.
Der Nutzen ist Diagnose, nicht Redundanz:
| Extern | Intern | Diagnose |
|---|---|---|
| up | up | alles in Ordnung |
| up | down | Tunnel oder Scrape-Konfiguration, nicht der Dienst |
| down | up | Edge, DNS oder Zertifikat, nicht der Dienst |
| down | down | der Dienst |
Mit nur einer Sonde weiß man im Ernstfall nicht, wo zu suchen ist. Das gilt für id., ota.,
api., ci. und fw. gleichermaßen.
Routing über das project-Label, eigener Kanal je Projekt, eigener Dead-Man-Switch je
Projekt. Ein globaler Heartbeat beweist nur, dass Alertmanager lebt — er verdeckt, dass die
Metriken eines einzelnen Projekts seit Stunden fehlen.
Schweregrade folgen der Ausfallwirkung, nicht dem Gefühl:
| Ausfall | Schwere | Begründung |
|---|---|---|
upd-edge beide Knoten |
kritisch | Flotte bekommt keine neuen Zuweisungen |
toob-idp beide Knoten |
hoch | Operatoren gesperrt, Geräte unberührt |
upd-fleet |
hoch | kein Management, Auslieferung läuft |
reg-api beide Knoten |
mittel | Builds stehen |
ops-hub |
mittel | Blindflug, kein Betriebsausfall |
| Vault | niedrig | nur Deployments blockiert (P3) |
Dass ein Vault-Ausfall der niedrigste Schweregrad ist, ist kein Versehen — es ist die Bestätigung, dass P3 funktioniert.
Beibehalten aus dem Registry-Stack und auf alle Projekte ausgeweitet: Backup-Zeitstempel,
Restore-Test-Ergebnis, Token-TTLs, Service-Failures via OnFailure=notify-failure@%n,
DB-Firewall-Pruner. Cron- und Timer-Ergebnisse werden zu Metriken, nicht zu Logzeilen, die
niemand liest.
| Projekt | Supervisor | Begründung |
|---|---|---|
toob-registry |
Nomad | Dynamische Firecracker-Worker, Autoscaling, Scheduling über einen heterogenen Pool |
| alle anderen | systemd | Fester Satz zustandsloser Prozesse |
Nomad existiert in genau einem Projekt, weil genau ein Projekt dynamische Workloads hat.
Ein Nomad im Identity- oder Update-Spoke bräuchte entweder einen eigenen Server — bei
bootstrap_expect = 1 ein neuer Single Point — oder müsste in die Kontrollebene greifen, was P1
bricht: Scheduling und Service-Discovery sind Laufzeit.
Was systemd leistet: Neustart bei Fehlschlag, Startreihenfolge, cgroup-Ressourcengrenzen,
Härtung (NoNewPrivileges, ProtectSystem=strict, CapabilityBoundingSet), Failure-Metrik.
Was es nicht leistet, und wer es stattdessen tut:
| Fehlende Fähigkeit | Ersatz |
|---|---|
| Knotenübergreifendes Failover | Caddy mit lb_policy first auf den Nachbarknoten, Cloudflare-LB darüber |
| Rolling Deploy | Deploy-Skript: Knoten A aus dem Upstream, tauschen, /ready prüfen, zurück, dann B |
| Bin-Packing | nicht erforderlich, feste Zuordnung |
| Service-Discovery | /etc/hosts, statische Menge |
Der Rolling Deploy sind rund fünfzig Zeilen Bash. Die Ausfallfläche dieses Mechanismus ist bei Nichtbenutzung exakt null — die eines Orchestrators nicht.
| Vault | Zitadel | |
|---|---|---|
| Wofür | Maschinen-Secrets, Signierschlüssel, PKI | Menschen und Kunden-Workloads |
| Wer fragt | Deploy-Pipeline, Vault-Agents | Browser, Kunden-CI |
| Bei Ausfall | keine Deployments | kein Operator-Login |
| Datenpfad betroffen | nein (P3) | nein |
Beide sind keine Laufzeitabhängigkeit des Geräte-Pfads. Der Device-Token wird lokal per HMAC gegen die Update-Datenbank geprüft und kennt weder Vault noch Zitadel.
Vault OSS kennt keine Namespaces. Mandantentrennung läuft über Pfade und Policies:
secret/projects/registry/…
secret/projects/update/…
secret/projects/identity/…
secret/projects/staging/…
secret/platform/… nur Kontrollebene
transit/keys/registry-package-signing
transit/keys/registry-image-signing
transit/keys/update-firmware-signing ← eigene Policy, eigener Zugriffspfad
transit/keys/autounseal-platform
pki/ nur registry-intern (Nomad-mTLS)
Die Trennung der Transit-Keys ist die wichtigste Einzelmaßnahme des gesamten Dokuments. Ein kompromittierter Registry-Release-Pfad darf keine Firmware signieren können. Die öffentlichen Hälften dieser Schlüssel liegen in eFuses ausgelieferter Geräte — sie sind bei einer Migration zu erhalten und niemals neu zu erzeugen.
Keine Vault-PKI im Update-Datenpfad. Die 24-Stunden-Zertifikate der Registry sind dort richtig, weil kurze TTLs erzwingen, dass Rotation nachweislich funktioniert. Im Update-Pfad wären sie eine Verletzung von P3.
ops-kms hält den Transit-Key, mit dem ops-vault-* sich selbst entsiegelt. Reboots heilen
sich, ohne dass je ein Unseal-Key auf einer Maschine liegt. In Produktion existieren nur
PGP-verschlüsselte Recovery-Keys, 5 Shares, Schwelle 3.
Organisationen bilden Mandanten ab. OIDC für die Web-UIs beider Produkte (Registry und Fleet-Management), Workload-Identity-Federation für Kunden-CI — damit braucht keine Kunden-Pipeline ein langlebiges Secret bei uns.
GitHub bleibt als Anmeldemethode erhalten, aber hinter Zitadel statt daneben. Industriekunden haben nicht alle GitHub, und zwei Identitätsmodelle für dieselben Menschen sind ein Dauerärgernis.
TODO: Zitadel-HA-Modell verifizieren. Zwei Instanzen gegen eine Postgres-Instanz sind nach Dokumentation zulässig; das ist vor dem Produktivgang praktisch zu bestätigen, nicht anzunehmen.
| Hostname | Ziel | Compute beteiligt |
|---|---|---|
ci.the-toob.com |
reg-api ×2, Geo-LB |
ja |
cdn.the-toob.com |
Object Storage (Pakete) | nein |
ota.the-toob.com |
upd-edge ×2, Geo-LB |
ja |
fw.the-toob.com |
Object Storage (Firmware) | nein |
api.the-toob.com |
upd-fleet |
ja |
id.the-toob.com |
idp ×2, Geo-LB |
ja |
stg.the-toob.com |
stg-all |
ja |
Die beiden Blob-Routen laufen an unserer Compute vorbei. Das ist die wichtigste Verfügbarkeitseigenschaft der Plattform: alle fünf Projekte können ausfallen, und laufende wie wiederholte Firmware-Downloads funktionieren weiter.
Zwingende Cloudflare-Regeln auf fw.:
- Compress off. Transparentes gzip verschiebt Bytegrenzen und zerstört jeden Resume.
Cache-Control: immutable, langes TTL, Origin Cache Lock gegen den Miss-Sturm nach Publish.- Kein Redirect. Der MCU-HTTP-Client folgt ihnen nicht zuverlässig; ein
302wäre ein stiller Fehlerpfad quer durch die Flotte.
Auf allen Compute-Routen: Caddy mit Cloudflare-Origin-CA (Full Strict), trusted_proxies auf
die CF-Ranges, X-Real-IP aus CF-Connecting-IP, Security-Header, /metrics öffentlich
geblockt. Metriken laufen nicht über Caddy, sondern direkt per mTLS vom Prometheus.
Getrennte Rulesets und Rate-Limits pro Hostname. Ein WAF-Regelfehler auf ci. darf ota.
nicht treffen.
| Instanz | Nutzer | Warum getrennt |
|---|---|---|
toob-registry-db |
Registry | Build-getrieben, selten geschrieben |
toob-update-db |
toob-edge + toob-fleet |
Pro Gerät pro Boot geschrieben |
toob-idp-db |
Zitadel | Anderer Lebenszyklus, anderes Backup-Regime |
toob-staging-db |
Staging | Wegwerfbar |
toob-edge und toob-fleet teilen sich eine Instanz. Getrennte Datenbanken würden
verteilte Transaktionen erzwingen — die lazy Materialisierung einer Zuweisung und der
Outbox-Eintrag müssen atomar sein. Die Isolation kommt stattdessen über getrennte
Postgres-Rollen ohne BYPASSRLS.
Object-Storage-Buckets:
| Bucket | Inhalt | Lifecycle |
|---|---|---|
toob-firmware |
Firmware-Artefakte, mandantenpräfigiert | niemals löschen, solange Geräte im Feld sind |
toob-registry-packages |
Paket-Tarballs | Temp-Releases 7 Tage |
toob-vault-backups |
Snapshots | 30 Tage |
toob-terraform-state |
State aller Module | versioniert |
Der Firmware-Bucket hat eine andere Löschregel als alles andere: Ein Gerät, das drei Jahre im Schrank lag, muss seinen Update-Pfad noch vorfinden.
DB-Firewall-Pruner wird zum Plattformdienst auf ops-hub und bedient alle Instanzen. Eine
Konfigurationszeile pro Projekt statt eines Timers pro Projekt.
| Was | Wie | Wohin | Frequenz |
|---|---|---|---|
| Primary Vault | Raft-Snapshot | toob-vault-backups |
täglich |
| Unseal-Vault | tar des File-Storage | …/unseal-snapshots/ |
täglich |
| Nomad | operator snapshot save |
…/snapshots/ |
täglich |
| Postgres ×4 | Ubicloud Managed Backups | extern | laufend |
| Terraform-State | S3-Backend, versioniert | toob-terraform-state |
bei jedem Apply |
Alle Terraform-Module nutzen das S3-Backend. Im Bestand laufen cloudflare, database und
s3 auf lokalem State — der State für DNS, Load-Balancer, Produktionsdatenbank und Paket-Bucket
liegt damit auf einem Laptop. Geht er verloren, sind die Ressourcen verwaist und ein apply
versucht, sie neu anzulegen. Das ist ein Datenverlust-Szenario ohne Angreifer und der billigste
Fix im ganzen Dokument.
Monatlicher automatischer Restore-Test. Neuester Snapshot wird in einen wegwerfbaren Vault-Container zurückgespielt, es wird geprüft, dass er entsiegelt und ein Kanarienvogel-Schlüssel lesbar ist, Ergebnis als Metrik. Alarm bei Fehlschlag und bei Ausbleiben.
Das hebt BackupStale von „eine Datei ist neu genug" auf „wir könnten tatsächlich
wiederherstellen". Ein Backup, das nie zurückgespielt wurde, ist eine Vermutung.
Wiederherstellungsreihenfolge bei Totalverlust: ops-kms aus Tarball → ops-vault-* aus
Raft-Snapshot (entsiegelt über den wiederhergestellten KMS) → Nomad-Snapshot → Jobs und
systemd-Units neu deployen → Datenbanken aus Ubicloud-Backups.
ops-ci ──build──► Image/Binary ──sign──► ops-vault (Transit) ──verify──► Deploy in den Spoke
Schlüsselzugriff ausschließlich von ops-ci. Kein Spoke-Knoten hat einen Netzpfad zum
Signierschlüssel. Ein kompromittierter upd-edge kann keine Firmware signieren, keine Images
signieren und keine Releases publizieren.
Der Cosign-Token entfällt. Bisher ein -period=720h-Dauercredential mit manueller
Erneuerung. ops-ci authentifiziert sich per AppRole und zieht pro Lauf einen kurzlebigen
Token.
Digest-Pinning bleibt. Deployments referenzieren sha256:…, nie einen mutable Tag. Für den
Update-Service gilt dasselbe Prinzip eine Ebene höher: Firmware-Artefakte sind content-adressiert.
Promotion statt Rebuild. Ein Artefakt wird einmal gebaut und signiert, durchläuft
toob-staging und wandert mit demselben Digest nach Produktion. Kein zweiter Build, kein
„lokal ging es".
Secret-Seeding ohne Operator-Environment. Der bisherige SEED_*-Mechanismus bedeutet, dass
beim Bootstrap jedes Produktionsgeheimnis in der Shell des Operators steht, inklusive History.
Kurzfristig: Seeding aus einer 0600-Datei via vault kv put @file. Mittelfristig: ops-ci
führt das Seeding aus, kein Mensch sieht die Werte im Klartext.
Cloudflare-Token pro Projekt statt eines Tokens mit Zone:Edit für alles.
Ein Terraform-Modul project-baseline erzeugt aus einem Projektnamen:
Netz, Firewall-Baseline, WireGuard-Peer-Registrierung am Hub, Vault-Pfade mit Policies und
AppRoles, Postgres-Instanz mit Firewall-Pinning, Prometheus-Scrape-Job, Grafana-Ordner,
Alert-Route, Dead-Man-Switch, Backup-Eintrag, Terraform-State-Key.
Damit kostet ein weiteres Projekt einen terraform apply und einen Ansible-Lauf. Das ist die
Voraussetzung dafür, dass kundendedizierte Stacks — ein Single-Tenant-Deployment ist ein
Multi-Tenant-Deployment mit genau einem Mandanten — ein Konfigurationsvorgang sind und kein Fork.
Die Zentralisierung des Zugangs erzeugt ein neues Risiko: Stirbt ops-hub, kommt niemand mehr
irgendwo hinein. Zwei dokumentierte Notpfade:
- Hetzner Cloud Console (VNC) — funktioniert ohne jedes Netz, für jeden Knoten, aus dem Webinterface. Primärer Weg, kostet nichts.
- Schlafendes WireGuard-Peer-Profil je Projekt, offline hinterlegt, das erst nach manueller Aktivierung über die Console akzeptiert wird.
Halbjährliche Übung mit Protokoll und gemessener Zeit bis zum Zugriff. Ein ungeübter Notpfad ist keiner.
toob-ops wird vollständig heruntergefahren. Geprüft wird, dass ci., ota. und fw.
weiter antworten und ein Geräte-Check-in durchläuft. Das Ergebnis muss von außerhalb erhoben
werden — der eigene Prometheus ist ja aus.
Einführung in drei Stufen: zweimal manuell gegen toob-staging, dann einmal manuell gegen
Produktion in einem Fenster mit niedrigem Verkehr, erst danach automatisiert und wöchentlich.
Ein Test, der beim ersten automatischen Lauf die Produktion trifft, ist selbst ein Risiko.
Nebenwirkung fürs Runbook: Der Test meldet Operatoren ab, weil Zitadel… nein — Zitadel liegt in
toob-identity und läuft weiter. Der Test unterbricht Deployments und Beobachtung, sonst nichts.
Genau das soll er beweisen.
| Aufgabe | Frequenz | Automatisiert |
|---|---|---|
| Restore-Test | monatlich | ja |
| Abschalttest | wöchentlich | ja (nach Einführung) |
| Break-Glass-Übung | halbjährlich | nein |
| Golden Image neu bauen | bei CVE | nein |
| PGP-Quorum prüfen (5 Operatoren erreichbar) | halbjährlich | nein |
| Token-TTL-Alarme abarbeiten | ereignisgesteuert | Alarm ja, Behebung nein |
| Ausfall | Geräte-Updates | Builds | Operator-Login | Deployments |
|---|---|---|---|---|
toob-ops komplett |
laufen | laufen | läuft | blockiert |
| Vault | laufen | laufen | läuft | blockiert |
toob-identity |
laufen | laufen | blockiert | blockiert |
toob-registry |
laufen | blockiert | läuft | teilweise |
upd-edge beide |
Downloads laufen, keine neuen Zuweisungen | laufen | läuft | läuft |
toob-update-db |
Downloads laufen, Check-in 503 |
laufen | läuft | läuft |
upd-fleet |
laufen | laufen | läuft | läuft |
| Object Storage | nur solange CDN-Cache trägt | blockiert | läuft | läuft |
| Cloudflare | blockiert | blockiert | blockiert | läuft |
Fünf von neun Ausfällen berühren die Flotte nicht. Das ist der Zweck der gesamten Struktur.
Zwei Zeilen verdienen einen Kommentar. Der Check-in antwortet bei DB-Ausfall mit 503 und
nicht mit 204 — ein 204 würde „kein Update vorhanden" behaupten und Geräte in falscher
Sicherheit lassen. Und Cloudflare ist der einzige echte Single Point der Plattform; der
Ausweichpfad (DNS direkt auf die Origins, Rate-Limiting entfällt) gehört ins Runbook, mit dem
ehrlichen Hinweis, dass er nur für Stunden trägt.
Größenordnung, vor Umsetzung zu verifizieren.
| Posten | Monatlich |
|---|---|
toob-ops: 6 Knoten |
~35 € |
toob-identity: 2 Knoten |
~10 € |
toob-registry: 2 Knoten + variable Worker |
~16 € + Last |
toob-update: 3 Knoten |
~15 € |
toob-staging: 1 Knoten |
~5 € |
| Managed Postgres ×4 | ~40–80 € |
| Object Storage + Traffic | volumenabhängig |
| Grundlast | ~120–160 € |
Das ist ein deutlicher Anstieg gegenüber dem heutigen Registry-Stack. Zwei ehrliche Einsparoptionen, beide mit benanntem Preis:
toob-stagingweglassen. Spart ~15 € und kostet die Übungsfläche für Migrationen und den Abschalttest. Für einen Dienst, dessen Fehler Flash-Erase-Budget im Feld verbrauchen, halte ich das für die schlechtere Wahl.ops-cimitops-hubzusammenlegen. Spart ~5 € und gibt die Trennung zwischen Build-Abhängigkeiten Dritter und dem WireGuard-Hub auf. Vertretbar in der Anfangsphase, mit einem Ticket zur späteren Trennung.
Der Traffic ist der eigentliche Skalierungsfaktor — und genau deshalb ist die Blob-Auslieferung ohne eigene Compute (§8) die wichtigste Kostenentscheidung: Die Flottengröße skaliert nicht in die Serverrechnung.
Reihenfolge ist bindend: ops-kms vor ops-vault, Restore-Test vor beidem.
| Schritt | Inhalt | Risiko | Rückweg |
|---|---|---|---|
| 1 | Projekt toob-ops, ops-hub, WireGuard-Hub |
null — Bestand unberührt | Projekt löschen |
| 2 | Terraform-State der drei lokalen Module nach S3 | null | State-Datei behalten |
| 3 | Spoke-Peers in Registry, Dial-Out testen | niedrig | Bastion bleibt bis Schritt 6 |
| 4 | Restore-Test aufsetzen und grün bekommen | null | — |
| 5 | ops-kms aufsetzen, Tarball einspielen, Primary umschwenken |
mittel | alte Instanz stehen lassen |
| 6 | ops-vault-1/2/3, Raft-Snapshot einspielen, Clients umziehen |
hoch | alter Cluster bleibt |
| 7 | Monitoring nach ops-hub, Registry-Bastion abbauen |
niedrig | Compose-Stack zurück |
| 8 | toob-identity aufsetzen, Registry-Login migrieren |
mittel | GitHub-OAuth bleibt parallel |
| 9 | project-baseline-Modul, toob-staging als Nachweis |
niedrig | — |
| 10 | toob-update als Spoke aus dem Modul |
niedrig | — |
| 11 | Abschalttest manuell, dann automatisiert | mittel | — |
Schritt 6 ist der einzige wirklich heikle. Der Raft-Snapshot enthält die Transit-Keys, deren öffentliche Hälften bereits Vertrauensanker in ausgelieferten Geräten sind — sie dürfen unter keinen Umständen neu erzeugt werden. Angekündigtes Wartungsfenster, vorher grüner Restore-Test, alter Cluster bleibt bis zur Bestätigung stehen.
toob-update kann bereits nach Schritt 1 gebaut werden und übergangsweise Secrets aus dem alten
Vault beziehen. Es gibt keinen Grund, die Migration abzuwarten.
| Punkt | Warum offen |
|---|---|
| Zitadel-HA gegen eine Postgres-Instanz | Dokumentation gelesen, nicht praktisch bestätigt |
| Cloudflare-Datenresidenz | Für den EU-Souveränitätsanspruch ist zu klären, ob Data Localization erforderlich wird — spätestens wenn der erste Kunde seinen Auditor mitbringt |
| Ubicloud-Preise und HA-Optionen | Kostenbild §14 basiert auf Schätzung |
| Loki-Mandantentrennung | Erforderlich, bevor Kunden Grafana-Leserechte bekommen, nicht danach |
| Vier-Augen-Prinzip für Produktions-Releases | Als Mandanten-Einstellung vorgesehen, Ablauf noch nicht entworfen |
- Fünf Projekte, Projektgrenze = Isolationsgrenze.
- Kontrollebene über den Produkten, nicht in einem davon — beseitigt die Asymmetrie, dass Vault im Netz des Systems liegt, das fremden Code ausführt.
toob-identityals eigener Spoke, nicht in der Kontrollebene — ein öffentlich erreichbarer Dienst gehört nicht neben Vault.- Spokes wählen sich ein — kein Projektnetz hat einen eingehenden Port.
- Keine Laufzeit-Secrets unter 30 Tagen im Datenpfad — macht Vault teilbar ohne Verfügbarkeitskopplung.
- Nomad in genau einem Projekt, weil genau eines dynamische Workloads hat.
- Zwei Sonden pro öffentlichem Dienst — Diagnose statt Redundanz.
- Drei Vault-Knoten oder einer, niemals zwei.
- Getrennte Transit-Keys pro Produkt — die wichtigste Einzelmaßnahme.
- Blob-Auslieferung ohne eigene Compute — alle Projekte können ausfallen, Downloads laufen.
- Restore-Test und Abschalttest sind automatisiert — Invarianten ohne Test sind Hoffnungen.
Die Kontrollebene baut und beobachtet. Sie liefert nicht aus. Wenn man sie abschaltet und alle Produkte weiterlaufen, ist der Schnitt richtig — und genau das wird einmal pro Woche nachgewiesen, nicht angenommen.