← Todo el material
Curso Avanzado 24 de agosto de 2026

Seguridad de IA — Módulo 7: datos, privacidad y PII

Séptimo módulo del Curso de Seguridad de IA: cómo se filtra información sensible en un sistema con LLM y cómo se contiene. Fuga de PII de terceros por retrieval sin permisos, memorización del modelo, minimización de datos, redacción, aislamiento por tenant y qué exigen GDPR y la ley chilena sobre datos personales.

☰ Índice del contenido

En el módulo 6 montamos guardrails de entrada y salida, y uno de los ejemplos fue un redactor de PII en la respuesta del agente: antes de devolverle texto al usuario, un filtro tapaba correos, RUT y tarjetas. Ese ejemplo resolvía un síntoma. Este módulo ataca la enfermedad completa. Redactar la salida sirve para que el modelo no escupa un dato que ya tenía en el contexto — pero la pregunta que ese guardrail no responde es la que de verdad importa: ¿por qué ese dato estaba en el contexto? La mayoría de las fugas de datos en sistemas con LLM no ocurren en la salida: ocurren mucho antes, cuando el retrieval le entrega al modelo un fragmento que nunca debió ver. La privacidad no se gana tapando la boca del modelo. Se gana controlando qué llega a su contexto.

Un aviso, como siempre en este curso: esto es material defensivo. Sirve para que protejas los datos que tus usuarios te confiaron, no para extraer los que otros guardan. Cuando muestre un vector de fuga es para construir el control que lo cierra.

La tesis del módulo

La mayor fuga de datos en un sistema con LLM no es el modelo “recordando” su entrenamiento: es el retrieval entregándole a un usuario un fragmento que pertenecía a otro. La privacidad se gana en la capa de datos, no en el prompt. Todo el mundo teme que el modelo memorice y regurgite. El incidente real, el que llega a los tribunales, es el usuario A que preguntó algo inocente y recibió, dentro de una respuesta útil, el teléfono o el diagnóstico de un usuario B — porque el índice no filtró por permisos.

Qué es PII y por qué las categorías importan

PII (Personally Identifiable Information, información que identifica a una persona) es cualquier dato que, solo o combinado con otros, permite señalar a un individuo concreto: nombre, RUT, correo, teléfono, dirección, IP, un identificador de dispositivo. La ley no trata toda la PII igual. Hay una capa de datos sensibles o de categorías especiales que reciben protección reforzada porque su fuga hace daño desproporcionado:

  • Salud. Diagnósticos, tratamientos, historia clínica. Un chunk de una consulta médica filtrado no es un dato de contacto: es información que puede costarle a alguien un empleo o un seguro.
  • Financiera. Números de tarjeta, saldos, morosidad, información crediticia.
  • Otras categorías especiales. Origen étnico, afiliación política o sindical, religión, orientación sexual, datos biométricos y genéticos.

Esta distinción no es académica: define el nivel de control que debes poner. Un correo de contacto filtrado es un problema; un diagnóstico filtrado es un incidente reportable con plazos legales. Cuando clasifiques tus datos para decidir qué proteger y cuánto, esta jerarquía es el mapa.

Qué exigen las leyes (y por qué te obligan a diseñar distinto)

No eres abogado y este módulo no reemplaza asesoría legal, pero hay tres principios del marco regulatorio que se traducen directamente en decisiones de arquitectura. El GDPR europeo (General Data Protection Regulation, el reglamento de datos de la UE) los formuló y hoy son el estándar de facto:

  • Minimización. Solo puedes tratar los datos que necesitas para el propósito declarado. Traducción a tu pipeline: no metas en el contexto del modelo un campo “por si acaso”.
  • Limitación de propósito. Los datos recogidos para una cosa no se reutilizan para otra sin base legal. Traducción: el corpus que armaste para dar soporte no es un dataset de entrenamiento libre.
  • Derecho al borrado (el “derecho al olvido”). La persona puede exigir que borres sus datos. Traducción, y aquí duele: tienes que poder borrar de verdad — también de los índices y embeddings, no solo de la tabla principal.

