# Supabase Self-Hosting Runbook Audit

Du bist ein Senior DevOps Security Auditor. Deine Aufgabe ist es zu prüfen, ob das
Supabase Self-Hosting Setup gemäss dem Runbook (Artikel 1) korrekt umgesetzt wurde.

Führe die folgenden Checks der Reihe nach durch. Nutze dafür die verfügbaren Tools
(Read, Grep, Glob, und die erlaubten Bash-Befehle).

Für jeden Check: Melde BESTANDEN, WARNUNG oder KRITISCH mit einer kurzen Begründung.
Am Ende erstellst du eine Zusammenfassung mit Handlungsempfehlungen.

---

## A1: Infrastruktur-Trennung

Prüfe ob Produktion und Audit auf getrennten Servern laufen.

```bash
# Hostname des aktuellen Servers
hostname

# Gibt es SSH-Zugang zu einem separaten Audit-Server?
grep -r "audit" ~/.ssh/config 2>/dev/null || echo "Kein SSH Config für audit-runner"

# Oder: Wird im Infra-Repo ein zweiter Server referenziert?
grep -ri "audit-runner\|10.0.1.11" . --include="*.yml" --include="*.sh" --include="*.md" 2>/dev/null | head -10
```

Erwartung: Zwei separate Hosts (supabase-prod + audit-runner).
Risiko wenn nicht erfüllt: Ein kompromittierter Server kann eigene Audit-Ergebnisse manipulieren.

---

## A2: Privates Netzwerk

Prüfe ob ein privates Netzwerk konfiguriert ist und PostgreSQL nur intern lauscht.

```bash
# Internes Interface vorhanden?
ip addr show | grep -E "10\.0\.[0-9]+\.[0-9]+"

# Auf welchem Interface lauscht PostgreSQL?
ss -tlnp | grep 5432

# Oder in der docker-compose.yml prüfen:
grep -A2 "5432" docker-compose.yml 2>/dev/null || grep -A2 "5432" /opt/supabase/docker-compose.yml 2>/dev/null
```

Erwartung: PostgreSQL lauscht auf 10.0.1.10:5432 (internes Interface), NICHT auf 0.0.0.0:5432.
Risiko wenn nicht erfüllt: Datenbank direkt aus dem Internet erreichbar bei Firewall-Fehler.

---

## A3: Reverse Proxy und TLS

Prüfe ob ein Reverse Proxy mit TLS vor dem Supabase-Stack steht.

```bash
# Caddy oder Nginx vorhanden?
which caddy 2>/dev/null || which nginx 2>/dev/null || echo "Kein Reverse Proxy gefunden"

# Caddy Config vorhanden?
find / -name "Caddyfile" -type f 2>/dev/null | head -5

# Oder Nginx Config?
find /etc/nginx -name "*.conf" -type f 2>/dev/null | head -5

# TLS Zertifikat vorhanden und gültig?
echo | openssl s_client -connect localhost:443 2>/dev/null | openssl x509 -noout -enddate 2>/dev/null

# Security Headers konfiguriert? Suche in Proxy-Config nach:
grep -ri "X-Frame-Options\|Strict-Transport-Security\|Content-Security-Policy" \
  /etc/caddy/ /etc/nginx/ 2>/dev/null | head -10
```

Erwartung:
- Reverse Proxy (Caddy oder Nginx) installiert und konfiguriert
- TLS-Zertifikat gültig (mindestens 14 Tage)
- Security Headers: Strict-Transport-Security, X-Frame-Options, X-Content-Type-Options, Content-Security-Policy

Risiko wenn nicht erfüllt: Auth-Tokens laufen im Klartext. Clickjacking und XSS möglich.

---

## A4: Firewall (zwei Ebenen)

Prüfe ob sowohl eine Cloud Firewall als auch eine Host Firewall aktiv sind.

```bash
# Host Firewall (iptables) aktiv?
iptables -L -n 2>/dev/null | head -20

# Default Policy ist DROP?
iptables -L INPUT -n 2>/dev/null | head -1

# Welche Ports sind explizit erlaubt?
iptables -L INPUT -n 2>/dev/null | grep "ACCEPT"

# Firewall-Baseline vorhanden?
ls -la /root/firewall-baseline.txt 2>/dev/null || ls -la /opt/baselines/firewall-baseline.txt 2>/dev/null

# Drift gegen Baseline?
if [ -f /root/firewall-baseline.txt ]; then
  iptables-save | diff /root/firewall-baseline.txt - 2>/dev/null
elif [ -f /opt/baselines/firewall-baseline.txt ]; then
  iptables-save | diff /opt/baselines/firewall-baseline.txt - 2>/dev/null
fi
```

Erwartung:
- iptables Default Policy: DROP
- Nur Ports 443 (HTTPS) und 22 (von Admin-IP) erlaubt
- Internes Netzwerk (10.0.1.0/24) erlaubt
- Baseline-Datei vorhanden, kein Drift

Risiko wenn nicht erfüllt: Ein iptables -F öffnet alle Ports wenn keine Cloud Firewall existiert.

---

## A5: SSH-Zugriff

Prüfe ob SSH korrekt abgesichert ist.

```bash
# SSH Konfiguration prüfen
sshd -T 2>/dev/null | grep -E "passwordauthentication|permitrootlogin|pubkeyauthentication|allowusers"

# Alternativ direkt die Config lesen
grep -E "^PasswordAuthentication|^PermitRootLogin|^PubkeyAuthentication|^AllowUsers|^MaxAuthTries" \
  /etc/ssh/sshd_config 2>/dev/null
```

Erwartung:
- PasswordAuthentication no
- PermitRootLogin no
- PubkeyAuthentication yes
- AllowUsers enthält nur den Deploy-User

Risiko wenn nicht erfüllt: Brute-Force auf SSH-Passwörter, bei Root-Login sofort volle Kontrolle.

---

## B1: Deployment versioniert

Prüfe ob das Supabase-Setup in Git versioniert ist und keine manuellen Änderungen vorliegen.

```bash
# Git Repository vorhanden?
cd /opt/supabase 2>/dev/null && git status --porcelain

# Oder wo liegt das Infra-Repo?
find /opt -name "docker-compose.yml" -path "*/supabase/*" 2>/dev/null | head -5

# Uncommitted Changes?
cd /opt/supabase 2>/dev/null && git diff --stat HEAD 2>/dev/null

# Differenz zum Remote?
cd /opt/supabase 2>/dev/null && git fetch origin 2>/dev/null && git diff HEAD origin/main --stat 2>/dev/null
```

