Guía9 min

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.

TelegramSlackGitHub
Candado de verificación delante de un webhook que llega a un agente

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

Verificación de firma antes de llamar al modelo

CanalHeader / campoQué comparar (docs 2026-09-03)
TelegramX-Telegram-Bot-Api-Secret-Tokensecret_token 1–256, charset [A-Za-z0-9_-]
SlackX-Slack-Signature + X-Slack-Request-Timestampv0= HMAC-SHA256; ventana 5 min
GitHubX-Hub-Signature-256sha256= + HMAC hex; UTF-8; timing-safe
Customel que tú definasmismo HMAC, raw body

Errores comunes

POST anónimo que el agente trató como update real

SíntomaCausaFix
Firma Slack nunca cierraparseaste JSON antesraw body
Preview firma prodsecret en All envssecret distinto (preview)
Telegram sin headerno pasaste secret_tokensetWebhook de nuevo
200 a basuraverify al finalverify primero, 401
Replay Slackno chequeas timestampventana corta, docs Slack

Relación con el resto

Checklist

  • Telegram secret_token set 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

  1. Lee raw body.
  2. Verifica firma / secret_token (401 si falla).
  3. Parsea JSON.
  4. Idempotencia del update_id / event_id si Telegram reintenta.
  5. 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.