Docs

Certificate Pinning — Guia Operacional

Guia completa de certificate pinning en nvito-client: arquitectura de protección, rotación de certificados, monitoreo automático, procedimientos de emergencia y troubleshooting.

PublicadoMarzo 2026Equipo de desarrollo, operaciones, CTO

Que es Certificate Pinning

Certificate pinning es una técnica de seguridad que valida el certificado SSL/TLS del servidor contra hashes SHA-256 conocidos (pins). Protege contra ataques Man-in-the-Middle (MITM) donde un atacante con una CA comprometida o acceso a un proxy corporativo intercepta trafico HTTPS.

Sin pinning, cualquier certificado firmado por una CA confiable del sistema operativo es aceptado. Con pinning, solo se aceptan certificados cuyos pins coincidan con los configurados en la app.

Por que Nvito lo implementa

nvito-client (app movil) maneja datos sensibles: tokens JWT, información de invitados, RSVPs, códigos QR. Un ataque MITM permitiria a un atacante interceptar estos datos. Certificate pinning agrega una capa adicional de protección sobre HTTPS.

Arquitectura de protección (5 capas)

El sistema tiene 5 capas de protección para garantizar que la app nunca deje de funcionar por renovacion de certificados:

CapaComponenteQue haceAutomático?
1Dual-pinAcepta leaf certificate actual + intermediate CA como backupSi
2Circuit breakerSi el pinning falla 3 veces consecutivas, usa fetch nativo + reporta telemetriaSi
3Cron NestJSVerifica certificado cada lunes 9AM, envia email + WhatsApp si expira en menos de 30 díasSi
4UptimeRobotVerifica endpoint /health/certificate-status cada 6h, alerta independienteSi
5OTA UpdateDistribuye pins nuevos sin pasar por App Store/Play StoreSemi-auto (1 comando)

Archivos involucrados

nvito-client

  • src/api/certificate-pinning.ts — Modulo principal: pins, secureFetch, circuit breaker
  • src/api/client.ts — Cliente HTTP que usa secureFetch()
  • app.json — Configuración de expo-updates para OTA

nvito-api

  • src/modules/health/certificate-check.service.ts — Verifica certificado via TLS
  • src/modules/health/health.controller.ts — Endpoints /health/certificate-status y /health/pinning-report
  • src/modules/scheduler/certificate-scheduler.service.ts — Cron semanal

Variables de entorno (nvito-api)

  • CERTIFICATE_CHECK_HOST — Hostname a verificar (ej: api.nvito.mx). Si está vacio, la verificacion se omite.
  • CERTIFICATE_CHECK_PORT — Puerto (default: 443)
  • CERTIFICATE_ALERT_EMAIL — Email del admin para alertas

Obtencion de pins — Paso a paso

Prerequisitos

  • Acceso a terminal con openssl instalado
  • El dominio de producción (api.nvito.mx) debe tener certificado SSL activo

Obtener pin del leaf certificate (certificado principal)

openssl s_client -connect api.nvito.mx:443 -showcerts < /dev/null 2>/dev/null | \
  openssl x509 -pubkey -noout | \
  openssl pkey -pubin -outform DER | \
  openssl dgst -sha256 -binary | \
  openssl enc -base64

El resultado es una cadena base64 como: Lt6FclMCwqOVd+xKh1mdNVYqD+5SfreN0tLvufVeWa4=

Obtener pin del intermediate CA (certificado de respaldo)

openssl s_client -connect api.nvito.mx:443 -showcerts < /dev/null 2>/dev/null | \
  awk '/-----BEGIN CERTIFICATE-----/&#123;n++&#125; n==2' | \
  openssl x509 -pubkey -noout | \
  openssl pkey -pubin -outform DER | \
  openssl dgst -sha256 -binary | \
  openssl enc -base64

Donde colocar los pins

En nvito-client/src/api/certificate-pinning.ts:

export const CERTIFICATE_PINS: Record<string, string[]> = &#123;
  'api.nvito.mx': [
    'sha256/Lt6FclMCwqOVd+xKh1mdNVYqD+5SfreN0tLvufVeWa4=', // Leaf
    'sha256/kO/IpuhlNGVQKz4DsJnvYCNVx1bCtPCp8mPexO/Eqmw=', // Intermediate CA
  ],
&#125;;

Siempre incluir mínimo 2 pins: leaf + intermediate CA. El intermediate CA no cambia al renovar el leaf certificate, actuando como backup.

Rotación de certificados — Procedimiento estandar

Cuando ejecutar este procedimiento

  • El cron semanal envia alerta de que el certificado expira en menos de 30 dias
  • UptimeRobot alerta que certificate-status retorna warning: true
  • Renovacion planificada del certificado SSL

Pasos

Paso 1 — Obtener el nuevo pin

Ejecutar los comandos openssl de la sección anterior contra el nuevo certificado. Si el certificado aun no se ha renovado en el servidor, obtener el pin del certificado pendiente del proveedor SSL.

Paso 2 — Actualizar certificate-pinning.ts

Agregar el nuevo pin como segundo elemento del array (mantener el pin actual como primero):

