TL;DR
- Un valor filtrado funciona hasta que algo lo invalida: una revocación, el vencimiento de un lease o una rotación que ocurra primero. En un incidente de 2026, una clave administrativa de una plataforma interna de LiteLLM se subió a un repositorio público el 2026-05-27, se marcó para revocación y todavía funcionaba cuando se utilizó el 2026-10-10, 136 días después (postmortem de LiteLLM).
- Elimina primero los secretos persistentes, es decir, los valores que siguen siendo válidos hasta que alguien los revoca. Los jobs de GitHub obtienen tokens OIDC de corta duración, los secretos revocables quedan fuera de las variables de Railway y Terraform lee los secretos como valores efímeros para que nunca lleguen al estado.
- Luego rota lo que quede y elige un mecanismo para cada secreto: OIDC, un lease dinámico, rotación de dos fases o un valor estático con un límite y una fecha de vencimiento.
- Tres aspectos determinan el diseño y aún no están verificados: si Infisical puede conectarse a un Postgres privado para gestionar leases, si la aplicación deja de usar un lease revocado en un solo ciclo de consulta y si los gateways requieren Advanced o Enterprise. El ejercicio del final pone a prueba los dos primeros.
- Los requisitos de cada plan determinan el costo. Los secretos dinámicos requieren Infisical Advanced, las aprobaciones requieren Infisical Enterprise y los revisores obligatorios en un repositorio privado requieren GitHub Enterprise.
Tesis ([INFERENCE], criterio editorial): tener menos secretos persistentes es mejor que rotarlos más rápido. Qué la refutaría: si un lease revocado sigue funcionando después de un ciclo de consulta, los leases no limitan la exposición mejor que un calendario de rotación y este diseño es incorrecto.
Trabajaremos con un stack concreto: una aplicación en Railway con un Postgres privado, claves de OpenRouter, autenticación con Auth0 y un servicio Agno AgentOS, desplegado desde GitHub Actions. Infisical almacena los valores; Terraform define la topología. Al terminar tendrás un mecanismo para cada secreto, un workflow de CI y una configuración de Terraform para adaptar, y un ejercicio capaz de demostrar que el diseño es incorrecto. Para los secretos dentro del proceso de un agente, lee Mantén los secretos fuera del agente. Para permisos de bases de datos y seguridad a nivel de fila, lee Preparar bases de datos para agentes de IA seguros.
Un valor filtrado funciona hasta que algo lo invalida
Un calendario de rotación solo ayuda si se ejecuta antes de que actúe el atacante, y nadie sabe cuándo ocurrirá eso. En cada uno de los incidentes siguientes, un valor persistente estaba al alcance de un intruso y siguió siendo válido hasta que alguien lo invalidó.
- Railway, octubre de 2026. Railway informó que, como máximo, el 1.85% de los workspaces, alrededor de 100,000, tenían algunas variables de entorno potencialmente comprometidas. Indicó a los clientes que se registraron antes del 2026-10-21 que rotaran sus claves almacenadas y recomendó eliminar el acceso público a las bases de datos (informe preliminar de Railway, 2026-10-22, fuente primaria).
- LiteLLM, 2026. Un archivo de configuración subido el 2026-05-27 a un repositorio público contenía una clave administrativa de un despliegue interno de LiteLLM Agent Platform. El equipo marcó la clave para revocación. El 2026-10-10 el atacante la utilizó y la plataforma devolvió un token de GitHub CLI perteneciente a un propietario de la organización (postmortem de LiteLLM, fuente primaria).
- Tokens de GitHub robados. Los tokens con acceso de escritura leyeron secretos de workflows. La recomendación de Wiz: revisores obligatorios y una relación de confianza OIDC limitada a ramas o entornos protegidos (Wiz, fuente primaria; reportado por The Hacker News, 2026-10-26, fuente secundaria).
Dónde está cada secreto
| Secreto | Mecanismo | Qué requiere la rotación |
|---|---|---|
| Acceso de CI a Infisical | GitHub OIDC; sin credenciales almacenadas (referencia de GitHub OIDC, GitHub OIDC de Infisical) | Nada por ejecución. Elimina Universal Auth predeterminado de la identidad después de agregar OIDC |
| Credencial de bootstrap del servicio | Client secret de Universal Auth (Universal Auth de Infisical) | Limita los usos a 1 o configura un TTL corto; vuelve a desplegar si está en las variables de Railway |
| Lease de Postgres | Secreto dinámico: una cuenta nueva por lease, a partir de una plantilla de nombre de usuario, con un TTL (secretos dinámicos de PostgreSQL en Infisical) | Nada: el lease vence o se elimina |
| Credencial estática de Postgres | Rotación de dos fases entre dos usuarios (introducción a la rotación de Infisical) | Rotación programada; el rol de rotación necesita CREATEROLE (conexión de PostgreSQL) |
| Clave de aplicación de OpenRouter | Clave por aplicación con un límite y una fecha de vencimiento (Security Center de OpenRouter) | Rotar; revocar ante cualquier sospecha |
| Clave de administración de OpenRouter | Con alcance de organización o workspace; puede crear, editar y eliminar claves (claves de administración de OpenRouter) | Mantenerla fuera de las aplicaciones; limitar su alcance; configurar un vencimiento y rotarla antes de que ocurra |
| Client secret de Auth0 | Rotación de una sola fase mediante Infisical (rotación de Auth0 en Infisical, rotación de Auth0) | Desactivar la rotación automática y rotar durante una ventana programada; Private Key JWT con dos credenciales solo está disponible en Enterprise (Private Key JWT de Auth0) |
| Valores de Terraform | Lectura efímera (Terraform 1.10+) o argumento de solo escritura (1.11+) (Terraform: administrar datos sensibles) | Los valores efímeros no dejan nada en el estado |
El stack de un vistazo
flowchart LR
GHA["Job de GitHub Actions<br/>entorno: production"] -- "Token OIDC" --> INF["Infisical"]
TF["Terraform"] -- "topología; lecturas efímeras" --> INF
INF -- "enviar configuración, sin secretos revocables" --> RW["Variables de Railway"]
INF -. "mediante relay; sin verificar (paso 2 del ejercicio)" .-> GW["Gateway en el entorno de Railway"]
GW -- "TCP 5432" --> PG[("Postgres, sin puerto público")]
APP["Servicio de aplicación"] -- "credencial de bootstrap" --> INF
APP -- "lease o credencial rotada" --> PG
CI sin secretos almacenados
El job solicita un token OIDC a GitHub y lo envía a Infisical. Infisical verifica el sujeto, la audiencia y los claims del token y luego devuelve un token de acceso (GitHub OIDC de Infisical). No hay nada de larga duración almacenado en GitHub. Tres ajustes hacen esto posible.
Configura el sujeto para el entorno, con el formato que usa tu repositorio. Un job que especifica production produce repo:<org>/<repo>:environment:production. Los repositorios creados después del 15 de julio de 2026 usan un formato inmutable que agrega los IDs del propietario y del repositorio, repo:<org>@<org-id>/<repo>@<repo-id>:environment:production (referencia de GitHub OIDC). Usa valores explícitos, no patrones glob, y lee el sub real de una ejecución de prueba con actions-oidc-debugger antes de guardar.
Convierte el sujeto en un control de acceso. El sujeto de un job asociado a un entorno no incluye la rama, por lo que cualquier rama a la que un token robado pueda hacer push obtiene el mismo sujeto. Agrega reglas de protección al entorno. Los revisores obligatorios permiten hasta seis revisores y requieren una aprobación; activa “prevent self-reviews” y desactiva la opción de omisión para administradores (entornos de GitHub). Restringe las ramas de despliegue a ramas protegidas, una función que Pro y Team admiten para repositorios privados. En la identidad de Infisical, configura Claims, por ejemplo, un claim ref para refs/heads/main.
Elimina la configuración predeterminada. Las identidades nuevas comienzan con Universal Auth. Elimina esa configuración y sus client secrets después de agregar OIDC (GitHub OIDC de Infisical). Luego configura el TTL y el TTL máximo del token de acceso para que coincidan con la duración de un job. La tabla de campos de esa página indica 30 días y una nota indica 7,200 segundos, así que configura ambos explícitamente.
Los revisores obligatorios tienen una limitación según el plan. En GitHub Free, Pro o Team, solo funcionan en repositorios públicos, por lo que un repositorio privado necesita Enterprise.
on:
workflow_dispatch: # use your own release trigger
jobs:
deploy:
runs-on: ubuntu-latest
environment: production # sets the OIDC subject and the protection rules
permissions:
id-token: write # lets the job request an OIDC token
contents: read
steps:
- uses: actions/checkout@<full-commit-sha-of-v5> # v5
- name: Fetch deploy secrets from Infisical
uses: Infisical/secrets-action@<full-commit-sha-of-v1.0.18> # v1.0.18
with:
method: "oidc"
identity-id: ${{ vars.INFISICAL_IDENTITY_ID }}
domain: "https://app.infisical.com"
project-slug: "<project-slug>"
env-slug: "prod"
secret-path: "/deploy"
export-type: "env"
Fija ambas actions a los SHA completos de sus commits, a menos que tu repositorio use lockfiles de dependencias de workflows, que GitHub recomienda como primera opción y que reemplazan la fijación por SHA (uso seguro de GitHub). Un tag puede cambiar de referencia. El README de secrets-action enumera las entradas y la configuración de OIDC (secrets-action).
Runtime: mantén los secretos revocables fuera de las variables de Railway
La regla es simple: un secreto que puedas necesitar revocar rápidamente no debería depender de un deploy para cambiar.
- Railway solo aplica un cambio de variable a un servicio en ejecución mediante un deploy. Por defecto, un cambio vuelve a desplegar el servicio. El campo
skipDeploysde la API desactiva ese comportamiento, y la guía de Railway indica que debes iniciar un deploy manualmente cuando una rotación deba surtir efecto de inmediato (guía de Railway). - Cada servicio conserva una credencial de bootstrap. El equipo de Railway indicó en abril de 2026 que los servicios en ejecución no cuentan con identidad de workload mediante OIDC (Railway Station, 2026-04-29). Esa credencial es un secreto persistente [INFERENCE]. El ejemplo de bootstrap de Infisical combina un client secret limitado a un uso, o con un TTL corto, con un token de acceso periódico que el workload renueva continuamente. Ese token no tiene una duración máxima y cualquier interrupción de la renovación, incluido un reinicio, requiere un nuevo client secret. Si configuras un TTL para el client secret, agrega una alerta de vencimiento de credenciales (Universal Auth).
- Las variables selladas están ocultas para la API de Railway y
railway run(guía de Railway). No ocultan el valor al código que se ejecuta en el servicio [INFERENCE]. - Mantén la base de datos privada. Un proxy TCP expone el puerto de un servicio a internet (proxy TCP de Railway). Elimina cualquier proxy del servicio de base de datos y confirma que no quede ningún dominio público. La red privada solo llega a servicios dentro del mismo entorno de proyecto (red privada de Railway).
- Mantén los secretos del servidor fuera de las compilaciones estáticas. Astro reemplaza los valores de entorno durante la compilación (variables de entorno de Astro).
- Para el servicio del agente, usa el modo JWT. AgentOS usa una
OS_SECURITY_KEYcompartida cuando no hay una fuente de JWT configurada. Con tokens de Auth0, configura las variables de entorno de JWT yauthorization=True, y luego elimina la clave compartida. Una clave de seguridad válida también puede generar tokens de cuentas de servicio (seguridad de Agno AgentOS).
Postgres: rotación, leases y la ruta
Dos mecanismos, con costos diferentes.
- La rotación alterna entre dos usuarios, de modo que una contraseña sigue siendo válida mientras cambia la otra (introducción a la rotación de Infisical). Ese solapamiento es el punto delicado: con la rotación de dos fases, la credencial inactiva sigue siendo válida hasta el siguiente ciclo, por lo que una contraseña filtrada puede sobrevivir dos intervalos. Si sospechas de una filtración, revoca primero; no esperes a la rotación programada. El rol de rotación necesita
CREATEROLE(conexión de PostgreSQL). UsaCREATEROLE, noSUPERUSER. Otorga cada usuario administrado al rol de rotación conWITH ADMIN OPTION, como muestra la misma página, y activa Platform Managed Credentials para que Infisical configure la contraseña de la conexión.CREATEROLEpuede crear y modificar roles, así que trata ese rol como privilegiado. - Los leases crean una cuenta nueva por lease, cuyo nombre se genera a partir de una plantilla de nombre de usuario, con un TTL (secretos dinámicos). Al eliminar un lease, se revocan sus credenciales en el proveedor (CLI de Infisical: secretos dinámicos). Los secretos dinámicos son una función de Advanced (precios de Infisical).
Esta es la parte que no pudimos resolver: la ruta que usa Infisical para llegar a un Postgres sin puerto público, necesaria tanto para la rotación como para los leases. El gateway de Infisical se ejecuta dentro de tu red y establece una conexión saliente en modo relay (introducción al gateway). La página de despliegue del gateway indica que una base de datos a la que se accede mediante una conexión de aplicación, un secreto dinámico o un destino de rotación usa el gateway como ruta de red (despliegue del gateway). Ninguna de las páginas de Postgres que leímos muestra cómo seleccionar un gateway. El ejercicio de abajo lo pone a prueba.
Terraform: topología en código, valores como lecturas efímeras
Terraform escribe en los archivos de estado y de plan cualquier valor que incluyas en la configuración. El argumento sensitive solo lo oculta en la salida de la CLI (Terraform: administrar datos sensibles). Desde Terraform 1.10, los valores y recursos efímeros nunca llegan a los archivos de estado ni de plan. Desde 1.11, los argumentos de solo escritura hacen lo mismo para los recursos administrados, cuando el proveedor los admite.
El proveedor de Infisical administra la topología: infisical_project, infisical_identity e infisical_project_role (proyecto, identidad, rol de proyecto). Los roles personalizados requieren Advanced según la página de precios o Enterprise según la documentación de RBAC, así que verifica el plan antes de depender de ellos. El proveedor lee valores mediante el recurso efímero infisical_secret (documentación del secreto).
terraform {
required_version = ">= 1.10"
required_providers {
infisical = {
source = "infisical/infisical"
}
}
}
variable "infisical_client_secret" {
type = string
sensitive = true
ephemeral = true # never written to state or plan
}
provider "infisical" {
host = "https://app.infisical.com"
auth = {
universal = {
client_id = "<machine-identity-client-id>"
client_secret = var.infisical_client_secret
}
}
}
ephemeral "infisical_secret" "postgres_password" {
workspace_id = "<project-id>"
env_slug = "prod"
folder_path = "/db"
name = "POSTGRES_PASSWORD"
}
locals {
db_password = ephemeral.infisical_secret.postgres_password.value
}
El valor solo puede pasar a argumentos de solo escritura u otros contextos efímeros. En cualquier otro lugar, como un argumento normal de un recurso administrado, Terraform rechaza el plan con un error, por lo que el valor nunca llega al estado (bloque efímero).
Hay dos advertencias.
- El recurso de client secret de Universal Auth de la identidad devuelve
client_secretcomo un atributo Sensitive de solo lectura (documentación del proveedor). Terraform lo conserva en el estado y cualquiera que pueda leer el estado puede leer el secreto. Crea ese secreto fuera de Terraform o conserva el estado en un backend cifrado con controles de acceso (Terraform: administrar datos sensibles). - El proveedor comunitario de Railway marca los valores de las variables como
Sensitive, no como solo escritura (documentación del proveedor de Railway). Mantén los valores de Railway en la sincronización de Infisical, no en el estado de Terraform.
Planes y costos al 2026-11-02
| Concepto | Precio o límite (plan) | Fuente |
|---|---|---|
| Identidades, personas y máquinas | $20 Pro; $40 Advanced; por identidad al mes, facturación anual (Pro, Advanced) | precios de Infisical |
| Agent Vault | Primeras 5,000 solicitudes intermediadas al mes gratis, luego $2 por cada 1,000 (Pro) | precios de Infisical |
| Secretos dinámicos; rotación completa | Advanced; Pro solo rota bases de datos públicas | precios de Infisical |
| Gateways | Advanced en precios; Enterprise en la introducción al gateway (contradicción) | precios, introducción al gateway |
| Aprobaciones: políticas de cambio, solicitudes de acceso | Enterprise | políticas de cambio, precios |
| Retención del registro de auditoría | Pro 30 días; Advanced 90 días; Enterprise personalizado | precios, registros de auditoría |
| Roles personalizados | Advanced en precios; Enterprise en la documentación de RBAC (contradicción) | precios, RBAC |
| Revisores obligatorios en un repositorio privado | Enterprise (Free, Pro, Team: solo repositorios públicos) | entornos de GitHub |
El costo mensual aproximado es identidades × $40 + max(0, llamadas intermediadas − 5,000) ÷ 1,000 × $2, en Advanced con facturación anual. Una llamada intermediada es aquella en la que el proxy de Agent Vault adjunta una credencial real antes de reenviarla (precios). Ejemplo: 6 identidades de servicio y 200,000 llamadas intermediadas al mes suman 6 × $40 + 195 × $2 = $630, sin contar las personas ni el precio de Enterprise si los gateways lo requieren.
Un ejercicio que puede fallar
- Cambia una variable de prueba que no sea un secreto mediante la API de Railway con
skipDeploys: true. El contenedor en ejecución debería conservar el valor anterior hasta un deploy. Esto confirma que un cambio de variable no se aplica en vivo por sí solo (guía de Railway). - Emite un lease de Postgres para la aplicación y ejecuta una rotación a través del gateway. Esto pone a prueba la ruta. Agent Vault no aplica aquí, porque solo adjunta credenciales a solicitudes HTTP/1.1 (proxies de Agent Vault).
- Elimina el lease. La aplicación debería dejar de usarlo dentro de un ciclo de consulta de Infisical Agent, cinco minutos por defecto (Infisical Agent), y debería adoptar un reemplazo sin un deploy.
- Revoca una clave de OpenRouter de una aplicación y luego emite un reemplazo. Agent Vault consulta cada 60 segundos por defecto, por lo que el proxy debería usar el reemplazo dentro de un ciclo de consulta, sin un deploy (proxies de Agent Vault).
Aprobado: la aplicación se recupera sin un deploy y las credenciales revocadas dejan de funcionar dentro de un ciclo de consulta. Fallido: la aplicación conserva la credencial revocada o necesita un deploy para ver el nuevo valor. Una falla en el paso 2 significa que el diseño del runtime todavía no funciona, porque falta la ruta. Una falla en el paso 3 o 4 significa que la tesis es incorrecta para ese secreto.
Qué sigue sin verificarse
- Si una conexión de aplicación de Postgres o un secreto dinámico puede seleccionar un gateway. Ninguna página de Postgres muestra un selector. El paso 2 lo pone a prueba.
- Si la aplicación deja de usar un lease revocado y adopta un reemplazo dentro de un ciclo de consulta. El paso 3 lo pone a prueba.
- Si una sesión de Agent Vault adopta una clave de OpenRouter reemplazada dentro de un ciclo de consulta. El paso 4 lo pone a prueba.
- El plan necesario para el gateway: Advanced en la página de precios, Enterprise en la introducción al gateway.
- Si la rotación de Auth0 puede hacerse sin interrupciones para tu aplicación. Auth0 indica que puede aceptarse tanto el secreto anterior como el nuevo durante un máximo de 30 segundos, lo cual es una ventana de propagación, no un solapamiento garantizado. Desactiva la rotación automática, despliega el nuevo secreto y realiza el cambio durante una ventana programada.
Fuentes
- LiteLLM: la cadena de ataque detrás del lanzamiento de v1.107.0: postmortem de fuente primaria; eventos del 2026-05-27 y 2026-10-10.
- Railway: informe preliminar del incidente de seguridad de octubre: 2026-10-22, fuente primaria.
- Railway: sincronizar secretos desde un administrador externo: actualizado el 2026-10-29, fuente primaria.
- Railway Station: pregunta sobre OIDC para workloads: 2026-04-29, respuesta del equipo.
- Railway: proxy TCP y red privada: sin fecha.
- Wiz: Gheist: 2026-10-25, fuente primaria; The Hacker News: 2026-10-26, fuente secundaria.
- GitHub: referencia de OpenID Connect, despliegues y entornos y uso seguro: sin fecha.
- actions/checkout v5 y Infisical secrets-action v1.0.18: runtime y entradas verificados; sin fecha.
- Infisical: autenticación con GitHub OIDC, secrets-action, Universal Auth: sin fecha.
- Infisical: introducción a la rotación, rotación de Auth0, conexión de PostgreSQL, secretos dinámicos de PostgreSQL, CLI de secretos dinámicos: sin fecha.
- Infisical: introducción al gateway y despliegue del gateway: sin fecha.
- Infisical: Agent y proxies de Agent Vault: sin fecha.
- Infisical: precios: consultado el 2026-11-02; los precios y los requisitos de los planes cambian.
- Infisical: políticas de cambio, RBAC, registros de auditoría: sin fecha.
- Proveedor de Terraform para Infisical: documentación de proyecto, identidad, rol de proyecto, client secret y secreto efímero; sin fecha.
- Auth0: rotar client secrets y Private Key JWT: sin fecha.
- OpenRouter: Security Center: 2026-09-28; claves de la API de administración: sin fecha.
- Terraform: bloque efímero y administrar datos sensibles: sin fecha.
- Proveedor comunitario de Terraform para Railway: variable: sin fecha.
- Agno: seguridad de AgentOS y Astro: variables de entorno: sin fecha.