Files
HiveGrid-Wireframes/hivegrid-cloud/docs/ops/prod-rollout-hivegrid.md
T

25 KiB
Raw Blame History

Prod-Rollout HiveGrid — Runbook (dedizierter Hetzner-Account)

Umsetzung von AD-24 (Prod unter eigenem Hetzner-Account, self-contained). Stand 2026-06-19. Go-Gates: Jeder Schritt mit 🟥 ist kostenwirksam / schwer reversibel und wird vor dem create einzeln mit dem Owner (Raffael) bestätigt. 🟩 = read-only / vorbereitend.

0. Voraussetzungen (🟩 vorhanden)

  • Hetzner-API-Token (Prod): ~/.cli-tokens/hcloud-hivegrid-prod — verifiziert (Account leer).
  • S3 (Prod): ~/.cli-tokens/prod-s3-bucket-hetzner-credentials — Auth verifiziert (Projekt p15040939, fsn1), Bucket noch nicht angelegt.
  • Cloudflare (Zone hivegrid.de): ~/.cli-tokens/cloudflare-token (Global Key + Email → X-Auth-Email/X-Auth-Key).
  • Gitea/CI: Deploy-Pipeline .gitea/workflows/deploy.yml (Promote-Pfad → Prod).
  • Entscheidungen: Ingress = Cloudflare-Proxy + Coolify/Traefik (kein Hetzner-LB). Region fsn1, x86 cx-Linie.

1. Netzwerk-Fundament (🟥 Provisionierung, aber günstig/reversibel)

  1. SSH-Key im Prod-Account hinterlegen (aktuell 0) — Public-Key des Deploy-/Admin-Keys.
  2. Private Network (Hetzner Cloud Network), z.B. 10.0.0.0/16, Subnetz fsn1.
  3. Firewall:
    • eingehend 80,443/tcp → control/web/api (bzw. nur über Cloudflare-IP-Ranges, wenn proxied).
    • 22/tcp SSH nur von Admin-/Bastion-IP.
    • db + services: kein eingehender Public-Traffic, nur über das Private Network.

2. Server provisionieren (🟥 kostenwirksam — Go pro Batch)

Alle in fsn1, im Private Network, mit Firewall + SSH-Key. cx-Linie (x86):

VM Typ Specs Zweck
hivegrid-control cx33 4/8/80 Coolify + Orchestrierung/Bastion
hivegrid-web cx33 4/8/80 Web (SSR)
hivegrid-api cx33 4/8/80 API
hivegrid-worker cx43 8/16/160 5 Worker-Queue-Gruppen + Meili + ClamAV + mCaptcha
hivegrid-db cx33 4/8/80 Postgres + PgBouncer + Redis
  1. Volume (~50 GB) an hivegrid-db für Postgres-Daten (nicht auf Boot-Disk).
  2. (optional) Server-Backups für hivegrid-db aktivieren.

3. Coolify auf control (🟥)

  1. Coolify auf hivegrid-control installieren (eigenständige Prod-Instanz).
  2. web/api/worker/db als remote Server in Coolify einhängen (über Private Network).
  3. Container-Registry-Zugang (Gitea-Registry) + Cosign-Keys in Coolify hinterlegen.

4. Daten- + Zentral-Dienste (🟥)

  1. db-VM: Postgres (+ DB hivegrid, DB mcaptcha), PgBouncer (Transaction-Pooling, Port 6432), Redis. DATABASE_URL → PgBouncer, DIRECT_URL → 5432.
  2. worker+services-VM: Meilisearch (Master-Key), ClamAV (clamd), mCaptcha (zeigt auf db-Postgres mcaptcha + Redis).
  3. DEK-Master-Key (AD-8) generieren + sicher hinterlegen (nicht ins Repo).
  4. S3-Bucket hivegrid-prod (fsn1) anlegen + Versioning; prod-s3-bucket-hetzner-credentials um bucket=hivegrid-prod ergänzen.

