Identidad y acceso · Lección 01

RBAC y ABAC: diseñar la autorización

La mayoría de las brechas graves no rompen el cifrado ni adivinan contraseñas: piden algo que no les toca y el servidor se lo da. Autenticar es saber quién eres; autorizar es decidir qué puedes hacer, y es donde se cae casi todo. Esta lección separa las dos preguntas, fija los dos modelos que vas a usar el resto de tu vida —roles y atributos— y, sobre todo, te obliga a escribir la política como un dato que un test pueda ejercitar. Al final vas a levantar un proveedor de identidad local y a probar una política RBAC/ABAC caso por caso.

Al terminar sabrás

  • Separar autenticación de autorización y decir cuál falló en un incidente concreto.
  • Modelar acceso con RBAC y reconocer la explosión de roles antes de sufrirla.
  • Modelar acceso con ABAC comparando atributos del sujeto con atributos del recurso.
  • Elegir entre los dos modelos, o combinarlos, según el caso real que tengas delante.
  • Escribir la política como datos, con un motor de decisión separado del código de negocio.
  • Probar cada regla con un test que falle cuando la autorización se rompa.
Antes de empezar · qué sabes ya

Tres preguntas rápidas. No puntúan para completar la lección: ajustan la profundidad de lo que viene.

¿Autenticación y autorización son lo mismo?

Autenticar es demostrar identidad; autorizar es decidir permisos. Puedes estar perfectamente autenticado y no tener derecho a lo que pides.

RBAC decide el acceso principalmente según…

RBAC (control de acceso basado en roles) asigna permisos a roles y roles a sujetos. La decisión mira qué rol tienes.

Necesitas que un usuario solo vea los pedidos de su propia región. ¿Qué modelo encaja mejor?

La relación "la región del usuario debe igualar la región del pedido" es una comparación de atributos: ABAC. Con RBAC acabarías creando un rol por región.

0 / 3

1Por qué importa

Cuando una API expone GET /pedidos/{id} y no comprueba que el pedido pertenece a quien pregunta, no hay ningún exploit sofisticado: el atacante cambia el número y lee lo ajeno. Eso es un fallo de autorización, no de autenticación. El usuario estaba perfectamente logueado; el sistema simplemente no decidió si tenía derecho a ese objeto. Año tras año, los fallos de control de acceso encabezan las listas de vulnerabilidades reales, y casi siempre por la misma razón: la decisión de acceso estaba dispersa, implícita o directamente ausente.

Diseñar la autorización bien hace tres cosas prácticas. Primero, convierte una intuición (“esto solo lo pueden ver los administradores”) en una regla que se puede leer, revisar y probar. Segundo, concentra la decisión en un sitio, de modo que añadir un endpoint no signifique reinventar quién puede llamarlo. Tercero, hace que el acceso sea auditable: si un día alguien pregunta “¿por qué este usuario pudo hacer esto?”, hay una respuesta escrita, no una arqueología de if repartidos por el código.

El objetivo real

No estás memorizando siglas. Estás aprendiendo a escribir una función que, dada una identidad, una acción y un recurso, devuelva permitido o denegado con un motivo, y a probar esa función con casos concretos. Ese es el entregable de la lección.

2Conceptos

Vocabulario mínimo
  • Autenticación: demostrar quién eres. Produce una identidad de confianza.
  • Autorización: decidir qué puede hacer esa identidad. Produce permitido o denegado.
  • Sujeto · acción · recurso: las tres piezas de toda decisión de acceso.
  • RBAC: permisos agrupados en roles; el sujeto tiene roles.
  • Explosión de roles: cuando cada matiz de permiso obliga a inventar un rol nuevo.
  • ABAC: la decisión compara atributos del sujeto, del recurso y del entorno.
  • PDP / PEP: el punto que decide (Policy Decision Point) y el que aplica (Policy Enforcement Point).
  • Default-deny: si ninguna regla concede, se niega. El silencio es un no.

3Explicación profunda

