# Cloudflare (DNS + seguridad) + Sentry (observabilidad) — Plan de implementación

> **Para el ejecutor:** Este es un runbook de infraestructura, no una feature de código pura. Los pasos usan checkbox (`- [ ]`). Cada fase trae **verificación** y **rollback**. La parte de Sentry (código) puede ejecutarse con `superpowers:subagent-driven-development`; la de Cloudflare es dashboard + config de servidor por SSH.

**Objetivo:** Poner Credify GO detrás de Cloudflare (DNS + WAF/DDoS/rate-limit + CDN) sin perder ningún registro DNS existente, y añadir Sentry para capturar errores de backend (Laravel) y frontend (PWA Vue) en producción.

**Decisiones tomadas:** Cloudflare **plan Free** (con ruta a Pro para el OWASP Core Ruleset). Sentry **SaaS nube** con scrubbing estricto de PII.

**Arquitectura:** No se migra nada del stack. Cloudflare se antepone como proxy inverso (naranja) delante de la VM Azure actual (`20.201.122.12`, Apache + php-fpm). Sentry se integra vía SDK (composer + npm); los source maps se suben en build (WSL) y **no** se publican en prod.

**Orden recomendado:** **Sentry primero** (bajo riesgo, reversible, da visibilidad de errores) → **Cloudflare después** (cambio de nameservers, mayor riesgo, ya observable con Sentry). Son independientes; se pueden hacer por separado.

---

## Contexto real auditado (2026-08-13)

| Ítem | Valor actual |
|---|---|
| Nameservers | `ns19.domaincontrol.com`, `ns20.domaincontrol.com` (GoDaddy) |
| A `credifygo.com` | `20.201.122.12` (TTL 3600, directo) |
| `www` | CNAME → `credifygo.com` → A `20.201.122.12` |
| AAAA / MX apex | ninguno |
| TXT apex | `google-site-verification=3wnlNwA4OXu-xrD4GqdOyJSlLUsU4A8wih25im4J2as` (GSC) |
| `_dmarc` | `v=DMARC1; p=quarantine; adkim=r; aspf=r; rua=mailto:dmarc_rua@onsecureserver.net;` |
| `resend._domainkey` | `p=MIGfMA0…` (DKIM Resend ✓) |
| `send.credifygo.com` MX | `10 feedback-smtp.sa-east-1.amazonses.com` (Resend ✓) |
| `send.credifygo.com` TXT | `v=spf1 include:dc-fd741b8612._spfm.send.credifygo.com ~all` (Resend ✓) |
| Cert TLS origen | Let's Encrypt (válido hasta 2026-11-06), autorrenovado por certbot |
| Server | Apache (sin Cloudflare) |
| Repo | Sin `sentry/sentry-laravel`, sin `@sentry/*`, Vite sin `build.sourcemap`, `bootstrap/app.php` con `withExceptions(...)->report()` |

**Regla de oro de la migración DNS:** en Cloudflare hay que recrear **exactamente** todos los registros de arriba (sobre todo los de Resend y el TXT de GSC) **antes** de cambiar los nameservers. Un registro perdido = correo o verificación caídos.

---

# TRACK A — Sentry (hacer primero)

**Responsable de cuenta:** el usuario crea la organización/proyectos en sentry.io y entrega los secretos (DSN ×2, Auth Token, slugs). Yo cableo el código. (No creo cuentas.)

## Fase A0 — Provisión en Sentry (usuario)

- [ ] Crear org en sentry.io (región **EU** recomendada por residencia de datos; o US). Free tier.
- [ ] Crear **2 proyectos**: `credify-laravel` (plataforma: Laravel/PHP) y `credify-pwa` (plataforma: Vue).
- [ ] Copiar los **2 DSN** (uno por proyecto).
- [ ] Crear un **Auth Token** (Settings → Auth Tokens) con scope `project:releases` + `org:read` para subir source maps en build. Anotar **org slug** y **project slug** del PWA.
- [ ] Entregarme: `SENTRY_LARAVEL_DSN`, `VITE_SENTRY_DSN`, `SENTRY_AUTH_TOKEN`, org slug, project slug PWA.

## Fase A1 — Backend Laravel (código)

**Archivos:** `composer.json` (require), `config/sentry.php` (nuevo, publicado), `bootstrap/app.php` (modificar), `.env` (dev) + `.env` prod.