5. App-Env + Deploy (🟥)

  1. Prod-Env je App setzen (analog Staging-Keys, aber Prod-Werte): DATABASE_URL/DIRECT_URL, REDIS/BULLMQ_REDIS_URL, MEILI_URL/MEILI_API_KEY, MCAPTCHA_*, CLAMAV_HOST/PORT, SESSION/CRYPTO/JWT-Secrets, APP_BASE_DOMAIN/Host-Vars (Prod-Domains), Sentry SENTRY_ENVIRONMENT=production. WEB_NOINDEX NICHT setzen (Prod soll indexierbar sein).
  2. Migrationen: prisma migrate deploy als Pre-Deployment-Command (wie Staging).
  3. Promote-Deploy über die Pipeline (oder Coolify) → Images ausrollen.

6. Ingress / DNS (🟥 outward-facing)

  1. Cloudflare-Records (Zone hivegrid.de):
    • app. , api. , captcha. → Prod-IPs, proxied (orange).
    • admin. → Prod-IP, dns-only + Firewall/Cloudflare-Access-Restriktion.
    • Kein Wildcard.
  2. Coolify/Traefik: Domains je App + Let's-Encrypt am Origin; Cloudflare SSL-Modus Full (strict).

7. Endabnahme (🟩 verifizieren)

  • app./api. /healthz + /readyz = 200 (DB erreichbar, Migration angewandt).
  • Login inkl. 2FA-Gate; mCaptcha greift; Suche (Meili) antwortet; File-Upload-Scan (ClamAV) ok.
  • Prod ist indexierbar: /login trägt kein X-Robots-Tag: noindex; robots.txt allow-all.
  • Sentry-Events landen unter environment=production.

8. Rollback / Notfall

  • Deploy-Rollback: vorheriges Image-Tag in Coolify re-deployen.
  • DB: Volume-Snapshot / Backup einspielen (forward-only Migrationen → Inverse-SQL als Kommentar je Migration).
  • DNS: TTL niedrig halten während des Cutovers.

Inventar (provisioniert 2026-06-19, fsn1)

Ressource Name ID Public IP Private IP
Server control hivegrid-prod-control (cx33) 143044798 157.90.243.228 10.0.0.2
Server web hivegrid-prod-web (cx33) 143044800 167.233.109.142 10.0.0.3
Server api hivegrid-prod-api (cx33) 143044801 167.233.110.100 10.0.0.4
Server worker+services hivegrid-prod-worker (cx43) 143044803 49.13.24.190 10.0.0.5
Server db hivegrid-prod-db (cx33) 143044805 178.105.208.72 10.0.0.6
Volume (db-Daten) hivegrid-prod-db-data 50 GB 106081942 — /dev/disk/by-id/scsi-0HC_Volume_106081942
Network hivegrid-prod-net 10.0.0.0/16 12347309 — —
Firewall app (22/80/443) hivegrid-prod-fw-app 11162316 — —
Firewall db (22) hivegrid-prod-fw-db 11162317 — —
SSH-Key hivegrid-prod-node1 (~/.ssh/node1) 113985925 — —

Offen / Härtung: SSH derzeit von 0.0.0.0/0 (key-only) erreichbar → später auf Admin-/Bastion-IP bzw. nur über control einschränken. Öffentliche 80/443 später auf Cloudflare-IP-Ranges begrenzen.

Coolify (Schritt 3) — erledigt 2026-06-19

  • Coolify v4.1.2 auf control installiert (alle Container healthy).
  • Root-Admin headless angelegt (raffael@tool-ctrl.de) via RootUserSeeder (docker exec -e ROOT_USER_* — Passwort-Policy: mixedCase+Ziffer+Symbol+uncompromised).
  • API aktiviert + API-Token erzeugt (Sanctum, mit team_id; Coolifys createToken-Override braucht Team-Kontext, daher direkt über tokens()->create). GET /api/v1/servers → 200.
  • Zugang (Secrets in ~/.cli-tokens/coolify-prod, nicht im Repo): UI http://coolify.hivegrid.de:8000 bzw. http://157.90.243.228:8000.
  • ⚠ Härtung offen: Coolify-FQDN auf https://coolify.hivegrid.de (443) umstellen + Port 8000 wieder schließen.

