← Todos los artículos

ARQUITECTURA

Control de acceso en RAG: quién puede ver qué

Cuando los documentos de la empresa se entregan a un sistema RAG, el modelo de permisos existente no suele viajar con ellos. Una nota de implementación sobre qué capa aplica el acceso, cómo llega la identidad hasta la consulta y cuándo un cambio de permisos llega de verdad al índice.

Los documentos de una organización nunca son un montón plano. La carpeta de recursos humanos no la abre cualquiera, la correspondencia jurídica está acotada a una lista con nombres y apellidos, las hojas de cálculo de finanzas tienen los lectores contados. Esa estructura se fue acumulando durante años en servidores de archivos, bibliotecas de SharePoint, grupos LDAP y roles de SSO. Después esos mismos documentos se entregan a un sistema RAG. La primera pregunta que conviene hacerse es adónde fue a parar el modelo de permisos existente durante esa entrega.

En la mayoría de las primeras instalaciones la respuesta es: a ninguna parte. El documento se trocea en chunks, se convierte en embeddings y se escribe en un único índice. Los datos de permisos del sistema de origen no atraviesan ninguno de esos pasos, así que el índice en sí es un plano sin permisos. Esta es una nota de implementación sobre cómo volver a colocar las capas.

Cómo un único índice aplana el modelo de permisos

En una instalación sencilla la búsqueda por similitud recorre todo el corpus. Vuelven los k chunks más próximos y, en el momento de la consulta, nada indica de qué biblioteca, carpeta o lista de acceso proceden. Esos chunks pasan al modelo como contexto, y el modelo trata todo lo que recibe como material sobre el que puede responder. El contenido de un archivo que ese usuario nunca habría podido abrir reaparece en la respuesta, reescrito en lenguaje natural.

El OWASP Gen AI Security Project lo recoge bajo LLM08:2025, Vector and Embedding Weaknesses: compartir una única base de datos vectorial entre entornos multiinquilino o con varios niveles de clasificación hace que el contexto pase de unos usuarios o consultas a otros (context leakage). En la práctica no es una cuestión del comportamiento del modelo, sino una cuestión clásica de autorización, y la entrada suele comentarse junto a la clase Broken Access Control del OWASP Top 10. Su primera medida recomendada es igual de directa: controles de acceso de grano fino y almacenes de vectores y embeddings conscientes de los permisos, con una separación lógica y de acceso estricta de los conjuntos de datos dentro del almacén.

El OWASP RAG Security Cheat Sheet va más allá y exige que esos metadatos vivan a nivel de chunk y no a nivel de documento: clasificación, propietario, roles autorizados e inquilinos autorizados junto a cada chunk vectorial. El razonamiento es práctico. Un permiso que cuelga solo del documento puede quedarse sin esa asociación al pasar por el chunking, y lo que queda son chunks sobre los que no hay nada que filtrar.

Dónde se aplica el filtro

El control de acceso puede aplicarse en tres puntos distintos, y esos tres puntos no son equivalentes.

  • Separación en el momento de indexar: los distintos niveles de clasificación van a índices, colecciones o namespaces separados. Es el aislamiento más fuerte, porque el contenido que queda fuera de la autorización sencillamente no está en el conjunto que se busca. El precio es la flexibilidad: una consulta que abarca permisos mixtos tiene que repartirse entre varios índices, y los cambios en el sistema de origen obligan a reubicar contenido.
  • Filtrado por metadatos en el momento de la consulta: el contenido se queda en un solo índice y el predicado del filtro se ejecuta junto con la búsqueda. El coste operativo es el más bajo, porque el contenido permanece en un solo sitio y el filtro trabaja dentro del motor.
  • Filtrado posterior a la recuperación: la búsqueda se ejecuta sin filtro y la lista devuelta se recorta en el código de la aplicación. En términos de seguridad es la posición menos sólida, y además reduce el recall de forma medible.

Azure AI Search nombra esta distinción formalmente a través del parámetro vectorFilterMode (véase Azure AI Search — Filters in vector search). En el modo preFilter el predicado del filtro se aplica durante el recorrido del grafo HNSW, y la documentación afirma que el prefiltrado garantiza que se devuelvan k resultados si existen en el índice. En el modo postFilter cada shard se recorre sin filtro y el predicado se aplica después; con filtros muy selectivos eso reduce el recall y produce falsos negativos. strictPostFilter, en versión preliminar, aplica el filtro una vez determinado el top-k global y puede devolver cero resultados con filtros selectivos. Los índices creados después del 15 de octubre de 2023, aproximadamente, usan preFilter por defecto.

