# IMPLEMENTACIÓN A PRODUCCIÓN — `CL032BACK`

> **Documento vivo de despliegue.** Servidor **4. Público** de [ARQUITECTURA_SERVIDORES.md](ARQUITECTURA_SERVIDORES.md).
>
> Se llena **de arriba hacia abajo**. La Fase 0 se ejecuta **comando por comando** y cada bloque
> `Respuesta del servidor` se rellena a mano con lo que devuelva la terminal. **Las Fases 1 en
> adelante no se redactan hasta cerrar la Fase 0**: los comandos de instalación dependen de lo
> que realmente haya en la máquina, y un runbook escrito a ciegas es un runbook falso.

---

## Servidor destino

| Dato | Valor | Verificado |
|------|-------|-----------|
| Proveedor | HostGator México (VPS) | ✅ |
| Hostname | `vpsmx-2025092.vpshostgator.com` | ✅ |
| IP pública | `108.174.154.63` | ✅ |
| Sistema operativo | AlmaLinux v9.8.0 STANDARD kvm | ✅ |
| Panel | cPanel/WHM 134.0.54 | ✅ |
| Acceso | `root` vía WHM → Configuración del servidor → Terminal | ✅ |
| Carga al momento de la revisión | 0.10 / 0.11 / 0.09 | ✅ |
| **Naturaleza** | **Plataforma compartida multi-proyecto**, no dedicada — ver **H1** | ✅ |
| Cuentas hoy | `raquetacam` (en producción) · `reservacionvilla` (creada y **vacía**, destino) | ✅ |
| Cuentas previstas | Más cuentas de cPanel y más proyectos, según confirma el equipo | ✅ |