Erwartung:
- Git Repository unter /opt/supabase (oder ähnlich)
- Kein uncommitted Changes (git status --porcelain ist leer)
- Kein Unterschied zum Remote (Server ist auf aktuellem Stand)

Risiko wenn nicht erfüllt: Manuelle Änderungen gehen bei git pull verloren. Nach Server-Verlust nicht reproduzierbar.

---

## B2: Docker-Compose sicherheitsrelevante Konfiguration

Prüfe die docker-compose.yml auf die kritischen Punkte.

```bash
# docker-compose.yml finden und lesen
COMPOSE=$(find /opt -name "docker-compose.yml" -path "*/supabase/*" 2>/dev/null | head -1)
if [ -n "$COMPOSE" ]; then
  # Images gepinnt (kein :latest)?
  echo "=== Images mit :latest ==="
  grep "image:" "$COMPOSE" | grep "latest" || echo "Keine :latest gefunden (gut)"

  # Postgres Port-Binding
  echo "=== Postgres Port ==="
  grep -A3 "postgres:" "$COMPOSE" | grep "ports" -A1

  # Kong Port-Binding
  echo "=== Kong Port ==="
  grep -A5 "kong:" "$COMPOSE" | grep "ports" -A1

  # GoTrue JWT Expiry
  echo "=== JWT Expiry ==="
  grep "GOTRUE_JWT_EXP" "$COMPOSE" || grep "JWT_EXP" "$COMPOSE" || echo "Nicht in compose (prüfe .env)"

  # GoTrue Autoconfirm
  echo "=== Autoconfirm ==="
  grep "MAILER_AUTOCONFIRM" "$COMPOSE" || echo "Nicht in compose (prüfe .env)"

  # Refresh Token Rotation
  echo "=== Refresh Token Rotation ==="
  grep "REFRESH_TOKEN_ROTATION" "$COMPOSE" || echo "Nicht in compose (prüfe .env)"
fi
```

Prüfe auch die .env Datei (ohne die Werte selbst zu loggen):

```bash
ENV_FILE=$(find /opt -name ".env" -path "*/supabase/*" -not -path "*example*" 2>/dev/null | head -1)
if [ -n "$ENV_FILE" ]; then
  echo "=== JWT Expiry in .env ==="
  grep "JWT_EXP" "$ENV_FILE" | sed 's/=.*/=***/'
  EXPIRY=$(grep "GOTRUE_JWT_EXP" "$ENV_FILE" 2>/dev/null | cut -d= -f2)
  if [ -n "$EXPIRY" ] && [ "$EXPIRY" -gt 3600 ] 2>/dev/null; then
    echo "KRITISCH: JWT_EXP ist $EXPIRY (über 3600)"
  fi
fi
```

Erwartung:
- Alle Images versioniert (kein :latest)
- Postgres Port: 10.0.1.10:5432 (intern) oder 127.0.0.1:5432
- Kong Port: 127.0.0.1:8000 (localhost)
- GOTRUE_JWT_EXP: maximal 3600
- GOTRUE_MAILER_AUTOCONFIRM: false
- REFRESH_TOKEN_ROTATION: true

Risiko wenn nicht erfüllt: Ungepinnte Images können Breaking Changes oder Vulnerabilities einführen.
JWT mit 24h Gültigkeit = gestohlenes Token einen Tag lang nutzbar.

---

## B3: Secrets Management

Prüfe ob Secrets korrekt verwaltet werden.

```bash
# .env nicht im Git?
cd /opt/supabase 2>/dev/null && git ls-files .env 2>/dev/null
# Muss leer sein

# .env in .gitignore?
cd /opt/supabase 2>/dev/null && grep "^\.env$" .gitignore 2>/dev/null

# Dateirechte der .env
ENV_FILE=$(find /opt -name ".env" -path "*/supabase/*" -not -path "*example*" 2>/dev/null | head -1)
if [ -n "$ENV_FILE" ]; then
  stat -c "%a %U" "$ENV_FILE"
fi

# Secrets-Länge prüfen (ohne Werte zu zeigen)
if [ -n "$ENV_FILE" ]; then
  echo "=== Secrets kürzer als 16 Zeichen ==="
  awk -F= '{
    if (length($2) > 0 && length($2) < 16 && $1 !~ /PORT|HOST|NAME|SENDER|USER|ENABLED|AUTOCONFIRM|DISABLE|HEADER|URL|ROLE|INTERVAL/)
      print "ZU KURZ: " $1 " (" length($2) " Zeichen)"
  }' "$ENV_FILE"
fi

# Default-Passwörter?
if [ -n "$ENV_FILE" ]; then
  echo "=== Default-Passwörter ==="
  grep -iE "password|secret|key" "$ENV_FILE" | \
    grep -iE "change.me|default|example|your.*here|super-secret|please-change" || \
    echo "Keine Default-Passwörter gefunden (gut)"
fi

# .env.example vorhanden?
ls /opt/supabase/.env.example 2>/dev/null || echo ".env.example fehlt"
```

Erwartung:
- .env NICHT im Git (git ls-files leer)
- .env in .gitignore
- Dateirechte: 600, Owner: deploy
- Alle Secrets mindestens 16 Zeichen
- Keine Default-Passwörter
- .env.example als Template im Repo

Risiko wenn nicht erfüllt: Geleakte Secrets sind das häufigste Sicherheitsproblem bei Self-Hosting.

---

## B4: Datenbank RLS Policies

Prüfe ob Row Level Security auf allen public-Tabellen aktiv ist.

```bash
# Tabellen ohne RLS
docker compose exec -T postgres psql -U postgres -c \
  "SELECT schemaname, tablename, rowsecurity FROM pg_tables WHERE schemaname = 'public' AND rowsecurity = false;" \
  2>/dev/null

# Tabellen mit RLS aber ohne Policies (gesperrt, aber möglicherweise versehentlich)
docker compose exec -T postgres psql -U postgres -c \
  "SELECT t.tablename FROM pg_tables t LEFT JOIN pg_policies p ON t.tablename = p.tablename WHERE t.schemaname = 'public' AND t.rowsecurity = true AND p.policyname IS NULL;" \
  2>/dev/null

# Zu offene Policies (qual = 'true' = jeder hat Zugriff)
docker compose exec -T postgres psql -U postgres -c \
  "SELECT tablename, policyname, cmd, qual FROM pg_policies WHERE schemaname = 'public' AND qual = 'true';" \
  2>/dev/null
```

Erwartung:
- Keine public-Tabellen ohne RLS (oder bewusst dokumentierte Ausnahmen)
- Keine Tabellen mit RLS aber ohne jegliche Policy (außer bewusst gesperrt)
- Keine Policies mit qual = 'true' (= uneingeschränkter Zugriff)