En Chile, el marco cambió de golpe. La Ley 21.719, publicada el 13 de diciembre de 2024, reforma la vieja Ley 19.628 —que pasa a llamarse Ley sobre Protección de los Datos Personales— y entra en plena vigencia el 1 de diciembre de 2026. Crea la Agencia de Protección de Datos Personales (APDP) con potestad fiscalizadora y sancionatoria, incorpora los derechos ARCO (acceso, rectificación, cancelación, oposición) más portabilidad, obliga a notificar las brechas a la Agencia y a los afectados, y sube el techo de multas a 20.000 UTM (con reincidencia que puede llegar al 4% de los ingresos anuales). El primer año hay un régimen más blando para PyMEs, pero el principio operativo ya rige: la Agencia audita evidencia, no buenas intenciones. Si tu sistema con LLM procesa datos de personas en Chile, esto te aplica.

Ojo: “los datos están en la nube, el proveedor cumple” no te cubre. El responsable del tratamiento eres tú, el que decide para qué se usan los datos. Que Google o AWS tengan sus certificaciones no traslada tu responsabilidad legal sobre cómo tu aplicación expone esos datos.

Los cinco vectores de fuga

Un sistema con LLM tiene cinco puntos donde la información sensible se puede escapar. Los ordeno de más común y grave a más citado y menos frecuente — porque el miedo popular está invertido.

Vector 1 — Fuga de PII de terceros por retrieval sin filtro de permisos

Este es el más común y el más grave en aplicaciones reales, y casi nadie lo pone primero. El agente recupera fragmentos de un índice compartido para responder. Si el índice no filtra por quién pregunta, el retrieval puede traer un chunk que pertenece a otro usuario, otro cliente, otro caso. El modelo no tiene forma de saber que ese fragmento no era para esta persona: lo recibe como contexto legítimo y lo usa.

El escenario típico: una empresa indexa todos sus tickets de soporte en un solo vector store para que el asistente responda. El usuario A pregunta “¿cuándo llega mi pedido?” y el retrieval, buscando por similitud semántica, trae el ticket del usuario B que hablaba de un pedido parecido —con su nombre, su dirección y su teléfono adentro. El asistente responde amablemente con datos de otra persona. Nadie atacó nada. La similitud semántica hizo su trabajo y la arquitectura no tenía una barrera.

Es el mismo problema que en red teaming llamamos “fuga de PII de terceros”, ahora visto desde la capa de datos: el arreglo no está en el prompt ni en un guardrail de salida, está en el filtro de permisos dentro del retrieval.

Vector 2 — Memorización y regurgitación del modelo

Memorización (training data extraction) es cuando el modelo reproduce, literal o casi, fragmentos de su corpus de entrenamiento —a veces con PII adentro. Es real y hay papers que lo demuestran, pero conviene calibrar el miedo: en modelos grandes bien entrenados la memorización verbatim de un dato específico es rara, más probable en datos que aparecieron muchas veces (duplicados) y en secuencias de baja entropía. Para la mayoría de las aplicaciones, este vector es mucho menos probable que el vector 1. El riesgo se dispara si tú haces fine-tuning con datos de clientes: ahí sí estás enseñándole al modelo PII concreta, y puede salir después. La defensa vive en la preparación del dataset de fine-tuning (deduplicar, redactar antes de entrenar), no en el runtime.

Vector 3 — Datos que se van al proveedor por API

Cuando llamas a un modelo por API, el prompt —con todo lo que metiste en el contexto— viaja al proveedor. Dos preguntas concretas que debes responder con su documentación en la mano, no de memoria: ¿lo usan para entrenar? y ¿cuánto lo retienen? Los proveedores serios ofrecen por defecto no-entrenamiento en los planes de API (distinto de los productos de consumidor) y una retención acotada para abuso, a veces con opción de retención cero bajo acuerdo. Verifica el plan y el contrato que tienes tú, porque las condiciones difieren entre API, plataforma empresarial y producto gratuito. Y si el dato es de una categoría especial, la pregunta previa es si siquiera debería salir de tu perímetro sin redactar.

