Seguridad e identidad

Arquitectura de seguridad de aplicaciones: threat modeling y principios de diseño

Intermedio11 min de lecturaActualizado

Esta página se tradujo del inglés con ayuda de IA y se cotejó con el original. Leer la versión en inglés

La seguridad que se añade tarde sale cara y se queda corta. Esta guía muestra cómo tratar la seguridad como una propiedad de la arquitectura: representar el sistema como flujos de datos y fronteras de confianza, enumerar amenazas con STRIDE, aplicar los principios clásicos de protección, decidir qué capa aplica cada control e integrar toda la práctica en el pipeline de entrega.

Lo que aprenderás

  • Dibujar un diagrama de flujo de datos con fronteras de confianza y usarlo para razonar sobre la superficie de ataque
  • Enumerar y priorizar amenazas con STRIDE y convertir cada una en un control concreto
  • Aplicar mínimo privilegio, defensa en profundidad, mediación completa, fallo seguro y valores seguros por defecto a diseños reales
  • Elegir la capa correcta para la autenticación, la autorización y la validación de entradas
  • Gestionar los secretos de la aplicación sin meterlos nunca en el control de versiones
  • Incorporar las actividades de seguridad al pipeline de entrega en lugar de dejarlas para una auditoría previa al lanzamiento

Casi todas las brechas graves se remontan a una decisión, no a una errata: un servicio al que se le dieron credenciales de base de datos que nunca necesitó, un endpoint interno que todo el mundo daba por inalcanzable, una integración que se fiaba de un identificador proporcionado por el llamante. Son problemas de arquitectura, y ningún cuidado en el código posterior los compensa del todo. Diseñar la seguridad desde el principio significa preguntarse, mientras las cajas y las flechas todavía cuestan poco de mover, quién puede hablar con qué, qué se le permite hacer a cada uno y qué pasa cuando una de las cajas cae en manos de un atacante.

Esta guía trata de ese trabajo de diseño. Explica cómo modelar un sistema para que las amenazas se vuelvan visibles, el pequeño conjunto de principios que han sobrevivido a cincuenta años de práctica, dónde encaja cada tipo de control en una aplicación por capas, cómo gestionar los secretos y cómo mantener todo esto dentro de la entrega habitual en lugar de convertirlo en una carrera de última hora antes del lanzamiento. Las categorías de riesgo concretas se recogen en la guía OWASP Top 10; aquí nos situamos un nivel por encima, decidiendo la forma del sistema que hace improbables esos riesgos.

La seguridad es una propiedad del diseño

Los requisitos funcionales describen lo que el sistema debe hacer. Los requisitos de seguridad describen lo que no debe hacer nunca, lo pida quien lo pida, y los requisitos negativos no se pueden probar de forma exhaustiva; por eso tienen que ser estructurales. «El servicio de informes no puede leer los datos de pago de los clientes» es una afirmación sólida cuando ese servicio no tiene ninguna credencial que llegue a esa tabla; es una afirmación débil cuando solo es una convención de revisión de código.

Tres preguntas de arquitectura soportan casi todo el peso:

  • ¿Cuáles son los activos? Datos personales, movimientos de dinero, credenciales, registros de auditoría, disponibilidad de un flujo crítico. Priorízalos, porque no puedes protegerlo todo por igual.
  • ¿Dónde están las fronteras de confianza? Cada punto en el que los datos pasan de un sitio que controlas a otro que no controlas, o entre dos componentes con distinto nivel de privilegio, es una frontera que necesita autenticación, autorización y validación.
  • ¿Cuál es el radio de impacto? Si un componente queda totalmente comprometido, ¿hasta dónde llega el atacante? Un diseño cuya respuesta es «a todo» necesita trabajo, por muy bueno que sea su tratamiento de entradas.

Threat modeling con flujos de datos y STRIDE

Un threat model útil empieza con un diagrama que cualquiera del equipo podría dibujar: actores externos, procesos, almacenes de datos, los flujos entre ellos y líneas discontinuas para las fronteras de confianza. El detalle es el enemigo: una página, al nivel de «el navegador habla con el API gateway, que llama al servicio de pedidos, que escribe en la base de datos de pedidos y publica un evento».

[Browser] --https--> | TB1 | [API gateway] --mTLS--> [Order service] --TLS--> (Orders DB)
                                   |                        |
                                   v                        v
                            [Identity provider]      | TB2 | [Payment provider]