RBAC parte de una observación cierta: los permisos se repiten por puesto de trabajo. Un contable hace lo mismo que otro contable, así que en vez de asignar permisos a personas se asignan a un rol contable y se mete a la persona en él. Es simple, se audita de un vistazo (“¿quién es admin?”) y encaja con cómo piensan las organizaciones. Por eso es el modelo por defecto y casi siempre el punto de partida correcto.

RBAC se rompe cuando el permiso depende de datos, no solo de puesto. “Un vendedor puede ver pedidos” es un rol; “un vendedor puede ver sus pedidos, los de su región, del trimestre actual” ya no lo es. Si intentas expresarlo con roles, creas vendedor-norte-q1, vendedor-sur-q2 y así hasta el infinito: es la explosión de roles. El síntoma es un sistema con cientos de roles que nadie se atreve a tocar porque nadie sabe ya qué concede cada uno. Un catálogo de roles que no se puede auditar es control de acceso roto con buena presentación.

ABAC ataca justo ese caso. En vez de preguntar “¿qué rol tienes?”, la política compara atributos: sujeto.region == recurso.region, recurso.estado == "abierto", hora ∈ horario_laboral. La decisión se evalúa por petición, con los datos de esa petición, así que expresa relaciones que RBAC no puede. El precio es que una política de atributos mal escrita es más difícil de leer que una lista de roles, y que necesitas que esos atributos vengan de una fuente de confianza —normalmente los claims de un token verificado, que es de lo que van las tres lecciones siguientes.

En la práctica no se elige uno: se combinan. RBAC pone el trazo grueso (“solo el rol admin escribe”) y ABAC pone el fino (“y además solo en recursos de su departamento”). La política del laboratorio de esta lección hace exactamente eso, y el orden importa: primero la regla de rol, que es barata y descarta la mayoría, y luego la de atributo.

Hay una separación de la que depende que todo esto sea mantenible: la del PDP y el PEP. El PDP es el sitio que decide —una función autorizar(sujeto, accion, recurso)—, y el PEP es cada punto del código que aplica esa decisión antes de tocar el dato. Si la decisión vive en un solo PDP, añadir un endpoint es llamar a una función; si vive esparcida en if por los controladores, cada endpoint es una oportunidad nueva de olvidarse. Y sobre todo: default-deny. La función arranca negando y solo concede si una regla lo dice explícitamente. Un fallo así falla cerrado, no abierto.

4Ejemplo sencillo

# Un edificio con recepcion

AUTENTICAR   la recepcion confirma que eres quien dices (DNI)
AUTORIZAR    tu tarjeta abre SOLO las puertas que te tocan

# RBAC: la tarjeta lleva un rol impreso

rol "limpieza"   abre todas las plantas, de noche
rol "finanzas"   abre solo la planta 3
Problema: "finanzas, pero solo su propio despacho" -> ¿un rol por despacho?

# ABAC: la puerta compara atributos al vuelo

regla: puerta.planta == tarjeta.planta  Y  hora en horario
Una sola regla cubre a todos los despachos, sin inventar roles.
RBAC imprime el permiso en la tarjeta; ABAC lo decide en la puerta comparando datos. El día que el permiso depende de "cuál" y no solo de "quién", necesitas la puerta que compara.

5Ejemplo técnico

El laboratorio lab-iam-oidc trae una política real que combina los dos modelos. Tres usuarios, dos recursos de departamentos distintos, y una función que decide:

SujetoAcciónRecursoDecisión y motivo
alice · admin · ventasescribirinforme-comprasPermitido: admin cruza departamentos.
bob · lector · ventasleerinforme-ventasPermitido: rol lector y departamento coincide.
bob · lector · ventasescribirinforme-ventasDenegado. RBAC: escribir exige rol admin.
bob · lector · ventasleerinforme-comprasDenegado. ABAC: dep del sujeto ≠ dep del recurso.
carol · lector · comprasleerinforme-comprasPermitido: lector y departamento coincide.

Fíjate en los dos “denegado” de bob: uno lo para RBAC (le falta el rol para escribir) y el otro lo para ABAC (su departamento no es el del recurso). Un mismo usuario, dos motivos distintos. Devolver el motivo no es un lujo: es lo que hace la política depurable y auditable.

6Diagrama

