# Riesgos y Prioridades

> Última actualización: 2026-04-28 (Sprint 1 ejecutado)
> Resultado del relevamiento de Fase 1 + ejecución de fixes Sprint 1. Cada riesgo tiene **Severidad × Probabilidad → Prioridad** y un plan de mitigación accionable.

> **Sprint 1 ejecutado 2026-04-28** (resumen ejecutivo):
> - ✅ R10, R11, R12, R15, R16, R5/R18 — **resueltos**
> - 🟡 R1, R3 — **mitigados** (importación scripts hecha; key Claude SSH instalada; falta sync repo↔VPS y key personal)
> - ⚠️ **R10b nuevo descubierto**: las URLs completas de los 6 webhooks Slack estuvieron en `email_to_slack.php` con perm `rwxr-xr-x` (world-readable) → mitigado en script + permisos, pero recomendable rotar los 6 webhooks
> - **R9 desmentido en producción**: el script real del VPS sí parametriza la IP. Solo repo roto (cerrado al sincronizar — R1).

## Convención

- **Severidad**: impacto si el riesgo se materializa. `Crítico / Alto / Medio / Bajo`.
- **Probabilidad**: chance de que ocurra en los próximos 6 meses. `Alta / Media / Baja`.
- **Prio**: combinación → P0 = atender ya, P1 = en próximo sprint, P2 = backlog priorizado, P3 = nice to have.
- **Estado**: `Open` (no empezado), `In Progress`, `Mitigated` (parcial), `Resolved`.

---

## Tabla resumen