Coolify-Remote-Server + DB (Schritt 3–4) — erledigt 2026-06-19

  • Docker 29.6.0 auf web/api/worker/db installiert (get.docker.com), in Coolify als Remote-Server eingehängt → alle usable=true. node1-Key in Coolify (privkey_uuid). UUIDs in ~/.cli-tokens/coolify-prod.
  • Coolify-Projekt HiveGrid (m74l9z6an57cw0316pir9rrb) / Environment production (h3woz1vm70yf6ajpzj0ch2fz).
  • Postgres 16 (managed) + Redis 7 (managed) auf db-VM. Creds + URLs in ~/.cli-tokens/hivegrid-prod-services.
  • Cross-Server-Konnektivität (Designentscheidung): Coolify-managed DBs laufen im Docker-Netz der db-VM; die App-Server (web/api/worker) erreichen sie über das private Hetzner-Netz (10.0.0.6:5432/6379), nicht über Container-DNS. DBs sind via public_port auf dem Host gebunden (0.0.0.0), aber public durch fw-db (nur 22) dicht — verifiziert: web→db privat OPEN, public DICHT.

Offen (nächste Schritte):

  • Meilisearch + ClamAV + mCaptcha als Coolify-Applications/Services auf worker-VM (kein managed-DB-Typ), exposed aufs private Netz. mCaptcha nutzt Postgres-DB mcaptcha + Redis.
  • PgBouncer vor Postgres (Compose auf db-VM) → DATABASE_URL über Pooler, DIRECT_URL direkt.
  • DEK-Master-Key + S3-Bucket hivegrid-prod.
  • App-Deploy (web/api/worker) nach mitapp-Muster: image-retag / build-once-deploy-via-pull (nicht rebuild) + migrate deploy.
  • DNS app./api./admin./captcha. → Prod-IPs (Cloudflare) + Sentry (ToolCTRL-Instanz, prod-DSNs).

Zentral-Dienste + Storage (Schritt 4) — erledigt 2026-06-19

  • Meilisearch v1.10 + ClamAV 1.4 als Coolify-Compose-Service hivegrid-prod-search-av auf worker-VM, privat exposed (10.0.0.5:7700 / :3310). Verifiziert: Meili von api-VM cross-server {"status":"available"}, ClamAV healthy. Master-Key in ~/.cli-tokens/hivegrid-prod-services.
  • mCaptcha-Stack (mcaptcha + eigene Postgres + mcaptcha/cache) als Coolify-Service hivegrid-prod-mcaptcha auf worker-VM, FQDN captcha.hivegrid.de (DNS dns-only → worker, LE-Cert via Traefik im Hochlauf). Secrets gespeichert. Folgeschritt: Account+Sitekey in der mCaptcha-Instanz anlegen → neue MCAPTCHA_SITEKEY_*/ACCOUNT_SECRET in App-Env (Code-/Env-Anpassung).
  • S3-Bucket hivegrid-prod (fsn1) angelegt + Versioning; prod-s3-bucket-hetzner-credentials ergänzt.

App-Deploy — Muster verstanden (mitapp / build-once-retag)

  • Image-Schema: git.tool-ctrl.de/toolctrl-labs/hivegrid/{web,api,worker}:<commit-sha>, build_pack dockerimage, Port 8080.
  • Promote = retag, kein Rebuild: Prod deployt denselben SHA-Tag, der getestet/gestaged ist (aktuell 9ae3963d… = live auf Staging → Image existiert in der Gitea-Registry).
  • 7 Coolify-Apps (nicht 3!): web, api, worker-general, worker-mail, worker-import, worker-webhook, worker-plugin — web/api aus /web+/api-Image, die 5 Worker aus /worker-Image mit unterschiedlichem WORKER_QUEUE_GROUP.
  • Deploy-Trigger: scripts/coolify-deploy.mjs (PATCH docker_registry_image_tag → deploy), migrate deploy als Pre-Deployment-Command.
  • Sentry (ToolCTRL-Instanz): gleiche DSNs wie Staging, nur SENTRY_ENVIRONMENT=production (Estate-Regel: Staging↔Prod = Tag, kein eigenes Projekt).