Una petición llega al punto que aplica la política, que consulta al punto que decide; el que decide combina roles del sujeto y atributos del recurso y devuelve permitido o denegado peticion sujeto+accion+recurso PEP aplica PDP autorizar() RBAC: roles ABAC: atributos permitido denegado + motivo
El PEP nunca decide: pregunta. El PDP combina la regla de rol y la de atributo y responde una sola cosa, con motivo. Centralizar la decisión es lo que impide que cada endpoint reinvente el acceso.
Ver el mismo diagrama como código Mermaid (editable)
flowchart LR
    P[peticion: sujeto+accion+recurso] --> PEP[PEP · aplica]
    PEP --> PDP[PDP · autorizar]
    RBAC[RBAC: roles] --> PDP
    ABAC[ABAC: atributos] --> PDP
    PDP --> OK[permitido]
    PDP --> NO[denegado + motivo]

7Código

Esta es la política del laboratorio, escrita como una función pura: entra sujeto, acción y recurso; sale una decisión y su motivo. Es el PDP entero, y cabe en la pantalla:

# cliente.py — el PDP del laboratorio
def autorizar(claims, accion, recurso):
    roles = set(claims.get("roles", []))
    dep = claims.get("dep")
    meta = RECURSOS.get(recurso)
    if meta is None:
        return (False, "recurso desconocido")   # default-deny
    # RBAC: primero la regla barata de rol
    if accion == "escribir" and "admin" not in roles:
        return (False, "RBAC: escribir exige rol admin")
    if accion == "leer" and not roles & {"admin", "lector"}:
        return (False, "RBAC: leer exige rol lector o admin")
    # ABAC: el atributo del sujeto debe casar con el del recurso
    if dep != meta["dep"] and "admin" not in roles:
        return (False, f"ABAC: dep '{dep}' no coincide")
    return (True, "permitido")

Lo que la hace correcta no es que sea corta: es que arranca negando, que los atributos (roles, dep) vienen de un token verificado y no de la petición, y que devuelve el motivo. Como es una función pura, probarla es trivial, y ese es el punto entero.

8Qué puede salir mal

Los errores clásicos

Autenticar y darlo por autorizado. El error número uno: comprobar que hay sesión y servir el objeto sin comprobar que ese objeto es de ese usuario. Es exactamente el IDOR que viste en la fase de web. Autenticar no autoriza.

Explosión de roles. Modelar cada matiz como un rol nuevo lleva a un catálogo inmanejable. Cuando veas vendedor-norte-lectura-q1, el modelo ya se rompió: eso era un atributo disfrazado de rol.

Allow-by-default. Una política que concede salvo que una regla lo prohíba falla abierto: cualquier caso que olvidaste queda permitido. La forma segura es al revés.

Decidir con datos de la petición. Si el atributo que decide (dep, el id de tenant) viene de un campo que el cliente controla y no de un token verificado, el atacante se lo pone él. Los atributos de decisión salen de una fuente de confianza.

9Cómo lo abusaría un atacante

Vista desde el otro lado

Alguien que ataca la autorización no busca romper nada: busca la petición que el servidor atiende sin comprobar. Prueba a cambiar el identificador de un recurso por el de otro usuario. Prueba a mandar el atributo de decisión él mismo —un ?dep=compras, un campo role en el cuerpo— por si el backend se fía. Y prueba las acciones que nadie testeó: el DELETE del que solo existía el botón para admins, por si el botón era la única defensa.

El terreno más fértil es la explosión de roles. En un sistema con trescientos roles, siempre hay uno mal definido que concede de más, y nadie lo revisa porque revisarlos es imposible. El atacante no necesita un rol de admin: le basta un rol legítimo que, por acumulación histórica, arrastra un permiso que ya nadie recuerda haber concedido.

Y explota la dispersión. Si la decisión de acceso está repetida en veinte controladores, con que uno se saltara la comprobación ya hay una puerta. Por eso centralizar el PDP no es estética: reduce a uno el número de sitios donde te puedes equivocar.

10Cómo defenderlo

