# Auditoría del Runbook de Supabase Self-Hosting

Eres un Senior DevOps Security Auditor. Tu tarea es verificar si la configuración
de Supabase Self-Hosting se ha implementado correctamente según el runbook (artículo 1).

Ejecuta las siguientes comprobaciones en orden. Utiliza las herramientas disponibles
(Read, Grep, Glob y los comandos Bash permitidos).

Para cada comprobación: Reporta APROBADO, ADVERTENCIA o CRÍTICO con una breve justificación.
Al final, elabora un resumen con recomendaciones de actuación.

---

## A1: Separación de infraestructura

Verifica si producción y auditoría se ejecutan en servidores separados.

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

Expectativa: Dos hosts separados (supabase-prod + audit-runner).
Riesgo si no se cumple: Un servidor comprometido puede manipular sus propios resultados de auditoría.

---

## A2: Red privada

Verifica si hay una red privada configurada y PostgreSQL solo escucha internamente.

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

Expectativa: PostgreSQL escucha en 10.0.1.10:5432 (interfaz interna), NO en 0.0.0.0:5432.
Riesgo si no se cumple: Base de datos accesible directamente desde Internet en caso de error en el firewall.

---

## A3: Reverse Proxy y TLS

Verifica si hay un Reverse Proxy con TLS delante del stack de Supabase.

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

Expectativa:
- Reverse Proxy (Caddy o Nginx) instalado y configurado
- Certificado TLS válido (mínimo 14 días)
- Security Headers: Strict-Transport-Security, X-Frame-Options, X-Content-Type-Options, Content-Security-Policy

Riesgo si no se cumple: Los tokens de autenticación circulan en texto plano. Clickjacking y XSS posibles.

---

## A4: Firewall (dos niveles)

Verifica si tanto un Cloud Firewall como un Host Firewall están activos.

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

Expectativa:
- iptables Default Policy: DROP
- Solo puertos 443 (HTTPS) y 22 (desde IP de administración) permitidos
- Red interna (10.0.1.0/24) permitida
- Archivo baseline existente, sin drift

Riesgo si no se cumple: Un iptables -F abre todos los puertos si no existe un Cloud Firewall.

---

## A5: Acceso SSH

Verifica si SSH está correctamente asegurado.

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

Expectativa:
- PasswordAuthentication no
- PermitRootLogin no
- PubkeyAuthentication yes
- AllowUsers contiene solo el usuario de despliegue

Riesgo si no se cumple: Fuerza bruta en contraseñas SSH; con login root, control total inmediato.

---

## B1: Despliegue versionado

Verifica si la configuración de Supabase está versionada en Git y no hay cambios manuales.

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

Expectativa:
- Repositorio Git en /opt/supabase (o similar)
- Sin cambios no confirmados (git status --porcelain está vacío)
- Sin diferencias con el remote (servidor actualizado)

Riesgo si no se cumple: Los cambios manuales se pierden con git pull. No reproducible tras pérdida del servidor.

---

## B2: Configuración relevante para seguridad en docker-compose

Verifica la docker-compose.yml en los puntos críticos.

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

Verifica también el archivo .env (sin registrar los valores en sí):

```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 "CRÍTICO: JWT_EXP es $EXPIRY (superior a 3600)"
  fi
fi
```

Expectativa:
- Todas las imágenes versionadas (sin :latest)
- Puerto Postgres: 10.0.1.10:5432 (interno) o 127.0.0.1:5432
- Puerto Kong: 127.0.0.1:8000 (localhost)
- GOTRUE_JWT_EXP: máximo 3600
- GOTRUE_MAILER_AUTOCONFIRM: false
- REFRESH_TOKEN_ROTATION: true

Riesgo si no se cumple: Imágenes sin versión fija pueden introducir breaking changes o vulnerabilidades.
JWT con 24h de validez = un token robado es utilizable durante un día entero.

---

## B3: Gestión de secretos

Verifica si los secretos se gestionan correctamente.

```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"
```

Expectativa:
- .env NO está en Git (git ls-files vacío)
- .env en .gitignore
- Permisos de archivo: 600, propietario: deploy
- Todos los secretos con al menos 16 caracteres
- Sin contraseñas por defecto
- .env.example como plantilla en el repositorio

