Ir al contenido

Denylist de tools

WhyStack pide acceso de lectura privilegiado a todo tu stack, y para una parte de él la respuesta legítima es no. Las operaciones que leen security groups, ACLs y reglas de firewall describen la superficie de ataque de tu infraestructura; algunos equipos no van a autorizar eso, y no deberían tener que desconectar AWS por completo (perdiendo de paso costos, compute y RDS) para poder decirlo.

La denylist es cómo dices “puedes leer mis métricas y mis logs, pero no mis security groups”.

Una denylist que viviera solo en nuestro control-plane sería configuración nuestra, que tendrías que confiar en que respetamos. Aplicada en el borde, la puedes verificar tú: lees tu propio manifiesto de deployment. Es el mismo razonamiento que ya rige tus credenciales (nunca custodiadas fuera de tu infraestructura), la redacción de PII (en el borde, antes de que salga nada) y el gate de escritura.

Eso tiene una consecuencia que preferimos decir sin rodeos: la pantalla del control-plane es un borrador. Hasta que el artefacto se despliega, nada cambia: ni la superficie de tools ni lo que el agente puede leer. Ocultar tools a partir de una política que compusiste pero nunca desplegaste produciría la sensación de protección sin la protección, y un control de seguridad que existe solo en nuestra interfaz no es un control de seguridad.

Cuatro niveles, resueltos como unión. El más fino gana donde se superponen:

Nivel Ejemplo Cuándo usarlo
Grupo con nombre network-posture, iam El camino principal. Un recorte curado que acompaña al catálogo.
Capability security, audit Cuando toda la categoría está fuera de discusión.
Connector amplitude Cuando toda la fuente está fuera de discusión.
Operación network.listSecurityGroups Ajuste fino, cuando ninguno de los recortes de arriba sirve.

Porque una capability es demasiado gruesa para el caso que motiva esta funcionalidad. La capability network cubre la lectura de security groups y la de VPCs, subnets, load balancers, target groups y transit gateways. Negarla por completo también tumba el mapa de recursos, el blast radius y la topología de DNS, que probablemente es lo opuesto de lo que quieres.

El grupo network-posture aísla exactamente las operaciones que describen la postura de red (regla de firewall, ACL, security group) y deja el resto en pie.

Puedes escribir una, y para el ajuste fino es la herramienta correcta. Pero una lista literal compuesta en marzo no cubre la operación de security group que se agregó al catálogo en mayo, y nadie relee su denylist en cada release. El artefacto guarda la intención, no la lista expandida: el grupo compuesto en marzo cubre la operación agregada en mayo.

Un archivo JSON que montas en el deployment del runner (ConfigMap, archivo montado, lo que use tu GitOps):

{
"version": 1,
"deny": {
"groups": ["network-posture", "iam"],
"capabilities": ["audit"],
"connectors": ["amplitude"],
"ops": ["query.runAthenaQuery"]
}
}

Que sea un archivo y no un puñado de variables de entorno es deliberado: un archivo JSON en tu repositorio de GitOps es diffable, pasa por un pull request y lo revisa tu equipo de seguridad. Una variable de entorno enterrada en un values.yaml no la revisa nadie.

La ruta del archivo va en RUNNER_DENY_POLICY_FILE. Para el caso simple, cuatro variables hacen lo mismo sin artefacto, y las dos fuentes se combinan si usas ambas:

Variable Contenido
RUNNER_DENY_POLICY_FILE Ruta al artefacto JSON
RUNNER_DENIED_GROUPS Grupos con nombre, separados por comas
RUNNER_DENIED_CAPABILITIES Capabilities
RUNNER_DENIED_CONNECTORS Connectors
RUNNER_DENIED_OPS Operaciones

Un artefacto ilegible, un JSON inválido o una clave desconocida dentro de él hacen fallar el boot en lugar de degradarse a “nada negado”: caer en una política vacía cuando pediste una negación es el peor valor por defecto posible.

Grupo Cubre
network-posture Security groups (resumen, asociaciones, los abiertos al mundo) y reglas de firewall en AWS y GCP. No cubre VPCs, subnets, load balancers, rutas ni DNS: negar la postura no debe tumbar el mapa de recursos.
iam Higiene de claves y credenciales: access keys (incluida la búsqueda por sufijo), claves antiguas de service accounts de GCP, API/app keys.
audit-trail El rastro de quién hizo qué: CloudTrail (lookup más la cadena de consultas de Lake) y los audit logs de GCP. No cubre el inventario de recursos: negar el rastro no debe tumbar el descubrimiento de lo que existe.
end-user-activity El individuo: búsqueda y actividad de usuarios en Amplitude, más los session replays. No cubre las métricas agregadas de producto (usuarios activos, volumen de eventos, retención, conversión): negar esto protege a la persona, no apaga tu embudo.