- [ ] **Instalar el SDK**
```bash
composer require sentry/sentry-laravel
php artisan sentry:publish --dsn="<SENTRY_LARAVEL_DSN>"
```
(Genera `config/sentry.php` y añade las claves `SENTRY_*` al `.env`.)

- [ ] **Enganchar la captura en `bootstrap/app.php`** (Laravel 11+ requiere esto explícito; ya hay un bloque `withExceptions`):
```php
// arriba
use Sentry\Laravel\Integration;

->withExceptions(function (Exceptions $exceptions) {
    Integration::handles($exceptions); // ← añadir como primera línea del closure
    // … el ->report(TypeError …) existente se mantiene debajo
})
```

- [ ] **Scrubbing de PII en `config/sentry.php`** (fintech: nunca enviar montos/documentos):
```php
'send_default_pii' => false,
'before_send' => function (\Sentry\Event $event): ?\Sentry\Event {
    $request = $event->getRequest();
    if (isset($request['data']) && is_array($request['data'])) {
        foreach (['password','password_confirmation','monto','amount','saldo',
                  'documento','cedula','nit','telefono','phone','email'] as $k) {
            if (array_key_exists($k, $request['data'])) {
                $request['data'][$k] = '[filtered]';
            }
        }
        $event->setRequest($request);
    }
    return $event;
},
```

- [ ] **`.env` (dev y prod)**:
```
SENTRY_LARAVEL_DSN=<dsn laravel>
SENTRY_TRACES_SAMPLE_RATE=0.2
SENTRY_PROFILES_SAMPLE_RATE=0
SENTRY_SEND_DEFAULT_PII=false
SENTRY_ENVIRONMENT=production        # en dev: local
# SENTRY_RELEASE lo inyecta el deploy (SHA)
```

- [ ] **PHPStan L5 + Pint** sobre los cambios (preflight obligatorio antes de push — ver `credify-ci-phpstan-preflight`):
```bash
vendor/bin/pint app/ config/ bootstrap/
vendor/bin/phpstan analyse --level=5 app/ config/ bootstrap/
```

- [ ] **Verificación local**: `php artisan sentry:test` → aparece un evento de prueba en el proyecto `credify-laravel`.

## Fase A2 — Frontend PWA Vue (código + source maps)

**Archivos:** `package.json`, `resources/js/pwa/main.js` (init), `vite.config.js` (sourcemap + plugin Sentry).

- [ ] **Instalar SDK + plugin de build**
```bash
npm i @sentry/vue @sentry/vite-plugin
```

- [ ] **Init en `resources/js/pwa/main.js`** (antes de `app.mount`), reutilizando el `BUILD_INFO` que ya existe como release:
```js
import * as Sentry from '@sentry/vue';
// … tras crear `app`, antes de mount:
if (import.meta.env.VITE_SENTRY_DSN) {
  Sentry.init({
    app,
    dsn: import.meta.env.VITE_SENTRY_DSN,
    environment: import.meta.env.MODE === 'production' ? 'production' : 'local',
    release: __BUILD_INFO__.version,      // ya definido por vite.config
    tracesSampleRate: 0.1,
    // Sin Session Replay por ahora (PII). Si se activa luego:
    // maskAllText: true, blockAllMedia: true, maskAllInputs: true
    sendDefaultPii: false,
  });
}
```

- [ ] **`vite.config.js`**: activar sourcemaps ocultos + subir a Sentry y **borrarlos** de `public/build` tras subir (nunca servir `.map` en prod):
```js
import { sentryVitePlugin } from '@sentry/vite-plugin';
// en defineConfig:
build: { sourcemap: 'hidden' /* + resto de tu build */ },
plugins: [
  // … laravel(), vue(), VitePWA(), writeBuildVersion
  process.env.SENTRY_AUTH_TOKEN && sentryVitePlugin({
    org: '<org-slug>',
    project: '<project-slug-pwa>',
    authToken: process.env.SENTRY_AUTH_TOKEN,
    release: { name: BUILD_INFO.version },
    sourcemaps: { filesToDeleteAfterUpload: ['./public/build/**/*.map'] },
  }),
].filter(Boolean),
```