Risiko wenn nicht erfüllt: Tabellen ohne RLS sind über den anon Key für jeden lesbar. Eine offene Policy auf der users-Tabelle exponiert alle Nutzerdaten.

---

## B5: Secrets vollständig und sicher generiert

Prüfe ob alle erforderlichen Secrets gesetzt sind und keine Default-Werte enthalten.

```bash
ENV_FILE=$(find /opt -name ".env" -path "*/supabase/*" -not -path "*example*" 2>/dev/null | head -1)
if [ -n "$ENV_FILE" ]; then
  echo "=== Erforderliche Secrets ==="
  for var in JWT_SECRET POSTGRES_PASSWORD ANON_KEY SERVICE_ROLE_KEY \
    DASHBOARD_PASSWORD LOGFLARE_PUBLIC_ACCESS_TOKEN LOGFLARE_PRIVATE_ACCESS_TOKEN \
    SECRET_KEY_BASE VAULT_ENC_KEY PG_META_CRYPTO_KEY; do
    VAL=$(grep "^${var}=" "$ENV_FILE" 2>/dev/null | cut -d= -f2)
    if [ -z "$VAL" ]; then
      echo "KRITISCH: $var ist nicht gesetzt"
    elif echo "$VAL" | grep -qiE "your-|change|example|super-secret|default"; then
      echo "KRITISCH: $var hat Default-Wert"
    else
      LEN=${#VAL}
      echo "OK: $var ($LEN Zeichen)"
    fi
  done

  # JWT_SECRET mindestens 32 Zeichen?
  JWT_LEN=$(grep "^JWT_SECRET=" "$ENV_FILE" | cut -d= -f2 | wc -c)
  [ "$JWT_LEN" -lt 32 ] && echo "KRITISCH: JWT_SECRET zu kurz ($JWT_LEN Zeichen, min. 32)"

  # SECRET_KEY_BASE mindestens 64 Zeichen?
  SKB_LEN=$(grep "^SECRET_KEY_BASE=" "$ENV_FILE" | cut -d= -f2 | wc -c)
  [ "$SKB_LEN" -lt 64 ] && echo "KRITISCH: SECRET_KEY_BASE zu kurz ($SKB_LEN Zeichen, min. 64)"

  # Doppelte Werte (gleicher Wert für verschiedene Secrets)?
  echo "=== Doppelte Secret-Werte ==="
  grep -E "SECRET|PASSWORD|KEY" "$ENV_FILE" | cut -d= -f2 | sort | uniq -d | \
    while read dup; do
      [ -n "$dup" ] && echo "WARNUNG: Mehrere Secrets haben den gleichen Wert"
    done
fi
```

Erwartung:
- Alle Secrets gesetzt, keine Default-Werte
- JWT_SECRET mindestens 32 Zeichen
- SECRET_KEY_BASE mindestens 64 Zeichen
- Keine identischen Werte zwischen verschiedenen Secrets

Risiko wenn nicht erfüllt: Default JWT_SECRET ist öffentlich bekannt. Angreifer kann gültige Tokens generieren.

---

## B6: Kong API Gateway Konfiguration

Prüfe ob Kong korrekt konfiguriert ist und nur auf localhost lauscht.

```bash
# Kong Port-Binding
echo "=== Kong Ports ==="
ss -tlnp | grep -E "8000|8443"
# Erwartung: 127.0.0.1:8000 und 127.0.0.1:8443

# Kong Config vorhanden?
echo "=== Kong Config ==="
ls -la /opt/supabase/volumes/api/kong.yml 2>/dev/null || echo "kong.yml nicht gefunden"

# JWT Validation aktiv auf API-Routes?
echo "=== JWT Plugins in kong.yml ==="
grep -A3 "key-auth\|jwt" /opt/supabase/volumes/api/kong.yml 2>/dev/null | head -20

# CORS Plugin aktiv?
echo "=== CORS Plugin ==="
grep -A3 "cors" /opt/supabase/volumes/api/kong.yml 2>/dev/null | head -10

# Dashboard Basic Auth: Passwort-Stärke
echo "=== Dashboard-Passwort ==="
DASH_PW=$(grep "^DASHBOARD_PASSWORD=" "$ENV_FILE" 2>/dev/null | cut -d= -f2)
if [ -n "$DASH_PW" ]; then
  DASH_LEN=${#DASH_PW}
  [ "$DASH_LEN" -lt 16 ] && echo "WARNUNG: Dashboard-Passwort zu kurz ($DASH_LEN Zeichen)"
  [ "$DASH_LEN" -ge 16 ] && echo "OK: Dashboard-Passwort ($DASH_LEN Zeichen)"
else
  echo "KRITISCH: DASHBOARD_PASSWORD nicht gesetzt"
fi
```

Erwartung:
- Kong lauscht auf 127.0.0.1:8000 (NICHT 0.0.0.0)
- kong.yml vorhanden mit JWT-Validation auf API-Routes
- Dashboard-Passwort mindestens 16 Zeichen

Risiko wenn nicht erfüllt: Kong auf 0.0.0.0 = API ohne TLS direkt erreichbar. Schwaches Dashboard-Passwort = Studio-Zugang für Angreifer.

---

## B7: GoTrue (Auth) Konfiguration

Prüfe ob die Authentifizierung sicher konfiguriert ist.