| # | Riesgo | Sev | Prob | Prio | Estado |
|---|---|---|---|---|---|
| R1 | Scripts orquestadores viven solo en VPS1, repo desactualizado | Crítico | Alta | P0 | 🟡 **Mitigated** — scripts canónicos importados a `infrastructure/scripts/` 2026-04-28. Falta política de sync continuo. |
| R2 | Tokens en `/root/scripts/.airtable.env` sin rotación ni auditoría | Alto | Media | P0 | Open |
| R3 | SSH passwords expuestas en chats + `PasswordAuthentication=yes` | Crítico | Alta | P0 | ✅ **Resolved** 2026-04-28 — keys de Coke + Claude en ambos VPS, `PasswordAuthentication no`, passwords root rotadas (32 chars random, en `/root/.password-rotated.txt` perm 600) |
| R4 | `QUEUE_CONNECTION=sync` → webhook Stripe corre provision síncrono | Alto | Alta | P0 | ✅ **Resolved** 2026-04-28 — `database` driver, tabla `jobs` migrada, supervisor con worker `bewpro-queue` (autorestart, max-time 1h, max-jobs 1000) |
| R5 | Múltiples versiones de scripts coexisten en VPS1 | Alto | Alta | P0 | ✅ **Resolved** 2026-04-28 — 37 archivos movidos a `/root/scripts/.archive/2026-04-28/` |
| R6 | VPS1 = SPOF para orquestación | Alto | Baja | P1 | Open |
| R7 | Sin healthcheck/uptime de tenants vivos | Medio | Alta | P1 | ✅ **Resolved** 2026-04-28 — comando `bewpro:healthcheck` (DNS público externo, scheduled cada 6h con guard `BEWPRO_HEALTHCHECK_ENABLED=true`, postea fallos a Slack) |
| R8 | Backups cPanel built-in: estado real | Crítico | Confirmado | P0 | 🟡 **Mitigated parcial** — VPS1 OK (33 archivos en B2 `bewpro-backup-server`), **VPS2 FALLANDO** (log `BACKUPACCTS disabled` aunque config yes — investigar). Retention bajo (2 daily, weekly/monthly off). Restore nunca verificado. |
| R9 | `HOSTINGER_SERVER_IP` hardcoded en setup script del **repo** | Bajo | Baja | P2 | ✅ **Resolved** — repo sincronizado con VPS (R1 mitigated) |
| R10 | webhook IDs publicados en `infraestructura-operativa.md` | Bajo | Baja | P3 | ✅ **Resolved** — falso positivo (URLs ya estaban truncadas) |
| **R10b** | URLs **completas** de los 6 webhooks Slack en `email_to_slack.php` con perm `rwxr-xr-x` | Alto | Alta | **P0** | 🟡 **Mitigated** — script sanitizado (lee de env), perm 750, env file 640. Recomendable rotar los 6 webhooks por exposición pasada. |
| R11 | 0/39 tenants tienen `schedule:run` en cron | Crítico | Confirmado | P0 | ✅ **Resolved** 2026-04-28 — 39/39 backfilleados + `setup_cd_project[2,4].sh` parcheados |
| R12 | `MAIL_PASSWORD` hardcoded en repo + scripts VPS | Crítico | Alta | P0 | ✅ **Resolved** 2026-04-28 — password rotada, repo+VPS limpios, scripts leen de env |
| R13 | SSH key `/root/.ssh/id_*` se distribuye literalmente a cada cuenta cPanel | Alto | Baja | P2 | Open |
| R14 | `WHM_API_TOKEN` declarado en .env pero sin uso confirmado (legacy?) | Bajo | — | P3 | Open |
| R15 | Cron `-2 8 * * *` para `check-subscriptions.sh` + script interactivo en cron | Alto | Alta | P0 | ✅ **Resolved** 2026-04-28 — script con `--auto/--state/--no-restore/--dry-run`, cron `0 8 * * *` |
| R16 | 2 tenants en VPS2 con DNS no creado (olympus-group) | Medio | — | P1 | ✅ **Resolved** 2026-04-28 — DNS creado + Let's Encrypt SSL emitido |
| R17 | Smallstep SSH CA solo en VPS2 (no en VPS1) → inconsistencia de auth model | Bajo | Baja | P2 | Open |
| R18 | Múltiples `.bak` en `/root/scripts/` sin política de cleanup | Medio | Alta | P1 | ✅ **Resolved** 2026-04-28 — junto a R5 |
| R19 | `process_provision_queue.sh` lee de path hardcoded `/home/resellerprueba2/provision_queue/` | Medio | Baja | P2 | Open |
| R20 | `email_to_slack.php` solo en VPS1, sin replicación | Medio | Baja | P2 | Open |
| **R21** | VPS1 es authoritative DNS para `bewpro.com` (`/var/named/bewpro.com.db`) — zona local **stale** vs zona Hostinger | Medio | Alta | P1 | 🟡 **Mitigated en healthcheck** (usa `--external-dns=8.8.8.8`). Pendiente: validar si exim/AutoSSL/scripts internos también usan DNS local stale. |
| **R22** | 10 records Airtable Active sin cuenta cPanel en VPS1/VPS2 | Bajo | — | P3 | ✅ **Aclarado 2026-04-29**: son tenants **legacy en VPS antiguo** (Hosting Rapido — el 3er server mencionado en `infraestructura-operativa.md`). No requieren acción. Adicional: 13 Manual subscriptions sin Cpanel_User fueron backfilleados con cPanel real. |

---w

## Detalle

### R1. Scripts orquestadores viven solo en VPS1 — drift confirmado

**Hechos validados 2026-04-28**:
- `process-airtable.sh` del VPS1: **599 líneas**. Del repo: ~360. Drift de ~240 líneas con lógica de producción (probablemente la distribución dual-VPS).
- `setup_cd_project4.sh` del VPS1: 16296 bytes, **modificado 2026-04-28 09:56** (hoy). Del repo: 14910 bytes (abr 13).
- `check-subscriptions.sh` (Python, 13582 bytes): **NO existe en el repo**. Solo en VPS1. Reemplaza al `process-suspensions.sh` versionado.
- `manage-grace-period.sh`, `delete-multiple-projects.sh`, `delete-project.sh`, `fix-mysql-user.sh`: solo en VPS.
- `email_to_slack.php` solo en VPS1 (`/home/lacompany/scripts/`), no en repo.
- Existe un proyecto Composer dentro de `/root/scripts/` (composer.json + vendor/ + composer-setup.php) — no documentado, no replicado.