- [ ] **Secreto de build**: `SENTRY_AUTH_TOKEN` vive **solo en el entorno de build (WSL/CI), nunca commiteado ni en prod**. Como el build siempre corre en WSL (nunca en la VM — ver `credify-prod-vm`), los `.map` se suben desde ahí y jamás llegan a prod. Añadir `SENTRY_AUTH_TOKEN` a `~/.bashrc` de WSL o exportarlo antes de `npm run build`.

- [ ] **Verificación local**: `npm run build` sube source maps a `credify-pwa` (log del plugin) y **no** deja `.map` en `public/build`:
```bash
npm run build && find public/build -name '*.map' | head   # debe salir vacío
```

## Fase A3 — Deploy backend + release por SHA

- [ ] Añadir al script de deploy (tras `git pull`, antes de `optimize`) la inyección del release:
```bash
SHA=$(git rev-parse --short HEAD)
sed -i "s#^SENTRY_RELEASE=.*#SENTRY_RELEASE=${SHA}#" .env || echo "SENTRY_RELEASE=${SHA}" >> .env
```
- [ ] Deploy backend-only (ver `credify-prod-vm`): `git pull` → `composer install --no-dev --optimize-autoloader` → `php artisan optimize` → `sudo chown -R www-data:www-data storage/framework/views` → `sudo systemctl reload php8.3-fpm` → `php artisan queue:restart`.
- [ ] Subir `public/build` nuevo (con Sentry init compilado) por scp (swap de assets — **usar `sudo -n rm -rf public/build.old`** por el gotcha de `www-data`, ver `credify-prod-vm`).

## Fase A4 — Verificación en prod

- [ ] Backend: `php artisan sentry:test` en la VM → evento en `credify-laravel` con `environment=production` y `release=<SHA>`.
- [ ] Frontend: forzar un error de prueba temporal en la PWA (o `Sentry.captureMessage('test')` desde consola) → evento en `credify-pwa` con **stack legible** (source maps aplicados).
- [ ] Confirmar scrubbing: el evento de prueba **no** contiene `monto`/`documento`/etc.
- [ ] Configurar una **alerta** (email) en cada proyecto: "notificar en cada nueva issue".
- [ ] **CHANGELOG.**

## Rollback Sentry
Trivial y sin riesgo: quitar `SENTRY_LARAVEL_DSN`/`VITE_SENTRY_DSN` del `.env` (el SDK no envía sin DSN) o revertir el commit. No afecta el funcionamiento de la app.

---

# TRACK B — Cloudflare (después de Sentry)

**Responsable de cuenta:** el usuario crea la cuenta Cloudflare y hace el cambio de nameservers en GoDaddy (yo no creo cuentas ni tengo acceso a GoDaddy). Yo hago la **config del servidor de origen** por SSH (Apache real-IP, TrustProxies, cert de origen, firewall) y te guío en el dashboard.

## Fase B0 — Preparación (bajar riesgo antes de tocar nada)

- [ ] **Bajar el TTL** del registro A y del CNAME `www` en GoDaddy a **600s** (o el mínimo) **24–48 h antes** del corte, para que un rollback propague rápido.
- [ ] Confirmar el inventario DNS de arriba (re-`Resolve-DnsName` por si cambió algo).
- [ ] Elegir ventana de bajo tráfico para el cambio de nameservers.

## Fase B1 — Alta del dominio en Cloudflare (usuario, dashboard)

- [ ] Crear cuenta en cloudflare.com y **Add a site** → `credifygo.com` → plan **Free**.
- [ ] Cloudflare escanea e importa los registros. **Revisar uno por uno contra la tabla de contexto** y añadir los que falten. Estado objetivo:

| Tipo | Nombre | Contenido | Proxy |
|---|---|---|---|
| A | `credifygo.com` | `20.201.122.12` | **Proxied (naranja)** |
| CNAME | `www` | `credifygo.com` | **Proxied (naranja)** |
| TXT | `credifygo.com` | `google-site-verification=3wnlNwA4OXu-xrD4GqdOyJSlLUsU4A8wih25im4J2as` | DNS only |
| TXT | `_dmarc` | `v=DMARC1; p=quarantine; adkim=r; aspf=r; rua=mailto:dmarc_rua@onsecureserver.net;` | DNS only |
| TXT | `resend._domainkey` | `p=MIGfMA0…` (valor completo actual) | DNS only |
| MX | `send` | `feedback-smtp.sa-east-1.amazonses.com` (prio 10) | DNS only |
| TXT | `send` | `v=spf1 include:dc-fd741b8612._spfm.send.credifygo.com ~all` | DNS only |