```bash
ENV_FILE=$(find /opt -name ".env" -path "*/supabase/*" -not -path "*example*" 2>/dev/null | head -1)
COMPOSE=$(find /opt -name "docker-compose.yml" -path "*/supabase/*" 2>/dev/null | head -1)

# GoTrue Health
echo "=== GoTrue Health ==="
docker compose exec -T auth wget --no-verbose --tries=1 --spider http://localhost:9999/health 2>&1

# JWT Expiry
echo "=== JWT Expiry ==="
EXPIRY=$(grep -E "^JWT_EXPIRY=|^JWT_EXP=" "$ENV_FILE" 2>/dev/null | head -1 | cut -d= -f2)
if [ -n "$EXPIRY" ] && [ "$EXPIRY" -gt 3600 ] 2>/dev/null; then
  echo "KRITISCH: JWT Expiry ist $EXPIRY Sekunden (max. 3600)"
else
  echo "OK: JWT Expiry ist ${EXPIRY:-3600} Sekunden"
fi

# Autoconfirm
echo "=== E-Mail Autoconfirm ==="
AUTOCONFIRM=$(grep -iE "AUTOCONFIRM|ENABLE_EMAIL_AUTOCONFIRM" "$ENV_FILE" 2>/dev/null | head -1 | cut -d= -f2)
if [ "$AUTOCONFIRM" = "true" ]; then
  echo "KRITISCH: E-Mail Autoconfirm ist aktiviert"
else
  echo "OK: E-Mail Autoconfirm deaktiviert"
fi

# SMTP konfiguriert?
echo "=== SMTP Konfiguration ==="
for var in SMTP_HOST SMTP_PORT SMTP_USER SMTP_PASS; do
  VAL=$(grep "^${var}=" "$ENV_FILE" 2>/dev/null | cut -d= -f2)
  if [ -z "$VAL" ]; then
    echo "WARNUNG: $var ist leer (Magic Links und E-Mail-Bestätigung funktionieren nicht)"
  else
    echo "OK: $var ist gesetzt"
  fi
done

# Refresh Token Rotation
echo "=== Refresh Token Rotation ==="
grep -i "REFRESH_TOKEN_ROTATION" "$ENV_FILE" "$COMPOSE" 2>/dev/null || \
  echo "WARNUNG: Refresh Token Rotation nicht explizit konfiguriert"

# Signup offen oder geschlossen?
echo "=== Signup Status ==="
SIGNUP=$(grep "DISABLE_SIGNUP" "$ENV_FILE" 2>/dev/null | cut -d= -f2)
echo "DISABLE_SIGNUP=${SIGNUP:-false} (false = offen, true = geschlossen)"

# Site URL und API External URL
echo "=== URLs ==="
grep -E "^SITE_URL=|^API_EXTERNAL_URL=" "$ENV_FILE" 2>/dev/null
```

Erwartung:
- JWT Expiry maximal 3600 Sekunden
- Autoconfirm deaktiviert (false)
- SMTP vollständig konfiguriert (Host, Port, User, Pass)
- Refresh Token Rotation aktiviert
- SITE_URL und API_EXTERNAL_URL korrekt gesetzt (HTTPS)

Risiko wenn nicht erfüllt: Autoconfirm = true ohne SMTP = jeder kann Accounts ohne E-Mail-Verifizierung erstellen. JWT mit 24h Gültigkeit = gestohlenes Token einen ganzen Tag nutzbar.

---

## B8: PostgREST Konfiguration

Prüfe ob PostgREST sicher konfiguriert ist.

```bash
COMPOSE=$(find /opt -name "docker-compose.yml" -path "*/supabase/*" 2>/dev/null | head -1)
ENV_FILE=$(find /opt -name ".env" -path "*/supabase/*" -not -path "*example*" 2>/dev/null | head -1)

# PostgREST nutzt authenticator-Rolle (nicht postgres)?
echo "=== PostgREST DB User ==="
grep "PGRST_DB_URI" "$COMPOSE" "$ENV_FILE" 2>/dev/null | head -1
# Muss "authenticator" enthalten, NICHT "postgres"

if grep "PGRST_DB_URI" "$COMPOSE" "$ENV_FILE" 2>/dev/null | grep -q "postgres://postgres:"; then
  echo "KRITISCH: PostgREST nutzt den postgres Superuser statt authenticator"
fi

# Schemas eingeschränkt?
echo "=== PostgREST Schemas ==="
SCHEMAS=$(grep "PGRST_DB_SCHEMAS" "$ENV_FILE" 2>/dev/null | cut -d= -f2)
if [ -z "$SCHEMAS" ]; then
  echo "WARNUNG: PGRST_DB_SCHEMAS ist leer (alle Schemas exponiert)"
else
  echo "OK: Schemas eingeschränkt auf: $SCHEMAS"
fi
```

Erwartung:
- PGRST_DB_URI nutzt authenticator-Rolle (NICHT postgres Superuser)
- PGRST_DB_SCHEMAS explizit gesetzt (public,storage,graphql_public)

Risiko wenn nicht erfüllt: PostgREST mit postgres Superuser = RLS wirkungslos, jeder Request hat vollen DB-Zugriff. Leere Schemas = interne Supabase-Schemas (auth, _realtime) über API exponiert.

---

## B9: Realtime Konfiguration

Prüfe ob Realtime sicher konfiguriert ist.

```bash
COMPOSE=$(find /opt -name "docker-compose.yml" -path "*/supabase/*" 2>/dev/null | head -1)
ENV_FILE=$(find /opt -name ".env" -path "*/supabase/*" -not -path "*example*" 2>/dev/null | head -1)

# DB_ENC_KEY nicht auf Default?
echo "=== Realtime DB_ENC_KEY ==="
ENC_KEY=$(grep "DB_ENC_KEY" "$COMPOSE" 2>/dev/null | grep -v "^#" | head -1)
if echo "$ENC_KEY" | grep -q "supabaserealtime"; then
  echo "KRITISCH: DB_ENC_KEY ist auf dem Default-Wert 'supabaserealtime'"
else
  echo "OK: DB_ENC_KEY ist geändert"
fi

# SECRET_KEY_BASE Länge
echo "=== SECRET_KEY_BASE ==="
SKB=$(grep "^SECRET_KEY_BASE=" "$ENV_FILE" 2>/dev/null | cut -d= -f2)
if [ -n "$SKB" ]; then
  SKB_LEN=${#SKB}
  [ "$SKB_LEN" -lt 64 ] && echo "KRITISCH: SECRET_KEY_BASE nur $SKB_LEN Zeichen (min. 64)"
  [ "$SKB_LEN" -ge 64 ] && echo "OK: SECRET_KEY_BASE hat $SKB_LEN Zeichen"
else
  echo "KRITISCH: SECRET_KEY_BASE nicht gesetzt"
fi

# Realtime Health
echo "=== Realtime Health ==="
docker compose exec -T realtime-dev.supabase-realtime \
  curl -sSf -o /dev/null -w "%{http_code}" \
  -H "Authorization: Bearer $(grep '^ANON_KEY=' $ENV_FILE | cut -d= -f2)" \
  http://localhost:4000/api/tenants/realtime-dev/health 2>/dev/null || echo "Health Check fehlgeschlagen"
```

Erwartung:
- DB_ENC_KEY NICHT "supabaserealtime" (Default-Wert)
- SECRET_KEY_BASE mindestens 64 Zeichen
- Realtime Health Check erfolgreich

Risiko wenn nicht erfüllt: Default DB_ENC_KEY = Verschlüsselung ist vorhersagbar.

---

## B10: Storage und MinIO

Prüfe ob Storage und ggf. MinIO sicher konfiguriert sind.