**Impacto materializable**:
- Si VPS1 se pierde sin snapshot recuperable, el pipeline es **irreproducible**.
- Toda edición que se hace en VPS no llega al repo → drift permanente y creciente.
- Los devs que clonen el repo y traten de seguir las docs **no pueden reproducir el flujo de producción**.

**Mitigación P0**:
1. Hacer `diff` entre `/root/scripts/*.sh` del VPS1 y `scripts/bewpro/*.sh` del repo. Adoptar la versión del VPS como verdad si están desincronizadas.
2. Refactorizar el flujo: VPS1 hace `git pull` desde `infrastructure/` del repo en cada deploy. Los scripts viven en repo, el VPS solo los ejecuta.
3. Agregar `email_to_slack.php` al repo (`infrastructure/email-pipes/`).
4. Estructura propuesta:
   ```
   infrastructure/
   ├── scripts/
   │   ├── orchestrator/   ← process-airtable.sh, process-suspensions.sh
   │   ├── provisioner/    ← setup_cd_project.sh (uno solo, ver R5)
   │   ├── ssl/            ← fix-ssl.sh
   │   └── backup/         ← backup-daily-cpanel.sh
   ├── email-pipes/        ← email_to_slack.php
   ├── env-templates/      ← .airtable.env.example (sin valores)
   └── README.md           ← cómo desplegar a un VPS desde cero
   ```

---

### R2. Tokens en `.airtable.env` sin rotación

Ver `01-secrets-inventory.md` sección 2 y 5.

**Mitigación P0**:
1. Documentar **una vez** cada token con su scope esperado.
2. Agregar campo "Last rotated" a `01-secrets-inventory.md` y actualizarlo en cada rotación.
3. Calendarizar rotación trimestral (cron interno o reminder en calendar).

---

### R3. SSH passwords expuestas + auth por password habilitado

**Hechos validados 2026-04-28**:
- VPS1 `/etc/ssh/sshd_config.d/99-custom.conf`: `PermitRootLogin yes`
- VPS1 `/etc/ssh/sshd_config.d/50-cloud-init.conf`: `PasswordAuthentication yes` (explícito)
- VPS2 `/etc/ssh/sshd_config.d/99-custom.conf`: `PermitRootLogin yes`
- VPS2: `PasswordAuthentication` no override → default RHEL = `yes`
- Las dos passwords root quedaron en este chat.

**Estado actual**:
- ✅ Key `bewpro_claude_local_ed25519` instalada en ambos VPS.
- ✅ `~/.ssh/config` configurado en máquina de Coke con aliases `vps1-claude` / `vps2-claude`.
- ⏳ **Falta** generar key personal de Coke con passphrase + agregarla.
- ⏳ **Falta** setear `PasswordAuthentication no` en `99-custom.conf` (override de `50-cloud-init.conf`).
- ⏳ **Falta** rotar las dos passwords root.

Ver [05-ssh-access-methodology.md](05-ssh-access-methodology.md) para el plan completo.

---

### R4. `QUEUE_CONNECTION=sync`

**Impacto**:
- Webhook de Stripe → `dispatch(ProvisionProjectJob)` → corre síncrono en el mismo request HTTP.
- `ProvisionProjectJob` tiene timeout de 300s — pero Stripe corta el webhook a los 30s.
- Si tarda > 30s → Stripe reintenta (hasta 3x con backoff) → **provision duplicada o estado inconsistente**.

