Firmar webhooks del agente IA: Telegram, Slack y GitHub
Resumen
Cómo no ejecutar un POST anónimo como si fuera Telegram: secret_token de setWebhook, firma Slack v0, HMAC de GitHub y 401 si falta. El token del bot en env no verifica el inbound. Preview usa otro secret. Fuentes oficiales, 3 septiembre 2026.

Qué resuelve
Esta pieza se queda en la decisión práctica: qué instalar, qué riesgo agrega y cómo aplicarlo sin romper operación.
Si el endpoint del agente es público (tiene que serlo), cualquiera puede POST. Sin verificar la firma, un extraño dispara tools con tu API key. Secretos inyectan la llave. Esta guía cubre quién llama. Fuentes oficiales consultadas el 3 de septiembre de 2026.
La regla
Inbound = 401 si la firma no cierra. Antes de un solo fetch al modelo. El token del bot en Authorization de salida no prueba que el POST de entrada sea de Telegram.
Preview: otro secret. Si Preview y Production comparten el secret_token, un leak del PR firma prod.
Telegram
Bot API, método setWebhook (hoy): parámetro opcional secret_token. Si lo pones, cada POST trae X-Telegram-Bot-Api-Secret-Token. Largo 1–256; solo A-Z, a-z, 0-9, _ y -. Compara con timing-safe (crypto.timingSafeEqual). Si no coincide, 401 y no proceses el update.
Otros techos del mismo método, consultados hoy: max_connections 1–100, default 40 (simultáneas HTTPS al webhook); drop_pending_updates para tirar la cola al (re)setear; ip_address fija el origen en vez del DNS; certificate si usas TLS self-signed. Ninguno de esos sustituye el header. url tiene que ser HTTPS; string vacía quita el webhook.
Sin secret_token, el POST anónimo es “un update”. Ponlo. Rótalo si se filtró (kill switch si ya ejecutaron tools).
Slack
Docs de verifying requests (hoy): header X-Slack-Signature (o x-slack-signature; el case no se asume) y X-Slack-Request-Timestamp. El ejemplo oficial descarta si |now - timestamp| > 60 * 5 (cinco minutos): replay. Basestring v0:{timestamp}:{raw body} con dos dos-puntos. HMAC-SHA256 con el signing secret tratado como UTF-8 (no lo pases a binario a mano); prefijo v0= + hex. Compara con hmac.compare / timing-safe, no con ===. Lee el raw body, sin deserializar JSON: un espacio de más invalida la firma. El ejemplo de Slack usa form body (token=…&team_id=…), no un JSON “bonito”.
GitHub
Docs de validating deliveries: header X-Hub-Signature-256. HMAC hex digest; el valor siempre empieza con sha256=. Payload en UTF-8. Compara con crypto.timingSafeEqual, no con ===. Útil si el agente se dispara desde Actions o issues. No confíes en la IP sola.
Workers / Vercel
El verify va en el mismo handler que el webhook, no “más tarde en un worker”. Un 200 antes de verificar es el incidente. Ejemplo de signing de Cloudflare es el patrón (HMAC + compare), no un bypass.
Tabla

| Canal | Header / campo | Qué comparar (docs 2026-09-03) |
|---|---|---|
| Telegram | X-Telegram-Bot-Api-Secret-Token | secret_token 1–256, charset [A-Za-z0-9_-] |
| Slack | X-Slack-Signature + X-Slack-Request-Timestamp | v0= HMAC-SHA256; ventana 5 min |
| GitHub | X-Hub-Signature-256 | sha256= + HMAC hex; UTF-8; timing-safe |
| Custom | el que tú definas | mismo HMAC, raw body |
Errores comunes

| Síntoma | Causa | Fix |
|---|---|---|
| Firma Slack nunca cierra | parseaste JSON antes | raw body |
| Preview firma prod | secret en All envs | secret distinto (preview) |
| Telegram sin header | no pasaste secret_token | setWebhook de nuevo |
| 200 a basura | verify al final | verify primero, 401 |
| Replay Slack | no chequeas timestamp | ventana corta, docs Slack |
Relación con el resto
- Dónde vive el secret: secretos.
- Hostname HTTPS: TLS.
- Apagar si ya abusaron: kill switch.
- Red de la DB: red privada.
Checklist
- Telegram
secret_tokenset y comparado - Slack/GitHub: HMAC sobre raw body
- 401 antes de cualquier tool
- Secret de Preview ≠ Production
- Timing-safe compare
- Ensayo: POST sin header → 401; POST firmado → 200
FAQ
¿IP allowlist de Telegram? Complemento, no sustituto. Las IPs cambian.
¿Basic auth en el path? Telegram no lo usa. secret_token sí.
¿Workers sin crypto.subtle? Node crypto en Docker; Web Crypto en Workers. No implementes HMAC “a ojo”.
Orden en el handler
- Lee raw body.
- Verifica firma / secret_token (401 si falla).
- Parsea JSON.
- Idempotencia del
update_id/event_idsi Telegram reintenta. - Recién ahí, tools y modelo.
Logs: registra verify=fail sin el secret ni el body completo. Un log del payload es otra fuga.
CDN/proxy: algunos reescriben el body. Si Slack falla siempre, el proxy está mutando bytes. Prueba contra el origen (TLS directo).
No pongas el secret en la query (?token=). Telegram documenta el header. Query acaba en access logs.
Compose/VPS: el verify no “espera a nginx”. Si nginx hace 200 y proxy_pass después, ya contestaste OK a un POST falso. El 401 sale de Node.
El ensayo (Preview): curl -X POST sin header → 401. Con el header correcto → el bot responde. En prod no pruebes con el token de clientes.
Si el verify está después del modelo, ya pagaste el token. Muévelo a la primera línea del handler.
Rotación: genera un secret nuevo, ponlo en Production, setWebhook / Slack app config, luego borra el viejo. Un minuto con dos secrets aceptados es mejor que un minuto a 401 masivo. No rotar en el mismo deploy que un refactor del parser.
Slack exige raw body: si Express ya corrió express.json(), perdiste los bytes. Usa express.raw() o el request.text() de Workers/Vercel antes de JSON.parse. GitHub documenta el mismo orden. Un middleware que “ayuda” a parsear es la causa número uno de firmas que nunca cierran en staging.
WhatsApp Cloud API tiene su propio X-Hub-Signature-256 (mismo patrón que GitHub). No inventes un header. Lee la doc del canal y copia el verify; el resto de esta guía aplica igual: raw body, 401 primero, Preview ≠ Production.
El charset de Telegram no admite espacios ni +. Si generas el token con openssl rand -hex 32 cabe (64 hex, dentro de 1–256). Si usas base64, recórtalo: = y / no pasan. Slack y GitHub no tienen ese charset; ahí el secret puede ser más largo, pero el verify sigue siendo HMAC sobre bytes crudos.
max_connections default 40 no es un rate limit de tools: es cuántas HTTPS simultáneas usa Telegram hacia tu webhook. Bájalo si el isolate de Workers se ahoga; no lo subas a 100 “por si acaso” en un proceso de 128 MB. drop_pending_updates=true al rotar el secret_token evita que la cola vieja (firmada con el secret anterior) te llene de 401.
GitHub insiste en UTF-8 porque el payload puede traer unicode. Si tu runtime decodifica latin-1, el HMAC no cierra y echas la culpa a “el secret mal copiado”. Fija encoding antes de hashear.
Siguiente paso: si las firmas cierran y igual hay spam interno, kill switch. Sin runtime: curso.
Lecturas relacionadas
Sigue explorando Deploy y otras piezas para builders.