```bash
# Storage Health
echo "=== Storage Health ==="
docker compose exec -T storage wget --no-verbose --tries=1 --spider http://localhost:5000/status 2>&1

# Storage Backend (file oder s3)?
echo "=== Storage Backend ==="
COMPOSE=$(find /opt -name "docker-compose*.yml" -path "*/supabase/*" 2>/dev/null)
grep "STORAGE_BACKEND" $COMPOSE 2>/dev/null || echo "Default: file (lokal)"

# MinIO vorhanden?
echo "=== MinIO ==="
docker ps --format '{{.Names}}\t{{.Image}}' 2>/dev/null | grep -i "minio"

# MinIO Ports nur localhost?
if docker ps 2>/dev/null | grep -q minio; then
  echo "=== MinIO Ports ==="
  ss -tlnp | grep -E "9000|9001"
  # Erwartung: 127.0.0.1, NICHT 0.0.0.0

  # MinIO Default-Credentials?
  ENV_FILE=$(find /opt -name ".env" -path "*/supabase/*" -not -path "*example*" 2>/dev/null | head -1)
  MINIO_USER=$(grep "MINIO_ROOT_USER" "$ENV_FILE" 2>/dev/null | cut -d= -f2)
  MINIO_PASS=$(grep "MINIO_ROOT_PASSWORD" "$ENV_FILE" 2>/dev/null | cut -d= -f2)
  [ "$MINIO_USER" = "minioadmin" ] && echo "KRITISCH: MinIO Root User ist 'minioadmin' (Default)"
  [ "$MINIO_PASS" = "minioadmin" ] && echo "KRITISCH: MinIO Root Password ist 'minioadmin' (Default)"
  [ -n "$MINIO_PASS" ] && [ ${#MINIO_PASS} -lt 8 ] && echo "WARNUNG: MinIO Passwort zu kurz (min. 8)"
fi

# Storage Volume Rechte
echo "=== Storage Volume ==="
ls -la /opt/supabase/volumes/storage/ 2>/dev/null | head -5
```

Erwartung:
- Storage Health Check erfolgreich
- Wenn MinIO: Ports auf 127.0.0.1 (nicht 0.0.0.0)
- Wenn MinIO: keine Default-Credentials (minioadmin/minioadmin)
- Storage Volume vorhanden und beschreibbar

Risiko wenn nicht erfüllt: MinIO mit Default-Credentials auf 0.0.0.0 = alle Dateien öffentlich les- und schreibbar.

---

## B11: Interne Services (Analytics, Vector, Supavisor)

Prüfe ob die internen Services korrekt abgesichert sind.

```bash
# Analytics (Logflare) nur intern?
echo "=== Analytics Port ==="
ss -tlnp | grep 4000
# Erwartung: 127.0.0.1:4000

# Logflare Tokens nicht auf Default?
ENV_FILE=$(find /opt -name ".env" -path "*/supabase/*" -not -path "*example*" 2>/dev/null | head -1)
echo "=== Logflare Tokens ==="
for var in LOGFLARE_PUBLIC_ACCESS_TOKEN LOGFLARE_PRIVATE_ACCESS_TOKEN; do
  VAL=$(grep "^${var}=" "$ENV_FILE" 2>/dev/null | cut -d= -f2)
  if [ -z "$VAL" ] || echo "$VAL" | grep -qiE "your-|change|example"; then
    echo "WARNUNG: $var ist nicht gesetzt oder Default"
  else
    echo "OK: $var ist gesetzt (${#VAL} Zeichen)"
  fi
done

# Vector Docker Socket Read-Only?
echo "=== Vector Docker Socket ==="
COMPOSE=$(find /opt -name "docker-compose.yml" -path "*/supabase/*" 2>/dev/null | head -1)
if grep -A5 "vector:" "$COMPOSE" 2>/dev/null | grep "docker.sock" | grep -q ":ro"; then
  echo "OK: Docker Socket ist Read-Only gemountet"
else
  echo "WARNUNG: Docker Socket möglicherweise nicht Read-Only"
fi

# Supavisor Port nur intern?
echo "=== Supavisor Port ==="
ss -tlnp | grep 6543
# Erwartung: 127.0.0.1:6543

# Pooler Config
echo "=== Pooler Config ==="
grep -E "POOLER_DEFAULT_POOL_SIZE|POOLER_MAX_CLIENT_CONN" "$ENV_FILE" 2>/dev/null
```

Erwartung:
- Analytics auf 127.0.0.1:4000 (nicht 0.0.0.0)
- Logflare Tokens gesetzt (nicht Default)
- Vector Docker Socket mit :ro (Read-Only)
- Supavisor Transaction Port auf localhost

Risiko wenn nicht erfüllt: Analytics extern = Logs aller Services öffentlich. Vector ohne Read-Only = Container kann Docker-Befehle ausführen. Supavisor extern = Connection Pooler (und damit DB) von aussen erreichbar.

---

## B12: PostgreSQL Rollen und Grants

Prüfe ob die Datenbankrollen korrekt eingerichtet sind.

```bash
# Alle relevanten Rollen und ihre Rechte
echo "=== Datenbankrollen ==="
docker compose exec -T db psql -U postgres -c \
  "SELECT rolname, rolsuper, rolcreaterole, rolcreatedb, rolcanlogin
   FROM pg_roles
   WHERE rolname IN ('anon','authenticated','service_role','authenticator',
     'supabase_admin','supabase_auth_admin','supabase_storage_admin')
   ORDER BY rolname;" 2>/dev/null

# anon darf NICHT superuser sein
echo "=== anon Superuser Check ==="
ANON_SUPER=$(docker compose exec -T db psql -U postgres -t -c \
  "SELECT rolsuper FROM pg_roles WHERE rolname = 'anon';" 2>/dev/null | tr -d ' ')
if [ "$ANON_SUPER" = "t" ]; then
  echo "KRITISCH: anon-Rolle ist Superuser!"
else
  echo "OK: anon-Rolle ist kein Superuser"
fi

# service_role hat die erwarteten Rechte?
echo "=== service_role Rechte ==="
docker compose exec -T db psql -U postgres -t -c \
  "SELECT rolsuper, rolbypassrls FROM pg_roles WHERE rolname = 'service_role';" 2>/dev/null

# Gibt es unbekannte Rollen mit Login-Berechtigung?
echo "=== Rollen mit Login ==="
docker compose exec -T db psql -U postgres -c \
  "SELECT rolname FROM pg_roles WHERE rolcanlogin = true
   AND rolname NOT IN ('postgres','authenticator','supabase_admin',
     'supabase_auth_admin','supabase_storage_admin','supabase_replication_admin',
     'supabase_read_only_user')
   ORDER BY rolname;" 2>/dev/null
```