La conclusión que importa aquí: los filtros de seguridad suelen ser muy selectivos. Un usuario concreto ve por lo general un pequeño porcentaje del corpus. Ese es justo el régimen en el que el postfiltrado devuelve en silencio menos resultados de los que hay. El prefiltrado paga la corrección en latencia, y Microsoft publicó las cifras en el mismo documento: con 1 millón de vectores y 1536 dimensiones, el prefiltrado es alrededor de un 30 % más lento cuando se filtra más del 30 % del conjunto de datos, y unas 7 veces más lento cuando se filtra menos del 2 %. Con 100.000 vectores, filtrar por debajo del 0,1 % hace que el prefiltrado sea aproximadamente un 50 % más lento. El presupuesto de latencia y el modelo de seguridad hay que planificarlos juntos.

Filtrado de seguridad con metadatos

El patrón concreto más habitual escribe los identificadores de los grupos autorizados en cada chunk como una colección filtrable. En el patrón de filtro de seguridad de Azure AI Search (Azure AI Search — Security filters for trimming results) el campo se define así.

json
{
  "name": "group_ids",
  "type": "Collection(Edm.String)",
  "filterable": true,
  "retrievable": false
}

Que retrievable esté en false es deliberado: los metadatos de permisos están ahí para filtrar, no para devolverlos en el cuerpo de la respuesta. En el momento de la consulta el filtro tiene esta forma.

text
group_ids/any(g: search.in(g, 'grp-finance, grp-legal-readers'))

La misma documentación advierte de que construir en su lugar una cadena de igualdades del tipo Id eq ’id1’ or Id eq ’id2’ exige mucho cuidado para mantenerla correcta y da bastante trabajo de mantenimiento, y de que con cientos o miles de valores los tiempos de respuesta se estiran hasta el orden de muchos segundos, mientras que de search.in se espera una respuesta por debajo del segundo. También marca el límite con claridad: a través del principal de seguridad no hay ni autenticación ni autorización; el principal es solo una cadena de texto. El filtro da por supuesto que la identidad se derivó correctamente más arriba; la autenticación sigue siendo cosa de la capa superior.

Del lado relacional, la row level security de Postgres hace el mismo trabajo. La guía de pgvector de Supabase (Supabase — RAG with Permissions) aplica las políticas a la tabla document_sections, y la búsqueda semántica las sigue respetando.

sql
alter table document_sections enable row level security;

create policy "read permitted sections"
on document_sections for select
to authenticated
using (
  document_id in (
    select id from documents
    where owner_id = auth.uid()
  )
);

La identidad llega mediante auth.uid() a través de REST, o mediante una variable de sesión de current_setting() en una conexión directa a Postgres. La documentación añade una advertencia concreta: con un Foreign Data Wrapper, la RLS es sensible a la latencia, y analizar el plan de consulta antes de pasar a producción es obligatorio.

Elegir el filtrado en el motor en lugar del recorte en la aplicación no es solo una cuestión de velocidad. El argumento que da Azure es que filtrar dentro del motor ahorra además código propio para resolver grupos anidados y recorrer ACL de varios niveles. Una vez escrito, ese código se convierte en una segunda copia del modelo de permisos de la organización, y dos copias acaban separándose con el tiempo.

Llevar la identidad hasta la consulta

De dónde sale el valor del filtro pesa tanto como el filtro. Si confías en la lista de grupos que envía el cliente, el filtro no aplica nada: el recorte depende de un valor que elige quien llama. El flujo correcto deriva la identidad de un token verificado.

En la vía de ACL nativa de Azure AI Search (Azure AI Search — Document-level access control) el token del usuario viaja en la cabecera x-ms-query-source-authorization. El servicio extrae del token los claims de usuario, grupo y scope, los compara con los metadatos de permisos del índice y devuelve únicamente los documentos autorizados. Aparte, se comprueba que la aplicación que llama tenga el rol Search Index Data Reader: la comprobación es, por tanto, en dos etapas. Estas capacidades están en una versión preliminar de la API REST; confirma la cadena de versión exacta en la documentación actual de Microsoft Learn antes de escribirla en una llamada. El servicio documenta cuatro enfoques oficiales: filtros de seguridad (GA, independientes de la API), ACL de tipo POSIX con ámbitos RBAC, etiquetas de confidencialidad de Microsoft Purview y ACL de SharePoint M365; los tres últimos, en versión preliminar.