TB1  internet / platform boundary   — untrusted input, session establishment
TB2  organisation / third party     — outbound secrets, response we do not control

Después, pasa cada elemento y cada flujo por STRIDE, una regla mnemotécnica que asocia cada categoría de amenaza con la propiedad que vulnera:

AmenazaPropiedad perdidaControl típico
Spoofing (suplantación)AutenticidadAutenticación fuerte de usuarios y de servicios (TLS mutuo, tokens firmados)
Tampering (manipulación)IntegridadFirmas, transporte con protección de integridad, validación en el servidor, registros de auditoría inmutables
Repudiation (repudio)No repudioLogs append-only con identidad fiable, transacciones firmadas
Information disclosure (revelación de información)ConfidencialidadCifrado en tránsito y en reposo, minimización, mensajes de error cuidadosos
Denial of service (denegación de servicio)DisponibilidadCuotas, rate limiting, timeouts, bulkheads, límites de recursos al parsear
Elevation of privilege (escalada de privilegios)AutorizaciónMínimo privilegio, mediación completa, comprobaciones a nivel de recurso

Registra cada hallazgo como un riesgo de una línea con el control que lo resuelve, un responsable y una decisión. Basta con ordenar por probabilidad e impacto; una puntuación elaborada rara vez cambia lo que el equipo hace a continuación.

- id: "TM-014"
  element: "Order service -> Payment provider"
  threat: "Information disclosure"
  description: "API key for the payment provider is read from an environment variable and printed by the outbound HTTP logger."
  likelihood: "medium"
  impact: "high"
  control: "Redact Authorization headers in the client interceptor; move the key to the secret manager with 90-day rotation."
  status: "accepted-fix"
  owner: "payments"

Junto al modelo, escribe abuse cases al lado de las user stories a las que dan la vuelta. Si la story es «un cliente descarga su factura», el abuse case es «un cliente descarga la factura de otra persona cambiando el identificador en la URL», y se convierte en un test. Los equipos que añaden un abuse case por cada story relevante detectan la mayoría de los defectos de control de acceso antes de que lleguen a existir.

Los principios que siguen dando resultado

Mínimo privilegio. Cada identidad (persona, servicio, proceso batch) recibe el conjunto de permisos más reducido que le permita hacer su trabajo, y nada más. En la práctica, esto significa usuarios de base de datos separados por servicio con permisos limitados a las tablas que tocan, roles cloud acotados a recursos concretos y contenedores que no se ejecutan como root. También significa que los privilegios caducan: una elevación temporal que nunca se revoca equivale a una permanente.

Defensa en profundidad. Da por hecho que cualquier control aislado fallará. Una consulta parametrizada, una cuenta de base de datos sin permisos de DROP y una alerta ante un volumen de consultas inusual defienden el mismo activo con mecanismos distintos.

Mediación completa. Cada acceso a cada objeto protegido se comprueba, cada vez, en el servidor. Cachear una decisión de autorización durante toda la sesión es justo lo que permite que «el usuario era admin cuando inició sesión» sobreviva a su degradación.

Fallo seguro. Cuando algo sale mal (el servicio de políticas no responde a tiempo, falta un claim, un certificado no se puede validar), el resultado seguro es denegar. Es fácil hacerlo al revés en un código que trata una excepción como «desconocido, así que se permite».

public boolean canView(User user, Document doc) {
    try {
        return policyEngine.evaluate(user, "document:read", doc);
    } catch (PolicyUnavailableException e) {
        log.warn("policy engine unavailable, denying document {}", doc.id(), e);
        return false;                 // deny on failure, never "return true" to keep the page working
    }
}

Valores seguros por defecto. El estado inicial debe ser el seguro: registro cerrado, endpoints de depuración desactivados, TLS obligatorio, sin cuentas de ejemplo. Quien necesite algo más permisivo lo activa de forma explícita y deja rastro.

Minimizar la superficie de ataque. Cada endpoint expuesto, feature flag activado, paquete instalado y puerto abierto es algo más que defender. Eliminar una consola de administración que no se usa es un control más fuerte que protegerla bien.

Separación de funciones. La persona o el pipeline que escribe un cambio no debería ser el único capaz de aprobarlo y desplegarlo, y el servicio que emite abonos no debería ser también el que los aprueba.

No confiar en la oscuridad. Da por hecho que el atacante ha leído tu código fuente y tu documentación. Los secretos son claves y tokens, no URL ni nombres de esquema.