Offen (Go-Live-Phase): Registry-Pull-Credential in Prod-Coolify · 7 Apps anlegen (Docker-Image) · App-Env Prod (aus Staging-Env transformiert: Prod-IPs/Domains, frische Secrets, prod-Sentry-Tag, kein WEB_NOINDEX) · erster Promote-Deploy + migrate deploy · DNS app./api./admin. · Endabnahme · mCaptcha-Sitekey + PgBouncer (Optimierung) als Folgeschritte.

App-Deploy + Go-Live (Schritt 5–6) — LIVE 2026-06-19

  • Registry-Pull: docker login git.tool-ctrl.de auf web/api/worker (User gitea-admin; Härtung: scoped Bot-PAT). Image hivegrid/{web,api,worker}:9ae3963d… (= Staging-Stand) gepullt.
  • 7 Coolify-Apps (Docker-Image): hivegrid-web, -api, -worker-{general,mail,import,webhook,plugin} (Worker je WORKER_QUEUE_GROUP). UUIDs in ~/.cli-tokens/coolify-prod.
  • Prod-Env aus Staging transformiert (Bug gefixt: bei doppelten build/runtime-Keys nicht-leeren Wert bevorzugen — sonst Mailjet u.a. leer → Worker-Crash). Frische, app-konsistente Secrets in ~/.cli-tokens/hivegrid-prod-app-secrets. Demo-Seed AUS, WEB_NOINDEX raus (Prod indexierbar).
  • Postgres-Image korrigiert: postgres:16-alpine → pgvector/pgvector:pg16 (Migration 04_enable_pgvector brauchte die Extension). Migration im laufenden web-Container ausgeführt → alle Migrationen angewandt (inkl. P2 UserSession+TrustedDevice).
  • DNS (Cloudflare): app./admin. → web-VM, api. → api-VM. SSL-Mode strict. app/api proxied (Edge-TLS/WAF), admin dns-only (Härtung folgt), captcha dns-only bis Cert.
  • Endabnahme grün: app.hivegrid.de + api.hivegrid.de /healthz + /readyz = 200 (DB-backed), /login rendert „Anmelden | HiveGrid", kein X-Robots-Tag (indexierbar). Alle 7 Container healthy.

Symptom: SSO-Login loopte (Audit zeigte wiederholt admin.login.sso.success, aber Folge-Request nicht eingeloggt). Ursache: resolveCookieDomain(APP_BASE_DOMAIN) band das Session-Cookie an app.hivegrid.de → galt nicht für admin.hivegrid.de (getrennte Hosts). Auf Staging unsichtbar, weil dort app+admin denselben Host teilen. Fix: APP_BASE_DOMAIN=hivegrid.de (Parent-Domain) auf allen Apps → Cookie-Domain hivegrid.de gilt für app./admin./api.; abgeleitete Hosts bleiben korrekt (getDerivedHosts → app./admin./api.hivegrid.de). Verifiziert: Set-Cookie: Domain=hivegrid.de. Lesson: Multi-Host-Prod braucht APP_BASE_DOMAIN = Registrable-Domain, nicht den app-Host.