Qué hacer con esto mañana
  • Empieza en default-deny: la función niega y solo concede con una regla explícita.
  • Usa RBAC para el trazo grueso y ABAC para lo que depende de datos. No fuerces roles donde toca un atributo.
  • Centraliza la decisión en un PDP: una función autorizar(), y que cada endpoint la llame.
  • Toma los atributos de decisión de un token verificado, nunca de la petición del cliente.
  • Haz que la política devuelva el motivo. El motivo va al log de decisiones; al usuario, lo justo.
  • Escribe un test por regla: un caso que se permite y uno que se niega, para cada rama.

11Laboratorio

Estado: laboratorio local lab-iam-oidc
Targethttp://localhost:8470 (cliente) · http://localhost:8471 (idp)
Autorizaciónsistema local creado por ti para entrenamiento
El proveedor de identidad, el cliente y los usuarios de este laboratorio son tuyos y están aislados. Ninguna de estas pruebas debe hacerse contra un idp, una cuenta o un sistema que no sea tuyo o para el que no tengas autorización escrita.

Levanta el proveedor de identidad y su cliente:

cd cursos/seguridad/labs/lab-iam-oidc docker compose up -d --build docker compose ps

Abre http://localhost:8470, pulsa entrar, elige un usuario y prueba a leer y escribir cada recurso. Verás la política decidiendo, con su motivo. La regla está en autorizar(): la puedes ejercitar sin navegador.

ejercitar el PDP directamente
docker compose exec -T cliente python3 -c "import cliente; print(cliente.autorizar({'roles':['lector'],'dep':'ventas'},'escribir','informe-ventas'))" (False, 'RBAC: escribir exige rol admin') docker compose exec -T cliente python3 -c "import cliente; print(cliente.autorizar({'roles':['lector'],'dep':'ventas'},'leer','informe-compras'))" (False, "ABAC: dep 'ventas' no coincide con 'compras'") # dos negativas, dos motivos: una por rol, otra por atributo

Cuando termines:

docker compose down -v

12Ejercicios

Clasifica cinco decisiones

Para cada regla, di si es más natural con RBAC, con ABAC, o con los dos: (1) “solo administradores borran usuarios”; (2) “un médico ve historiales de sus propios pacientes”; (3) “nadie modifica una factura ya pagada”; (4) “el equipo de soporte lee tickets”; (5) “un usuario edita solo su propio perfil”.

Pregúntate si la regla depende solo de “quién” (puesto) o también de “cuál” (un dato del recurso).
“Sus propios”, “ya pagada”, “su propio” son comparaciones de atributos: huelen a ABAC.
Varias son un rol grueso (RBAC) más un atributo fino (ABAC) encima.
Solución

(1) RBAC puro: depende del rol. (2) RBAC + ABAC: rol médico y además paciente.medico == sujeto. (3) ABAC: depende del estado del recurso. (4) RBAC puro. (5) ABAC: perfil.dueño == sujeto, típicamente con un rol usuario de base.

Clave: R · R+A · A · R · A. Las que llevan “propio”, “ya” o “su” son atributos; forzarlas a roles es el camino a la explosión.

Añade una regla a la política

Con el lab-iam-oidc levantado, modifica autorizar() para añadir una acción borrar que solo permita el rol admin y solo sobre recursos de su propio departamento (aunque sea admin). Ejercítala con los tres usuarios y comprueba que se comporta como esperas.

Empieza copiando la rama de escribir y endureciéndola.
Para “aunque sea admin”, quita la excepción de admin en la comprobación ABAC de esta acción.
Prueba alice (admin/ventas) borrando informe-compras: debe denegarse por el atributo, no por el rol.
Solución

Una rama nueva antes del return final:

if accion == "borrar":
    if "admin" not in roles:
        return (False, "RBAC: borrar exige rol admin")
    if dep != meta["dep"]:
        return (False, "ABAC: borrar solo en tu departamento")
    return (True, "permitido")

Clave: alice borra informe-ventas (permitido) pero no informe-compras (ABAC, sin excepción de admin). bob no borra nada (RBAC). La diferencia con escribir es que aquí el atributo aplica también al admin.

RBAC o ABAC para multi-tenant

Investiga cómo modelan el aislamiento entre clientes (tenants) los sistemas SaaS reales. ¿El tenant_id es un rol o un atributo? Argumenta por qué modelarlo como rol lleva a la explosión, y qué tiene que ser cierto del tenant_id para que sea seguro usarlo como atributo de decisión.