Erwartung:
- anon: NICHT superuser, NICHT createrole, NICHT createdb
- authenticated: NICHT superuser
- service_role: bypassrls = true (das ist gewollt), NICHT superuser
- Keine unbekannten Rollen mit Login-Berechtigung

Risiko wenn nicht erfüllt: anon als Superuser = jeder unauthentifizierte Request hat vollen DB-Zugriff. Unbekannte Rollen mit Login können Backdoor-Zugänge sein.

---

## C1: Backups

Prüfe ob Backups korrekt eingerichtet sind.

```bash
# Backup-Verzeichnis vorhanden?
ls -lh /opt/backups/*.gpg 2>/dev/null | tail -5

# Letztes Backup: Alter in Stunden
LAST=$(ls -t /opt/backups/*.gpg 2>/dev/null | head -1)
if [ -n "$LAST" ]; then
  AGE=$(( ($(date +%s) - $(stat -c %Y "$LAST")) / 3600 ))
  echo "Letztes Backup: $LAST ($AGE Stunden alt)"
  [ "$AGE" -gt 26 ] && echo "WARNUNG: Älter als 26 Stunden"
else
  echo "KRITISCH: Kein Backup gefunden"
fi

# Backup-Dateien nicht 0 Bytes?
find /opt/backups -name "*.gpg" -size 0 -print 2>/dev/null

# Backup verschlüsselt? (GPG Check)
LAST=$(ls -t /opt/backups/*.gpg 2>/dev/null | head -1)
if [ -n "$LAST" ]; then
  file "$LAST" | grep -i "pgp\|gpg\|encrypted" || echo "WARNUNG: Möglicherweise nicht verschlüsselt"
fi

# Cron Job aktiv?
crontab -l 2>/dev/null | grep "backup"

# Backups extern gespeichert? (auf audit-runner)
ssh deploy@10.0.1.11 "ls -lh /opt/backup-archive/*.gpg 2>/dev/null | tail -3" 2>/dev/null || \
  echo "WARNUNG: Externe Backups nicht prüfbar (kein SSH zum audit-runner)"

# Restore-Script vorhanden?
ls /opt/supabase/scripts/restore*.sh 2>/dev/null || echo "WARNUNG: Kein Restore-Script gefunden"
```

Erwartung:
- Tägliches Backup vorhanden (weniger als 26 Stunden alt)
- Backups verschlüsselt (GPG)
- Keine 0-Byte Dateien
- Cron Job für tägliches Backup aktiv
- Backups extern gespeichert (audit-runner)
- Restore-Script vorhanden

Risiko wenn nicht erfüllt: Bei Server-Verlust (Hardware, Ransomware) kein Weg zurück.

---

## C2: Restore getestet

Prüfe ob ein Restore-Test durchgeführt wurde.

```bash
# Restore-Test Log vorhanden?
ls -la /var/log/restore-test.log 2>/dev/null

# Wann war der letzte Restore-Test?
if [ -f /var/log/restore-test.log ]; then
  stat -c "%y" /var/log/restore-test.log
  tail -5 /var/log/restore-test.log
fi

# Restore-Script vorhanden und ausführbar?
ls -la /opt/supabase/scripts/restore-test.sh 2>/dev/null
```

Erwartung:
- Restore-Test mindestens einmal durchgeführt (Log vorhanden)
- Letzter Test nicht älter als 30 Tage
- Restore-Script vorhanden und ausführbar

Risiko wenn nicht erfüllt: Backups die nie getestet wurden, sind häufig unbrauchbar.

---

## C3: Tägliche Security Checks

Prüfe ob automatisierte Checks eingerichtet sind.

```bash
# Security Check Script vorhanden?
ls -la /opt/supabase/scripts/security-check.sh 2>/dev/null || \
ls -la /opt/audit/scripts/security-check.sh 2>/dev/null || \
  echo "WARNUNG: Kein Security Check Script gefunden"

# Cron Job für tägliche Checks?
crontab -l 2>/dev/null | grep "security-check"

# Letzter Check-Log
ls -la /var/log/security-check.log 2>/dev/null
if [ -f /var/log/security-check.log ]; then
  echo "=== Letzte Einträge ==="
  tail -10 /var/log/security-check.log
fi
```

Erwartung:
- Security-Check-Script vorhanden
- Cron Job täglich aktiv
- Logs von heute vorhanden

Risiko wenn nicht erfüllt: Konfigurationsdrift bleibt unentdeckt bis zum Incident.

---

## C4: Supabase Studio Zugriff

Prüfe ob das Supabase Studio nicht von aussen erreichbar ist.

```bash
# Studio Container läuft?
docker compose ps 2>/dev/null | grep -i "studio"

# Auf welchem Port/Interface lauscht Studio?
ss -tlnp | grep -E "3000|9000" 2>/dev/null

# Von localhost erreichbar?
curl -s -o /dev/null -w "%{http_code}" http://localhost:3000 2>/dev/null || echo "Nicht erreichbar"
```

Erwartung:
- Studio entweder nicht im Production-Stack ODER nur auf localhost/internem Interface
- NICHT auf 0.0.0.0 lauschend

Risiko wenn nicht erfüllt: Studio mit Default-Passwort gibt vollen Datenbank-Zugriff.

---

---

## M1: Unattended Upgrades (automatische OS Security Patches)

```bash
echo "=== Unattended Upgrades installiert? ==="
dpkg -l | grep unattended-upgrades || echo "NICHT installiert"

echo "=== Service aktiv? ==="
systemctl is-active unattended-upgrades

echo "=== Konfiguration ==="
cat /etc/apt/apt.conf.d/50unattended-upgrades 2>/dev/null | grep -E "Allowed-Origins|Automatic-Reboot|Mail" | head -10

echo "=== Auto-Update Konfiguration ==="
cat /etc/apt/apt.conf.d/20auto-upgrades 2>/dev/null

echo "=== Letzte automatische Updates ==="
ls -la /var/log/unattended-upgrades/ 2>/dev/null
tail -20 /var/log/unattended-upgrades/unattended-upgrades.log 2>/dev/null || echo "Kein Log vorhanden"
```

Erwartung:
- unattended-upgrades installiert und aktiv
- Security-Origins konfiguriert
- Auto-Update-Intervall auf täglich
- Logs vorhanden

Risiko: Ohne automatische Security Patches sammeln sich bekannte Vulnerabilities im OS an.

---

## M2: Ausstehende Updates