Härtung/Abschluss-Runde — 2026-06-19 (nach SSO-Bestätigung)

  • ✅ Admin-Fallback aus: ADMIN_FALLBACK_LOGIN_ENABLED=false → SSO (Authentik) ist einziger Admin-Pfad.
  • ✅ Sentry aktiviert: SENTRY_DSN_WEB/API/WORKER aus zentraler Instanz gesetzt (environment=production), alle 7 Apps redeployed + healthy. (Waren vorher leer → Prod lief ohne Error-Tracking.)
  • ✅ mCaptcha komplett scharf: Root-Cause des fehlenden LE-Certs war service ... port is missing (Traefik kannte Ziel-Port nicht — Compose-Service ohne Port-Deklaration). Fix: Compose mit korrekter Coolify-Magic - SERVICE_FQDN_MCAPTCHA_7000=captcha.hivegrid.de + expose: 7000 → Host+Port-Label generiert, LE-Cert geholt (captcha.hivegrid.de HTTP 302, gültiges Cert). Account hivegrid + Captcha-Widget via mCaptcha-API angelegt → MCAPTCHA_SITEKEY_LOGIN + MCAPTCHA_ACCOUNT_SECRET + MCAPTCHA_INSTANCE_URL auf web gesetzt + redeployed. Creds in ~/.cli-tokens/hivegrid-prod-services. CAPTCHA_FAIL_OPEN=true bewusst belassen (captcha aktiv, aber mCaptcha-Ausfall sperrt nicht den Login).

CI-Promote verdrahtet + getestet — 2026-06-19

  • Gitea-Repo-Variablen (Production-Slot): COOLIFY_API_URL, COOLIFY_PROJECT_UUID, COOLIFY_ENVIRONMENT_NAME/UUID, COOLIFY_RESOURCE_UUIDS (7 Apps), COOLIFY_PROVISION_WORKER_GROUPS=false + Secret COOLIFY_TOKEN. (Multi-Server ok: Resource-Auflösung ist server-agnostisch per Image-Name+Queue-Group; Auto-Provisioning aus, da Apps existieren.)
  • App-Tags auf main umgestellt + :main-Alias in Registry erstellt (= aktueller SHA). Promote aliased Staging-Image → :main → deployt.
  • Worker-Apps korrekt konfiguriert (vom CI-Drift-Check erzwungen, macht Prod robuster): Health-Ports 9101–9105 + /readyz-Health-Checks; Worker-ENV (WORKER_ENABLED/HEALTH_PORT/ADMIN_PORT/CONCURRENCY je Gruppe; general: OUTBOX_DISPATCHER_/WORKFLOW_TRIGGER_OUTBOX_; mail: TRANSACTIONAL_MAIL_OUTBOX_REQUIRED/MARKETING_SEQUENCES_ENABLED).
  • Test-Promote (runs 8436–8438): iterativ alle Drifts gefixt; 8438 deployt Prod erfolgreich (production worker groups + web-health + dependent deploys grün; Prod app/api /readyz 200, alle 7 Container healthy, migrate status = up to date).
  • ⚠ Ein Step bleibt rot: „Verify production migration status after web deploy" läuft auf dem CI-Runner und braucht DB-Zugang — die Prod-DB liegt aber im privaten Netz des separaten Prod-Accounts (10.0.0.6), für den Runner strukturell unerreichbar. Kein Prod-Problem (Migration lief als Pre-Deploy + migrate status im Container = up to date). Kein Prod-Problem (Migration lief als Pre-Deploy + migrate status im Container = up to date).
  • Container-Verify-Versuch (revertiert b833c82e): Step auf SSH-Jump (Runner→control→web-Container) umgeschrieben → scheiterte am Preflight-Wiring-Guard scripts/verify-gitea-deploy-workflow.mjs (erzwingt exakte CI-Runner-Step-Struktur + verbietet secret-backed env in echo/SSH). Der Guard läuft im Preflight bei jedem Push → die Änderung machte die geteilte Pipeline rot (auch Staging). Sofort revertiert, Preflight wieder grün. Konsequenz: Jede Änderung am Step erfordert, zwei bewusste CI-Guards (Struktur + Secret-Leak) + Tests umzuschreiben → invasiv, mit Owner abzustimmen. Optionen: (A) Guard so anpassen, dass der Step bei privater DB nicht erzwungen wird; (B) Container-SSH-Verify voll umsetzen mit Host-Key-Pinning (Security-Review: kein StrictHostKeyChecking=no in committetem CI-Code) + Guards/Tests; (C) so lassen — Promote deployt erfolgreich, nur der redundante Schluss-Check ist bei Prod rot.