'api.nvito.mx': [
  'sha256/PIN_ACTUAL',           // Certificado actual (aun valido)
  'sha256/PIN_NUEVO',            // Nuevo certificado (pre-pin)
  'sha256/PIN_INTERMEDIATE_CA',  // Backup del CA
],

Paso 3 — Commit y push

git add src/api/certificate-pinning.ts
git commit -m "chore: pre-pin nuevo certificado SSL para rotación"
git push

Paso 4 — Distribuir via OTA

eas update --branch production --message "Rotación de pins SSL"

Este comando pública el nuevo bundle JS. Los usuarios reciben el update automáticamente al abrir la app. No necesitan actualizar desde el store.

Paso 5 — Esperar adopción

Esperar mínimo 48 horas para que el 95% de los usuarios reciban el OTA update.

Paso 6 — Renovar certificado en el servidor

Renovar el certificado SSL en Coolify/Railway. La app acepta ambos pins (viejo + nuevo).

Paso 7 — Verificar

# Verificar que el servidor tiene el nuevo certificado
openssl s_client -connect api.nvito.mx:443 < /dev/null 2>/dev/null | \
  openssl x509 -noout -dates -fingerprint

# Verificar endpoint de status
curl https://api.nvito.mx/v1/health/certificate-status | jq .

Paso 8 — Limpiar pin antiguo (siguiente release)

En el siguiente ciclo, remover el pin antiguo y mantener solo el actual + intermediate CA.

Monitoreo automático

Cron NestJS (CertificateSchedulerService)

  • Frecuencia: Cada lunes a las 9:00 AM (America/Mexico_City)
  • Que verifica: Conecta via TLS al hostname configurado, extrae info del certificado
  • Cuando alerta: Si el certificado expira en menos de 30 días o si hay error de conexión
  • Como alerta: Log WARN con instrucciones de rotación. (Email/WhatsApp pendiente de integración)

Endpoint GET /health/certificate-status

  • URL: https://api.nvito.mx/v1/health/certificate-status
  • Publico: Si, sin autenticación
  • Respuesta: JSON con status, daysUntilExpiry, warning, fingerprint256
  • Uso: Configurar UptimeRobot para verificar cada 6 horas

Endpoint POST /health/pinning-report

  • URL: https://api.nvito.mx/v1/health/pinning-report
  • Publico: Si, rate limited (10 req/min por IP)
  • Uso: nvito-client envia reportes automáticos cuando el pinning falla

UptimeRobot (configuración manual)

  1. Crear monitor HTTP(S) keyword en UptimeRobot (plan gratuito)
  2. URL: https://api.nvito.mx/v1/health/certificate-status
  3. Keyword: "warning":true (alertar si aparece)
  4. Frecuencia: cada 6 horas
  5. Notificación: email del admin

Circuit breaker

Como funciona

  1. Cada fallo de pinning incrementa un contador
  2. Al llegar a 3 fallos consecutivos, el circuit breaker se abre
  3. Con circuit breaker abierto: secureFetch usa fetch nativo (sin pinning)
  4. Cada fallo envia telemetria a /health/pinning-report
  5. El circuit breaker se resetea al reiniciar la app

Implicaciones

  • La app nunca deja de funcionar por fallos de pinning
  • El equipo recibe alertas via telemetria para actuar rápido
  • Mientras el circuit breaker está abierto, la app opera sin protección de pinning (temporalmente)

Escenarios catastroficos y pasos de emergencia

Escenario A: Certificado se renovo sin rotar pins

Diagnostico: Usuarios reportan errores de conexión. Telemetria muestra fallos de pinning.

Tiempo estimado de resolución: 2-4 horas

Pasos:

  1. El circuit breaker se activa automáticamente — la app sigue funcionando
  2. Obtener el nuevo pin del certificado actual: openssl s_client -connect api.nvito.mx:443 ...
  3. Actualizar certificate-pinning.ts con el pin nuevo
  4. Publicar OTA: eas update --branch production
  5. En 24h el 95% de usuarios tienen el fix

Escenario B: Intermediate CA cambio

Diagnostico: Fallos de pinning masivos. Ni el leaf ni el intermediate pin coinciden.

Tiempo estimado de resolución: 2-4 horas

Pasos:

  1. Circuit breaker protege a los usuarios
  2. Obtener AMBOS pins nuevos (leaf + intermediate): ejecutar los dos comandos openssl
  3. Actualizar certificate-pinning.ts con los 2 pins nuevos
  4. Publicar OTA: eas update --branch production

Escenario C: App rechaza TODAS las conexiónes

Diagnostico: La app no puede comunicarse con el API. El circuit breaker NO se activo (posiblemente por un bug).

Tiempo estimado de resolución: 4-8 horas

Pasos:

  1. Publicar OTA de emergencia desactivando pinning temporalmente:
    • En certificate-pinning.ts: cambiar pins a ['PLACEHOLDER'] para forzar fetch nativo
    • eas update --branch production
  2. Investigar por que el circuit breaker no se activo
  3. Corregir el bug, restaurar pins correctos, publicar otro OTA