En Google Vertex AI Search (Google Cloud — Vertex AI Search: data source access control), las ACL se aportan en la ingesta a través del campo acl_info de los metadatos del documento, con la estructura readers → principals → group_id o user_id. La identidad procede de Google Identity o de Workforce Identity Federation (Microsoft Entra ID, Okta, Ping), y el atributo google.subject debe corresponderse con el campo de email del proveedor externo. Dos límites condicionan el diseño de forma directa: 3.000 lectores por documento, y que la configuración de ACL queda fijada al crear el data store y no se puede cambiar después.

Qué ocurre cuando cambian los permisos

Es la parte de la arquitectura que más a menudo se pasa por alto. Cuando se retira a un usuario de un grupo, el efecto en el sistema de origen es inmediato; en el índice, no. La documentación de Azure lo dice de forma explícita: se produce un desfase temporal antes de que la API en versión preliminar reconozca los cambios en esas restricciones de acceso o de permisos. Los cambios de permisos en el sistema de origen —pertenencia a grupos de Entra, ACL de ADLS Gen2, etiquetas de Purview asignadas, ACL de SharePoint— solo se reflejan en los resultados de búsqueda una vez que esos metadatos se han sincronizado con el índice: mediante una ejecución del indexador, una actualización por la API de push o una recarga desde Purview.

En SharePoint la distinción es todavía más fina. Los cambios en elementos con permisos propios se recogen de forma incremental en cada ejecución correcta del indexador, mientras que los cambios heredados de un ámbito superior —sitio, biblioteca, lista o carpeta— exigen una recarga explícita: /resync con options: ["permissions"], o /resetdocs. Por eso un permiso restringido a nivel de biblioteca puede no propagarse por sí solo, aunque el indexador se ejecute según lo programado.

Elasticsearch resuelve la misma tarea con otra arquitectura (Elasticsearch — Document level security and connector access control sync). Hay dos tipos de sincronización separados: la de contenido, hacia un índice con el prefijo search-, y la de control de acceso, hacia un índice oculto con el prefijo .search-acl-filter-<INDEX-NAME>. El campo en los documentos de contenido se llama _allow_access_control; el acceso se concede en cuanto al menos una entrada del documento de control de acceso del usuario coincide con una entrada de ese campo, y un valor vacío mantiene el documento cerrado para todo el mundo.

json
{
  "title": "Q3 procurement note",
  "body": "...",
  "_allow_access_control": [
    "group:finance-readers",
    "group:procurement"
  ]
}

El método documentado para gestionar los cambios de permisos consiste en dar una expiration a la clave de API de Elasticsearch y programar sincronizaciones de control de acceso recurrentes; si cambian los permisos de un usuario, hay que actualizar o volver a crear la clave de API. En el comportamiento general de DLS, cuando un usuario tiene varios roles sobre el mismo índice las consultas de rol se combinan con OR: basta con que coincida un solo rol para que el documento sea visible. DLS no se aplica a las API de escritura y, como se ejecuta en cada consulta, tiene un pequeño coste de rendimiento.

La regla de diseño que se deriva es corta: el intervalo de actualización es un parámetro de seguridad. El tiempo que tarda un cambio de permisos en llegar al índice pertenece a la especificación escrita del sistema, y se elige por nivel de clasificación.

Llevar los metadatos de permisos a través del chunking

La estrategia de partición en almacenes multiinquilino entra en el mismo cuadro. Qdrant recomienda una sola colección por modelo de embeddings con partición sobre el payload (Qdrant — Multitenancy); desde la v1.11.0, el parámetro is_tenant: true en un índice de tipo keyword agrupa físicamente los vectores de un inquilino, y a gran escala payload_m en hnsw_config junto con un m global de 0 da una indexación independiente por inquilino. La misma documentación señala que las consultas globales sin filtro de grupo se vuelven más lentas porque tienen que recorrer todos los grupos. Pinecone recomienda namespaces para aislar inquilinos y define los operadores de metadatos $eq, $ne, $gt, $gte, $lt, $lte, $in, $nin, $exists, $and y $or, de los cuales solo $and y $or se admiten en el nivel superior de la expresión de filtro. Weaviate aísla a cada inquilino en su propio shard con su propio índice vectorial. Amazon Bedrock Knowledge Bases construye el control de acceso a partir de filtros de metadatos y admite hasta 10 KB de metadatos propios por documento.