CI-Promote KOMPLETT GRÜN — 2026-06-20 (Run 8453)

Der erste echte Prod-Promote deckte drei latente Bugs im nie-gelaufenen Production-Pfad der geteilten deploy.yml auf (Prod gab es vorher nicht). Alle gefixt, lokal/manuell verifiziert, mit committeten Zwischenständen:

  1. migration-status-verify (war rot): CI-Runner erreicht die private Prod-DB nicht (separater Account, AD-24). Fix: DB_MIGRATE_STATUS_ALLOW_UNREACHABLE=true (eingebauter Script-Mechanismus, erkennt P1001 → skip) + DATABASE_URL-Secret. Struktur unverändert → Wiring-Guard + Tests grün.
  2. production-dependent-health worker-check (war rot): Step nutzte remote_ssh, definierte es aber nie (Copy-Paste-Lücke ggü. Staging) → „worker container not found". Fix: SSH-Setup + remote_ssh mit ProxyCommand (nicht ProxyJump — Key-Passthrough) über control-Bastion zur worker-VM (Multi-Server), gepinnte Host-Keys, Secret via Zwischen-var (umgeht Secret-Guard). Lokal verifiziert: Container gefunden + health-probe ready.
  3. web-health-revision-mismatch (war rot): /healthz über app.hivegrid.de lieferte eine 40 min alte Cloudflare-gecachte revision (cf-cache-status: HIT) statt der frisch deployten. Fix: CF-Cache-Rule /healthz+/readyz → bypass (Health-Endpoints nie cachen) + Purge. Kein deploy.yml-Eingriff.
  • Gitea-Config: Prod-Coolify-Variablen + Secrets (COOLIFY_TOKEN, DATABASE_URL, COOLIFY_PROD_SSH_KEY/KNOWN_HOSTS, Host/Worker-IP-vars). Apps auf Tag main, :main-Alias in Registry.
  • Ergebnis: Promote 8453 komplett grün (web-health + migration + dependent-health alle success). Prod läuft auf 94959b31, app/api /readyz = ready. Künftige Prod-Deploys: deploy.yml workflow_dispatch action=promote, staged_sha=<:staging-SHA>, backup_confirmed=true.

