Inicio Expertise Proyectos Soluciones CV Stack Blog Contacto
Volver al blog
Telecom 15 de mayo de 2026 6 min lectura

WebRTC en producción:
el bug del certificado DTLS

El endpoint PJSIP estaba en la base de datos. El softphone enviaba credenciales correctas. El registro SIP fallaba con "No matching endpoint found" después del 401. Horas de diagnóstico para descubrir que Asterisk descartaba el endpoint silenciosamente por no poder leer la clave privada del certificado DTLS.

Asterisk WebRTC DTLS PJSIP JsSIP
Softphone WebRTC conectado a Asterisk con DTLS SRTP y tenants aislados
JsSIP Browser Softphone REGISTER → 401 ← credentials → WebSocket WSS/TLS Asterisk PJSIP sorcery realtime → DB ✓ read asterisk.key → FAIL Permission denied (root:600) "No matching endpoint" PostgreSQL ps_endpoints row exists ✓ FIX: chown asterisk:asterisk asterisk.key Sorcery descarta el endpoint silenciosamente — DTLS cert ilegible

El contexto: PBX multi-tenant con WebRTC

Estaba construyendo un PBX SaaS multi-tenant sobre Asterisk 20 + Kamailio + PJSIP Realtime. Los endpoints viven en PostgreSQL, sin ningún archivo pjsip.conf con extensiones. Kamailio maneja el tenant-routing inyectando el X-Tenant-ID en el header SIP, y Asterisk valida contra la DB via sorcery.

El softphone es JsSIP corriendo en el browser, conectando via WebSocket a Asterisk. Para WebRTC el audio va cifrado con DTLS-SRTP. Asterisk necesita un certificado X.509 para hacer el handshake DTLS con el browser.

El síntoma

JsSIP conectaba al WebSocket perfectamente. Enviaba el REGISTER. Asterisk respondía con 401 Unauthorized (normal: desafía credenciales). JsSIP re-enviaba con las credenciales correctas. Y ahí:

[ERROR] No matching endpoint found for 'REGISTER' from '[email protected]'
Síntoma confuso

La fila id='1001' existía en ps_endpoints. Las credenciales en ps_auths eran correctas. pjsip show endpoint 1001 en la CLI de Asterisk devolvía: "Unable to find object 1001 in container".

El endpoint estaba en la base de datos pero Asterisk no lo "veía". Como si nunca se hubiera cargado en memoria.

Diagnóstico: core debug + pjsip logger

La clave estuvo en habilitar debug en runtime:

asterisk -rx 'core set debug 3'
asterisk -rx 'pjsip set logger on'

En los logs apareció algo que nunca había visto antes:

[DEBUG] res_pjsip_endpoint_identifier_user.c: Endpoint '1001' failed to instantiate
[ERROR] res_config_pgsql.c: Failed to load DTLS certificate
[ERROR] /var/lib/asterisk/keys/asterisk.key: Permission denied

Sorcery intentaba instanciar el endpoint desde la DB, cargaba la configuración DTLS, intentaba leer la clave privada del certificado y fallaba con Permission denied. Ante ese error, descartaba el endpoint completo. Sin warning visible en logs normales. Sin entrada en el container de objetos.

La causa raíz: el certificado creado como root

El certificado DTLS lo había generado durante el setup inicial del servidor, corriendo como root:

openssl req -new -x509 -days 3650 -nodes \
  -out /var/lib/asterisk/keys/asterisk.crt \
  -keyout /var/lib/asterisk/keys/asterisk.key \
  -subj '/CN=aria-pbx'

El resultado: ambos archivos con owner: root y permisos 600. Asterisk corre como el usuario asterisk, no puede leer una clave privada de root con permisos 600.

Lo que lo hace especialmente traicionero: Asterisk no levanta ningún error durante el startup. Solo falla silenciosamente al instanciar el endpoint la primera vez que alguien intenta registrarse.

El fix en vivo

# Corregir ownership y permisos
docker exec asterisk chown asterisk:asterisk \
  /var/lib/asterisk/keys/asterisk.key \
  /var/lib/asterisk/keys/asterisk.crt

docker exec asterisk chmod 640 \
  /var/lib/asterisk/keys/asterisk.key

# Recargar PJSIP para que vuelva a intentar instanciar endpoints
docker exec asterisk asterisk -rx 'module reload res_pjsip.so'

Después del reload, pjsip show endpoint 1001 mostraba el endpoint correctamente. El registro funcionó en el primer intento.

El fix permanente: Dockerfile

La solución correcta es generar el certificado durante el build de la imagen con el ownership correcto desde el inicio:

RUN mkdir -p /var/lib/asterisk/keys \
    && openssl req -new -x509 -days 3650 -nodes \
        -out /var/lib/asterisk/keys/asterisk.crt \
        -keyout /var/lib/asterisk/keys/asterisk.key \
        -subj '/CN=aria-pbx' \
    && chown asterisk:asterisk \
        /var/lib/asterisk/keys/asterisk.crt \
        /var/lib/asterisk/keys/asterisk.key \
    && chmod 640 /var/lib/asterisk/keys/asterisk.key

Config PJSIP para WebRTC

Para que el endpoint soporte WebRTC con DTLS correctamente, los campos en ps_endpoints deben incluir:

media_encryption         = dtls
dtls_auto_generate_cert  = no
dtls_cert_file           = /var/lib/asterisk/keys/asterisk.crt
dtls_private_key         = /var/lib/asterisk/keys/asterisk.key
dtls_verify              = fingerprint
dtls_setup               = actpass
use_avpf                 = yes
force_avp                = yes
ice_support              = yes
bundle                   = yes
rtcp_mux                 = yes
transport                = transport-ws
transport-ws obligatorio

Si el endpoint tiene transport=transport-udp (el default), Asterisk rechaza los REGISTER que llegan via WebSocket. Los endpoints WebRTC deben apuntar explícitamente al transport WebSocket.

La lección de fondo

Sorcery Realtime en Asterisk tiene un comportamiento que hay que conocer: cuando falla al instanciar un objeto (por cualquier razón: error de permisos, campo inválido, dependencia faltante), lo descarta silenciosamente. El objeto simplemente no existe en memoria. El síntoma siempre es el mismo: "No matching endpoint found" aunque la fila esté perfectamente en la DB.

El diagnóstico correcto siempre empieza por:

  1. core set debug 3 para ver los intentos de instanciación.
  2. pjsip set logger on para ver el flujo completo del registro.
  3. Buscar explícitamente errores de carga de recursos (certs, archivos de configuración).
  4. Verificar que Asterisk pueda leer todos los archivos que referencia, no solo que existan.

Takeaway

En sistemas Realtime con Sorcery, la presencia de un objeto en la DB no garantiza que Asterisk lo cargue en memoria. Siempre verificar que todos los recursos referenciados (certs, archivos) sean legibles por el usuario que corre el proceso — no solo por root.