> ⚠️ Los registros de correo y verificación van **DNS only (nube gris)** — nunca proxied. Solo el tráfico web (A apex, www) va proxied.

- [ ] **No cambiar los nameservers todavía.** Primero completar la config de origen (Fase B2).

## Fase B2 — Config del servidor de origen (yo, por SSH) — ANTES del corte

Estos cambios son inocuos con los nameservers aún en GoDaddy (el tráfico sigue directo), y dejan el origen listo para cuando Cloudflare quede delante.

- [ ] **Restaurar IP real del cliente (Apache `mod_remoteip`)** — sin esto, Laravel ve a todos como la IP de Cloudflare y el rate-limit/throttle/logs quedan inservibles:
```apache
# /etc/apache2/conf-available/cloudflare-remoteip.conf
RemoteIPHeader CF-Connecting-IP
# + RemoteIPTrustedProxy con los rangos de Cloudflare (script los baja)
```
```bash
sudo a2enmod remoteip
# generar los RemoteIPTrustedProxy desde https://www.cloudflare.com/ips-v4 y -v6
sudo a2enconf cloudflare-remoteip
sudo apache2ctl configtest && sudo systemctl reload apache2
```

- [ ] **Laravel `TrustProxies`**: confiar en el proxy (tras el firewall solo-CF, es seguro). En `bootstrap/app.php` `->withMiddleware(...)`:
```php
$middleware->trustProxies(at: '*', headers:
    Request::HEADER_X_FORWARDED_FOR | Request::HEADER_X_FORWARDED_PROTO |
    Request::HEADER_X_FORWARDED_HOST | Request::HEADER_X_FORWARDED_PORT);
```
(Con `mod_remoteip` reescribiendo `REMOTE_ADDR`, `X-Forwarded-For` ya llega saneado.)