Escenario D: No se puede publicar OTA

Diagnostico: El servidor de EAS Update no responde o hay problema con la cuenta de Expo.

Tiempo estimado de resolución: 24-48 horas

Pasos:

  1. El circuit breaker protege a los usuarios existentes
  2. Publicar release de emergencia en App Store y Play Store con pins corregidos
  3. Activar forced update si está disponible
  4. Resolver el problema con EAS para futuras actualizaciones

Verificacion manual

Verificar que los pins son correctos

# Obtener pin actual del servidor
CURRENT_PIN=$(openssl s_client -connect api.nvito.mx:443 -showcerts < /dev/null 2>/dev/null | \
  openssl x509 -pubkey -noout | \
  openssl pkey -pubin -outform DER | \
  openssl dgst -sha256 -binary | \
  openssl enc -base64)

echo "Pin actual del servidor: sha256/$CURRENT_PIN"
echo "Comparar con los pins en certificate-pinning.ts"

Verificar endpoint de status

curl -s https://api.nvito.mx/v1/health/certificate-status | jq .

Respuestá esperada:

&#123;
  "status": "ok",
  "hostname": "api.nvito.mx",
  "daysUntilExpiry": 75,
  "warning": false,
  "fingerprint256": "AA:BB:CC:..."
&#125;

Probar pinning en dispositivo

  1. Build de desarrollo: npx expo run:ios o npx expo run:android
  2. Verificar que las llamadas al API funcionan
  3. En producción: build con eas build --platform all --profile production

Checklist para despliegue a Producción

Acción requerida al desplegar a producción

Los ambientes DEV y TEST ya tienen certificate pinning configurado y funcional. Cuando el ambiente de producción este desplegado, se deben completar los siguientes pasos para replicar la protección. Hasta que no se completen, producción operara sin pinning (fetch nativo via placeholders).

  1. Obtener pins del certificado de producción:

    # Leaf certificate
    openssl s_client -connect api.nvito.mx:443 -showcerts < /dev/null 2>/dev/null | \
      openssl x509 -pubkey -noout | openssl pkey -pubin -outform DER | \
      openssl dgst -sha256 -binary | openssl enc -base64
    
    # Intermediate CA
    openssl s_client -connect api.nvito.mx:443 -showcerts < /dev/null 2>/dev/null | \
      awk '/-----BEGIN CERTIFICATE-----/&#123;n++&#125; n==2' | \
      openssl x509 -pubkey -noout | openssl pkey -pubin -outform DER | \
      openssl dgst -sha256 -binary | openssl enc -base64
    
  2. Reemplazar placeholders en nvito-client/src/api/certificate-pinning.ts:

    • Buscar PLACEHOLDER_LEAF_CERTIFICATE_PIN_BASE64 y reemplazar con el pin del leaf
    • Buscar PLACEHOLDER_INTERMEDIATE_CA_PIN_BASE64 y reemplazar con el pin del intermediate CA
  3. Configurar variables de entorno en Railway (nvito-api):

    • CERTIFICATE_CHECK_HOST = api.nvito.mx
    • CERTIFICATE_CHECK_PORT = 443
    • CERTIFICATE_ALERT_EMAIL = email del administrador
  4. Instalar react-native-ssl-pinning y descomentar la implementación real en secureFetch (requiere EAS Build nativo)

  5. Publicar OTA: eas update --branch production --message "Enable SSL pinning for production"

  6. Configurar UptimeRobot (gratuito):

    • Monitor HTTP(S) keyword en https://api.nvito.mx/v1/health/certificate-status
    • Keyword de alerta: "warning":true
    • Frecuencia: cada 6 horas
    • Notificación: email del admin
  7. Verificar que el endpoint responde correctamente:

    curl -s https://api.nvito.mx/v1/health/certificate-status | jq .
    

FAQ

Se puede desactivar el pinning temporalmente?

Si. Cambiar los pins a strings con PLACEHOLDER en certificate-pinning.ts y publicar OTA. El código detecta placeholders y usa fetch nativo.

Que pasa si no configuro CERTIFICATE_CHECK_HOST?

El cron semanal se ejecuta pero retorna not_configured sin hacer nada. El endpoint certificate-status también retorna not_configured. No hay impacto negativo.

Cada cuanto se renuevan los certificados?

Depende del proveedor. Let's Encrypt renueva cada 90 días. Cloudflare varia. El cron alerta 30 días antes.

El pinning afecta el desarrollo local?

No. En desarrollo (__DEV__ = true), secureFetch usa fetch nativo automáticamente. No se validan pins.

Que pasa con el upload a S3 (URLs pre-firmadas)?

El upload de archivos usa fetch nativo directamente porque las URLs pre-firmadas son de dominios dinámicos (CDN). El pinning solo aplica a api.nvito.mx.

Como actualizo los pins cuando no tengo producción?

Los pins tienen valor PLACEHOLDER por defecto. Cuando despliegues a producción, ejecuta los comandos openssl, reemplaza los placeholders, y pública un OTA.

Esta pagina fue util?