Riesgo si no se cumple: La filtración de secretos es el problema de seguridad más frecuente en self-hosting.

---

## B4: Políticas RLS de base de datos

Verifica si Row Level Security está activo en todas las tablas públicas.

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

Expectativa:
- Ninguna tabla pública sin RLS (o excepciones documentadas conscientemente)
- Ninguna tabla con RLS pero sin políticas (salvo bloqueo intencionado)
- Ninguna política con qual = 'true' (= acceso sin restricciones)

Riesgo si no se cumple: Las tablas sin RLS son legibles por cualquiera a través de la clave anon. Una política abierta en la tabla users expone todos los datos de usuario.

---

## B5: Secretos completos y generados de forma segura

Verifica si todos los secretos requeridos están configurados y no contienen valores por defecto.

```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 "CRÍTICO: $var ist nicht gesetzt"
    elif echo "$VAL" | grep -qiE "your-|change|example|super-secret|default"; then
      echo "CRÍTICO: $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 "CRÍTICO: 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 "CRÍTICO: 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 "ADVERTENCIA: Mehrere Secrets haben den gleichen Wert"
    done
fi
```

Expectativa:
- Todos los secretos configurados, sin valores por defecto
- JWT_SECRET con al menos 32 caracteres
- SECRET_KEY_BASE con al menos 64 caracteres
- Sin valores idénticos entre diferentes secretos

Riesgo si no se cumple: El JWT_SECRET por defecto es de conocimiento público. Un atacante puede generar tokens válidos.

---

## B6: Configuración de Kong API Gateway

Verifica si Kong está correctamente configurado y solo escucha en localhost.

```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 "ADVERTENCIA: Dashboard-Passwort zu kurz ($DASH_LEN Zeichen)"
  [ "$DASH_LEN" -ge 16 ] && echo "OK: Dashboard-Passwort ($DASH_LEN Zeichen)"
else
  echo "CRÍTICO: DASHBOARD_PASSWORD nicht gesetzt"
fi
```

Expectativa:
- Kong escucha en 127.0.0.1:8000 (NO en 0.0.0.0)
- kong.yml presente con validación JWT en las rutas de API
- Contraseña del dashboard con al menos 16 caracteres

Riesgo si no se cumple: Kong en 0.0.0.0 = API accesible directamente sin TLS. Contraseña débil del dashboard = acceso de atacantes al Studio.

---

## B7: Configuración de GoTrue (Auth)

Verifica si la autenticación está configurada de forma segura.

```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 "CRÍTICO: 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 "CRÍTICO: 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 "ADVERTENCIA: $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 "ADVERTENCIA: 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
```

Expectativa:
- JWT Expiry máximo 3600 segundos
- Autoconfirm desactivado (false)
- SMTP completamente configurado (Host, Port, User, Pass)
- Refresh Token Rotation activado
- SITE_URL y API_EXTERNAL_URL correctamente configurados (HTTPS)

Riesgo si no se cumple: Autoconfirm = true sin SMTP = cualquiera puede crear cuentas sin verificación de correo electrónico. JWT con 24h de validez = un token robado es utilizable durante un día entero.

---

## B8: Configuración de PostgREST

Verifica si PostgREST está configurado de forma segura.

```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 "CRÍTICO: 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 "ADVERTENCIA: PGRST_DB_SCHEMAS ist leer (alle Schemas exponiert)"
else
  echo "OK: Schemas eingeschränkt auf: $SCHEMAS"
fi
```

Expectativa:
- PGRST_DB_URI utiliza el rol authenticator (NO el superusuario postgres)
- PGRST_DB_SCHEMAS explícitamente configurado (public,storage,graphql_public)

Riesgo si no se cumple: PostgREST con el superusuario postgres = RLS ineficaz, cada petición tiene acceso completo a la BD. Schemas vacíos = schemas internos de Supabase (auth, _realtime) expuestos a través de la API.

---

## B9: Configuración de Realtime

Verifica si Realtime está configurado de forma segura.

```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 "CRÍTICO: 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 "CRÍTICO: SECRET_KEY_BASE nur $SKB_LEN Zeichen (min. 64)"
  [ "$SKB_LEN" -ge 64 ] && echo "OK: SECRET_KEY_BASE hat $SKB_LEN Zeichen"
else
  echo "CRÍTICO: 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"
```