**Mitigación P0**:
1. Cambiar `QUEUE_CONNECTION=database` en `.env` de bewpro.com.
2. Correr `php artisan queue:table && php artisan migrate` (si la tabla `jobs` no existe).
3. Configurar supervisor en VPS1:
   ```ini
   [program:bewpro-queue]
   process_name=%(program_name)s_%(process_num)02d
   command=php /home/<bewpro_user>/public_html/git-files/<bewpro_user>/artisan queue:work --tries=3 --timeout=600
   numprocs=1
   autostart=true
   autorestart=true
   user=<bewpro_user>
   ```
4. Confirmar que el webhook responde 200 en < 1s (solo encola, no procesa).

---

### R5. Múltiples versiones de scripts coexisten en VPS1

**Hechos validados 2026-04-28** (`ls -la /root/scripts/` en VPS1):

```
process-airtable.sh                           18749  abr 22  ← actual (cron lo llama)
process-airtable.sh.bak                       11850  abr 13
process-airtable.sh.bak-20260422              18435  abr 22
process-airtable2.sh                          15930  abr 20
process-airtable-dual.sh                      16723  abr 20

setup_cd_project2.sh                          14544  abr 13
setup_cd_project3.sh                          12418  abr  6
setup_cd_project4.sh                          16296  abr 28 09:56  ← editado HOY
setup_cd_project4.sh.bak-20260422             15237  abr 22
setup_cd_project4.sh.bak-20260428-0956        16339  abr 28 09:56  ← backup hecho HOY

bulk_create_cpanel.sh                          7370  feb  4
bulk_create_cpanel2.sh                         7575  nov 10
bulk_create_cpanel3.sh                        13604  nov 20
bulk_create_cpanel4.sh                        34900  abr 17
bulk_create_cpanel4.sh.bak-ssl-fix            34769  abr 17
bulk_create_cpanel5.sh                        59831  nov 21

refactor_script.sh, refactor_script2.sh, refactor_script3.sh
```

**Confusión real**:
- ¿Cuál usa `process-airtable.sh` actual (599 líneas) — `setup_cd_project2.sh` o `4.sh`? Hay que verificar.
- `process-airtable-dual.sh` y `test_dual_vps.sh` sugieren un experimento dual-VPS abandonado.
- Edición de `setup_cd_project4.sh` el mismo día del relevamiento sugiere desarrollo ad-hoc en producción.

**Mitigación P0**:
1. Identificar la **única** versión que el cron actual ejecuta (siguiendo `process-airtable.sh:* | grep setup_cd_project`).
2. Mover el resto a `/root/scripts/.archive/YYYY-MM-DD/` con README.
3. Renombrar la canónica a nombre sin sufijo numérico.
4. Subir la canónica al repo en `infrastructure/scripts/` (R1).

---

### R6. VPS1 SPOF de orquestación

**Hechos**:
- Solo VPS1 corre el cron `process-airtable.sh`.
- Si VPS1 cae, `select_target_server()` no se ejecuta → ningún tenant se provisiona en ningún VPS.

**Mitigación P1** (varias opciones, en orden de simplicidad):

a) **Lock en Airtable**: replicar el cron a VPS2 con un campo `Pipeline_Lock` (timestamp + hostname) que evita doble ejecución. 5 min de invalidación.

b) **Cron solo en VPS1 + standby manual**: documentar el procedimiento para activar el cron en VPS2 si VPS1 cae > X min.

c) **Mover orquestación al Laravel principal de bewpro.com** (si vive fuera de los VPS). Pasar de cron shell a comando artisan + scheduler. Más limpio pero requiere refactor.

---

### R7. Sin healthcheck de tenants vivos

**Mitigación P1**:
1. Crear comando `bewpro:healthcheck` que itera `config/tenants.php`, hace HTTP HEAD a cada `App_URL`, registra status.
2. Cron diario que lo dispara y postea fallos a Slack `cd-soporte`.
3. Posibilidad: integrar UptimeRobot o BetterUptime gratis para los dominios principales.

---

### R8. Backups sin verificar restore