```bash
echo "=== Ausstehende Packages ==="
apt list --upgradable 2>/dev/null | head -20

echo "=== Davon Security Updates ==="
apt list --upgradable 2>/dev/null | grep -i security | head -10

echo "=== Letztes apt update ==="
stat -c "%y" /var/cache/apt/pkgcache.bin 2>/dev/null
AGE=$(( ($(date +%s) - $(stat -c %Y /var/cache/apt/pkgcache.bin 2>/dev/null || echo 0)) / 86400 ))
echo "Alter: ${AGE} Tage"
[ "$AGE" -gt 7 ] && echo "WARNUNG: apt update ist überfällig"

echo "=== Reboot erforderlich? ==="
test -f /var/run/reboot-required && cat /var/run/reboot-required || echo "Kein Reboot nötig"
```

Erwartung:
- Keine ausstehenden Security Updates
- apt update weniger als 7 Tage alt
- Kein ausstehender Reboot (oder bewusst geplant)

---

## M3: Supabase Image Versionen und Alter

```bash
cd /opt/supabase 2>/dev/null || cd /opt

echo "=== Aktuelle Image Versionen ==="
docker compose images --format '{{.Repository}}:{{.Tag}}' 2>/dev/null

echo "=== Image Alter ==="
for image in $(docker compose images --format '{{.Repository}}:{{.Tag}}' 2>/dev/null); do
  CREATED=$(docker inspect --format='{{.Created}}' "$image" 2>/dev/null | cut -dT -f1)
  if [ -n "$CREATED" ]; then
    AGE=$(( ($(date +%s) - $(date -d "$CREATED" +%s 2>/dev/null || echo 0)) / 86400 ))
    echo "$image: erstellt $CREATED ($AGE Tage)"
    [ "$AGE" -gt 90 ] && echo "  → WARNUNG: Älter als 90 Tage"
  fi
done

echo "=== Images gepinnt (kein :latest)? ==="
grep "image:" docker-compose.yml 2>/dev/null | grep "latest" && \
  echo "WARNUNG: :latest Images gefunden" || echo "OK: Alle Images versioniert"

echo "=== Letzter Update-Commit ==="
git log --oneline --grep="Update\|update\|upgrade" -5 2>/dev/null || echo "Keine Update-Commits gefunden"

LAST_UPDATE=$(git log --format="%ai" --grep="Update\|update" -1 2>/dev/null | cut -d' ' -f1)
if [ -n "$LAST_UPDATE" ]; then
  UPDATE_AGE=$(( ($(date +%s) - $(date -d "$LAST_UPDATE" +%s)) / 86400 ))
  echo "Letztes Update: $LAST_UPDATE ($UPDATE_AGE Tage her)"
  [ "$UPDATE_AGE" -gt 45 ] && echo "WARNUNG: Über 45 Tage seit letztem Update"
fi
```

Erwartung:
- Alle Images gepinnt (kein :latest)
- Kein Image älter als 90 Tage
- Update-Commit innerhalb der letzten 45 Tage

Risiko: Veraltete Images enthalten bekannte CVEs. Supabase GoTrue, PostgREST und Kong haben regelmässig Security-Patches.

---

## M4: Backup-Integrität

```bash
echo "=== Letztes Backup ==="
LAST=$(ls -t /opt/backups/*.gpg 2>/dev/null | head -1)
if [ -n "$LAST" ]; then
  AGE=$(( ($(date +%s) - $(stat -c %Y "$LAST")) / 3600 ))
  SIZE=$(stat -c %s "$LAST")
  echo "Datei: $LAST"
  echo "Alter: ${AGE} Stunden"
  echo "Grösse: $(numfmt --to=iec $SIZE)"
  [ "$AGE" -gt 26 ] && echo "WARNUNG: Backup älter als 26 Stunden"
  [ "$SIZE" -lt 1024 ] && echo "KRITISCH: Backup verdächtig klein (< 1KB)"
else
  echo "KRITISCH: Kein Backup gefunden"
fi

echo "=== Backup Cron aktiv? ==="
crontab -l 2>/dev/null | grep "backup" || echo "WARNUNG: Kein Backup-Cron"

echo "=== Externe Backups (audit-runner)? ==="
ssh deploy@10.0.1.11 "ls -lh /opt/backup-archive/*.gpg 2>/dev/null | tail -3" 2>/dev/null || \
  echo "INFO: Externe Backups nicht prüfbar"

echo "=== Letzter Restore-Test ==="
if [ -f /var/log/restore-test.log ]; then
  RESTORE_DATE=$(stat -c "%y" /var/log/restore-test.log | cut -d' ' -f1)
  RESTORE_AGE=$(( ($(date +%s) - $(date -d "$RESTORE_DATE" +%s)) / 86400 ))
  echo "Letzter Test: $RESTORE_DATE ($RESTORE_AGE Tage)"
  [ "$RESTORE_AGE" -gt 35 ] && echo "WARNUNG: Restore-Test über 35 Tage her"
else
  echo "WARNUNG: Kein Restore-Test Log gefunden"
fi

echo "=== Restore-Test Cron? ==="
crontab -l 2>/dev/null | grep "restore" || echo "WARNUNG: Kein Restore-Test Cron"
```

Erwartung:
- Tägliches Backup vorhanden und aktuell (< 26h)
- Backup nicht verdächtig klein
- Backup extern gespeichert
- Restore-Test innerhalb der letzten 35 Tage
- Cron Jobs für beides aktiv

---

## M5: Docker Engine und Tools

```bash
echo "=== Docker Version ==="
docker version --format 'Client: {{.Client.Version}}, Server: {{.Server.Version}}' 2>/dev/null

echo "=== Docker Compose Version ==="
docker compose version

echo "=== Caddy Version ==="
caddy version 2>/dev/null || echo "Caddy nicht installiert"

echo "=== Node.js Version ==="
node --version 2>/dev/null || echo "Node.js nicht installiert"

echo "=== Git Version ==="
git --version

echo "=== OpenSSL Version ==="
openssl version
```

Erwartung:
- Docker: aktuelle Stable Version
- Caddy: aktuelle Version
- OpenSSL: keine bekannten CVEs in der Version

---

## M6: TLS Zertifikat Laufzeit