Vector 4 — Fuga por logs, trazas y observabilidad

El que más muerde en la práctica después del vector 1, y el más silencioso. En el módulo de observabilidad montamos trazas que guardan el prompt completo, la respuesta y los argumentos de las tools —para poder depurar. Esas trazas son un segundo lugar donde vive la PII, muchas veces con menos controles que la base de datos principal, retenidas más tiempo y accesibles a todo el equipo de ingeniería. Un sistema que redacta perfecto en la salida al usuario y loguea el prompt crudo con el RUT adentro no protegió nada: solo movió la fuga a un panel interno. La observabilidad es necesaria; la observabilidad con PII cruda es una brecha esperando auditoría.

Vector 5 — El system prompt y los datos internos

El system prompt suele llevar reglas de negocio, nombres de tools, a veces ejemplos con datos reales pegados de apuro. Extraerlo (lo vimos como técnica de reconocimiento en red teaming) le muestra al atacante la estructura interna. La regla ya la dimos y se mantiene: trata el system prompt como público, y sobre todo, nunca pegues datos reales de clientes como ejemplos dentro de él.

Dónde fuga y dónde va cada control

Antes del código, el mapa. Cada vector vive en una etapa del pipeline, y cada control se planta en un lugar específico —no donde se ve el síntoma, sino donde nace el dato.

     DÓNDE PUEDE FUGAR EL DATO Y DÓNDE VA EL CONTROL

 usuario
    │  pregunta
    ▼
 ┌──────────────┐   control: filtro de permisos
 │  retrieval   │◄── por usuario/tenant EN la query
 │  (vector DB) │    (vector 1, el más grave)
 └──────┬───────┘
        │ chunks ya filtrados
        ▼
 ┌──────────────┐   control: minimizar + redactar
 │   contexto   │◄── PII antes de armar el prompt
 └──────┬───────┘
        │ prompt
        ▼
 ┌──────────────┐   control: plan sin entrenamiento,
 │  proveedor   │◄── retención corta (vector 3)
 │  del modelo  │
 └──────┬───────┘
        │ respuesta
        ├──────────────► ┌──────────────┐
        │                │  logs/trazas │ control:
        │  redactar      │              │◄─ redactar
        │  ANTES de      └──────────────┘   pre-log +
        ▼  loguear                          retención
 ┌──────────────┐                           corta
 │   usuario    │   control: guardrail de   (vector 4)
 │  (salida)    │◄── salida (seg-06)
 └──────────────┘

La lección del diagrama: el guardrail de salida del módulo 6 es la última línea, la de la esquina de abajo. Es real y hace falta, pero si es tu único control estás tapando la fuga en el punto más tarde posible. Los controles que de verdad protegen están arriba —en el retrieval y en el armado del contexto— donde el dato aún no se ha propagado.

Defensa 1: filtro de permisos en el retrieval

El control número uno. La idea: cada documento del índice lleva en su metadata (los campos que acompañan al vector: owner_id, tenant_id, etc.) a quién pertenece, y toda búsqueda incluye un filtro que exige que ese campo coincida con quien pregunta. No es un filtro que se aplica después de recuperar: es parte de la query, y la base de datos solo mira los vectores autorizados.

Verifiqué la firma contra la documentación de Qdrant (la API query_points con query_filter):

"""Retrieval con filtro de permisos por usuario y tenant.

La regla de oro: NUNCA se ejecuta una búsqueda sin filtro de
identidad. El filtro va DENTRO de la query, no como un post-filtro
en Python, porque un post-filtro ya recuperó (y pudo loguear) datos
que no correspondían.
"""
from qdrant_client import QdrantClient
from qdrant_client.http.models import Filter, FieldCondition, MatchValue