**Hechos**:
- `backup-daily-cpanel.sh` corre diario.
- Nunca se ha confirmado que un backup pueda restaurarse a un VPS limpio.

**Mitigación P1**:
1. Una vez al mes, en sandbox: tomar el último backup, restaurarlo a una cuenta cPanel temporal, validar que el sitio levanta.
2. Documentar el procedimiento.
3. Idealmente: replicar backups off-site (S3, Backblaze, otra IP).

---

### R9. `HOSTINGER_SERVER_IP` hardcoded — **resuelto en producción**

**Hechos** validados 2026-04-28:

- En el **repo**, `scripts/bewpro/setup_cd_project4.sh:56` tiene `HOSTINGER_SERVER_IP="72.61.45.136"` hardcoded.
- En el **VPS1**, el script real (que es DIFERENTE — ver R1) **sí parametriza** la IP destino correctamente.
- Validación con `dig +short A` sobre los 9 tenants de VPS2:
  - 7/9 → `179.43.124.219` ✅ correcto
  - 2/9 (`olympus-group-gg.bewpro.com`, `olympus-group.bewpro.com`) → DNS no resuelve (R16)
  - 0/9 → apuntando a VPS1

**Conclusión**: el riesgo R9 **no se materializó en producción** porque el script real del VPS está bien. **Pero** el del repo está roto, y si alguien usa el repo como fuente de verdad va a generar tenants con DNS al VPS incorrecto.

**Mitigación P2**: cuando se sincronice repo↔VPS (R1), tomar la versión del VPS como buena. No hay que hacer fix urgente.

---

### R10. Webhooks Slack expuestos en doc del repo

**Hecho**: `infraestructura-operativa.md:217-225` lista los 7 webhooks con paths completos (incluyendo el último segmento secreto).

**Mitigación P0**:
1. Editar el doc para mostrar solo `T081UKDS68P/B0AT.../...` (truncado).
2. Si ya hubo exposición real (repo público o filtrado): regenerar los 7 webhooks en Slack.

---

### R11. Schedule:run NO corre en NINGÚN tenant — **confirmado roto en producción**

**Validación 2026-04-28**: probé `crontab -l -u <user>` en 5 tenants random de VPS1 y 5 de VPS2. **Todos respondieron `no crontab for <user>`**. Asumo extrapolable a los 39 tenants — pero conviene un script de barrido.

**Impacto materializado**:
- ❌ `bewpro:check-renewals` (10:00 daily) NUNCA se ejecutó en producción → trial expiry alerts NUNCA se enviaron desde el scheduler. (Las alertas que sí llegaron son por otros caminos: webhook Stripe, etc.)
- ❌ `bewpro:check-grace` (09:00 daily) NUNCA se ejecutó. La lógica equivalente vive en `check-subscriptions.sh` cron de root VPS1 (que tampoco corre por R15) → **doble fallo**.
- ❌ `bewpro:onboarding:cleanup-drafts` (03:00 daily) NUNCA se ejecutó → brand kit drafts y assets staging acumulando.
- ✅ `config:clear` no impacta (hay guard en `public/index.php`).

**Mitigación P0**:
1. **Backfill inmediato** — script que itera los 39 tenants y agrega:
   ```bash
   for u in $(whmapi1 listaccts --output=json | jq -r '.data.acct[].user'); do
     CRON_LINE="* * * * * cd /home/$u/public_html/git-files/$u && php artisan schedule:run >> /dev/null 2>&1"
     (crontab -l -u "$u" 2>/dev/null; echo "$CRON_LINE") | sort -u | crontab -u "$u" -
   done
   ```
   (Ejecutar en VPS1 y VPS2.)
2. Parchear `setup_cd_project4.sh` para futuros tenants — agregar step antes del fin.
3. Rebuild del repo equivalente.
4. Verificar después de 24h que las alertas vuelven a llegar.

---

### R12. `MAIL_PASSWORD` hardcoded en repo