Expectativa:
- DB_ENC_KEY NO es "supabaserealtime" (valor por defecto)
- SECRET_KEY_BASE con al menos 64 caracteres
- Health Check de Realtime exitoso

Riesgo si no se cumple: DB_ENC_KEY por defecto = el cifrado es predecible.

---

## B10: Storage y MinIO

Verifica si Storage y, en su caso, MinIO están configurados de forma segura.

```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 "CRÍTICO: MinIO Root User ist 'minioadmin' (Default)"
  [ "$MINIO_PASS" = "minioadmin" ] && echo "CRÍTICO: MinIO Root Password ist 'minioadmin' (Default)"
  [ -n "$MINIO_PASS" ] && [ ${#MINIO_PASS} -lt 8 ] && echo "ADVERTENCIA: MinIO Passwort zu kurz (min. 8)"
fi

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

Expectativa:
- Health Check de Storage exitoso
- Si MinIO: puertos en 127.0.0.1 (no 0.0.0.0)
- Si MinIO: sin credenciales por defecto (minioadmin/minioadmin)
- Volumen de Storage existente y con permisos de escritura

Riesgo si no se cumple: MinIO con credenciales por defecto en 0.0.0.0 = todos los archivos son públicamente legibles y escribibles.

---

## B11: Servicios internos (Analytics, Vector, Supavisor)

Verifica si los servicios internos están correctamente asegurados.

```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 "ADVERTENCIA: $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 "ADVERTENCIA: 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
```

Expectativa:
- Analytics en 127.0.0.1:4000 (no 0.0.0.0)
- Tokens de Logflare configurados (no por defecto)
- Docker Socket de Vector con :ro (solo lectura)
- Puerto de transacción de Supavisor en localhost

Riesgo si no se cumple: Analytics externo = logs de todos los servicios públicos. Vector sin solo lectura = el contenedor puede ejecutar comandos Docker. Supavisor externo = Connection Pooler (y por tanto la BD) accesible desde fuera.

---

## B12: Roles y grants de PostgreSQL

Verifica si los roles de base de datos están correctamente configurados.

```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 "CRÍTICO: 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
```

Expectativa:
- anon: NO superusuario, NO createrole, NO createdb
- authenticated: NO superusuario
- service_role: bypassrls = true (es intencionado), NO superusuario
- Ningún rol desconocido con permiso de inicio de sesión

Riesgo si no se cumple: anon como superusuario = cada petición no autenticada tiene acceso completo a la BD. Roles desconocidos con login pueden ser accesos de puerta trasera.

---

## C1: Copias de seguridad

Verifica si las copias de seguridad están correctamente configuradas.

```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 "ADVERTENCIA: Älter als 26 Stunden"
else
  echo "CRÍTICO: 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 "ADVERTENCIA: 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 "ADVERTENCIA: Externe Backups nicht prüfbar (kein SSH zum audit-runner)"

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

Expectativa:
- Copia de seguridad diaria existente (menos de 26 horas de antigüedad)
- Copias de seguridad cifradas (GPG)
- Sin archivos de 0 bytes
- Tarea cron para copia de seguridad diaria activa
- Copias de seguridad almacenadas externamente (audit-runner)
- Script de restauración existente

Riesgo si no se cumple: En caso de pérdida del servidor (hardware, ransomware) no hay forma de recuperación.

---

## C2: Restauración probada

Verifica si se ha realizado una prueba de restauración.

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

Expectativa:
- Prueba de restauración realizada al menos una vez (log existente)
- Última prueba con menos de 30 días de antigüedad
- Script de restauración existente y ejecutable

Riesgo si no se cumple: Las copias de seguridad que nunca se han probado son frecuentemente inutilizables.

---

## C3: Comprobaciones de seguridad diarias

Verifica si hay comprobaciones automatizadas configuradas.

```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 "ADVERTENCIA: 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
```

Expectativa:
- Script de comprobación de seguridad existente
- Tarea cron activa diariamente
- Logs de hoy existentes

Riesgo si no se cumple: El desfase de configuración pasa desapercibido hasta que ocurre un incidente.

---

## C4: Acceso a Supabase Studio

Verifica si Supabase Studio no es accesible desde el exterior.

```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"
```