- [ ] **Certificado de origen (recomendado: Cloudflare Origin CA, 15 años)** — evita la dependencia de renovación de Let's Encrypt (que se complica detrás del proxy) y permite blindar el origen:
  - [ ] En Cloudflare: SSL/TLS → Origin Server → **Create Certificate** (cubre `credifygo.com` + `*.credifygo.com`). Guardar cert + key.
  - [ ] Instalar en Apache (nuevo vhost SSL apuntando al cert/key de origen), `configtest` + `reload`.
  - [ ] *(Alternativa si se prefiere seguir con Let's Encrypt: mantenerlo, pero cambiar la renovación a DNS-01 con un API token de Cloudflare, porque HTTP-01 se rompe detrás del proxy.)*

- [ ] **Backup**: `cp -r /etc/apache2 ~/apache2.bak-precf` y snapshot de la config antes de cambios.

## Fase B3 — Corte: cambiar nameservers (usuario) + verificación

- [ ] En **GoDaddy**: cambiar los nameservers a los 2 que asigna Cloudflare (p.ej. `xxx.ns.cloudflare.com`). Propagación: minutos a ~2 h (por el TTL bajado).
- [ ] En Cloudflare: SSL/TLS → **Full (strict)**; **Always Use HTTPS** ON; **Minimum TLS 1.2**.
- [ ] **Verificar** (esperar propagación):
```bash
# NS ya en Cloudflare
Resolve-DnsName credifygo.com -Type NS -Server 1.1.1.1
# El sitio responde vía Cloudflare (cabecera cf-ray) y 200
curl -sI https://credifygo.com | grep -iE 'cf-ray|server'
curl -s -o /dev/null -w "%{http_code}\n" https://credifygo.com
# Correo Resend intacto (DKIM/SPF/MX de send.)
Resolve-DnsName resend._domainkey.credifygo.com -Type TXT -Server 1.1.1.1
Resolve-DnsName send.credifygo.com -Type MX -Server 1.1.1.1
```
- [ ] Probar login PWA, `/admin`, y **la IP real** en logs de Laravel (no la de Cloudflare).
- [ ] **Re-verificar Resend**: enviar un correo de prueba (lead en `/demo`) → confirmar entrega. (El DNS no cambió de valores, solo de proveedor de zona.)
- [ ] **GSC**: la propiedad de dominio sigue verificada (el TXT se replicó); si Google re-verifica, ya está.

## Fase B4 — Endurecimiento (tras 24–48 h estable)

- [ ] **Bloquear el origen a solo-Cloudflare** (ufw) — que nadie llegue al `20.201.122.12` saltándose el WAF. **Fasear con cuidado de no cortar SSH (22 abierto):**
```bash
# permitir 80/443 SOLO desde rangos Cloudflare; mantener 22 abierto
for ip in $(curl -s https://www.cloudflare.com/ips-v4); do sudo ufw allow from $ip to any port 443 proto tcp; done
for ip in $(curl -s https://www.cloudflare.com/ips-v6); do sudo ufw allow from $ip to any port 443 proto tcp; done
sudo ufw delete allow 443/tcp   # quitar el "desde cualquier lado"
# repetir para 80 si se mantiene; 22 se deja como está
```
  - [ ] Verificar acceso directo bloqueado: `curl -I https://20.201.122.12 --resolve credifygo.com:443:20.201.122.12` debe **fallar/timeout**; vía dominio (Cloudflare) sigue 200.

- [ ] **Rate limiting (Free = 1 regla)** → gastarla en el endpoint más sensible: `POST /pwa/auth/login` (p.ej. 10 req/min por IP → managed challenge/block). Complementa el throttle de Laravel a nivel de borde.
- [ ] **WAF**: activar el **Cloudflare Free Managed Ruleset**, **Bot Fight Mode**, y 1–2 reglas custom (p.ej. challenge a `/admin` fuera de Colombia si aplica).
- [ ] **Caching** (evitar romper la PWA):
  - Regla: `/*` → dejar caché por defecto (Cloudflare solo cachea estáticos por extensión; **no** activar "Cache Everything" en rutas de app).
  - Regla: `/pwa-sw.js` y `/build/version.json` → **Bypass cache** (para que las actualizaciones del Service Worker propaguen al instante).
  - Estáticos `/build/*` (hasheados) → cachear agresivo (ya son inmutables).
- [ ] **HSTS**: activar solo cuando todo esté estable (es difícil de revertir).
- [ ] Evaluar **subir a Pro** para el OWASP Core Ruleset completo (upgrade sin re-hacer nada).

## Rollback Cloudflare
- **Rápido (durante el corte):** revertir los nameservers a `ns19/ns20.domaincontrol.com` en GoDaddy → como el TTL está en 600s, el tráfico vuelve directo a la VM en minutos. La zona de GoDaddy sigue intacta hasta que se borre.
- **Firewall:** si el lock de ufw corta el sitio, `sudo ufw allow 443/tcp` restaura el acceso directo. (Por eso el lock es Fase B4, separado del corte de DNS.)
- **Apache:** restaurar `~/apache2.bak-precf`.

---

## Riesgos / gotchas (resumen)

- **Perder un registro DNS** (Resend/GSC) al importar → correo o verificación caídos. Mitigado por la tabla objetivo + verificación post-corte.
- **IP real del cliente**: sin `mod_remoteip` + `TrustProxies`, el throttle de login/`/demo` y los logs quedan inútiles (todos = IP de Cloudflare). **Paso obligatorio.**
- **Renovación de cert Let's Encrypt detrás del proxy** (HTTP-01 se rompe): resuelto usando **Origin CA cert** (o pasando certbot a DNS-01).
- **Service Worker cacheado por Cloudflare**: bypass de `/pwa-sw.js` para no congelar versiones de la PWA.
- **Lock de firewall vs SSH**: fasearlo y no tocar el 22; tener el rollback de ufw a mano.
- **Source maps públicos**: el plugin de Sentry los borra de `public/build` tras subir; verificar que no queden `.map`.
- **PII en Sentry**: `send_default_pii=false` + `before_send` scrubbing; sin Session Replay por ahora.

## Verificación global (Definition of Done)
- Sentry captura errores reales de backend y frontend en prod, con stack legible y sin PII; alertas activas.
- `credifygo.com` sirve vía Cloudflare (cf-ray), SSL Full(strict), IP real en logs.
- Correo Resend y GSC intactos tras el corte.
- Origen accesible solo vía Cloudflare; rate-limit en login activo.
- CHANGELOG actualizado; PHPStan L5 + Pint verdes.