Dónde va cada control

Un error de diseño recurrente es aplicar una regla en una capa que se puede esquivar. Coloca los controles preguntándote qué podría llegar a la operación si la capa superior se saltara por completo.

AspectoDónde vaPor qué
Cifrado del transporte, rate limiting, reglas de enrutamiento generalesEdge / gatewayBarato de aplicar de manera uniforme; consulta API management y API gateways
Autenticación del llamanteEn el edge para la identidad, verificada de nuevo en cada servicioUn llamante interno también tiene que demostrar quién es
Autorización general (roles, scopes)Filtro o interceptor del frameworkSe aplica por defecto a todas las rutas
Autorización a nivel de recurso («¿este pedido es tuyo?»)Servicio de dominio o de aplicaciónSolo esa capa conoce la regla de propiedad
Validación sintáctica de entradasFrontera, en el servidorRechazar pronto y a bajo coste las entradas imposibles
Validación de reglas de negocio («el saldo no puede quedar en negativo»)Modelo de dominioEs un invariante del dominio, no un problema de entrada
Integridad referencial y unicidadRestricciones de base de datosLa última línea que ninguna ruta de código puede saltarse
Codificación de salidaCapa de vista / renderizadoLa codificación depende del contexto en el que acaba el dato

La validación en el cliente es una mejora de usabilidad y nada más. En un sistema distribuido, esta tabla se aplica servicio a servicio: en una arquitectura de microservicios, el gateway autentica al usuario final y cada servicio verifica el token de forma independiente y aplica su propia autorización, porque una posición en la red no es un permiso. Ese es el núcleo práctico del zero trust tal como lo describe el NIST SP 800-207.

Para la implementación de estas capas basada en servlets (cadenas de filtros, comprobaciones a nivel de método y validación de tokens stateless), consulta los fundamentos de Spring Security.

Los secretos también son arquitectura

Un secreto en un repositorio queda comprometido desde el momento en que se hace push, y reescribir el historial no deshace la filtración: hay que rotar la credencial. El objetivo de diseño es que ninguna persona llegue a escribir nunca un secreto de larga duración.

  • Prefiere la identidad de workload a los secretos compartidos. Las plataformas cloud pueden emitir credenciales de corta duración para una instancia en ejecución a partir de su identidad, lo que elimina por completo la clave estática.
  • Guarda lo que quede en un gestor de secretos, inyectado en tiempo de ejecución como variable de entorno o como fichero montado, nunca incrustado en una imagen ni en un fichero de configuración que viaje con el build.
  • Rota los secretos de forma periódica y ante cualquier sospecha, y diseña la aplicación para que recoja un valor nuevo sin necesidad de rebuild.
  • Separa los secretos por entorno, para que una clave de pruebas filtrada no sirva de nada en producción.
  • Busca secretos en el pipeline y en hooks de pre-commit, y trata cada coincidencia como un incidente, no como un detalle menor de la revisión de código.
  • Cifra los datos sensibles en reposo con claves que la aplicación no posee directamente, usando un servicio de gestión de claves, para que una copia de seguridad robada de la base de datos no sea un conjunto de datos robado.

Seguridad en el pipeline de entrega

Hacer shift left significa que el pipeline hace cumplir lo que decidió el diseño, de modo que los hallazgos de seguridad lleguen mientras el cambio todavía está fresco en la cabeza de quien lo hizo. Un pipeline de entrega práctico ejecuta, más o menos en este orden: detección de secretos y comprobación de dependencias en cada commit; análisis estático ajustado a una tasa baja de falsos positivos para que los desarrolladores sigan confiando en él; análisis del contenedor y de la imagen base en el build; comprobaciones de políticas sobre la infraestructura como código; un análisis dinámico contra un entorno de pruebas desplegado; y artefactos firmados con su procedencia, para poder demostrar que lo que se ejecuta en producción es exactamente lo que se construyó.

stages:
  - build            # unit tests, SAST, secret scan, SBOM generation
  - package          # container build, image scan, sign the artefact
  - verify           # deploy to ephemeral env, DAST, authorization test suite
  - release          # policy gate: no critical findings, provenance verified

Hay dos hábitos que importan más que la elección de herramientas. Primero, haz que el build falle solo con los hallazgos que el equipo ha acordado que son inaceptables, y haz seguimiento del resto con una fecha límite: un pipeline que falla siempre enseña a la gente a saltárselo. Segundo, escribe tests de regresión para cada defecto de seguridad que corrijas, sobre todo los de autorización; son tests normales que comprueban que una petición devuelve 403, y son la protección más barata contra la vuelta del mismo bug.