Härtungs-/Service-Runde 2 — 2026-06-20

  • ✅ mCaptcha scharf + proxied: captcha.hivegrid.de jetzt Cloudflare-proxied; Sitekey + Account-Secret bereits gesetzt (CAPTCHA_FAIL_OPEN=true als Safety belassen).
  • ✅ PgBouncer (alle 7 Apps): Coolify-Compose-Service hivegrid-prod-pgbouncer auf db-VM (edoburu/pgbouncer:v1.23.1-p3, transaction-pool, scram, 6432→5432). Apps: DATABASE_URL→10.0.0.6:6432?pgbouncer=true&connection_limit=10, DIRECT_URL→10.0.0.6:5432. Kanarienvogel web /readyz=ready verifiziert, dann api+5 Worker nachgezogen — alle healthy. Creds in ~/.cli-tokens/hivegrid-prod-services.
  • ✅ Coolify-Port 8000 zu (ohne Tunnel): Coolify hinter den schon laufenden Traefik (coolify-proxy) gehängt via Dynamic-Config-Datei /data/coolify/proxy/dynamic/coolify-ui.yaml (Host(coolify.hivegrid.de)→http://coolify:8080, certresolver letsencrypt) — file-watch, kein coolify-Restart, kein Selbst-Aussperr-Risiko. LE-Cert geholt, https://coolify.hivegrid.de/api/v1=200. Firewall fw-app: 8000 entfernt (jetzt 22/80/443). CI COOLIFY_API_URL→https://coolify.hivegrid.de/api/v1. Verifiziert: :8000 dicht, https-API 200, SSH+app/api weiter ok. (CF-Tunnel wurde erwogen, vom Owner abgelehnt → Traefik-Weg.)
  • ✅ Sentry gelöst (DSN-Host war falsch): Die alte DSN nutzte sentry.boiler.saas-ctrl.com (→Staging-VM, 503) — toter Host, richtiger Key/Projekt. Über node1-SSH zum Dedi (176.9.111.179, self-hosted Sentry) die Projekte/Keys aus der Sentry-Postgres geholt: org sentry, Projekte web=4/api=2/worker=3. Korrekte DSNs (https://<key>@sentry.saas-ctrl.com/<id>) gesetzt auf web/api/worker + SENTRY_ENVIRONMENT=production. Verifiziert: Test-Envelope von der web-VM → HTTP 200; Container hat …@sentry.saas-ctrl.com/4; alle 7 healthy. Creds in ~/.cli-tokens/hivegrid-prod-services. Hinweis: nutzt die generischen boilerplate-Projekte (web/api/worker, getrennt per environment-Tag) — dedizierte hivegrid-*-Sentry-Projekte wären Estate-Standard-Verfeinerung (Folgeschritt).

Bewusst NICHT blind ausgeführt (echte Aufgaben, kein Einzeiler — Risiko/Begründung):

  • Registry-Pull-Bot: scoped Bot-PAT statt gitea-admin (Least-Privilege; aktuell funktional).
  • CF-Tunnel-Rest: der bei Coolify-Port erwogene CF-Tunnel wurde verworfen; ein leerer Tunnel-Eintrag hivegrid-prod-coolify ohne Connector kann in Cloudflare gelöscht werden (API-Delete gab 400, harmlos da ohne cloudflared inaktiv).

Offene Folgeschritte (nicht-blockierend)

  • mCaptcha: LE-Cert für captcha.hivegrid.de verifizieren, dann proxied; Account+Sitekey anlegen → MCAPTCHA_* ins App-Env (bis dahin CAPTCHA_FAIL_OPEN=true).
  • Sentry: Events unter environment=production verifizieren (DSN aus Staging übernommen).
  • Härtung: SSH auf Bastion/Admin-IP einschränken · admin.hivegrid.de per Cloudflare-Access/IP schützen · scoped Registry-Bot statt gitea-admin · Coolify-FQDN auf https/443 + Port 8000 schließen.
  • Optimierung: PgBouncer vor Postgres · Coolify Pre-Deploy-Migrate-Mechanik fürs CI-Promote klären (lief manuell, Coolify-pre-deploy griff nicht).
  • CI-Promote: deploy.yml/Gitea-Variablen auf das Prod-Coolify verdrahten (für künftige promote-Deploys ohne manuelle Schritte).

Härtung (Schritt 7) — teilweise erledigt 2026-06-19

  • ✅ SSH-Angriffsfläche reduziert: neue FW hivegrid-prod-fw-web (nur 80/443) auf web/api/worker; public SSH dort dicht, Zugang nur über control (Bastion) + privates Netz. control behält 22/80/443/8000. db: kein public Port (fw-db rules geleert), nur privat. Verifiziert: control-SSH ok, web-SSH public dicht, web via control ok, app/api öffentlich ok, Coolify-Deploys (privates Netz) laufen weiter.
  • ✅ Demo-Seed AUS, WEB_NOINDEX raus (prod indexierbar).

Offene Härtung/Folgeschritte (priorisiert):

  1. mCaptcha public-FQDN: Traefik liefert noch Default-Cert für captcha.hivegrid.de (Compose-Service-Domain-Labels nicht generiert; fqdn per tinker reichte nicht). Container laufen intern. Domain am besten über Coolify-UI am mcaptcha-Service setzen → LE-Cert → proxied. Dann Account+Sitekey in der mCaptcha-Admin-UI anlegen → MCAPTCHA_SITEKEY_*/ACCOUNT_SECRET ins App-Env, CAPTCHA_FAIL_OPEN=false. Bis dahin: fail-open (kein Captcha-Zwang).
  2. Admin-SSO (Authentik) — eingerichtet 2026-06-19: OIDC-Provider + App hivegrid-admin-prod in auth.tool-ctrl.de (Provider pk 31, explicit-consent-Flow, PKCE, signing-key gesetzt). Scopes openid/email/profile + eigenes groups-Mapping (HiveGrid groups) → liefert den groups-Claim für die Admin-Autorisierung (toolctrl-admin). AUTHENTIK_ADMIN_CLIENT_ID/SECRET, AUTHENTIK_ISSUER_URL, AUTHENTIK_ADMIN_REDIRECT_URI, AUTHENTIK_ADMIN_POST_LOGOUT_REDIRECT_URI auf web gesetzt + redeployed (Creds in ~/.cli-tokens/hivegrid-prod-authentik). Verifiziert: /auth/admin/login → 302 zu Authentik mit korrekter redirect_uri/PKCE; SSO-Button sichtbar; OIDC-Discovery grün; raffael/akeshan/dejan/arthur in toolctrl-admin. Noch zu tun (interaktiv/Browser): einmal per SSO einloggen + prüfen, ob der erste Login einen AdminUser JIT-provisioniert (sonst AdminUser in Prod-DB seeden). Erst nach erfolgreichem SSO-Login ADMIN_FALLBACK_LOGIN_ENABLED=false setzen (aktuell true als Backup, allowlist raffael/akeshan). admin.hivegrid.de dns-only; optional Cloudflare-Access davor.
  3. Coolify-Port 8000 schließen, sobald Coolify hinter https://coolify.hivegrid.de (443) läuft (Port aktuell offen, key+token-geschützt; für laufende API-Schritte noch gebraucht).
  4. Registry: scoped Bot-PAT statt gitea-admin für den Image-Pull-Login.
  5. CI-Promote: deploy.yml/Gitea-Variablen aufs Prod-Coolify verdrahten (Deploys liefen manuell; Coolify-Pre-Deploy-Migrate griff nicht → Migration manuell im Container).
  6. Sentry Events unter environment=production verifizieren · PgBouncer (Pooling) ergänzen.

Gesamt-Status (Stand 2026-06-20) — ✅ VOLLSTÄNDIG LIVE + GEHÄRTET

app./api./admin.hivegrid.de öffentlich (HTTPS), DB-backed (Postgres 16/pgvector + PgBouncer + Redis), P2-Set + 5 Worker-Gruppen, Revision 94959b31.

Alles erledigt:

  • ✅ CI-Promote komplett grün (Run 8453) — workflow_dispatch action=promote, staged_sha=<:staging-SHA>, backup_confirmed=true. Drei latente Multi-Server-Promote-Bugs gefixt (migration-status via DB_MIGRATE_STATUS_ALLOW_UNREACHABLE, dependent-health via ProxyCommand-SSH, web-health via CF-Cache-Bypass).
  • ✅ SSH-gehärtet — control = Bastion, web/api/worker/db public-SSH dicht.
  • ✅ Admin-SSO über Authentik (auth.tool-ctrl.de, OIDC, groups-Claim → toolctrl-admin).
  • ✅ mCaptcha scharf + proxied (captcha.hivegrid.de).
  • ✅ PgBouncer vor Postgres, alle 7 Apps über 6432.
  • ✅ Coolify-Port zu — nur noch https://coolify.hivegrid.de (Traefik+LE), :8000 firewalled.
  • ✅ Sentry funktional — sentry.saas-ctrl.com (alte boiler.-DSN war tot/503).

Estate-Doku nachgezogen: infra/ README (Host-Inventar + DNS) + ADR-016 (dedizierter Account + eigenes Coolify) + LESSONS #13–#17 + onboard-pro-saas-Runbook.

Rest-Folgeschritte (Least-Privilege, kein Druck): scoped Registry-Bot statt gitea-admin · dedizierte hivegrid-*-Sentry-Projekte (statt geteilter boilerplate-Projekte) · Sentry-DSN-Host-Fix auch auf Staging nachziehen (dort steckt dieselbe tote boiler.-DSN) · leeren CF-Tunnel hivegrid-prod-coolify löschen.