La lista acompaña al catálogo: una operación nueva que describa la postura de red entra en network-posture sin que toques tu artefacto. Para eso existe el grupo.

Dos capas, con la misma estructura que el gate de escritura: una en lo que se anuncia, otra en lo que se ejecuta.

  1. Anuncio. En el boot, el runner expande tu política contra su propio catálogo y resta las operaciones negadas del conjunto que declara servir. El control-plane nunca expone una tool cuya operación no se sirve, así que simplemente no existe para el modelo.
  2. Ejecución. enforcement.ts, el mismo punto que ya rechaza cualquier operación fuera del catálogo, se niega a invocar una operación negada aunque la llamada llegue. Un control-plane con bugs, una versión vieja o un catálogo desincronizado no evaden la política.

La segunda capa existe justamente porque la primera depende de nosotros. La negación no se sostiene porque aceptamos ocultarla; se sostiene porque el proceso que corre dentro de tu infraestructura se niega a ejecutarla.

El runner anuncia de vuelta lo que la política negó de verdad: qué operaciones, y bajo qué regla (si cayó bajo un grupo, una capability, un connector, o se nombró directamente). Es la expansión aplicada, no la intención que escribiste.

Eso importa por dos razones. Primero, es como descubres en qué se expande una etiqueta: el network-posture que negaste se convierte en una lista concreta de operaciones, declarada por el proceso que las rechaza. Segundo, se contrasta con el borrador: la vista Flota muestra ambos lado a lado, y cualquier divergencia entre “lo que compuse” y “lo que está en vigor” es visible (típicamente un artefacto que aún no se ha desplegado, o una flota que corre versiones mezcladas).

Esta es la parte más importante de esta página, y la razón por la que la denylist no es una simple resta.

Si una tool simplemente desapareciera, tres situaciones se verían idénticas: el connector no está configurado, el connector está roto, y decidiste prohibirlo. Solo en la tercera la infraestructura existe, está sana, y el vacío es una decisión. Si le preguntaran por la exposición de red, el agente respondería “no encontré security groups permisivos”. Eso es falso, y es el peor resultado posible, porque un error hace que alguien investigue y una respuesta tranquilizadora no.

Así que la supresión es legible, en tres lugares:

  • La sesión conoce la política antes de necesitarla. La política se inyecta en el contexto de la sesión, así que el agente responde “eso está deshabilitado por la política de tu tenant” en lugar de “no encontré nada”.
  • La respuesta lleva el motivo. Una fuente negada se reporta como denied_by_policy, que cuenta como no pude leer, nunca como no existe. “La política me lo prohibió” no es prueba de ausencia.
  • La vista Flota distingue las tres situaciones desde afuera: nunca conectado · conectado y roto · conectado y restringido.

Un análisis que dependía de una operación negada corre con lo que queda y dice lo que faltó, en lugar de fallar por completo. Un análisis compuesto multiseñal suele tocar mucho más que la dimensión negada, y apagarlo haría que la capacidad desapareciera sin explicación.

Lo que no sale es el veredicto derivado. Un puntaje de seguridad calculado a partir de penalizaciones saldría más alto precisamente porque no se pudo leer la dimensión negada (una lectura que nunca ocurrió mejorando el número), así que el puntaje no se emite. Recibes lo que se observó, marcado como parcial, y ninguna conclusión que dependa de lo que la política ocultó.

El aprendizaje se detiene automáticamente: el conocimiento derivado (topología, mapeos) se extrae del resultado de las operaciones, y una operación negada no produce resultado.

Lo que se aprendió antes de que la política entrara en vigor se queda donde está. Eso es deliberado: el artefacto vive en un runner desechable, y un error de tipeo en un archivo no debe destruir en silencio meses de topología aprendida. La purga es una acción separada y auditada: la vista Flota muestra que existe estado anterior a la política y ofrece borrarlo.

Si el artefacto nombra una operación que el runner no conoce (renombrada en el catálogo, o compuesta contra una versión más nueva), el runner se niega a arrancar y nombra la entrada.

Parece severo, y es la decisión correcta: una directiva de negación que no tiene efecto es invisible hasta que se convierte en incidente. Los runners son cattle, un boot que falla hace ruido, y tu supervisor muestra el error de inmediato, mientras que una política parcial en silencio solo aparece el día en que alguien lee lo que no debía.

Una entrada también puede nombrar algo que el catálogo tuvo y quitó a propósito. El rechazo es el mismo: la regla ya no protege nada, y mantenerla es creer en una protección que ya no existe. Pero el mensaje es distinto: dice en qué versión salió el objetivo y qué usar en su lugar, en vez de mandarte a buscar una operación renombrada, que aquí sería la causa equivocada. Es el caso de la capability profiling y del connector witness, ambos eliminados en la versión 0.50.0 del catálogo: si tu política todavía los nombra, consulta el Profiler.