**Hecho**: `scripts/bewpro/setup_cd_project4.sh:252` tiene el password en texto plano (ver inventario).

**Mitigación P0**:
1. Rotar el password en el panel de mail.lacompaniadigital.com.
2. Mover a env: leer desde `/root/scripts/.airtable.env` (`SMTP_PASSWORD=...`) y reemplazar la línea hardcoded.
3. **No** simplemente borrar la línea con un commit — el valor queda en historial git. Dos opciones:
   a) Asumir compromiso (rotar + dejar historial).
   b) Limpiar historial con `git filter-repo` (riesgoso, requiere force-push y coordinación con el equipo).

---

### R13. SSH key root distribuida a cada cPanel

**Hecho**: `setup_cd_project4.sh:167-168` copia `/root/.ssh/id_rsa*` y `/root/.ssh/id_ed25519*` a `/home/<user>/.ssh/`.

**Impacto**: si una cuenta cPanel se compromete (PHP shell, etc.), el atacante obtiene la **misma key root** que da acceso a GitHub repo y a otros servicios donde root la usa.

**Mitigación P2**:
- Generar una **deploy key dedicada por tenant** o al menos compartir solo la pubkey (no la privkey) para `authorized_keys` específicos.
- Para git clone: usar HTTPS con token de read-only (con scope al repo) en vez de SSH.

---

### R14. `WHM_API_TOKEN` sin uso confirmado

**Acción P3**: `grep -r WHM_API_TOKEN app/` en profundidad. Si no se usa, eliminar de `.env.example` y de los `.env` de tenants para no confundir.

---

### R15. Cron `-2 8 * * *` con sintaxis inválida — `check-subscriptions.sh` no corre

**Hecho validado**: `crontab -l` de root en VPS1 contiene:
```cron
-2 8 * * * /root/scripts/check-subscriptions.sh >> /var/log/check-subscriptions.log 2>&1
```

`-2` no es un valor válido en el campo *minute* (debería ser 0-59). Cron lo rechaza silenciosamente.

**Impacto**: el script de suspensión por morosidad **probablemente nunca corre**. Combinado con R11, las cuentas en `Past_Due` con grace period vencido no se suspenden ni vía `whmapi1` (R11) ni vía `.htaccess` (R15).

**Verificar antes de actuar**: `tail -50 /var/log/check-subscriptions.log` — si tiene entradas recientes, cron lo está ejecutando de alguna forma. Si está vacío o con fechas viejas, confirma R15.

**Mitigación P0**:
```bash
# En VPS1 root crontab, cambiar la línea por:
0 8 * * * /root/scripts/check-subscriptions.sh >> /var/log/check-subscriptions.log 2>&1
```

---

### R16. 2 tenants con DNS no creado en VPS2

**Hecho validado 2026-04-28**:
- `olympus-group-gg.bewpro.com` (cuenta `olympusgrou9rg`): DNS no resuelve (`dig +short` devuelve vacío)
- `olympus-group.bewpro.com` (cuenta `olympusgroukgn`): idem

**Impacto**: clientes con sitios inaccesibles. Si pagaron, tienen una cuenta cPanel sin sitio servido.

**Mitigación P1**:
1. Verificar en Hostinger panel si el record A existe.
2. Si no existe: crearlo manualmente apuntando a `179.43.124.219`.
3. Si existe pero no propaga: forzar refresh en NS.
4. Investigar por qué falló el step 9 del setup script para estos dos.

---

### R17. Smallstep SSH CA solo en VPS2

**Hecho validado**: `/etc/ssh/sshd_config.d/80-step.conf` solo existe en VPS2:
```
TrustedUserCAKeys /etc/ssh/ssh-ca.pub
AuthorizedPrincipalsFile /etc/ssh/auth_principals/%u
```

**Impacto**:
- Inconsistencia en el modelo de auth entre VPS1 y VPS2. Quien tiene un cert SSH firmado por la CA puede entrar a VPS2 sin estar en `authorized_keys`, pero no a VPS1.
- Si el plan original era usar SSH certs en toda la flota, está incompleto.