Expectativa:
- Studio no incluido en el stack de producción O solo en localhost/interfaz interna
- NO escuchando en 0.0.0.0

Riesgo si no se cumple: Studio con contraseña por defecto otorga acceso completo a la base de datos.

---

---

## M1: Unattended Upgrades (parches automáticos de seguridad del SO)

```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"
```

Expectativa:
- unattended-upgrades instalado y activo
- Orígenes de seguridad configurados
- Intervalo de actualización automática configurado a diario
- Logs presentes

Riesgo: Sin parches automáticos de seguridad, las vulnerabilidades conocidas del SO se acumulan con el tiempo.

---

## M2: Actualizaciones pendientes

```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"
```

Expectativa:
- Sin actualizaciones de seguridad pendientes
- apt update con menos de 7 días de antigüedad
- Sin reboot pendiente (o conscientemente planificado)

---

## M3: Versiones y antigüedad de imágenes Supabase

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

Expectativa:
- Todas las imágenes con versión fija (sin :latest)
- Ninguna imagen con más de 90 días
- Commit de actualización en los últimos 45 días

Riesgo: Las imágenes desactualizadas contienen CVEs conocidos. Supabase GoTrue, PostgREST y Kong reciben parches de seguridad con regularidad.

---

## M4: Integridad de copias de seguridad

```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"
```

Expectativa:
- Copia de seguridad diaria presente y actual (< 26h)
- Copia de seguridad no sospechosamente pequeña
- Copia de seguridad almacenada externamente
- Prueba de restauración en los últimos 35 días
- Tareas cron activas para ambas operaciones

---

## M5: Docker Engine y herramientas

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

Expectativa:
- Docker: versión estable actual
- Caddy: versión actual
- OpenSSL: sin CVEs conocidos en la versión instalada

---

## M6: Validez del certificado TLS

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

Expectativa:
- Certificado válido al menos 14 días
- Servicio Caddy activo (para renovación automática)

---

## M7: Espacio en disco y limpieza

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

Expectativa:
- Uso de disco por debajo del 85%
- Sin grandes cantidades de imágenes Docker huérfanas
- Las copias de seguridad antiguas se limpian (retención)

---

## M8: Monitorización de mantenimiento en Audit-Runner

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

Expectativa:
- Cron de comprobación de mantenimiento en audit-runner activo (semanal, lunes)
- Cron de monitor de publicaciones de seguridad activo (diario)
- Trivy instalado
- Informes recientes presentes
- Alertas por correo electrónico configuradas

---

## M9: Configuración de Auto-Patch

```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"
```

Expectativa:
- auto-patch.sh presente y ejecutable
- Cron: 03:00 diariamente (DESPUÉS del backup a las 02:00)
- Los logs recientes muestran ejecuciones exitosas
- El cron del backup DEBE ejecutarse antes del cron de auto-patch

---

## Resumen

Elabora ahora un resumen con el siguiente formato:

```
# Auditoría Supabase Self-Hosting - [FECHA]

## Resultado

APROBADO:     X de Y comprobaciones (Y = aprox. 34 comprobaciones: A1-A5, B1-B12, C1-C4, M1-M9)
ADVERTENCIA:  X comprobaciones
CRÍTICO:      X comprobaciones

## Hallazgos críticos (actuar inmediatamente)
- ...

## Advertencias (resolver esta semana)
- ...

## Aprobados
- ...

## Resumen de estado de servicios
| Servicio      | Estado  | Hallazgo                   |
|---------------|---------|----------------------------|
| 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 | ...                        |

## Estado de actualizaciones
| Área                          | Última actualización | Antigüedad | Estado   |
|-------------------------------|---------------------|------------|----------|
| Parches de seguridad del SO   | ...                 | X días     | OK/WARN  |
| Imágenes Supabase             | ...                 | X días     | OK/WARN  |
| Docker Engine                 | v...                |            | OK/WARN  |
| Caddy                         | v...                |            | OK/WARN  |
| Certificado TLS               | ...                 | X días     | OK/WARN  |

## Próximos pasos recomendados
1. ...
2. ...
3. ...
```

Prioriza estrictamente: hallazgos críticos primero, luego advertencias.
Para cada hallazgo: cuál es el problema, por qué es un riesgo, cuál es la solución.