client = QdrantClient(url="http://localhost:6333")


def buscar_autorizado(vector, usuario_id: str, tenant_id: str, k: int = 5):
    """Devuelve solo chunks cuyo dueño es `usuario_id` DENTRO del
    tenant `tenant_id`. Ambas condiciones en `must` = AND: las dos
    tienen que cumplirse."""
    filtro = Filter(must=[
        FieldCondition(key="tenant_id",
                       match=MatchValue(value=tenant_id)),
        FieldCondition(key="owner_id",
                       match=MatchValue(value=usuario_id)),
    ])
    return client.query_points(
        collection_name="tickets",
        query=vector,
        query_filter=filtro,   # el filtro es parte de la búsqueda
        limit=k,
    ).points


def buscar(vector, usuario_id: str, tenant_id: str, k: int = 5):
    """Envoltura que hace IMPOSIBLE llamar al retrieval sin identidad:
    si falta un id, no busca. La barrera es estructural, no un
    'acordarse de pasar el filtro'."""
    if not usuario_id or not tenant_id:
        raise ValueError("retrieval sin identidad: bloqueado")
    return buscar_autorizado(vector, usuario_id, tenant_id, k)

Lo importante no es la sintaxis de Qdrant —cada base tiene la suya, Pinecone usa un dict filter, pgvector lo hace con un WHERE sobre columnas— sino el patrón: la identidad de quien pregunta entra a la query como condición obligatoria, y la única forma de llamar al retrieval pasa por una función que rechaza buscar sin identidad. Un desarrollador apurado no puede “olvidar” el filtro porque no hay puerta sin él.

Defensa 2: aislamiento por tenant a nivel de datos

El filtro de metadata es fuerte, pero comparte una debilidad: un bug en el filtro rompe el aislamiento de todos. Para datos de alto valor —o cuando cada cliente empresarial exige que sus datos no toquen los del vecino— el control sube de nivel: aislamiento por tenant físico. En lugar de un índice compartido filtrado, cada tenant tiene su propia colección (o su propia base). Ya no dependes de que la query lleve el filtro correcto: aunque el filtro fallara, la colección del tenant B simplemente no está en el alcance de la conexión del tenant A.

Nivel de aislamientoCómoCostoCuándo
Filtro de metadataUn índice, campo tenant_id en cada vectorBajoMuchos tenants pequeños, datos no críticos
Colección por tenantUna colección por cliente en la misma baseMedioPocos tenants, o exigencia contractual
Base por tenantInstancia separada por clienteAltoDatos regulados, categorías especiales

La regla: el aislamiento sube de nivel con la sensibilidad del dato. Tickets de soporte de un SaaS masivo viven bien con filtro de metadata. Historias clínicas o datos financieros de clientes empresariales piden colección o base separada.

Defensa 3: minimización y redacción antes del modelo

Minimizar es la defensa más barata y la más ignorada: el dato que no metes en el contexto no se puede filtrar. Antes de armar el prompt, pregúntate campo por campo si el modelo lo necesita para responder. El teléfono del cliente casi nunca hace falta para que el agente conteste una pregunta de horario. Si no aporta, no va.

Lo que sí necesita el modelo pero es sensible, se redacta o pseudonimiza antes de enviarlo: se reemplaza el valor real por un marcador ([EMAIL_1], [RUT_2]) que preserva la estructura del texto sin exponer el dato. El mismo redactor sirve para el vector 4 —los logs—: se redacta antes de escribir la traza.

"""Redactor de PII previo al logging (y opcionalmente al prompt).

Uso defensivo: evita que RUT, correos y tarjetas queden en texto
plano en las trazas de observabilidad. Se aplica ANTES de escribir
el log, no después: un dato ya escrito ya se filtró.
"""
import re