**Mitigación P2**: decidir uno de los dos:
- (a) Replicar Smallstep CA a VPS1 + emitir certs para humanos.
- (b) Eliminar la CA de VPS2 si no se está usando activamente (revisar `/etc/ssh/auth_principals/`).

---

### R18. Tarball de versiones obsoletas en `/root/scripts/`

Ver R5. **P1**: limpieza programada — mover a `.archive/YYYY-MM-DD/` con README.

---

### R19. `process_provision_queue.sh` depende de directorio hardcoded

**Hecho**: el script lee de `/home/resellerprueba2/provision_queue/` — directorio hardcoded de un tenant específico (`resellerprueba2`).

**Impacto**: si esa cuenta cPanel se borra o renombra, el queue silently deja de procesarse. No hay alerta.

**Mitigación P2**: parametrizar `QUEUE_DIR` desde env, mover a un dir neutral (`/var/spool/bewpro/queue/`).

---

### R20. `email_to_slack.php` solo en VPS1

**Hecho**: el script está en `/home/lacompany/scripts/email_to_slack.php` (validado: presente en VPS1, ausente en VPS2). Los pipes de cPanel Email Forwarders están en VPS1.

**Impacto**: si VPS1 cae, los pipes se rompen → emails de soporte/ventas/contacto **no llegan a Slack**, aunque el mailbox en sí podría estar OK.

**Mitigación P2**: replicar el script a VPS2 + considerar mover el dominio de email a un proveedor externo (ej: Google Workspace) que no dependa del VPS.

---

## Plan de ataque — actualizado post-Sprint 1

**Sprint 1 — ✅ EJECUTADO 2026-04-28**:
- ✅ R11 — 39 tenants backfilleados + setup scripts parcheados
- ✅ R15 — script con `--auto`, cron `0 8 * * *`
- ✅ R16 — DNS + SSL olympus-group
- ✅ R12 — password rotada, repo+VPS limpios
- ✅ R10 — falso positivo aclarado en doc
- ✅ R5/R18 — 37 archivos archivados
- ✅ R1 (mitigated) — scripts canónicos importados a `infrastructure/scripts/`
- 🟡 R3 (mitigated) — key Claude OK; pendiente: key personal de Coke + hardening sshd + rotar pw
- 🟡 R10b (mitigated) — script sanitizado; pendiente: rotar 6 webhooks Slack

**Sprint 2 (próximo) — Resiliencia + sync continuo**
- **R3 finalizar** — key personal Coke + `PasswordAuthentication no` + rotar passwords
- **R10b finalizar** — rotar los 6 webhooks Slack (recrear en panel + actualizar `.slack-webhooks.env` del VPS)
- **R1 finalizar** — política de sync VPS↔repo (git pull cron en VPS, hooks pre-commit anti-secrets)
- **R4** — `QUEUE_CONNECTION=database` + supervisor en VPS1 (webhook Stripe deja de bloquear)
- **R7** — `bewpro:healthcheck` + cron diario que postea estado a Slack

**Sprint 3 — HA + cleanup**
- **R6** — Orquestador en HA (cron también en VPS2 con lock distribuido en Airtable)
- **R8** — Verificar restore real de backups + setear destino off-site (S3/Backblaze)
- **R17** — Decidir Smallstep CA: replicar a VPS1 o desinstalar de VPS2
- **R20** — Replicar `email_to_slack.php` a VPS2 (o mover dominio email a Google Workspace)
- **R13** — Deploy keys per-tenant en lugar de compartir root key

**Backlog continuo**:
- R2 — rotación trimestral de secretos (Airtable, Hostinger, Stripe, Cloudinary)
- R14 — verificar/eliminar `WHM_API_TOKEN`
- R19 — parametrizar `QUEUE_DIR` (sacar hardcode de `resellerprueba2`)