**Lo que este servidor habilita y el hosting compartido no.** Con `root`, un gestor de procesos
(supervisor, ya instalado — ver **H2**) y una VPN se pueden correr procesos de larga vida. Eso cierra el último pendiente de
[ARQUITECTURA_SERVIDORES.md](ARQUITECTURA_SERVIDORES.md) §12 *("GoDaddy no permite ZeroTier en
hosting compartido → probablemente requiere VPS")*. Sin esa capacidad, este proyecto **no puede
funcionar**, no es una cuestión de comodidad — ver V15 y V17.

---

## Cómo usar este documento

1. Copia **un solo comando** a la terminal de WHM.
2. Pega la salida completa dentro del bloque ` ``` ` que le sigue.
3. Marca el veredicto: `[x] OK` o `[x] FALTA`.
4. Pasa al siguiente. **No instales nada durante la Fase 0** — es solo lectura.

Existe también [produccion/validar_servidor.sh](produccion/validar_servidor.sh), que corre todo
de golpe. Sirve para una foto rápida; **este documento es el registro formal**, porque deja
constancia de qué se vio y cuándo.

**Convención de los veredictos:**

| Marca | Significado |
|-------|-------------|
| ✅ OK | Cumple. Nada que hacer. |
| ❌ FALTA | **Bloqueante.** No se despliega hasta resolverlo. |
| ⚠️ AVISO | No bloquea el despliegue, pero se decide qué hacer antes de salir a producción. |

---

# FASE 0 · VALIDACIÓN DEL SERVIDOR (solo lectura)

## Bloque A — Identidad y recursos

### V1 · Sistema operativo y kernel

```bash
cat /etc/redhat-release; uname -r; arch; hostname -f
```

**Respuesta del servidor:**
```
root@vpsmx-2025092.vpshostgator.com [~]# cat /etc/redhat-release; uname -r; arch; hostname -f
AlmaLinux release 9.8 (Olive Jaguar)
5.14.0-611.54.3.el9_7.x86_64
x86_64
vpsmx-2025092.vpshostgator.com
```

**Qué esperamos:** AlmaLinux 9.8, `x86_64`. Importa porque define de qué repositorio sale Redis
y qué paquetes de EasyApache existen.

**Veredicto:** ✅ **OK** — AlmaLinux 9.8 x86_64, kernel 5.14. Define el camino de paquetes: EA4 para PHP, AppStream para Redis.

---

### V2 · Memoria, disco y swap

```bash
free -h; echo "---"; df -h /; echo "---"; swapon --show; echo "---"; nproc
```

**Respuesta del servidor:**
```
root@vpsmx-2025092.vpshostgator.com [~]# free -h; echo "---"; df -h /; echo "---"; swapon --show; echo "---"; nproc
               total        used        free      shared  buff/cache   available
Mem:           7.5Gi       1.7Gi       479Mi       190Mi       5.8Gi       5.8Gi
Swap:          4.0Gi       8.0Mi       4.0Gi
---
Filesystem      Size  Used Avail Use% Mounted on
/dev/sda3       196G   17G  171G   9% /
---
NAME       TYPE SIZE USED PRIO
/swapfile1 file   4G 8.7M   -2
---
4
```

**Qué esperamos:** ≥ 4 GB de RAM y ≥ 20 GB libres. Este servidor no corre una sola aplicación:
va a sostener **MySQL + Redis + PHP-FPM + tres daemons** (`schedule:work`, `queue:work`,
`reverb:start`). Con 2 GB el OOM killer empieza a matar procesos, y el primero que cae suele ser
el worker de colas — que es justo por donde viajan las reservaciones al mundo privado (regla R6).

**Veredicto:** ✅ **OK** — 7.5 GB de RAM, 4 vCPU, 171 GB libres y 4 GB de swap. Holgado incluso con la carga que el servidor ya sostiene (V3).

**Notas:** RAM 7.5 GB (1.7 GB en uso, 5.8 GB disponibles) · Disco 196 GB con **171 GB libres** (9 % usado) · Swap 4 GB · 4 vCPU.

---

### V3 · cPanel y cuentas existentes

```bash
/usr/local/cpanel/cpanel -V; echo "---"; whmapi1 listaccts | grep -E "user:|domain:|ip:"
```

**Respuesta del servidor:**
```
root@vpsmx-2025092.vpshostgator.com [~]# /usr/local/cpanel/cpanel -V; echo "---"; whmapi1 listaccts | grep -E "user:|domain:|ip:"
134.0 (build 54)
---
      domain: raquetacam.com
      ip: 108.174.154.63
      is_temporary_domain: 0
      user: raquetacam
      domain: reservaciones.villastur.com.mx
      ip: 108.174.154.63
      is_temporary_domain: 0
      user: reservacionvilla
```

**Qué esperamos:** Saber si ya existe la cuenta del portal o hay que crearla. **La base de datos
y el usuario de MySQL se crean desde cPanel, nunca a mano por SQL** — cPanel les antepone el
prefijo de la cuenta y lleva su propio registro; un usuario creado por fuera queda huérfano y
desaparece en el siguiente mantenimiento del panel.

**Veredicto:** ⚠️ **AVISO** — la cuenta destino **ya existe**, pero el servidor **no está vacío**: comparte máquina con `raquetacam.com`, un sistema en producción ajeno a este proyecto. Ver hallazgo **H1**, el más importante de toda la validación.

**Cuenta destino definida:** `reservacionvilla` / **`reservaciones.villastur.com.mx`** — ya creada, IP 108.174.154.63.

**Segunda cuenta en la misma máquina:** `raquetacam` / `raquetacam.com`. **Esto no estaba contemplado en la arquitectura.** Ver **H1**.

---

## Bloque B — PHP

### V4 · Versiones de PHP instaladas (EasyApache 4)

```bash
ls -1 /opt/cpanel/ | grep ^ea-php; echo "--- default del sistema ---"; whmapi1 php_get_system_default_version | grep version
```

**Respuesta del servidor:**
```
root@vpsmx-2025092.vpshostgator.com [~]# ls -1 /opt/cpanel/ | grep ^ea-php; echo "--- default del sistema ---"; whmapi1 php_get_system_default_version | grep version
ea-php80
ea-php81
ea-php82
ea-php83
--- default del sistema ---
  version: ea-php82
  command: php_get_system_default_version
  version: 1
```

**Qué esperamos:** que aparezca `ea-php84`. [composer.json](../composer.json) declara
`"php": "^8.4"` y `laravel/framework: ^13.8`; **con 8.3 el `composer install` se niega a
correr**. Los VPS de HostGator suelen entregarse con 8.1–8.3.

**Si falta:** WHM → Software → EasyApache 4 → añadir `ea-php84`. Es una instalación aditiva: las
versiones existentes no se tocan, y cada cuenta sigue con la suya hasta que se cambie.

**Veredicto:** ❌ **FALTA** — hay `ea-php80/81/82/83`, el sistema apunta a `ea-php82` y **`ea-php84` no existe**. Con 8.3 el `composer install` se niega a correr. Bloqueante **B1**.

---

### V5 · Extensiones de PHP 8.4 — **el chequeo más importante del bloque**

```bash
B=/opt/cpanel/ea-php84/root/usr/bin/php
if [ -x $B ]; then echo "PHP $($B -r 'echo PHP_VERSION;')"; for e in redis pdo_mysql mbstring bcmath intl gd zip curl fileinfo openssl tokenizer xml ctype json dom session filter; do $B -m | grep -qix $e && echo "  OK    $e" || echo "  FALTA $e -> ea-php84-php-$e"; done; else echo ">>> ea-php84 NO INSTALADO"; fi
```

**Respuesta del servidor:**
```
root@vpsmx-2025092.vpshostgator.com [~]# B=/opt/cpanel/ea-php84/root/usr/bin/php
if [ -x $B ]; then echo "PHP $($B -r 'echo PHP_VERSION;')"; for e in redis pdo_mysql mbstring bcmath intl gd zip curl fileinfo openssl tokenizer xml ctype json dom session filter; do $B -m | grep -qix $e && echo "  OK    $e" || echo "  FALTA $e -> ea-php84-php-$e"; done; else echo ">>> ea-php84 NO INSTALADO"; fi
>>> ea-php84 NO INSTALADO
```

**Qué esperamos:** las 16 en `OK`. Ninguna es decorativa en este proyecto:

| Extensión | Para qué, aquí |
|-----------|----------------|
| `redis` | `REDIS_CLIENT=phpredis` en el `.env`. Si falta, se puede caer a `predis` (ya está en `composer.json`), pero es más lento. |
| `bcmath` | Importes de reservación. Sumar dinero con flotantes produce diferencias de centavos que después nadie explica. |
| `intl` | Fechas y moneda `es-MX`. |
| `gd` | Documentos de identificación de invitados y comprobantes. |
| `pdo_mysql` | Sin comentario. |

**Si falta alguna:** `dnf install ea-php84-php-<extensión>` — están todas en el repositorio de
EasyApache, no hace falta compilar nada.

**Veredicto:** ❌ **FALTA** — derivado de V4: sin `ea-php84` no hay extensiones que revisar. Se reevalúa después de instalarlo.

**Extensiones faltantes:** no evaluable — `ea-php84` no está instalado. Se listan al terminar la Fase 1.

---

### V6 · Límites del `php.ini` de 8.4

```bash
/opt/cpanel/ea-php84/root/usr/bin/php -i 2>/dev/null | grep -E "^(memory_limit|max_execution_time|upload_max_filesize|post_max_size|date.timezone|disable_functions)"
```

**Respuesta del servidor:**
```
root@vpsmx-2025092.vpshostgator.com [~]# /opt/cpanel/ea-php84/root/usr/bin/php -i 2>/dev/null | grep -E "^(memory_limit|max_execution_time|upload_max_filesize|post_max_size|date.timezone|disable_functions)"
root@vpsmx-2025092.vpshostgator.com [~]#
```

**Qué esperamos:** `memory_limit` ≥ 256M y `date.timezone` en `America/Mexico_City`.

⚠️ **Lo que hay que mirar con lupa es `disable_functions`.** Los VPS con cPanel llegan a
deshabilitar `proc_open`, `exec` o `pcntl_*`. Si `proc_open` está deshabilitado, **el scheduler
de Laravel no puede lanzar sus tareas** y todo el Bloque G falla sin un error evidente: las
tareas simplemente no corren.

**Veredicto:** ❌ **FALTA** — sin salida porque el binario no existe (V4). **`disable_functions` sigue sin verificarse**, y es lo que decide si el scheduler puede lanzar sus tareas. Se reevalúa tras instalar 8.4.

---

### V7 · Herramientas de despliegue

```bash
for c in composer git unzip rsync node npm svn; do command -v $c >/dev/null && echo "OK    $c -> $(command -v $c)" || echo "FALTA $c"; done
```

**Respuesta del servidor:**
```
root@vpsmx-2025092.vpshostgator.com [~]# for c in composer git unzip rsync node npm svn; do command -v $c >/dev/null && echo "OK    $c -> $(command -v $c)" || echo "FALTA $c"; doneFALTA composerOK    git -> /usr/local/cpanel/3rdparty/lib/path-bin/git
OK    unzip -> /bin/unzip
OK    rsync -> /bin/rsync
FALTA node
FALTA npm
OK    svn -> /bin/svn
```

**Qué esperamos:** `composer`, `git` y `unzip` sí o sí. `node`/`npm` solo si `CL032FRONT` se
compila en este servidor (decisión pendiente — ver §Pendientes). `svn` porque el proyecto se
versiona con Subversion (`.svn/` en la raíz).

**Veredicto:** ❌ **FALTA** — **`composer` ausente** (bloqueante **B2**). `git`, `unzip`, `rsync` y `svn` presentes; `node`/`npm` ausentes, lo que obliga a decidir dónde se compila el front. ⚠️ El `git` que responde es el de cPanel (`/usr/local/cpanel/3rdparty/lib/path-bin/git`), no un git de sistema: sirve para leer, pero no conviene apoyar el despliegue en él.

---

## Bloque C — MySQL

### V8 · Versión y parámetros del servidor

```bash
mysql -V; echo "---"; mysql -e "SELECT VERSION() AS version, @@character_set_server AS charset, @@collation_server AS collation, @@default_storage_engine AS engine, @@max_connections AS conexiones, @@time_zone AS tz\G"
```

**Respuesta del servidor:**
```
root@vpsmx-2025092.vpshostgator.com [~]# mysql -V; echo "---"; mysql -e "SELECT VERSION() AS version, @@character_set_server AS charset, @@collation_server AS collation, @@default_storage_engine AS engine, @@max_connections AS conexiones, @@time_zone AS tz\G"mysql  Ver 15.1 Distrib 10.11.19-MariaDB, for Linux (x86_64) using  EditLine wrapper---
*************************** 1. row ***************************
   version: 10.11.19-MariaDB
   charset: utf8mb3
 collation: utf8mb3_general_ci
    engine: MyISAM
conexiones: 151
        tz: SYSTEM
```

**Qué esperamos:** MySQL **≥ 8** ([STACK_CL032BACK.md](../../STACK_CL032BACK.md) §1.3 pide 8.4
LTS), charset `utf8mb4`, engine `InnoDB`.

⚠️ **Si reporta MariaDB**, hay que detenerse y decidir: cPanel la instala por defecto y no es
intercambiable con MySQL 8 en detalles que aquí importan (JSON, CTEs, comportamiento de
`lockForUpdate`). Cambiar de motor **después** de cargar datos es mucho más caro que decidirlo
ahora.

**Veredicto:** ❌ **FALTA** — no es MySQL 8.4: es **MariaDB 10.11.19**, con `default_storage_engine = MyISAM` y charset `utf8mb3`. Bloqueante **B3**, y el más delicado de los técnicos. Ver **H3**.

**Motor y versión reales:** **MariaDB 10.11.19** · charset `utf8mb3` · collation `utf8mb3_general_ci` · **`default_storage_engine = MyISAM`** · `max_connections` 151 (compartidas con `raquetacam`) · `time_zone` `SYSTEM`.

---

### V9 · Bases de datos existentes

```bash
mysql -NBe "SHOW DATABASES;" | grep -viE "^(information_schema|performance_schema|mysql|sys)$"
```

**Respuesta del servidor:**
```
root@vpsmx-2025092.vpshostgator.com [~]# mysql -NBe "SHOW DATABASES;" | grep -viE "^(information_schema|performance_schema|mysql|sys)$"raquetacam_deportesroot@vpsmx-2025092.vpshostgator.com [~]#
```

**Qué esperamos:** probablemente vacío en un VPS nuevo. Si ya existe `asturias_api`, hay que
revisar su charset antes de tocarla: la base heredada usa `utf8mb4_general_ci` y sintaxis vieja
(`int(11)`), deuda D5 documentada en la arquitectura.

**Veredicto:** ✅ **OK** — solo existe `raquetacam_deportes`. La base del portal está por crearse: sin datos heredados que migrar ni charset viejo que arrastrar. La deuda **D5** no aplica aquí.

---

## Bloque D — Redis

> **Por qué Redis no es opcional.** No está aquí por rendimiento. `withoutOverlapping()` del
> scheduler necesita un lock atómico; el `.env` de producción pone sesiones, caché y colas sobre
> él; y el *rate limiting* del endpoint público de disponibilidad solo es preciso con Redis.
> Con drivers `file`/`database` el sistema arranca, pero los locks se vuelven poco confiables
> justo bajo carga — que es cuando importan.

### V10 · ¿Está instalado el servidor Redis?

```bash
command -v redis-server redis-cli; echo "--- rpm ---"; rpm -qa | grep -i redis
```

**Respuesta del servidor:**
```
root@vpsmx-2025092.vpshostgator.com [~]# command -v redis-server redis-cli; echo "--- rpm ---"; rpm -qa | grep -i redis
/bin/redis-server
/bin/redis-cli
--- rpm ---
redis-6.2.22-1.el9_8.x86_64
```

**Qué esperamos:** lo más probable es que **no esté**. AlmaLinux 9 con cPanel no lo trae de
fábrica.

**Veredicto:** ✅ **OK** — Redis **ya estaba instalado**: `redis-6.2.22-1.el9_8`, de AppStream. (La previsión de que faltaría era incorrecta; viene con el despliegue previo de la máquina.)

---

### V11 · Servicio y puerto

```bash
systemctl is-enabled redis 2>&1; systemctl is-active redis 2>&1; echo "--- puerto ---"; ss -lntp | grep 6379 || echo "nada escuchando en 6379"
```

**Respuesta del servidor:**
```
root@vpsmx-2025092.vpshostgator.com [~]# systemctl is-enabled redis 2>&1; systemctl is-active redis 2>&1; echo "--- puerto ---"; ss -lntp | grep 6379 || echo "nada escuchando en 6379"enabledactive
--- puerto ---
LISTEN 0      511        127.0.0.1:6379       0.0.0.0:*    users:(("redis-server",pid=238594,fd=6))
```

**Qué esperamos:** `enabled` + `active` + escuchando **solo en `127.0.0.1:6379`**.

⚠️ Dos fallos distintos que se ven parecidos: si está `active` pero **no** `enabled`, funciona
hoy y **muere en el primer reinicio del VPS** — y con él las sesiones, la caché y las colas del
portal. Y si escucha en `0.0.0.0`, está expuesto a Internet: en esta máquina eso es una
vulnerabilidad, no una configuración.

**Veredicto:** ✅ **OK** — `enabled` + `active`, escuchando **solo en `127.0.0.1:6379`**. Sobrevive a un reinicio y no está expuesto a Internet.

---

### V12 · Respuesta y configuración

```bash
redis-cli ping 2>&1; echo "--- config ---"; grep -E "^ *(bind|protected-mode|port|requirepass|maxmemory|maxmemory-policy|appendonly|supervised)" /etc/redis/redis.conf /etc/redis.conf 2>/dev/null
```

**Respuesta del servidor:**
```
root@vpsmx-2025092.vpshostgator.com [~]# redis-cli ping 2>&1; echo "--- config ---"; grep -E "^ *(bind|protected-mode|port|requirepass|maxmemory|maxmemory-policy|appendonly|supervised)" /etc/redis/redis.conf /etc/redis.conf 2>/dev/nullPONG--- config ---
/etc/redis/redis.conf:bind 127.0.0.1 -::1
/etc/redis/redis.conf:protected-mode yes/etc/redis/redis.conf:port 6379
/etc/redis/redis.conf:appendonly no
```

**Cómo leer la respuesta:**

| Salida | Lectura |
|--------|---------|
| `NOAUTH Authentication required` | ✅ **Lo correcto.** Tiene `requirepass`. |
| `PONG` | ⚠️ Responde sin autenticar. Hay que poner `requirepass` antes de producción. |
| `Connection refused` | ❌ Instalado pero caído. |
| `command not found` | ❌ No instalado (coherente con V10). |

También hay que fijar `maxmemory` y `maxmemory-policy`. Sin límite, Redis crece hasta agotar la
RAM del VPS y el OOM killer se lleva a MySQL por delante.

**Veredicto:** ❌ **FALTA** — responde `PONG` sin autenticar: **sin `requirepass`**, **sin `maxmemory`** y **sin `maxmemory-policy`**. Y la instancia es compartida con la otra aplicación. Bloqueante **B4**, ver **H6**.

---

### V13 · Qué versión de Redis ofrecen los repositorios

```bash
dnf -q list --available redis 2>/dev/null | tail -3; echo "--- modulos ---"; dnf -q module list redis 2>/dev/null | tail -5
```

**Respuesta del servidor:**
```
root@vpsmx-2025092.vpshostgator.com [~]# dnf -q list --available redis 2>/dev/null | tail -3; echo "--- modulos ---"; dnf -q module list redis 2>/dev/null | tail -5--- modulos ---AlmaLinux 9 - AppStreamName  Stream Profiles   Summary
redis 7      common [d] Redis persistent key-value database
Hint: [d]efault, [e]nabled, [x]disabled, [i]nstalled
```

**Por qué se pregunta:** el stack pide **Redis 7**. AlmaLinux 9 en AppStream suele ofrecer la
rama **6.2**; llegar a 7.x normalmente implica el repositorio Remi. Este comando dice cuál de
los dos caminos toca, en lugar de suponerlo.

> Para lo que este proyecto usa —caché con TTL, locks, colas y sesiones— **6.2 es
> funcionalmente suficiente**. Añadir un repositorio de terceros a la máquina expuesta a
> Internet tiene su propio costo de mantenimiento. Con el resultado de este comando en la mano
> se decide, y la decisión se anota abajo.

**Veredicto:** ✅ **OK** — AppStream ofrece el módulo `redis:7` sin repositorios de terceros; instalado está 6.2.22. Decisión tomada abajo.

**Versión disponible:** instalada **6.2.22** (AppStream); el módulo **`redis:7`** está disponible en AppStream, **sin necesidad de Remi**.

**Decisión: quedarse en 6.2.22.** Cubre todo lo que este proyecto usa —TTL, locks, colas y sesiones— y la instancia **es compartida con `raquetacam`**: subir de rama mayor arrastraría a una aplicación en producción que no es nuestra. El costo de no tener Redis 7 aquí es cero; el de romper el otro sitio, no.

---

### V14 · Extensión `redis` de PHP 8.4

```bash
/opt/cpanel/ea-php84/root/usr/bin/php -m 2>/dev/null | grep -ix redis && echo ">>> OK extension redis presente" || echo ">>> FALTA ea-php84-php-redis"
```

**Respuesta del servidor:**
```
root@vpsmx-2025092.vpshostgator.com [~]# /opt/cpanel/ea-php84/root/usr/bin/php -m 2>/dev/null | grep -ix redis && echo ">>> OK extension redis presente" || echo ">>> FALTA ea-php84-php-redis">>> FALTA ea-php84-php-redis
```

**Qué esperamos:** presente. El `.env` declara `REDIS_CLIENT=phpredis`, que **es esta
extensión**, no el paquete de composer.

**Si falta**, hay dos salidas: instalar `ea-php84-php-redis`, o cambiar el `.env` a
`REDIS_CLIENT=predis` — `predis/predis ^3.5` ya está en `composer.json`. Recomendación:
instalar la extensión; predis es PHP puro y en el endpoint de disponibilidad, que es el más
golpeado, la diferencia se nota.

**Veredicto:** ❌ **FALTA** — derivado de V4. Se resuelve con `ea-php84-php-redis` al instalar 8.4; el respaldo es `REDIS_CLIENT=predis`, ya presente en `composer.json`.

---

## Bloque E — Procesos persistentes

> **Lo que este bloque decide.** [routes/console.php](../routes/console.php) registra
> `disponibilidad:salud-intermedio` con `->everyTenSeconds()`, y el proyecto necesita además un
> worker de colas y un servidor de WebSockets. Los tres son procesos de larga vida: hace falta un
> **gestor de procesos**, que es justo lo que el hosting compartido no permite y este VPS sí.
>
> ⚠️ **Corrección (7-sep-2026).** Una versión previa de este documento afirmaba que el cron de
> cPanel *"no puede"* ejecutar una tarea de 10 segundos. **Es falso desde Laravel 11.** Cuando hay
> tareas sub-minuto, `schedule:run` no termina de inmediato: sigue vivo hasta el final del minuto
> despachándolas — se ve en
> `vendor/laravel/framework/src/Illuminate/Console/Scheduling/ScheduleRunCommand.php:239-288`
> (`repeatEvents()`, que hace *poll* cada 100 ms hasta `endOfMinute`). Un cron de cada minuto
> **sí** atiende `everyTenSeconds()`. La elección entre cron y daemon es un juicio de ingeniería,
> no una imposibilidad técnica; se argumenta en **H2**.

### V15 · Gestor de procesos y servicios del proyecto

```bash
systemctl --version | head -1; echo "--- unidades del proyecto ---"; for U in cl032-schedule cl032-queue cl032-reverb; do systemctl list-unit-files | grep -q "^$U.service" && echo "OK    $U ($(systemctl is-active $U))" || echo "FALTA $U.service"; done
```

**Respuesta del servidor:**
```
root@vpsmx-2025092.vpshostgator.com [~]# systemctl --version | head -1; echo "--- unidades del proyecto ---"; for U in cl032-schedule cl032-queue cl032-reverb; do systemctl list-unit-files | grep -q "^$U.service" && echo "OK    $U ($(systemctl is-active $U))" || echo "FALTA $U.service"; donesystemd 252 (252-67.el9_8.4.alma.1)--- unidades del proyecto ---
FALTA cl032-schedule.service
FALTA cl032-queue.serviceFALTA cl032-reverb.service
```

**Qué esperamos:** un gestor de procesos disponible; los tres servicios del proyecto **faltarán**
— se crean en la Fase 3. Lo que se valida aquí es que **se puedan** crear.

⚠️ **Este comando resultó incompleto.** Solo preguntó por systemd, y la máquina ya tiene
**supervisor** con dos programas en producción (`/etc/supervisord.d/`), descubierto después por
inspección directa. Ver **H2**: la conclusión cambia, y el gestor que se va a usar es supervisor,
no systemd. El comando corregido para futuras validaciones:

```bash
systemctl --version | head -1
command -v supervisord supervisorctl; systemctl is-enabled supervisord 2>&1; ls -1 /etc/supervisord.d/ 2>/dev/null
crontab -l -u USUARIO 2>/dev/null
```

| Servicio | Qué corre | Qué se rompe sin él |
|----------|-----------|--------------------|
| `cl032-schedule` | `php artisan schedule:work` | Las 8 tareas de [routes/console.php](../routes/console.php): salud del Intermedio (10 s), expiración de holds, barrido de leases, reconciliación de cupo, purga, réplica del padrón. **Los holds vencidos no se liberan y el cupo se pierde.** |
| `cl032-queue` | `php artisan queue:work` | Correos, WhatsApp y la salida hacia el mundo privado. |
| `cl032-reverb` | `php artisan reverb:start --port=6003` | El tiempo real del calendario en el front. |

**Veredicto:** ✅ **OK** *(con la verificación ampliada)* — systemd 252 disponible, **y además supervisor ya instalado y en uso** por `raquetacam`, con `autostart`/`autorestart` y `user=`. Sin CloudLinux estorbando (V20). Los tres servicios del portal faltan porque aún no se crean: es lo esperado, se hacen en la Fase 3 — **como programas de supervisor**, siguiendo la convención que la máquina ya tiene. Ver **H2**.

---

### V16 · Cron y procesos en marcha

```bash
systemctl is-active crond; echo "--- artisan corriendo ---"; ps aux | grep -E "artisan (schedule|queue|reverb)" | grep -v grep || echo "(ninguno)"; echo "--- limite de procesos ---"; ulimit -u
```

**Respuesta del servidor:**
```
root@vpsmx-2025092.vpshostgator.com [~]# systemctl is-active crond; echo "--- artisan corriendo ---"; ps aux | grep -E "artisan (schedule|queue|reverb)" | grep -v grep || echo "(ninguno)"; echo "--- limite de procesos ---"; ulimit -uactive--- artisan corriendo ---
raqueta+ 2232142  0.0  0.8 124856 68868 ?        S    Sep03   0:51 /usr/local/bin/ea-php83 /home/raquetacam/public_html/back.raquetacam.com/artisan reverb:start --host=127.0.0.1 --port=6002raqueta+ 2443324  0.0  0.9 202656 74600 ?        S    Sep04   1:48 /usr/local/bin/ea-php83 /home/raquetacam/public_html/back.raquetacam.com/artisan queue:work --queue=notifications-high,notifications,notifications-low --sleep=3 --tries=3 --timeout=90
--- limite de procesos ---
unlimited
```

**Veredicto:** ✅ **OK** *(corregido)* — `crond` activo y `ulimit -u` sin límite. Los daemons de `raquetacam` (`reverb:start --port=6002` y `queue:work`, bajo `ea-php83`) **sí están gestionados**: corren bajo **supervisor**, con `autostart=true`, `autorestart=true` y `user=raquetacam`. La lectura inicial de "daemons sin gestor" era incorrecta — `ps` no dice quién supervisa un proceso. Ver **H2**, que ahora recoge una convención que aprovechar, no un problema que corregir.

---

### V17 · ZeroTier — **el bloqueante mayor**

```bash
command -v zerotier-cli && zerotier-cli info && zerotier-cli listnetworks || echo ">>> ZEROTIER NO INSTALADO"
```

**Respuesta del servidor:**
```
root@vpsmx-2025092.vpshostgator.com [~]# command -v zerotier-cli && zerotier-cli info && zerotier-cli listnetworks || echo ">>> ZEROTIER NO INSTALADO"/sbin/zerotier-cli200 info 1560fa4748 1.16.2 ONLINE200 listnetworks <nwid> <name> <mac> <status> <type> <dev> <ZT assigned ips>
200 listnetworks d5e5fb65379a2dc5 fsg_intranet c6:38:fa:cd:22:b3 OK PRIVATE ztwdjgcshs 192.168.191.69/24
```

**Por qué importa más que Redis.** La regla **R2** dice que el público solo puede alcanzar el
Intermedio, y solo por ZeroTier. Hoy el `.env` apunta a
`DISPONIBILIDAD_INTERMEDIO_URL=http://cl032apidisponibilidad.local`, que en producción no
resuelve a nada. **Sin ZeroTier, el portal levanta pero no tiene disponibilidad que mostrar** —
y por diseño degrada a "no disponible" en lugar de inventar cupo, que es lo correcto pero
inservible para el socio.

**Dependencia externa:** este paso necesita que el **servidor Intermedio
(`CL032APIDISPONIBILIDAD`) ya exista y esté en la red ZeroTier**. Es un servidor que, según
[ARQUITECTURA_SERVIDORES.md](ARQUITECTURA_SERVIDORES.md) §2, *"es nuevo, no existe hoy"*.
Conviene confirmar su estado en paralelo a esta validación, porque es el camino crítico del
proyecto entero.

**Veredicto:** ✅ **OK** — y es la mejor noticia de la validación. ZeroTier **1.16.2 ONLINE**, nodo `1560fa4748`, unido a `fsg_intranet` (`d5e5fb65379a2dc5`) con IP **192.168.191.69/24**. Lo que se anticipaba como el bloqueante mayor ya está resuelto. Ver **H5**.

**Estado del Intermedio:** **pendiente de confirmar.** El salto está listo de este lado —ZeroTier en línea, unido a `fsg_intranet`, IP local `192.168.191.69`— pero falta verificar que `CL032APIDISPONIBILIDAD` exista, esté en la misma red y responda. Comandos en **H5**.

---

## Bloque F — Red y seguridad

### V18 · Puertos, firewall y SELinux

```bash
ss -lntp | grep -vE "127.0.0.1|::1"; echo "--- csf ---"; command -v csf && grep -E "^(TCP_IN|TCP_OUT) " /etc/csf/csf.conf 2>/dev/null || echo "sin CSF"; echo "--- firewalld ---"; systemctl is-active firewalld 2>&1; echo "--- selinux ---"; getenforce 2>&1
```

**Respuesta del servidor:**
```
root@vpsmx-2025092.vpshostgator.com [~]# ss -lntp | grep -vE "127.0.0.1|::1"; echo "--- csf ---"; command -v csf && grep -E "^(TCP_IN|TCP_OUT) " /etc/csf/csf.conf 2>/dev/null || echo "sin CSF"; echo "--- firewalld ---"; systemctl is-active firewalld 2>&1; echo "--- selinux ---"; getenforce 2>&1State  Recv-Q Send-Q Local Address:Port  Peer Address:PortProcess
LISTEN 0      80           0.0.0.0:3306       0.0.0.0:*    users:(("mariadbd",pid=485785,fd=26))LISTEN 0      45           0.0.0.0:2077       0.0.0.0:*    users:(("cpdavd - accept",pid=1416,fd=5))
LISTEN 0      45           0.0.0.0:2078       0.0.0.0:*    users:(("cpdavd - accept",pid=1416,fd=4))
LISTEN 0      45           0.0.0.0:2079       0.0.0.0:*    users:(("cpdavd - accept",pid=1416,fd=3))
LISTEN 0      45           0.0.0.0:2086       0.0.0.0:*    users:(("cpsrvd (SSL",pid=1157,fd=4))
LISTEN 0      45           0.0.0.0:2087       0.0.0.0:*    users:(("cpsrvd (SSL",pid=1157,fd=6))
LISTEN 0      45           0.0.0.0:2080       0.0.0.0:*    users:(("cpdavd - accept",pid=1416,fd=7))
LISTEN 0      45           0.0.0.0:2082       0.0.0.0:*    users:(("cpsrvd (SSL",pid=1157,fd=3))
LISTEN 0      45           0.0.0.0:2083       0.0.0.0:*    users:(("cpsrvd (SSL",pid=1157,fd=5))
LISTEN 0      45           0.0.0.0:2095       0.0.0.0:*    users:(("cpsrvd (SSL",pid=1157,fd=11))
LISTEN 0      45           0.0.0.0:2091       0.0.0.0:*    users:(("cpdavd - accept",pid=1416,fd=6))
LISTEN 0      45           0.0.0.0:2096       0.0.0.0:*    users:(("cpsrvd (SSL",pid=1157,fd=12))
LISTEN 0      5            0.0.0.0:9993       0.0.0.0:*    users:(("zerotier-one",pid=238000,fd=7))
LISTEN 0      128          0.0.0.0:22022      0.0.0.0:*    users:(("sshd",pid=471718,fd=7))
LISTEN 0      4096         0.0.0.0:993        0.0.0.0:*    users:(("imap-login",pid=1827831,fd=10),("imap-login",pid=1827827,fd=10),("dovecot",pid=1218459,fd=42))
LISTEN 0      4096         0.0.0.0:995        0.0.0.0:*    users:(("pop3-login",pid=1827833,fd=10),("pop3-login",pid=1827829,fd=10),("dovecot",pid=1218459,fd=49))
LISTEN 0      50           0.0.0.0:587        0.0.0.0:*    users:(("exim",pid=1218503,fd=7))
LISTEN 0      511          0.0.0.0:443        0.0.0.0:*    users:(("httpd",pid=2764387,fd=5),("httpd",pid=2729074,fd=5),("httpd",pid=2721068,fd=5),("httpd",pid=2682913,fd=5),("httpd",pid=2580778,fd=5),("httpd",pid=2231459,fd=5),("httpd",pid=472337,fd=5))
LISTEN 0      50           0.0.0.0:465        0.0.0.0:*    users:(("exim",pid=1218503,fd=6))
LISTEN 0      4096         0.0.0.0:143        0.0.0.0:*    users:(("imap-login",pid=1827831,fd=9),("imap-login",pid=1827827,fd=9),("dovecot",pid=1218459,fd=41))
LISTEN 0      9            0.0.0.0:21         0.0.0.0:*    users:(("pure-ftpd",pid=238427,fd=4))
LISTEN 0      50           0.0.0.0:25         0.0.0.0:*    users:(("exim",pid=1218503,fd=4))
LISTEN 0      50           0.0.0.0:26         0.0.0.0:*    users:(("exim",pid=1218503,fd=5))
LISTEN 0      128          0.0.0.0:53         0.0.0.0:*    users:(("pdns_server",pid=1218431,fd=7))
LISTEN 0      511          0.0.0.0:80         0.0.0.0:*    users:(("httpd",pid=2764387,fd=3),("httpd",pid=2729074,fd=3),("httpd",pid=2721068,fd=3),("httpd",pid=2682913,fd=3),("httpd",pid=2580778,fd=3),("httpd",pid=2231459,fd=3),("httpd",pid=472337,fd=3))
LISTEN 0      4096         0.0.0.0:4190       0.0.0.0:*    users:(("managesieve-log",pid=1827832,fd=9),("managesieve-log",pid=1827828,fd=9),("dovecot",pid=1218459,fd=46))
LISTEN 0      4096         0.0.0.0:110        0.0.0.0:*    users:(("pop3-login",pid=1827833,fd=9),("pop3-login",pid=1827829,fd=9),("dovecot",pid=1218459,fd=48))
LISTEN 0      4096         0.0.0.0:111        0.0.0.0:*    users:(("rpcbind",pid=238386,fd=4),("systemd",pid=1,fd=148))
LISTEN 0      80              [::]:3306          [::]:*    users:(("mariadbd",pid=485785,fd=29))
LISTEN 0      5                  *:9993             *:*    users:(("zerotier-one",pid=238000,fd=8))
LISTEN 0      128             [::]:22022         [::]:*    users:(("sshd",pid=471718,fd=8))
LISTEN 0      511             [::]:443           [::]:*    users:(("httpd",pid=2764387,fd=6),("httpd",pid=2729074,fd=6),("httpd",pid=2721068,fd=6),("httpd",pid=2682913,fd=6),("httpd",pid=2580778,fd=6),("httpd",pid=2231459,fd=6),("httpd",pid=472337,fd=6))
LISTEN 0      9               [::]:21            [::]:*    users:(("pure-ftpd",pid=238427,fd=5))
LISTEN 0      128             [::]:53            [::]:*    users:(("pdns_server",pid=1218431,fd=8))
LISTEN 0      511             [::]:80            [::]:*    users:(("httpd",pid=2764387,fd=4),("httpd",pid=2729074,fd=4),("httpd",pid=2721068,fd=4),("httpd",pid=2682913,fd=4),("httpd",pid=2580778,fd=4),("httpd",pid=2231459,fd=4),("httpd",pid=472337,fd=4))
LISTEN 0      4096            [::]:111           [::]:*    users:(("rpcbind",pid=238386,fd=6),("systemd",pid=1,fd=150))
--- csf ---
sin CSF
--- firewalld ---
inactive
--- selinux ---
Disabled
```

**Qué mirar:** que Redis y MySQL **no** aparezcan en la lista (solo deben escuchar en
loopback). Y si SELinux está en `Enforcing`, puede bloquear que PHP abra conexiones salientes
— el síntoma es un `curl` que funciona en la terminal y falla desde Laravel, que se diagnostica
mal muchas veces antes de sospechar de SELinux.

**Veredicto:** ❌ **FALTA** — **MariaDB escucha en `0.0.0.0:3306`**, sin CSF, sin firewalld y con SELinux `Disabled`. Bloqueante **B5**, y el de corrección más barata. Redis, en cambio, sí está bien (solo loopback). Ver **H4**.

---

### V19 · Salidas a servicios externos

```bash
for d in smtpdm-ap-southeast-1.aliyun.com:80 graph.facebook.com:443 repo.packagist.org:443; do h=${d%:*}; p=${d#*:}; timeout 6 bash -c "</dev/tcp/$h/$p" 2>/dev/null && echo "OK    $d" || echo "FALLA $d"; done
```

**Respuesta del servidor:**
```
root@vpsmx-2025092.vpshostgator.com [~]# for d in smtpdm-ap-southeast-1.aliyun.com:80 graph.facebook.com:443 repo.packagist.org:443; do h=${d%:*}; p=${d#*:}; timeout 6 bash -c "</dev/tcp/$h/$p" 2>/dev/null && echo "OK    $d" || echo "FALLA $d"; done
OK    smtpdm-ap-southeast-1.aliyun.com:80OK    graph.facebook.com:443
OK    repo.packagist.org:443
```

**Qué se está probando y por qué cada uno:**

| Destino | De dónde sale | Si falla |
|---------|--------------|----------|
| `smtpdm-ap-southeast-1.aliyun.com:80` | `MAIL_HOST` del `.env` | **No salen los correos de confirmación.** El puerto 80 para SMTP es inusual y algunos proveedores lo filtran en la salida. |
| `graph.facebook.com:443` | WhatsApp de Meta (`NOTIFICATION_WHATSAPP_ENABLED=true`) | No salen las notificaciones por WhatsApp. |
| `repo.packagist.org:443` | `composer install` | No se puede desplegar en absoluto. |

**Veredicto:** ✅ **OK** — las tres salidas responden: SMTP de Aliyun `:80`, Meta `:443` y packagist `:443`. Ninguna sorpresa de egreso.

---

### V20 · Límites de la plataforma

```bash
command -v lvectl && lvectl list | head -5 || echo "sin CloudLinux"; echo "--- shells ---"; grep "/home/" /etc/passwd | awk -F: '{print $1" -> "$7}'
```

**Respuesta del servidor:**
```
root@vpsmx-2025092.vpshostgator.com [~]# command -v lvectl && lvectl list | head -5 || echo "sin CloudLinux"; echo "--- shells ---"; grep "/home/" /etc/passwd | awk -F: '{print $1" -> "$7}'
sin CloudLinux--- shells ---
_imunify -> /bin/falseraquetacam -> /usr/local/cpanel/bin/noshell
reservacionvilla -> /usr/local/cpanel/bin/noshell
```

**Por qué:** si HostGator incluye **CloudLinux**, LVE impone techos de procesos y memoria por
cuenta, y **mata procesos de larga vida**. Cambia el diseño del despliegue: los tres daemons se
definen como unidades de systemd a nivel de sistema (no bajo la cuenta), que quedan fuera del
alcance de LVE. Y si la cuenta tiene `jailshell`, el proceso ve un `/home` falso y las rutas
absolutas de las unidades no apuntan a donde uno cree.

**Veredicto:** ✅ **OK** — sin CloudLinux, así que LVE no matará los daemons. Las cuentas usan `noshell`, lo cual **no** impide `User=` en una unidad de systemd: systemd ejecuta el binario directamente, no a través del shell de login. ⚠️ Existe el usuario `_imunify` → hay Imunify360 en la máquina; conviene saber si trae filtrado propio antes de tocar el firewall (**H4**).

---

### V21 · TLS y hostname

```bash
hostname -f; whmapi1 get_autossl_providers | grep -E "display_name|enabled"
```

**Respuesta del servidor:**
```
root@vpsmx-2025092.vpshostgator.com [~]# hostname -f; whmapi1 get_autossl_providers | grep -E "display_name|enabled"vpsmx-2025092.vpshostgator.com
      display_name: Let’s Encrypt™      enabled: 1
```

**Observación ya registrada:** se está entrando a WHM por `http://108.174.154.63:2086`, sin
TLS. La sesión de administración de la única máquina expuesta a Internet viaja en claro.
Conviene resolver hostname + AutoSSL **antes** de capturar en ese panel las credenciales de
producción (MySQL, Redis, secretos HMAC del `.env`).

**Veredicto:** ⚠️ **AVISO** — AutoSSL con Let's Encrypt habilitado, pero el hostname sigue siendo el genérico de HostGator y la administración viaja por `http://IP:2086` en claro.

---

## Resumen de la Fase 0

Ejecutada el **7 de septiembre de 2026** sobre `vpsmx-2025092.vpshostgator.com`.

| # | Verificación | Veredicto | Nota |
|---|--------------|-----------|------|
| V1 | Sistema operativo | ✅ OK | AlmaLinux 9.8, x86_64 |
| V2 | RAM / disco / swap | ✅ OK | 7.5 GB · 171 GB libres · swap 4 GB · 4 vCPU |
| V3 | cPanel y cuentas | ⚠️ AVISO | Cuenta lista, **pero el servidor es compartido** → H1 |
| V4 | PHP 8.4 instalado | ❌ FALTA | Solo hay 8.0–8.3 · **B1** |
| V5 | Extensiones de PHP | ❌ FALTA | Derivado de V4 |
| V6 | Límites php.ini / `disable_functions` | ❌ FALTA | **Sin verificar** · reevaluar tras B1 |
| V7 | composer / git / unzip | ❌ FALTA | Sin composer ni node · **B2** |
| V8 | MySQL ≥ 8 | ❌ FALTA | **MariaDB 10.11 + MyISAM** · **B3** → H3 |
| V9 | Bases existentes | ✅ OK | Base del portal por crear, sin herencia |
| V10 | Redis instalado | ✅ OK | **6.2.22 ya presente** |
| V11 | Redis servicio y puerto | ✅ OK | enabled + active, solo loopback |
| V12 | Redis auth y config | ❌ FALTA | Sin password, sin maxmemory · **B4** → H6 |
| V13 | Versión de Redis disponible | ✅ OK | Módulo 7 disponible; se decide quedarse en 6.2 |
| V14 | Extensión `redis` de PHP | ❌ FALTA | Derivado de V4 |
| V15 | Gestor de procesos | ✅ OK | systemd 252 **y supervisor ya en uso**; servicios por crear (Fase 3) |
| V16 | crond y procesos | ✅ OK | Daemons ajenos **sí gestionados** (supervisor) → H2 |
| V17 | **ZeroTier** | ✅ OK | **ONLINE en `fsg_intranet`, 192.168.191.69** |
| V18 | Firewall / SELinux | ❌ FALTA | **3306 al mundo, sin firewall** · **B5** → H4 |
| V19 | Salidas externas | ✅ OK | Aliyun, Meta y packagist alcanzables |
| V20 | CloudLinux / shells | ✅ OK | Sin LVE; `noshell` no estorba a systemd |
| V21 | TLS / hostname | ⚠️ AVISO | AutoSSL activo; hostname genérico, WHM en claro |

**Resultado:** 12 ✅ · 2 ⚠️ · **7 ❌** *(V16 corregido de ⚠️ a ✅ tras revisar `/etc/supervisord.d/` — ver H2)*

Los 7 fallos se agrupan en **5 causas corregibles** (B1–B5) más **un hallazgo de arquitectura** (H1) que no se resuelve con un comando. Ninguno es insalvable; dos de ellos —B3 y B5— habrían pasado inadvertidos hasta producción y ahí habrían salido caros.

| Id | Bloqueante | Verificaciones que arrastra | Se corrige en |
|----|-----------|------------------------------|---------------|
| **B1** | `ea-php84` no instalado | V4, V5, V6, V14 | Fase 1 |
| **B2** | `composer` no instalado | V7 | Fase 1 |
| **B3** | MariaDB con `MyISAM` por defecto + `'engine' => null` en el código | V8 | Fase 1 y 5 |
| **B4** | Redis sin `requirepass`, sin `maxmemory` y compartido | V12 | Fase 1 y 5 |
| **B5** | MariaDB en `0.0.0.0:3306` sin ningún firewall | V18 | **Antes que nada** |

**Fecha de la validación:** 7 de septiembre de 2026
**Ejecutó:** _(anotar)_

---

# HALLAZGOS

> Seis lecturas que no se ven mirando una verificación aislada, sino cruzando varias.

## H1 · No es un servidor dedicado, es una plataforma multi-proyecto — y tiene la llave de la red privada

**Evidencia:** V3 (dos cuentas de cPanel), V16 (daemons de `raquetacam` en marcha), V17 (ZeroTier unido a `fsg_intranet`), la prueba de aislamiento ejecutada el 7-sep-2026 (abajo) y la confirmación del equipo: **habrá más cuentas de cPanel y más proyectos en esta máquina**, y `raquetacam` migrará también a Redis.

[ARQUITECTURA_SERVIDORES.md](ARQUITECTURA_SERVIDORES.md) describe el «Servidor 4 · Público» como una máquina de un solo propósito, y las reglas **R2** y **R3** están escritas sobre esa premisa.

La realidad confirmada es otra, y más amplia de lo que sugería la primera lectura de la validación: **este VPS es la plataforma compartida de aplicaciones públicas de la casa.** Hoy tiene `raquetacam.com` en producción y la cuenta `reservacionvilla` recién creada y todavía vacía. Mañana, según confirma el equipo, habrá más cuentas de cPanel, más proyectos, y `raquetacam` usando también Redis. Todos comparten máquina, MariaDB, Redis, Apache, PHP y systemd.

Eso reencuadra el problema. No se trata de convivir con un vecino: **la máquina que sostiene la membresía de ZeroTier a `fsg_intranet` es una plataforma de hosting multi-inquilino en crecimiento.** Cada cuenta nueva incorpora una aplicación cuyo compromiso da ejecución en el mismo host donde vive la interfaz `ztwdjgcshs`, y desde ahí se alcanza al Intermedio. **La superficie de ataque hacia la red privada no es fija: crece con el negocio**, y lo hace por decisiones —dar de alta una cuenta— que nadie asocia hoy con la seguridad de Reservaciones.

**El diseño aguanta, y esto conviene decirlo con claridad.** R1 se sostiene (aquí no hay credenciales a las bases privadas), el Intermedio no tiene BD ni datos personales, y el techo del daño sigue siendo el que la arquitectura eligió deliberadamente: números de cupo. Justamente para esto se diseñó así.

Lo que cambió no es el techo, es **la probabilidad de llegar a él** y quién la controla. Y hay un detalle que sube de rango: lo único que separa a un inquilino cualquiera de este host del Intermedio es **R3** —API key más lista blanca de IP—, y **esa API key vive en un archivo del disco compartido**. Por eso los permisos del `.env` dejan de ser higiene y pasan a ser el control de seguridad primario de la frontera.

### Prueba de aislamiento — ejecutada el 7-sep-2026

```
drwx--x--x reservacionvilla reservacionvilla reservacionvilla
cat: /home/reservacionvilla/.env: No such file or directory
```

**Dos lecturas, y la segunda es la que importa:**

- **La prueba no está aprobada: está aplazada.** El `.env` todavía no existe porque no hay proyecto desplegado. Hay que repetirla en la Fase 5, con el archivo ya en su sitio.
- **Aun así ya se ve lo relevante.** `cat` respondió `No such file or directory`, **no** `Permission denied`. Con `drwx--x--x`, cualquier usuario del sistema **puede atravesar** `/home/reservacionvilla`. Es el modo estándar de cPanel y no es un error — pero significa que lo único que impedirá a otro inquilino leer el `.env` son **los permisos del directorio de la aplicación y del propio archivo**, no los del home.

Por lo tanto, al desplegar (Fase 5), y no como recomendación sino como requisito:

```bash
chown -R reservacionvilla:reservacionvilla /home/reservacionvilla/APP
chmod 750 /home/reservacionvilla/APP          # sin permisos para "otros"
chmod 600 /home/reservacionvilla/APP/.env     # solo el dueño
```

Y repetir la prueba, que ahora sí tiene que fallar:

```bash
sudo -u raquetacam cat /home/reservacionvilla/APP/.env 2>&1 | head -2
# esperado: Permission denied
```

### La decisión que sigue abierta

Es de negocio —un VPS extra cuesta dinero—, no técnica. Lo que no es admisible es que no quede escrita:

| Opción | Qué implica |
|--------|-------------|
| **A · VPS dedicado para CL032** | Restaura la premisa de la arquitectura. Es la única opción que **no depende de sostener disciplina operativa a futuro**: cada cuenta nueva en la plataforma compartida vuelve a poner en juego las mitigaciones. |
| **B · Plataforma compartida documentada** | Es el camino que la casa ya eligió de hecho. Exige registrar la desviación de §4 y **sostener las mitigaciones cada vez que se dé de alta una cuenta**. El costo no se paga una vez: es permanente, y recae en quien administre el servidor dentro de dos años. |

**Mitigaciones si se elige B** (ninguna es opcional):

1. **Usuario MySQL propio por cuenta**, con permisos solo sobre su base. cPanel ya lo hace si la base se crea desde el panel; no crearla a mano.
2. **Redis con ACL por proyecto**, no solo con índice distinto (**H6** — el índice ordena, no aísla).
3. **Permisos del `.env` verificados en cada despliegue**, con la prueba de arriba.
4. **Restringir la salida por ZeroTier a los procesos del portal.** Es la única mitigación que ataca la raíz en lugar del síntoma. En Linux cualquier proceso local puede emitir por `ztwdjgcshs`; se acota por dueño del proceso:

```bash
# PROBAR ANTES DE PERSISTIR: deja fuera a cualquier otro inquilino
iptables -A OUTPUT -o ztwdjgcshs -m owner --uid-owner root             -j ACCEPT
iptables -A OUTPUT -o ztwdjgcshs -m owner --uid-owner reservacionvilla -j ACCEPT
iptables -A OUTPUT -o ztwdjgcshs -j REJECT
```

`zerotier-one` corre como `root` y los tres daemons del portal correrán como `reservacionvilla`, así que ambas reglas son necesarias. Verificar la conectividad al Intermedio **antes** de hacerlas persistentes.

5. **Del lado del Intermedio**: que su lista blanca de R3 nombre la IP ZeroTier de este host, y que la API key sea **exclusiva de este proyecto y rotable** — no una compartida entre sistemas.

## H2 · La máquina ya tiene convención de daemons: supervisor — hay que seguirla

> **Corrección.** La versión anterior de este hallazgo decía que los daemons de `raquetacam`
> corrían sin gestor y que en la máquina no había supervisor. **Ambas cosas eran falsas.** La
> verificación V15 solo preguntó por systemd, y de la salida de `ps` deduje una ausencia que `ps`
> no puede mostrar: qué proceso supervisa a otro. La inspección directa de `/etc/supervisord.d/`
> lo desmiente.

**Lo que realmente hay:**

```ini
# /etc/supervisord.d/padel-queue.ini
[program:padel-queue]
command=/usr/local/bin/ea-php83 /home/raquetacam/.../artisan queue:work --queue=notifications-high,notifications,notifications-low --sleep=3 --tries=3 --timeout=90
autostart=true
autorestart=true
user=raquetacam
redirect_stderr=true
stdout_logfile=.../storage/logs/queue.log
stopwaitsecs=3600

# /etc/supervisord.d/padel-reverb.ini
[program:padel-reverb]
command=/usr/local/bin/ea-php83 .../artisan reverb:start --host=127.0.0.1 --port=6002
autostart=true
autorestart=true
user=raquetacam
```

Y el scheduler, por cron de cada minuto:

```
* * * * * cd /home/raquetacam/... && /usr/local/bin/ea-php83 artisan schedule:run >> .../storage/logs/scheduler.log 2>&1
```

**Está bien hecho**, y eso cambia la recomendación. No estamos introduciendo una convención en una
máquina que no la tiene: **estamos llegando a una que ya la tiene**, con `autostart`,
`autorestart`, `user=` por cuenta, logs dentro del proyecto y `stopwaitsecs=3600` para que un
`queue:work` termine el trabajo en curso antes de morir.

### Decisión: supervisor, no systemd

Proponer unidades de systemd solo porque son "más nativas" dejaría la máquina con **dos gestores
de procesos en paralelo** y a quien la administre dentro de dos años preguntándose cuál manda. En
una plataforma compartida (**H1**), la consistencia vale más que la preferencia técnica. Se sigue
la casa:

```ini
# /etc/supervisord.d/cl032-queue.ini   (y cl032-reverb.ini, cl032-schedule.ini)
[program:cl032-queue]
command=/opt/cpanel/ea-php84/root/usr/bin/php /home/reservacionvilla/APP/artisan queue:work --sleep=3 --tries=3 --timeout=90
directory=/home/reservacionvilla/APP
autostart=true
autorestart=true
user=reservacionvilla
redirect_stderr=true
stdout_logfile=/home/reservacionvilla/APP/storage/logs/queue.log
stopwaitsecs=3600
```

Dos ajustes respecto a la plantilla del vecino:

- **`--port=6003`** en Reverb, no 6002 (ver el registro de puertos).
- **Ruta a `ea-php84`**, no a `/usr/local/bin/ea-php83`. Cada cuenta corre su versión.

### Sobre el scheduler: cron o daemon

`raquetacam` usa `schedule:run` por cron cada minuto, que es el patrón estándar de Laravel. **Y
para nuestras tareas de 10 segundos también funcionaría** — ver la corrección al inicio del Bloque
E: desde Laravel 11 `schedule:run` se queda vivo hasta el final del minuto atendiendo las tareas
sub-minuto.

Aun así, para este proyecto se recomienda **`schedule:work` bajo supervisor**, por dos razones
concretas y no por purismo:

1. **El valor del ping de 10 s es su frescura.** Con cron, cada minuto hay un arranque de
   framework antes del primer tick; bajo carga ese hueco crece. Con un proceso vivo, el intervalo
   es parejo.
2. **Es lo que el propio código documenta.** [routes/console.php](../routes/console.php) dice, en
   varias tareas, *"Requiere `schedule:work`"*. Desviarse obligaría a corregir esos comentarios y
   a explicar la excepción; seguirlos no cuesta nada.

Es una preferencia argumentada, no un requisito. Si se prefiere alinear al 100 % con el vecino,
cron funciona — pero entonces hay que actualizar los comentarios de `routes/console.php` para que
el código no afirme algo que dejó de ser cierto.

### Lo que sí conviene revisar del lado del vecino

- **`supervisord` habilitado al arranque:** `systemctl is-enabled supervisord`. Si no lo está, un
  reinicio del VPS deja sin colas a los dos proyectos.
- **Rotación de logs.** Los `stdout_logfile` apuntan a `storage/logs/` y supervisor no rota por sí
  solo. Con `LOG_LEVEL=debug` eso llena disco; ver el anexo del `.env`.

## H3 · MariaDB con MyISAM por defecto — y el código lo hereda

**Dos hechos que por separado no alarman, y juntos sí:**

1. El servidor reporta `default_storage_engine = MyISAM` (V8).
2. [config/database.php:61](../config/database.php#L61) tiene `'engine' => null` (y lo mismo la conexión `mariadb`, línea 81).

`engine => null` significa que Laravel **no emite cláusula `ENGINE=`** en sus `CREATE TABLE`. Sin cláusula, manda el valor por defecto del servidor. Es decir: **si se migra hoy contra esta máquina, las tablas del portal nacen en MyISAM** — sin transacciones, sin `lockForUpdate`, sin claves foráneas.

Y toda la defensa antisobreventa de §8 es exactamente eso: una transacción con `lockForUpdate` que recalcula el cupo con el candado puesto. Sobre MyISAM, `beginTransaction()` **no falla: no hace nada**, en silencio. Es la deuda **D3** que la arquitectura señala como crítica en el maestro, a punto de reproducirse en el portal desde la primera migración.

**Corrección (Fases 1 y 5, no negociable):**

- `'engine' => 'InnoDB'` en la conexión que se use, o parametrizarlo por `.env`.
- Verificar después de migrar que no quede ni una tabla en MyISAM:

```sql
SELECT TABLE_NAME, ENGINE FROM information_schema.TABLES
 WHERE TABLE_SCHEMA = DATABASE() AND ENGINE <> 'InnoDB';
```

**El tema de fondo es que no es MySQL.** El stack pide MySQL 8.4 LTS y hay MariaDB 10.11.19:

| | Consideración |
|---|---|
| **A favor de aceptarla** | Laravel 13 trae driver `mariadb` de primera clase. 10.11 es LTS con soporte hasta feb-2028. InnoDB y `lockForUpdate` funcionan igual. Y **no hay replicación con las bases privadas** —el diseño es API + caché—, así que no se necesita compatibilidad binaria con el MySQL 8.0 de los maestros. |
| **En contra** | Diverge del documento de stack, y el `utf8mb3` por defecto obliga a forzar `utf8mb4` explícitamente en cada creación. |
| **Contra sustituirla** | El motor es **del servidor**, no de la cuenta: cambiarlo afecta también a `raquetacam` (**H1**). Es una operación de riesgo sobre un sistema en producción que no es nuestro. |

**Recomendación: aceptar MariaDB 10.11**, con `DB_CONNECTION=mariadb`, InnoDB forzado y `utf8mb4` explícito — y **actualizar [STACK_CL032BACK.md](../../STACK_CL032BACK.md) §1.3** para que refleje la realidad en lugar de contradecirla. Cambiar el motor del servidor por alinearse con un documento sería pagar un riesgo alto por un beneficio que este proyecto no usa.

Nota aparte: `max_connections = 151` se reparte entre las dos aplicaciones. Con tres daemons más PHP-FPM conviene dimensionar antes de abrir al público.

## H4 · MariaDB abierta a Internet, sin ningún firewall — corregir primero

```
LISTEN 0 80 0.0.0.0:3306 ... users:(("mariadbd",...))
--- csf ---       sin CSF
--- firewalld --- inactive
--- selinux ---   Disabled
```

En la máquina que la arquitectura define como *"la única superficie expuesta a Internet"*, y cuyo compromiso *"se asume, no se descarta"*, el puerto **3306 está abierto al mundo** y no hay **ningún** filtrado: ni CSF, ni firewalld, ni SELinux. También quedan expuestos el 21 (FTP) y el 111 (rpcbind), que ningún componente de este proyecto necesita.

Esta no es deuda de nuestro despliegue —la heredamos—, pero **pasa a ser nuestro problema desde el momento en que esta máquina tiene ruta a `fsg_intranet`**, que ya la tiene (V17).

Es el hallazgo con mejor relación esfuerzo/beneficio de toda la validación. Dos comprobaciones antes de tocar nada:

```bash
mysql -NBe "SELECT user,host FROM mysql.user WHERE host NOT IN ('localhost','127.0.0.1','::1');"
imunify360-agent version 2>/dev/null || echo "sin agente imunify"
```

La primera dice si alguien conecta a MariaDB desde fuera —si `raquetacam` lo hace, cerrar el puerto la tumba—. La segunda, si Imunify360 (V20) ya aporta filtrado propio. Con esas dos respuestas: `bind-address = 127.0.0.1` y CSF con una lista blanca corta.

## H5 · ZeroTier listo de este lado; falta confirmar el otro extremo

La mejor noticia de la validación: la pieza que se anticipaba como bloqueante mayor **ya está resuelta**. `zerotier-one` **1.16.2 ONLINE**, nodo `1560fa4748`, red `fsg_intranet` (`d5e5fb65379a2dc5`) en estado `OK PRIVATE`, IP **192.168.191.69/24**.

Falta la contraparte. La arquitectura dice que el Intermedio *"es nuevo, no existe hoy"*, y esta validación no puede responder por él. Desde esta misma terminal:

```bash
for ip in $(seq 1 254); do (ping -c1 -W1 192.168.191.$ip >/dev/null 2>&1 && echo "vivo: 192.168.191.$ip") & done; wait
```

Y una vez identificado el Intermedio:

```bash
curl -sS -o /dev/null -w 'HTTP %{http_code} en %{time_total}s\n' --max-time 10 http://IP_INTERMEDIO/api/salud
```

**Sin Intermedio no hay proyecto**: el portal levanta, pero degrada a "no disponible" en cada consulta de cupo — que es el comportamiento correcto por diseño, y aun así inservible para el socio. Es el camino crítico, y conviene atenderlo **en paralelo** a las Fases 1–3, no después de ellas.

## H6 · Redis compartido — y va a estarlo más

Redis está sano (V10, V11), pero es **una sola instancia para toda la plataforma** — y el equipo confirma que `raquetacam` también migrará a Redis, además de los proyectos que vengan. Lo que hoy es un vecino serán varios.

Los valores por defecto de Laravel los ponen a chocar de entrada:

- [config/database.php:162](../config/database.php#L162): `REDIS_DB` → **`0`**. Línea 175: `REDIS_CACHE_DB` → **`1`**. Son los defaults de cualquier instalación, así que cada proyecto nuevo aterriza en los mismos índices.
- El prefijo sale de `APP_NAME`, y el `.env` de este proyecto todavía dice `APP_NAME=Laravel`. Dos Laravel sin tocar esa clave comparten prefijo.

La consecuencia concreta: `php artisan cache:clear` ejecuta un **`FLUSHDB`**, que vacía la base completa de Redis, no solo las llaves de quien lo lanzó. Un despliegue rutinario de cualquier proyecto tiraría las sesiones y la caché de los demás. Con `QUEUE_CONNECTION=redis`, también trabajos encolados.

**Índice y prefijo no bastan, y conviene ser exacto sobre por qué:** el índice *ordena*, no *aísla*. Cualquier cliente autenticado puede `SELECT 3` y leer el espacio de otro proyecto. Y con un `requirepass` único, todos los inquilinos comparten la misma contraseña — es decir, ninguno está protegido de otro.

**La solución correcta está disponible aquí:** Redis 6.2 tiene **ACL**, y Laravel las soporta sin parches — [config/database.php:160](../config/database.php#L160) ya lee `REDIS_USERNAME`. Un usuario por proyecto, acotado a su prefijo de llaves:

```bash
redis-cli ACL SETUSER cl032 on '>CONTRASENA_LARGA' '~cl032_*' '&cl032_*' \
  +@all -flushall -flushdb -keys -shutdown -config -acl -debug -replicaof
redis-cli ACL SAVE
```

El patrón `~cl032_*` **impide leer las llaves de otro proyecto en cualquier índice**, que es la garantía que el índice por sí solo no da. `&cl032_*` hace lo mismo con los canales de pub/sub (verificar contra Reverb al probar).

**Dos concesiones honestas de este esquema:**

- Con `-flushdb`, `php artisan cache:clear` falla. Es el precio de que nadie pueda vaciar la caché ajena. Si se prefiere conservarlo, se permite `flushdb` y se asigna a cada proyecto un índice exclusivo — pero entonces la protección contra un borrado ajeno queda en la disciplina, no en el motor, porque las ACL de Redis **no pueden restringir `SELECT`**.
- La confidencialidad sí queda cubierta por el patrón de llaves en ambos casos. Lo que se negocia es la **disponibilidad**, no la lectura de datos ajenos.

Antes de nada, ver qué ocupa el vecino:

```bash
redis-cli INFO keyspace
redis-cli ACL LIST
```

Y fijar en la Fase 5, como mínimo:

```
APP_NAME="Reservaciones en Linea"
REDIS_DB=10
REDIS_CACHE_DB=11
REDIS_PREFIX=cl032_
REDIS_USERNAME=cl032
REDIS_PASSWORD=(generar)
```

⚠️ Poner `requirepass` (B4) **rompe a `raquetacam` si ya usa Redis sin contraseña**. Hay que coordinarlo con ese proyecto, no aplicarlo por sorpresa: es exactamente el tipo de cambio que la plataforma compartida vuelve delicado.

---

## Orden de atención sugerido

El orden no es por dificultad, sino por lo que cuesta descubrirlo tarde.

| # | Acción | Por qué va aquí |
|---|--------|-----------------|
| 1 | **H4** — cerrar 3306 y poner firewall | Riesgo activo **ahora mismo**, y la máquina ya tiene ruta a la red privada. Es de las correcciones más baratas. |
| 2 | **H1** — decidir VPS dedicado o convivencia documentada | Condiciona todo lo demás. Si se elige A, varias de estas correcciones cambian de sitio. |
| 3 | **H5** — confirmar el Intermedio | Camino crítico del proyecto. Puede avanzar en paralelo a lo demás. |
| 4 | **B1 + B2** — PHP 8.4, sus extensiones y composer | Sin esto no hay `composer install`; y con 8.4 arriba se cierran V5, V6 y V14. |
| 5 | **H3** — InnoDB forzado y decisión sobre MariaDB | Antes de la primera migración. Después cuesta mucho más. |
| 6 | **H6 + B4** — segregar y asegurar Redis | Antes de poner sesiones y colas encima. |
| 7 | **V21** — hostname y TLS de WHM | Antes de capturar credenciales de producción en el panel. |
| 8 | Confirmar `systemctl is-enabled supervisord` | Si no arranca con el sistema, un reinicio deja sin colas a todos los proyectos (**H2**). |

---

# CONVENCIÓN DE PLATAFORMA COMPARTIDA

> Nace de **H1**: este servidor no aloja un proyecto, aloja varios y va a alojar más. Sin un
> registro común, el tercer proyecto choca con el segundo por accidente — un puerto repetido, un
> índice de Redis compartido, una unidad de systemd con el mismo nombre. **Este apartado no es
> solo de CL032**: se mantiene para todo el servidor, y conviene que lo conozca quien dé de alta
> la siguiente cuenta.

## Registro de puertos

| Puerto | Ocupa | Estado |
|--------|-------|--------|
| 3306 | MariaDB | ⚠️ Expuesto a Internet — ver **H4** |
| 6379 | Redis (compartido) | ✅ Solo loopback |
| 6002 | Reverb de `raquetacam` | ✅ En uso desde el 3-sep-2026 |
| **6003** | **Reverb de CL032** | Reservado — ya está en el `.env` |
| 22022 | SSH | ✅ |

**Regla:** cada proyecto nuevo con Reverb toma el siguiente puerto libre a partir de 6004 y lo
anota aquí antes de usarlo.

## Registro de índices y usuarios de Redis

| Proyecto | `REDIS_DB` | `REDIS_CACHE_DB` | Prefijo | Usuario ACL |
|----------|-----------|------------------|---------|-------------|
| `raquetacam` | 0 / 1 (por confirmar) | | por confirmar | — |
| **CL032** | **10** | **11** | `cl032_` | `cl032` |

Confirmar lo del vecino con `redis-cli INFO keyspace` antes de fijar nada.

## Gestor de procesos y nomenclatura

**El gestor de la casa es supervisor** (`/etc/supervisord.d/*.ini`), no systemd — ver **H2**. No
introducir un segundo gestor en la máquina.

`cl0XX-<función>.ini` con `[program:cl0XX-<función>]` — para este proyecto: `cl032-schedule`,
`cl032-queue`, `cl032-reverb`. Siempre con `autostart=true`, `autorestart=true`, `user=` la cuenta
de cPanel correspondiente y `stdout_logfile` dentro de `storage/logs/` del proyecto.

| Proyecto | Programas | PHP |
|----------|-----------|-----|
| `raquetacam` | `padel-queue`, `padel-reverb` + `schedule:run` por cron | `ea-php83` |
| **CL032** | `cl032-queue`, `cl032-reverb`, `cl032-schedule` | `ea-php84` |

**Pendiente de la plataforma:** confirmar `systemctl is-enabled supervisord`. Si supervisor no
arranca con el sistema, un reinicio deja sin colas a **todos** los proyectos a la vez.

## Versión de PHP por cuenta

| Cuenta | Versión | Nota |
|--------|---------|------|
| `raquetacam` | `ea-php83` | No tocar: es producción ajena |
| `reservacionvilla` | **`ea-php84`** | Por instalar (**B1**) |
| *(sistema)* | `ea-php82` | Default de WHM; irrelevante si cada cuenta fija la suya |

Instalar `ea-php84` es **aditivo**: no altera la versión de las cuentas existentes. Es seguro
hacerlo sin coordinar con `raquetacam`.

## Cambios que exigen coordinación con los otros proyectos

Tres de las correcciones de esta validación **no son unilaterales**:

| Cambio | A quién afecta | Por qué |
|--------|---------------|---------|
| `requirepass` en Redis (**B4**) | A todo el que use Redis | Rompe a cualquier cliente que hoy conecte sin contraseña |
| Cerrar 3306 / instalar CSF (**H4**) | A quien conecte a MariaDB desde fuera | Verificar con la consulta a `mysql.user` de H4 |
| Cambiar el motor de base de datos (**H3**) | A todas las cuentas | Es del servidor, no de la cuenta — y por eso se recomienda **no** hacerlo |

---

# FASES 1 A N · INSTALACIÓN Y DESPLIEGUE

> **Pendientes de redactar hasta cerrar la Fase 0.** El esqueleto ya está decidido; los comandos
> concretos dependen de lo que arroje la validación.

| Fase | Contenido | Depende de |
|------|-----------|-----------|
| **1** | Preparación del sistema: PHP 8.4 + extensiones, Redis (versión según V13), composer | V4, V5, V13, V14 |
| **2** | Cuenta de cPanel, base de datos, usuario MySQL, `document_root` a `public/` | V3, V8, V9 |
| **3** | Los tres programas de supervisor (`cl032-*.ini`) + rotación de logs | V15, V16, V20, H2 |
| **4** | ZeroTier y verificación del salto al Intermedio | V17 |
| **5** | `.env` de producción (ver más abajo) y `php artisan optimize` | Todas |
| **6** | AutoSSL, HTTPS forzado, proxy WSS para Reverb, endurecimiento (S1–S8) | V18, V21 |
| **7** | Pruebas de aceptación y plan de reversa | — |

## Anexo · Diferencias `.env` local → producción (identificadas hoy)

El [.env](../.env) actual es de desarrollo. Estas son las claves que **cambian obligatoriamente**;
se cierran en la Fase 5, pero conviene tenerlas a la vista desde ahora:

| Clave | Local (hoy) | Producción | Por qué |
|-------|-------------|-----------|---------|
| `APP_ENV` | `local` | `production` | |
| `APP_DEBUG` | `true` | **`false`** | Medida **S5**. Un `true` filtra el `.env` completo —incluidos los secretos HMAC— en cualquier *stack trace*. |
| `APP_URL` | `http://cl032back.local` | dominio real HTTPS | |
| `CACHE_STORE` | `file` | `redis` | |
| `SESSION_DRIVER` | `file` | `redis` | |
| `QUEUE_CONNECTION` | `database` | `redis` | |
| `DB_USERNAME` | `root` | usuario de cPanel con permisos mínimos | Nunca root en producción. |
| `REDIS_PASSWORD` | — | obligatoria | Ver V12. |
| `DISPONIBILIDAD_INTERMEDIO_URL` | `http://cl032apidisponibilidad.local` | IP ZeroTier del Intermedio | Ver V17. |
| `SINCRONIZACION_IP_ALLOWLIST_HABILITADA` | `true` (local) | **`false`** | Reservaciones V2 **no está en ZeroTier** (solo el Intermedio): sale por Internet con IP pública dinámica y ninguna allowlist puede casar. Ver [04-SEGURIDAD-Y-FIRMA.md](replicacion/04-SEGURIDAD-Y-FIRMA.md) §4.1.1. |
| `SINCRONIZACION_IPS_PERMITIDAS` | `127.0.0.1,::1` | vaciar | Sin efecto con la allowlist apagada. Dejar `127.0.0.1` **y** encender la capa rechaza todo tráfico legítimo. |
| `INVITADOS_IPS_PERMITIDAS` | `127.0.0.1,::1` | IP de Tlalpan | Igual que la anterior. |
| `CORS_ALLOWED_ORIGINS` | `localhost:5173` | dominio del front | |
| `SANCTUM_STATELESS_DOMAINS` | `cl032back.local` | dominio real | |
| `REVERB_HOST` / `REVERB_SCHEME` | `127.0.0.1` / `http` | dominio / `https` | El navegador exige `wss://` desde una página HTTPS. |
| `LOG_LEVEL` | `debug` | `warning` o `error` | En `debug` el disco se llena. |
| `*_HMAC_SECRET` (4 claves) | valores de desarrollo | **regenerar** | Un secreto que estuvo en una máquina de desarrollo ya no es secreto. |
| `PADRON_REPLICA_HABILITADA` | `true` | **`false` al arrancar** | [routes/console.php](../routes/console.php) documenta que esta tarea *arranca apagada* hasta verificar los lectores de correo y celular. |

---

## Pendientes de decisión

- [x] ~~**Redis 6.2 de AppStream o 7.x de Remi**~~ → **Resuelto en V13: se queda en 6.2.22.**
- [ ] **Extensión `redis` o `predis`** — no resuelto: depende de instalar `ea-php84` primero (B1).
- [ ] **Dónde se compila `CL032FRONT`** — ¿node en este servidor, o se sube el `dist/` ya
      construido? Lo segundo evita instalar la cadena de node en la máquina expuesta.
- [ ] **Estado del servidor Intermedio** — camino crítico. El lado del portal ya está listo (V17); falta la contraparte. Ver **H5**.
- [x] ~~**Migración de datos de `asturias_api`**~~ → **No aplica: V9 confirma que la base no existe en este servidor.** Se crea limpia.
- [ ] **Respaldos** — no hay política definida para este servidor.
- [ ] **¿VPS dedicado, o plataforma compartida documentada?** — decisión de negocio abierta por **H1**, y la de mayor alcance de toda la lista. Ya no es "convivir con `raquetacam`": el equipo confirma que habrá más cuentas y más proyectos, así que se está decidiendo si la frontera con la red privada vive en una máquina multi-inquilino **de forma permanente**.
- [ ] **Repetir la prueba de permisos del `.env`** en la Fase 5 — hoy quedó aplazada porque el proyecto aún no está desplegado (**H1**).
- [ ] **Coordinar `requirepass` de Redis con `raquetacam`** antes de aplicarlo (**H6**).
- [ ] **¿Se acepta MariaDB 10.11 en lugar de MySQL 8.4?** — abierta por **H3**. Si se acepta, hay que actualizar el documento de stack.
- [ ] **Dónde se compila el front** — V7 confirma que no hay `node`/`npm`. Subir el `dist/` ya construido evita instalar la cadena de node en la máquina expuesta.
- [ ] **Sentry** — `sentry/sentry-laravel` está en `composer.json` pero no hay DSN en el `.env`.
      Sin él, el servidor público queda sin observabilidad de errores.

---

**Documento iniciado:** 7 de septiembre de 2026
**Referencias:** [ARQUITECTURA_SERVIDORES.md](ARQUITECTURA_SERVIDORES.md) ·
[STACK_CL032BACK.md](../../STACK_CL032BACK.md) · [routes/console.php](../routes/console.php)