Errores habituales

  • Modelar solo el camino feliz. El diagrama muestra el navegador y la API, y omite la herramienta de administración, la exportación nocturna y el acceso de lectura del equipo de soporte, que es precisamente donde están los privilegios interesantes.
  • Tratar la red interna como de confianza. En cuanto un servicio queda comprometido, una malla interna plana y sin autenticación lo entrega todo.
  • Autorizar a partir de identificadores que envía el cliente. Un identificador de tenant en el cuerpo de una petición es una afirmación, no un hecho; derívalo del principal autenticado.
  • Una única cuenta de base de datos compartida por todos los servicios. Hace imposible el mínimo privilegio y lleva el radio de impacto al máximo.
  • Registrar el payload en los logs. Tokens, números de tarjeta y datos personales acaban en un almacén de logs con un control de acceso mucho más débil que el de la base de datos de la que salieron.
  • Dar por cerrado un threat model y no volver a abrirlo nunca. El modelo es un artefacto vivo; una integración nueva lo invalida.
  • Comprar un escáner en lugar de tomar una decisión. Las herramientas encuentran tipos de bugs conocidos; no pueden decirte que un servicio nunca debió tener ese permiso.

Ideas clave

  • La seguridad la decide la arquitectura: los activos, las fronteras de confianza y el radio de impacto determinan lo que cualquier esfuerzo de código posterior puede lograr.
  • Un diagrama de flujo de datos de una página más un repaso con STRIDE produce controles concretos y con responsable más rápido que cualquier plantilla de documento.
  • Mínimo privilegio, defensa en profundidad, mediación completa, fallo seguro, valores seguros por defecto y una superficie de ataque reducida son los principios que siguen compensando el esfuerzo.
  • Coloca cada control en la capa que no se puede esquivar: reglas generales en el edge, autorización a nivel de recurso en el dominio, invariantes en la base de datos.
  • Los secretos van en un gestor o, mejor aún, se sustituyen por una identidad de workload de corta duración; todo lo que se haya subido a un repositorio hay que rotarlo.
  • Haz que el pipeline imponga el diseño (análisis, gates de políticas y tests de regresión de autorización) y mantén vivo el threat model a medida que el sistema cambia.

Fuentes y lecturas recomendadas

Preguntas frecuentes

¿En qué momento del proyecto hay que hacer el threat modeling?
En cuanto exista un boceto de los componentes y de los datos que circulan entre ellos, y otra vez cada vez que ese boceto cambie de forma significativa: una integración nueva, un nuevo almacén de datos, un nuevo tipo de usuario. Un primer modelo hecho en una pizarra en una hora vale más que un documento perfecto redactado cuando el código ya está escrito, porque en la fase de diseño la solución suele ser tomar otra decisión, no reescribir.
¿La defensa en profundidad no es simplemente trabajo duplicado?
No, porque cada capa falla de una forma distinta. Una regla del gateway la puede esquivar un llamante interno, un control del framework se lo puede saltar una nueva ruta de código, una restricción de la base de datos no se la puede saltar nadie. Las capas se eligen para que el punto ciego de un control sea el punto fuerte de otro. Lo que es un desperdicio es copiar la misma comprobación dos veces en la misma capa; lo que aporta valor es que el mismo invariante lo hagan cumplir mecanismos independientes.
¿Cómo convenzo a un equipo de dedicar tiempo de diseño a la seguridad?
Plantéalo en términos de coste. Un control de autorización que falta, detectado en un diagrama, cuesta una conversación; detectado en producción, cuesta un incidente, una notificación y un parche hecho con prisas. Lleva un abuse case por cada user story al refinement, mantén el modelo en el mismo tablero que la arquitectura y deja constancia de las decisiones tomadas, incluidos los riesgos que aceptaste de forma deliberada, para que nadie vuelva a discutirlas más adelante.
¿El zero trust sustituye a la seguridad de red?
Sustituye la suposición de que estar dentro de la red implica confianza. Cada petición tiene que seguir autenticándose, autorizándose y, siempre que se pueda, cifrándose, tanto si cruza internet como si cruza una subred. La segmentación de red sigue siendo valiosa como una capa más que limita hasta dónde puede moverse un intruso, pero ya no es la frontera sobre la que se apoya todo el diseño.