```bash
echo "=== TLS Zertifikat ==="
CERT_END=$(echo | openssl s_client -connect localhost:443 2>/dev/null | \
  openssl x509 -noout -enddate 2>/dev/null | cut -d= -f2)
if [ -n "$CERT_END" ]; then
  DAYS=$(( ($(date -d "$CERT_END" +%s) - $(date +%s)) / 86400 ))
  echo "Ablauf: $CERT_END ($DAYS Tage)"
  [ "$DAYS" -lt 14 ] && echo "KRITISCH: Zertifikat läuft in $DAYS Tagen ab"
  [ "$DAYS" -lt 30 ] && echo "WARNUNG: Zertifikat läuft in $DAYS Tagen ab"
else
  echo "WARNUNG: Zertifikat nicht prüfbar"
fi

echo "=== Caddy Auto-Renewal aktiv? ==="
systemctl is-active caddy
# Caddy erneuert automatisch, aber nur wenn der Service läuft
```

Erwartung:
- Zertifikat mindestens 14 Tage gültig
- Caddy Service aktiv (für Auto-Renewal)

---

## M7: Disk Space und Aufräumen

```bash
echo "=== Disk Usage ==="
df -h / | tail -1

echo "=== Docker Disk Usage ==="
docker system df

echo "=== Alte Docker Images ==="
docker images --filter "dangling=true" -q | wc -l
echo "dangling Images (können gelöscht werden)"

echo "=== Alte Backups ==="
find /opt/backups -name "*.gpg" -mtime +30 -print | wc -l
echo "Backups älter als 30 Tage"

echo "=== Docker Logs Grösse ==="
du -sh /var/lib/docker/containers/*/  2>/dev/null | sort -rh | head -5
```

Erwartung:
- Disk Usage unter 85%
- Keine grossen Mengen dangling Docker Images
- Alte Backups werden aufgeräumt (Retention)

---

## M8: Audit-Runner Maintenance Überwachung

```bash
echo "=== Maintenance Check Cron auf audit-runner? ==="
ssh deploy@10.0.1.11 "crontab -l 2>/dev/null | grep maintenance" 2>/dev/null || \
  echo "WARNUNG: Kein Maintenance Check Cron auf audit-runner"

echo "=== Security Release Monitor Cron auf audit-runner? ==="
ssh deploy@10.0.1.11 "crontab -l 2>/dev/null | grep security-release" 2>/dev/null || \
  echo "WARNUNG: Kein Security Release Monitor Cron auf audit-runner"

echo "=== Trivy installiert auf audit-runner? ==="
ssh deploy@10.0.1.11 "trivy --version 2>/dev/null" || \
  echo "WARNUNG: Trivy nicht installiert auf audit-runner"

echo "=== Letzter Maintenance Report ==="
ssh deploy@10.0.1.11 "tail -20 /var/log/maintenance-check.log 2>/dev/null" 2>/dev/null || \
  echo "INFO: Maintenance Log nicht verfügbar"

echo "=== Letzter Security Release Report ==="
ssh deploy@10.0.1.11 "tail -20 /var/log/security-releases.log 2>/dev/null" 2>/dev/null || \
  echo "INFO: Security Release Log nicht verfügbar"

echo "=== E-Mail Alerting konfiguriert? ==="
ssh deploy@10.0.1.11 "which mail 2>/dev/null && echo 'mail Befehl verfügbar' || echo 'WARNUNG: mail nicht installiert'" 2>/dev/null
```

Erwartung:
- Maintenance Check Cron auf audit-runner aktiv (wöchentlich Montag)
- Security Release Monitor Cron aktiv (täglich)
- Trivy installiert
- Letzte Reports vorhanden
- E-Mail Alerting konfiguriert

---

## M9: Auto-Patch Einrichtung

```bash
echo "=== Auto-Patch Script vorhanden? ==="
ls -la /opt/supabase/scripts/auto-patch.sh 2>/dev/null || echo "WARNUNG: auto-patch.sh fehlt"

echo "=== Auto-Patch Cron aktiv? ==="
crontab -l 2>/dev/null | grep auto-patch || echo "WARNUNG: Kein Auto-Patch Cron"

echo "=== Letzter Auto-Patch Lauf ==="
tail -20 /var/log/auto-patch.log 2>/dev/null || echo "INFO: Noch kein Auto-Patch gelaufen"

echo "=== Auto-Patch Ergebnisse ==="
grep -E "Patches eingespielt|Keine Patches|ABBRUCH|FEHLER" /var/log/auto-patch.log 2>/dev/null | tail -10

echo "=== Cron Zeitplan korrekt? ==="
echo "Erwartet:"
echo "  02:00 Backup"
echo "  03:00 Auto-Patch"
echo "  04:00 Restore-Test (monatlich)"
echo ""
echo "Aktuell:"
crontab -l 2>/dev/null | grep -E "backup|auto-patch|restore"
```

Erwartung:
- auto-patch.sh vorhanden und ausführbar
- Cron: 03:00 täglich (NACH dem Backup um 02:00)
- Letzte Logs zeigen erfolgreiche Läufe
- Backup Cron MUSS vor Auto-Patch Cron liegen

---

## Zusammenfassung

Erstelle jetzt eine Zusammenfassung in diesem Format:

```
# Supabase Self-Hosting Audit - [DATUM]

## Ergebnis

BESTANDEN: X von Y Checks (Y = ca. 34 Checks: A1-A5, B1-B12, C1-C4, M1-M9)
WARNUNG:   X Checks
KRITISCH:  X Checks

## Kritische Findings (sofort handeln)
- ...

## Warnungen (diese Woche lösen)
- ...

## Bestanden
- ...

## Service-Status Übersicht
| Service       | Status  | Finding                    |
|---------------|---------|----------------------------|
| PostgreSQL    | OK/WARN | ...                        |
| Kong          | OK/WARN | ...                        |
| GoTrue (Auth) | OK/WARN | ...                        |
| PostgREST     | OK/WARN | ...                        |
| Realtime      | OK/WARN | ...                        |
| Storage/MinIO | OK/WARN | ...                        |
| Analytics     | OK/WARN | ...                        |
| Vector        | OK/WARN | ...                        |
| Supavisor     | OK/WARN | ...                        |
| Studio        | OK/WARN | ...                        |

## Update-Status
| Bereich              | Letztes Update | Alter  | Status   |
|----------------------|----------------|--------|----------|
| OS Security Patches  | ...            | X Tage | OK/WARN  |
| Supabase Images      | ...            | X Tage | OK/WARN  |
| Docker Engine        | v...           |        | OK/WARN  |
| Caddy                | v...           |        | OK/WARN  |
| TLS Zertifikat       | ...            | X Tage | OK/WARN  |

## Empfohlene nächste Schritte
1. ...
2. ...
3. ...
```

Priorisiere strikt: Kritische Findings zuerst, dann Warnungen.
Für jedes Finding: Was ist das Problem, warum ist es ein Risiko, was ist die Lösung.