PATRONES = {
    # RUT chileno: 12.345.678-9 o 12345678-9
    "RUT":     re.compile(r"\b\d{1,2}\.?\d{3}\.?\d{3}-[\dkK]\b"),
    "EMAIL":   re.compile(r"\b[\w.+-]+@[\w-]+\.[\w.]+\b"),
    "TARJETA": re.compile(r"\b(?:\d[ -]?){13,16}\b"),
    "TEL":     re.compile(r"\b\+?56\s?9\s?\d{4}\s?\d{4}\b"),
}


def redactar(texto: str) -> str:
    """Reemplaza cada PII por un marcador tipado y numerado, estable
    dentro del mismo texto: el mismo correo recibe siempre [EMAIL_1].
    Así la traza sigue siendo legible para depurar sin exponer el dato.
    """
    contadores: dict[str, dict[str, int]] = {}
    resultado = texto
    for etiqueta, patron in PATRONES.items():
        vistos = contadores.setdefault(etiqueta, {})

        def _sub(m, vistos=vistos, etiqueta=etiqueta):
            valor = m.group(0)
            if valor not in vistos:
                vistos[valor] = len(vistos) + 1
            return f"[{etiqueta}_{vistos[valor]}]"

        resultado = patron.sub(_sub, resultado)
    return resultado


def log_seguro(logger, prompt: str, respuesta: str) -> None:
    """Nunca loguea crudo. Punto único de entrada a las trazas para
    que 'loguear sin redactar' no sea siquiera posible en el código."""
    logger.info("interaccion",
                prompt=redactar(prompt),
                respuesta=redactar(respuesta))

Cuidado: un redactor por regex es una red, no una garantía. Atrapa formatos conocidos (RUT, correo, tarjeta) y se le escapa la PII sin formato fijo: un nombre propio, una dirección escrita en prosa, un diagnóstico. Sirve para bajar el riesgo en logs y como una capa más, no como el único muro. Para PII en texto libre se combina con un detector de entidades (NER) o un modelo, asumiendo su costo y su tasa de error. Y ese detector, si es un LLM, vuelve a ser una defensa probabilística —no la pongas como única barrera en el vector 1.

Defensa 4: retención corta y cifrado de trazas

La PII que borras a los 30 días no puede filtrarse en el año 2. Dos controles concretos sobre las trazas del vector 4: retención corta (define un TTL y bórralas de verdad al vencer, no las acumules “por si acaso”) y cifrado en reposo y en tránsito. Sumado a la redacción previa, el objetivo es que un acceso indebido a tu observabilidad encuentre lo mínimo, tapado y por poco tiempo.

Defensa 5: el derecho al borrado en un mundo con embeddings

Aquí está el detalle que casi todos olvidan y que la Ley 21.719 y el GDPR vuelven obligatorio. Cuando un usuario ejerce su derecho al borrado, borrar la fila de la base principal no basta: su PII también vive replicada en los embeddings del vector store, en los logs, en las cachés y quizá en un checkpoint de fine-tuning. Un borrado real es un borrado en todos esos lugares.

        BORRADO REAL DE UN USUARIO (derecho al olvido)

  solicitud de borrado (usuario_id)
              │
   ┌──────────┼──────────┬─────────┬──────────┐
   ▼          ▼          ▼         ▼          ▼
 ┌──────┐ ┌────────┐ ┌───────┐ ┌───────┐ ┌────────┐
 │  BD  │ │ vector │ │ logs/ │ │ caché │ │backups │
 │ ppal │ │ store  │ │trazas │ │       │ │ cripto │
 └──┬───┘ └───┬────┘ └───┬───┘ └───┬───┘ └───┬────┘
    ▼         ▼          ▼         ▼         ▼
  delete   delete     purga     evict    destruir
  by id    by filter  by owner  por      la clave
           (owner_id)           clave    del user