Un tenant nuevo no debería obligar a crear roles nuevos.
Busca “tenant isolation” y “ABAC multitenancy”.
La pregunta clave es de dónde sale el tenant_id que usa la decisión.
Solución

El tenant_id es un atributo, no un rol: si fuera un rol, cada alta de cliente crearía roles (admin-tenant-42) y en un año tendrías miles. Como atributo, una sola regla sujeto.tenant == recurso.tenant cubre a todos. Para que sea seguro, el tenant_id de la decisión tiene que venir del token verificado del usuario, jamás de un parámetro que el cliente pueda cambiar; si el atacante elige su propio tenant, el aislamiento no existe.

Clave: atributo, no rol; y el atributo debe salir de una fuente de confianza (el token), no de la petición. Un tenant como rol es la explosión de roles con nombre corporativo.

Red Team · rompe una política ingenua

Dentro del lab-iam-oidc, y solo dentro de él: modifica autorizar() para que lea el departamento de un argumento controlable en vez de los claims del token (simula el error de fiarse de la petición). Demuestra con una llamada que un lector de ventas “se convierte” en compras y accede a lo ajeno. Después revierte.

El fallo es tomar el atributo de decisión de un sitio que el atacante controla.
Haz que dep venga de un parámetro en vez de claims["dep"].
Con eso, pasar dep="compras" a mano salta el ABAC.
Solución

Si dep se lee de un parámetro que el llamante elige, bob (lector/ventas) invoca la decisión con dep="compras" y el ABAC pasa: el atributo ya no describe al sujeto, sino lo que el atacante quiere. Es el mismo fallo que un ?role=admin en una query. La corrección es tomar dep solo de claims, que salen de un token verificado.

Clave: el atributo de decisión no puede venir de la petición. Revierte a claims["dep"] y vuelve a comprobar que bob no llega a compras.

Blue Team · registra las decisiones

Haz que autorizar() registre cada decisión con sujeto, acción, recurso, veredicto y motivo. Genera tráfico de los tres usuarios y localiza en el log los “denegado”. Responde: con esos campos, ¿podrías detectar a alguien tanteando recursos que no le tocan? ¿Qué patrón buscarías?

Un solo denegado es ruido; muchos del mismo sujeto contra recursos distintos, no.
Registra también el veredicto, no solo la petición.
La señal es una tasa alta de negativas ABAC de un mismo sujeto en poco tiempo.
Solución

Con sujeto, veredicto y motivo puedes detectar tanteo: un mismo sujeto acumulando negativas ABAC contra varios recursos en segundos es alguien probando qué le dejan. La regla de detección es una cuenta por ventana de tiempo de decisiones denegadas por sujeto; supera un umbral, alerta. Sin registrar el veredicto, esto es invisible: el log de acceso HTTP vería 403 pero no el motivo que los agrupa.

Clave: el entregable es el patrón: pico de negativas por sujeto en poco tiempo. Guárdalo: es material para la fase de detección.

Architecture Challenge · el PDP de un sistema real

Diseña la autorización de una aplicación con varios servicios. Define: dónde vive el PDP, cómo llega la política a cada servicio, de dónde salen los atributos de decisión, qué pasa cuando el PDP no responde, y cómo pruebas que una política nueva no rompe una existente antes de desplegarla.

Un PDP central es fácil de auditar pero es un punto único; uno por servicio escala pero se desincroniza.
Si el PDP no responde, ¿fallas abierto o cerrado? La respuesta segura solo hay una.
Una política como datos se puede versionar y probar con un banco de casos antes de desplegar.
Solución

Una propuesta razonable: la política como datos versionados en el repositorio, evaluada por una librería PDP embebida en cada servicio (baja latencia, sin punto único) o por un servicio PDP con la política replicada. Los atributos de decisión salen del token verificado y de metadatos del recurso, nunca de la petición. Si el PDP no puede decidir, se niega (fail-closed). Y cada cambio de política pasa un banco de casos —permitidos y denegados— en el pipeline, de modo que una regla nueva que rompa un caso existente no llega a producción.