Sea cual sea el almacén elegido, hay un paso de configuración que suele pasarse por alto. La documentación de Azure señala que, cuando un skillset trocea el documento —con la Text Split skill o con la vectorización integrada—, los campos con metadatos de permisos deben trasladarse desde los field mappings del indexador a las index projections. Para las etiquetas de Purview la formulación es explícita: sin esa proyección, las referencias a nivel de chunk no se filtran. Si la proyección no está puesta, el paso de chunking deja atrás los metadatos de permisos y quedan chunks sin filtrar. Desde fuera no se ve nada de esto: el índice se construye, la búsqueda funciona, solo que el filtro no llega a agarrarse a nada.

El registro de auditoría y la etapa de generación

La otra mitad del control de acceso es dejar constancia de qué consulta ha tocado qué documento. El OWASP RAG Security Cheat Sheet lo enumera punto por punto. Cada recuperación se registra con la identidad del agente o usuario que consulta y con los metadatos de control de acceso de los chunks devueltos, y el registro cubre todo el pipeline: consulta recibida, chunks recuperados con sus identificadores de documento y sus metadatos de control de acceso, entrada del modelo ensamblada, salida del modelo generada y todas las llamadas a herramientas que se hayan disparado. La parte que suele saltarse es la caché: los aciertos de caché deben registrarse con el mismo detalle que las recuperaciones frescas; de lo contrario, el rastro se interrumpe. Cada inserción, actualización y borrado en el índice debe quedar anotado con marca de tiempo y con la identidad que lo hizo. La cuarta medida de LLM08:2025 apunta en la misma dirección: mantener registros detallados e inmutables de la actividad de recuperación.

La última capa es la generación. Incluso con una recuperación correctamente filtrada, un flujo de varios pasos puede arrastrar hasta la respuesta contenido que queda fuera del alcance autorizado, a través de un resumen, de una nota intermedia o de la salida de una herramienta. Las medidas normativas de OWASP para esta etapa: validar todas las salidas del modelo antes de devolverlas, aplicar filtros de política y enmascaramiento para datos personales y secretos, enmascarar de forma dinámica según el nivel de acceso del usuario que consulta, y firmar los datos de atribución de fuentes para que quede constancia de que se mantienen intactos después de la generación. En la práctica eso significa que cada chunk sobre el que se apoya una respuesta arrastra consigo su identificador, y que la comprobación final se ejecuta en una capa independiente del modelo que produjo la respuesta.

Sobre el contenido que se cuela a través de un resumen no se conoce ningún estudio público con mediciones. Trátalo como una hipótesis de diseño y no como un hallazgo empírico: todo lo que entra en el contexto del modelo puede aparecer en la respuesta, así que el control corresponde a la capa de recuperación.

El control de acceso en RAG no es una disciplina de seguridad nueva. Es el modelo de permisos existente trasladado a una nueva ruta de datos. Todo se reduce a cuatro preguntas. ¿Viven los metadatos de permisos a nivel de chunk? ¿Se aplica el filtro dentro del motor, durante el recorrido de la búsqueda? ¿Se deriva la identidad de un token verificado en lugar de recibirla del cliente? ¿Y cuántos minutos pasan entre un cambio de permisos en el sistema de origen y su efecto en el índice? Un sistema que puede responder por escrito a esas cuatro preguntas es auditable. Donde quedan abiertas, queda abierta también la pregunta de qué devuelve el índice.

Fuentes

  • OWASP Gen AI Security Project — LLM08:2025 Vector and Embedding Weaknesses
  • OWASP — RAG Security Cheat Sheet
  • Azure AI Search — Filters in vector search (vectorFilterMode, prefilter/postfilter latency figures)
  • Azure AI Search — Security filters for trimming results
  • Azure AI Search — Document-level access control (ACLs, RBAC scopes, Purview labels, SharePoint indexer)
  • Supabase — RAG with Permissions (pgvector and row level security)
  • Google Cloud — Vertex AI Search: data source access control
  • Elasticsearch — Document level security and connector access control sync
  • Qdrant — Multitenancy
  • Pinecone — Namespaces and metadata filtering
  • Weaviate — Multi-tenancy
  • Amazon Bedrock Knowledge Bases — Metadata filtering

Más artículos

Tienes un sistema que construir, o uno que llevar más lejos.

No deck needed. 20 minutes. The rest is up to you.