El vector store se borra por el mismo owner_id que usaste para filtrar el retrieval —otra razón para etiquetar bien la metadata desde el día uno. Los backups son el caso duro: reescribir un backup histórico es caro o imposible, así que la práctica aceptada es el borrado criptográfico (cifrar los datos del usuario con una clave propia y, al borrar, destruir la clave: el dato queda ilegible aunque el backup lo contenga) más una política que garantice que ese usuario no se reindexa al restaurar. Diseña el borrado desde el principio; retrofitear “olvidar de verdad” a un sistema que esparció PII por cinco lugares sin llevar la cuenta es de los trabajos más ingratos que existen.

Tabla: vector × control × capa

Vector de fugaControl principalCapa donde vive
PII de terceros por retrievalFiltro de permisos en la queryDatos (retrieval)
Aislamiento entre clientesMetadata / colección / base por tenantDatos (índice)
Exceso de PII en el promptMinimización + redacción pre-modeloAplicación (armado de contexto)
Memorización del modeloDeduplicar y redactar antes de fine-tuningEntrenamiento
Datos al proveedorPlan sin entrenamiento, retención cortaContrato / infraestructura
PII en logs y trazasRedacción pre-log + retención + cifradoObservabilidad
Fuga del system promptTratarlo como público, sin datos realesAplicación (prompt)
Derecho al borrado incumplidoBorrado propagado + borrado criptográficoDatos + backups

La columna que decide el trabajo es la tercera. Casi ningún control vive en el prompt. Viven en la capa de datos, en el índice, en el contrato con el proveedor y en la observabilidad. El prompt es donde se ve el síntoma; la capa de datos es donde se cura.

Errores comunes

  • Indexar todo en un solo store sin owner_id. Es el error que produce el incidente más grave y más común: el usuario A recibe datos del usuario B por pura similitud semántica. El filtro de permisos en el retrieval no es opcional.
  • Post-filtrar en Python en vez de filtrar en la query. Si recuperas primero y filtras después, ya trajiste (y quizá logueaste) datos que no correspondían. El filtro va dentro de la búsqueda.
  • Redactar la salida pero loguear el prompt crudo. Mueve la fuga de la cara del usuario a un panel interno con menos controles y más retención. Se redacta antes de escribir la traza, no después.
  • Temerle a la memorización e ignorar el retrieval. El miedo popular está invertido: el vector realista y frecuente es el retrieval sin permisos, no el modelo regurgitando su corpus.
  • Meter datos “por si acaso” en el contexto. Lo que no entra no se filtra. La minimización es la defensa más barata y la más saltada.
  • Creer que borrar la fila principal es borrar. La PII vive replicada en embeddings, logs, cachés y backups. Un borrado que no los cubre no cumple el derecho al olvido.
  • Asumir que el proveedor “cumple por ti”. El responsable del tratamiento eres tú. Verifica retención y no-entrenamiento en tu plan concreto, y no saques categorías especiales sin redactar.

La frase para llevarse

“El modelo no filtra lo que memorizó: filtra lo que le pusiste en el contexto. La privacidad de un sistema con LLM se decide en la query del retrieval, no en la última línea del prompt.”

Qué viene

Módulo 8: seguridad de RAG. Este módulo dejó dos cabos que son, en realidad, el corazón del siguiente: el retrieval como superficie de fuga (vector 1) y el corpus como superficie de ataque (recuerda la injection indirecta plantada en un documento que el índice trae solo). En seg-08 juntamos las dos mitades: cómo se envenena un índice, cómo un chunk malicioso viaja desde la ingesta hasta el contexto, y cómo se endurece la cadena completa —ingesta, permisos, recuperación y uso— para que el sistema que más datos toca sea también el que mejor los cuida.

Material relacionado

SeguridadPrivacidadPIIDatosCumplimiento

Preguntas sobre este curso

Regístrate gratis para hacer preguntas y guardar tu progreso.

✉ Entérate de lo nuevo

Recibe un aviso cuando publique nuevos cursos, workshops o investigaciones. Sin spam.