Clave: no hay una única respuesta. Se evalúa que la decisión esté centralizada como concepto, que falle cerrado, que los atributos vengan de fuente de confianza y que la política se pruebe como código.

13Preguntas de comprensión

Comprobación

¿Qué problema aparece cuando modelas cada permiso fino como un rol nuevo?

Cuando el permiso depende de datos y lo fuerzas a roles, cada matiz crea un rol. El síntoma es un catálogo que nadie se atreve a tocar.

La política “dep del sujeto == dep del recurso” es un ejemplo de…

Comparar un atributo del sujeto con uno del recurso, en cada petición, es exactamente ABAC.

La política del laboratorio niega a bob (lector/ventas) leer informe-compras. ¿Por qué?

bob sí tiene rol lector, así que pasa el RBAC; lo para el ABAC, porque ventas ≠ compras y no es admin.

¿Por qué una política de autorización debe devolver el motivo de la negativa?

El motivo hace la política depurable y el acceso auditable. Otra cosa es cuánto de ese motivo se le enseña al usuario final.

0 / 4

14Reto adicional

Convierte tres reglas de negocio en política probable

Toma tres reglas de acceso de un proyecto tuyo, escritas en lenguaje de negocio, y conviértelas en llamadas a autorizar(sujeto, accion, recurso) con su veredicto esperado. Para cada una, escribe el caso que se permite y el que se niega. Si alguna regla no cabe en esa forma, es señal de que aún no la entiendes del todo: reescríbela hasta que quepa.

Cada regla tiene que nombrar un sujeto, una acción y un recurso concretos.
Decide para cada una si el corte lo hace un rol, un atributo o los dos.
Un test por regla: un caso verde y uno rojo. Si no sabes escribir el rojo, la regla está incompleta.
Solución

El ejercicio no tiene una respuesta única, pero sí una forma correcta: cada regla queda como una o dos ramas de autorizar(), con su motivo, y con dos casos de prueba. La dificultad real aparece con las reglas que mezclan condiciones (“un editor publica, pero no su propio artículo sin revisión de otro”): ahí ves que necesitas atributos del recurso (autor, estado) además del rol. Si una regla te obliga a inventar un rol por cada valor de un dato, reescríbela como atributo.

Clave: lo que se evalúa es que cada regla tenga sujeto, acción, recurso, veredicto con motivo y un par de casos. Una regla sin caso rojo no está terminada.

15Resumen

Puntos clave
  • Autenticar es saber quién eres; autorizar es decidir qué puedes hacer. Casi todas las brechas de acceso confunden las dos.
  • RBAC agrupa permisos en roles: simple y auditable, ideal para el trazo grueso.
  • Cuando el permiso depende de datos, RBAC lleva a la explosión de roles: ese matiz era un atributo.
  • ABAC decide comparando atributos por petición, y expresa relaciones que los roles no pueden.
  • Los dos se combinan: rol para lo grueso, atributo para lo fino, con default-deny.
  • La política es una función pura que devuelve permitido o denegado con motivo, con atributos de una fuente de confianza, y con un test por regla.
¿Qué diferencia hay entre autenticación y autorización?
Autenticar demuestra quién eres; autorizar decide qué puedes hacer. Estar autenticado no implica tener derecho a lo que pides.
¿Cuándo RBAC lleva a la explosión de roles?
Cuando el permiso depende de datos del recurso (región, dueño, estado) y lo fuerzas a un rol por cada valor. Eso era un atributo: usa ABAC.
¿De dónde deben salir los atributos que decide una política ABAC?
De una fuente de confianza, típicamente los claims de un token verificado. Nunca de la petición que el cliente controla.
¿Qué significa default-deny en un PDP?
La función arranca negando y solo concede si una regla lo dice. Cualquier caso olvidado queda denegado: falla cerrado, no abierto.

16Mis notas

Cierre de la lección

Estas notas se guardan solo en este navegador. No se envían a ningún servidor. Expórtalas desde el panel de progreso si cambias de máquina.

17Recursos

Los comentarios se cargan solo si los pides, desde GitHub Discussions. No se carga nada de terceros mientras no pulses.