lección 3
Día 2 -- GDPR: diseñar proceso de supresión
Sara te explica el artículo 17. Inventarías PII en 6 tablas, diseñas anonimización irreversible con SHA-256 + salt efímero y construyes el GDPRAnonymizer.
⏱ 45 min
### Miércoles, 9:00 AM -- La reunión con Sara
Sara Vidal te espera en la sala Ada Lovelace con un café cortado y una carpeta azul con el logo de la AEPD. Es puntual al segundo -- llegas a las 9:01 y ya tiene el portátil abierto con un cronograma proyectado en la pantalla. "Gracias por venir puntual. Voy directa al grano porque el reloj corre y cada día que pasa sin resolver esto es un día más cerca de tener que notificar incumplimiento."
1Sara Vidal (sala Ada Lovelace, 9:02 AM):2"Artículo 17 del RGPD. Derecho de supresión. El cliente3acc_00028473, Jenaro Portero Llorente Martínez, solicitó el borrado4de todos sus datos personales el 22 de febrero de 2024.56El plazo legal es de 30 días naturales. Hoy es 6 de marzo.7Llevamos 12 días consumidos. Me quedan 18 días naturales,8que son 12 laborables.910Si no tenemos el proceso implementado, ejecutado y AUDITADO11para el 23 de marzo, tengo la obligación legal de notificar12incumplimiento a la AEPD. Eso es una sanción de entre 10.00013y 20 millones de euros, o el 4% de la facturación anual --14lo que sea mayor. Para NeoBank, son 3.8 millones de euros.1516No es negociable. No es aplazable. No es 'ya lo hacemos la17semana que viene'. Es ESTO o es una multa."
Sara explica el deadline regulatorio -- sin margen de error
Sara te pasa un documento de 3 páginas con los requisitos legales exactos. Los lees con atención porque cada palabra tiene implicaciones técnicas:
- Anonimización IRREVERSIBLE -- no pseudonimización. No debe ser posible re-identificar al cliente bajo ninguna circunstancia, ni con acceso a la base de datos completa.
- Aplica a TODOS los sistemas: base operacional (PostgreSQL), data lake (S3 raw/silver/gold), warehouse (Redshift), backups, logs y cualquier sistema que contenga PII.
- Los datos transaccionales pueden CONSERVARSE si se elimina toda PII -- NeoBank necesita el histórico para reporting regulatorio al BdE, pero sin identificar al individuo.
- Debe quedar un registro de auditoría INMUTABLE que demuestre qué se borró, cuándo, por quién y con qué método.
- El proceso debe ser REPETIBLE y AUTOMATIZADO -- este no será el último cliente que lo pida. Con 2.1M clientes, estadísticamente recibiremos ~15 solicitudes/mes.
### Inventario de PII -- dónde vive Jenaro
Vuelves a tu escritorio y empiezas el inventario. Es el pasó más importante y el que más tiempo consume. Consultas el catálogo de datos (un Confluence desactualizado pero mejor que nada), revisas los schemas de Redshift con information_schema, y exploras los paths de S3 con aws s3 ls. Después de 2 horas de investigación, tienes el mapa completo:
1-- INVENTARIO DE PII PARA SOLICITUD GDPR-REQ-2024-01472-- Cliente: acc_00028473 (Jenaro Portero Llorente Martínez)3-- Fecha inventario: 2024-03-064-- Realizado por: Data Engineer56-- ═══════════════════════════════════════════════════════════════7-- TABLA 1: clients (tabla maestra de clientes)8-- ═══════════════════════════════════════════════════════════════9-- PII encontrada: nombre, email, teléfono, dirección, fecha_nacimiento, dni_hash10-- Registros afectados: 111-- Acción: ANONIMIZAR (mantener registro con campos hasheados)12SELECT account_id, nombre, email, telefono, dirección, fecha_nacimiento13FROM clients WHERE account_id = 'acc_00028473';1415-- ═══════════════════════════════════════════════════════════════16-- TABLA 2: transactions (histórico de movimientos)17-- ═══════════════════════════════════════════════════════════════18-- PII encontrada: account_id como FK (permite re-identificación)19-- Registros afectados: 4.122 transacciones en 14 meses20-- Acción: ANONIMIZAR account_id (mantener amounts para reporting BdE)21SELECT count(*) as total_txns,22 min(timestamp) as primera, max(timestamp) as última,23 sum(amount) as volumen_total24FROM transactions WHERE account_id = 'acc_00028473';25-- Resultado: 4122 txns, 2022-06-15 a 2024-02-20, 187.432,56 EUR2627-- ═══════════════════════════════════════════════════════════════28-- TABLA 3: kyc_documents (Know Your Customer)29-- ═══════════════════════════════════════════════════════════════30-- PII encontrada: scan_dni (imagen), selfie_url, dirección_verificada31-- Registros afectados: 3 (alta original + 2 actualizaciones)32-- Acción: BORRAR FÍSICAMENTE (sin valor analítico, alto riesgo)33SELECT document_type, created_at, verified_at34FROM kyc_documents WHERE account_id = 'acc_00028473';3536-- ═══════════════════════════════════════════════════════════════37-- TABLA 4: communications (emails y SMS enviados)38-- ═══════════════════════════════════════════════════════════════39-- PII encontrada: email_destino, telefono_destino, contenido del mensaje40-- Registros afectados: 847 comunicaciones41-- Acción: NULLIFICAR contenido, ANONIMIZAR destinatario42SELECT comm_type, count(*) as total43FROM communications WHERE account_id = 'acc_00028473'44GROUP BY comm_type;45-- email: 523, sms: 289, push: 354647-- ═══════════════════════════════════════════════════════════════48-- TABLA 5: support_tickets (atención al cliente)49-- ═══════════════════════════════════════════════════════════════50-- PII encontrada: nombre en texto libre, email, contenido conversaciones51-- Registros afectados: 12 tickets (4 cerrados, 8 resueltos)52-- Acción: NULLIFICAR contenido de texto libre, ANONIMIZAR contactó53SELECT ticket_id, status, created_at, subject54FROM support_tickets WHERE account_id = 'acc_00028473';5556-- ═══════════════════════════════════════════════════════════════57-- TABLA 6: fraud_alerts (alertas del modelo ML)58-- ═══════════════════════════════════════════════════════════════59-- PII encontrada: account_id, transaction_details (puede contener merchant names)60-- Registros afectados: 3 alertas (todas false positive)61-- Acción: ANONIMIZAR account_id, mantener para reentrenamiento del modelo62SELECT alert_id, alert_type, false_positive, timestamp63FROM fraud_alerts WHERE account_id = 'acc_00028473';6465-- ═══════════════════════════════════════════════════════════════66-- S3 PATHS AFECTADOS67-- ═══════════════════════════════════════════════════════════════68-- s3://neobank-lake/raw/clients/2022/06/acc_00028473.json69-- s3://neobank-lake/silver/transactions/account_id=acc_00028473/ (14 particiones mensuales)70-- s3://neobank-lake/gold/client_360/acc_00028473.parquet7172-- ═══════════════════════════════════════════════════════════════73-- RESUMEN74-- ═══════════════════════════════════════════════════════════════75-- Total tablas afectadas: 676-- Total registros: 4.98877-- Total paths S3: 16 archivos en 3 prefijos78-- Riesgo: ALTO (contiene KYC con documento de identidad escaneado)79-- Prioridad: P1 (deadline regulatorio)
Inventario completo de PII -- el mapa de lo que hay que anonimizar
El inventario de PII es el pasó MÁS IMPORTANTE y el que más tiempo consume. Si te dejas un sistema sin anonimizar, no solo incumples la ley -- puedes generar una brecha de datos. En empresas grandes existe "data lineage" automatizado (herramientas como Collibra o Alation). En NeoBank todavía no -- por eso tardas 2 horas revisando manualmente. Esta experiencia es exactamente por la que Alberto quiere que propongas un catálogo la semana que viene.
### Estrategia de anonimización -- SHA-256 con salt efímero
Con el inventario completo, diseñas la estrategia. La clave es distinguir entre 3 acciones diferentes según el tipo de dato:
- BORRAR físicamente: datos que no tienen valor analítico sin la persona (KYC, selfies, DNI escaneado). Se eliminan de la base y de S3.
- ANONIMIZAR con hash: datos que queremos conservar para analytics pero sin identificar a la persona (transacciones, alertas). El account_id se reemplaza por un hash irreversible.
- NULLIFICAR: campos de texto libre donde la PII está embebida en texto natural (tickets de soporte, contenido de emails). Se reemplazan por NULL.
La decisión técnica crucial: usar SHA-256 con un salt aleatorio que se DESTRUYE después de la ejecución. ¿Por qué? Sin salt, alguien con acceso a la base podría hashear todos los account_ids posibles (son secuenciales: CLI-0000001 a CLI-2100000) y hacer una tabla rainbow para re-identificar. Con salt efímero destruido, es matemáticamente imposible revertir el hash -- ni siquiera con fuerza bruta.
1import hashlib2import secrets3import json4from datetime import datetime, timezone5from typing import Dict, List, Optional6from dataclasses import dataclass, field78@dataclass9class AuditEntry:10 """Entrada inmutable del log de auditoría."""11 timestamp: str12 table: str13 action: str # "anonymized" | "deleted" | "nullified"14 field: Optional[str] = None15 records_affected: int = 11617class GDPRAnonymizer:18 """19 Anonimizador de datos personales según RGPD Artículo 17.2021 Diseño:22 - Salt efímero: se genera al inicio y se destruye al final23 - Hash irreversible: SHA-256 truncado a 16 chars hexadecimales24 - Audit trail: cada acción queda registrada con timestamp25 - Compliance report: documento generado para Legal/DPO2627 Uso:28 anon = GDPRAnonymizer("acc_00028473", "GDPR-REQ-2024-0147")29 anon.anonymize_record("clients", record, ["nombre", "email"])30 anon.delete_records("kyc_documents", 3)31 anon.destroy_salt()32 report = anon.generate_compliance_report()33 """3435 def __init__(self, account_id: str, request_id: str):36 self.account_id = account_id37 self.request_id = request_id38 self.salt = secrets.token_hex(32) # 256 bits de entropía39 self.audit_log: List[AuditEntry] = []40 self.started_at = datetime.now(timezone.utc)41 self._salt_destroyed = False4243 # Registrar inicio en audit log44 self.audit_log.append(AuditEntry(45 timestamp=self.started_at.isoformat(),46 table="system",47 action="process_started",48 field=f"account_id={account_id}",49 ))5051 def hash_value(self, value: str) -> str:52 """53 Hash irreversible con salt efímero.54 SHA-256(salt + ":" + value) truncado a 16 chars hex.55 """56 if self._salt_destroyed:57 raise RuntimeError("Salt destruido -- no se pueden generar más hashes")58 combined = f"{self.salt}:{value}".encode("utf-8")59 full_hash = hashlib.sha256(combined).hexdigest()60 return full_hash[:16]6162 def anonymize_record(self, table: str, record: Dict,63 pii_fields: List[str]) -> Dict:64 """65 Anonimiza los campos PII de un registro.66 Los campos no-PII permanecen intactos.67 """68 anonymized = record.copy()69 for field_name in pii_fields:70 if field_name in anonymized and anonymized[field_name] is not None:71 original_value = str(anonymized[field_name])72 anonymized[field_name] = f"ANON_{self.hash_value(original_value)}"73 self.audit_log.append(AuditEntry(74 timestamp=datetime.now(timezone.utc).isoformat(),75 table=table,76 action="anonymized",77 field=field_name,78 ))79 return anonymized8081 def nullify_fields(self, table: str, fields: List[str], count: int) -> None:82 """Registra la nullificación de campos de texto libre."""83 for field_name in fields:84 self.audit_log.append(AuditEntry(85 timestamp=datetime.now(timezone.utc).isoformat(),86 table=table,87 action="nullified",88 field=field_name,89 records_affected=count,90 ))9192 def delete_records(self, table: str, count: int) -> None:93 """Registra el borrado físico de registros."""94 self.audit_log.append(AuditEntry(95 timestamp=datetime.now(timezone.utc).isoformat(),96 table=table,97 action="deleted",98 records_affected=count,99 ))100101 def delete_s3_paths(self, paths: List[str]) -> None:102 """Registra la eliminación de archivos en S3."""103 for path in paths:104 self.audit_log.append(AuditEntry(105 timestamp=datetime.now(timezone.utc).isoformat(),106 table="s3",107 action="deleted",108 field=path,109 ))110111 def destroy_salt(self) -> None:112 """113 Destruye el salt de forma irreversible.114 Después de esto, es matemáticamente imposible revertir los hashes.115 """116 self.salt = None117 self._salt_destroyed = True118 self.audit_log.append(AuditEntry(119 timestamp=datetime.now(timezone.utc).isoformat(),120 table="system",121 action="salt_destroyed",122 ))123124 def generate_compliance_report(self) -> Dict:125 """Genera reporte para Sara/Legal con resumen del proceso."""126 actions_by_type = {}127 for entry in self.audit_log:128 actions_by_type[entry.action] = actions_by_type.get(entry.action, 0) + 1129130 return {131 "request_id": self.request_id,132 "account_id": self.account_id,133 "started_at": self.started_at.isoformat(),134 "completed_at": datetime.now(timezone.utc).isoformat(),135 "salt_destroyed": self._salt_destroyed,136 "total_audit_entries": len(self.audit_log),137 "actions_summary": actions_by_type,138 "compliant": self._salt_destroyed, # Solo es compliant si el salt fue destruido139 "audit_trail": [140 {"ts": e.timestamp, "table": e.table, "action": e.action, "field": e.field}141 for e in self.audit_log142 ],143 }144145# === DEMOSTRACIÓN ===146anonymizer = GDPRAnonymizer("acc_00028473", "GDPR-REQ-2024-0147")147148# Anonimizar registro de cliente149cliente_original = {150 "account_id": "acc_00028473",151 "nombre": "Jenaro Portero Llorente Martínez",152 "email": "j.portero.llorente@gmail.com",153 "telefono": "+34612847391",154 "dirección": "Calle Gran Vía 45, 3ºB, 28013 Madrid",155 "fecha_nacimiento": "1985-07-14",156 "fecha_alta": "2022-06-15",157 "saldo_actual": 12847.33,158}159160cliente_anonimizado = anonymizer.anonymize_record(161 "clients", cliente_original,162 pii_fields=["nombre", "email", "telefono", "dirección", "fecha_nacimiento"]163)164165print("=== ANTES vs DESPUÉS ===")166print(f"Nombre: {cliente_original['nombre']:40s} -> {cliente_anonimizado['nombre']}")167print(f"Email: {cliente_original['email']:40s} -> {cliente_anonimizado['email']}")168print(f"Saldo: {cliente_original['saldo_actual']:40} -> {cliente_anonimizado['saldo_actual']} (NO es PII)")169170# Registrar otras acciones171anonymizer.delete_records("kyc_documents", 3)172anonymizer.nullify_fields("support_tickets", ["contenido", "nombre_agente"], 12)173anonymizer.delete_s3_paths([174 "s3://neobank-lake/raw/clients/2022/06/acc_00028473.json",175 "s3://neobank-lake/gold/client_360/acc_00028473.parquet",176])177178# Destruir salt -- punto de no retorno179anonymizer.destroy_salt()180181# Generar reporte182report = anonymizer.generate_compliance_report()183print(f"\n=== REPORTE DE COMPLIANCE ===")184print(f"Request: {report['request_id']}")185print(f"Salt destruido: {report['salt_destroyed']}")186print(f"Acciones totales: {report['total_audit_entries']}")187print(f"Compliant: {report['compliant']}")
GDPRAnonymizer completo: hash irreversible + audit trail + compliance report
### El runbook de supresión -- proceso repetible
Sara insiste en que el proceso sea un runbook documentado -- un procedimiento pasó a pasó que cualquier persona del equipo pueda ejecutar cuando llegue la próxima solicitud (y llegarán: con 2.1M clientes, estadísticamente 15/mes).
- 01.Recibir solicitud GDPR de Legal con el account_id y request_id asignado
- 02.Verificar identidad del solicitante (lo hace Legal, no datos)
- 03.Ejecutar query de inventario para localizar TODOS los registros del cliente
- 04.Generar salt efímero y crear instancia de GDPRAnonymizer
- 05.Anonimizar tabla clients (nombre, email, teléfono, dirección, DNI)
- 06.Anonimizar FK en transactions (mantener amounts y timestamps)
- 07.BORRAR registros de kyc_documents (borrado físico, sin valor analítico)
- 08.NULLIFICAR contenido de communications y support_tickets
- 09.BORRAR paths de S3 específicos del cliente (raw + gold/client_360)
- 10.Ejecutar queries de VERIFICACIÓN post-anonimización
- 11.Destruir salt (punto de no retorno)
- 12.Generar compliance report y enviarlo a Sara con copia a Legal
### Verificación post-anonimización
Sara es inflexible en un punto: "Si no puedes DEMOSTRAR que no queda PII, no puedo certificar cumplimiento". Las queries de verificación son tan importantes como la anonimización en sí:
1-- VERIFICACIÓN POST-ANONIMIZACIÓN2-- Ejecutar DESPUÉS del proceso y ANTES de destruir el salt3-- Todas las queries deben devolver 0 registros con PII45-- 1. El account_id original NO debe existir en ninguna tabla6SELECT 'clients' as tabla, count(*) as matches7 FROM clients WHERE account_id = 'acc_00028473'8UNION ALL9SELECT 'transactions', count(*)10 FROM transactions WHERE account_id = 'acc_00028473'11UNION ALL12SELECT 'communications', count(*)13 FROM communications WHERE account_id = 'acc_00028473'14UNION ALL15SELECT 'support_tickets', count(*)16 FROM support_tickets WHERE account_id = 'acc_00028473'17UNION ALL18SELECT 'fraud_alerts', count(*)19 FROM fraud_alerts WHERE account_id = 'acc_00028473';20-- ESPERADO: todos 02122-- 2. Buscar PII textual (nombre, email, teléfono)23SELECT 'clients' as tabla, count(*) as matches24 FROM clients WHERE nombre LIKE '%Portero%'25 OR email LIKE '%j.portero%'26 OR telefono LIKE '%612847391%'27UNION ALL28SELECT 'support_tickets', count(*)29 FROM support_tickets WHERE contenido LIKE '%Portero%'30 OR contenido LIKE '%j.portero%';31-- ESPERADO: todos 03233-- 3. KYC debe estar BORRADO (no anonimizado)34SELECT count(*) as kyc_restante FROM kyc_documents35WHERE account_id = 'acc_00028473'36 OR account_id LIKE 'ANON_%2847391%';37-- ESPERADO: 0 (borrado físico completo)3839-- 4. Verificar que datos ANONIMIZADOS siguen disponibles para analytics40SELECT count(*) as txns_anonimizadas,41 sum(amount) as volumen_conservado,42 min(timestamp) as primera_txn,43 max(timestamp) as ultima_txn44FROM transactions WHERE account_id LIKE 'ANON_%';45-- ESPERADO: 4122 transacciones con importes intactos4647-- 5. Generar resumen para el compliance report48SELECT tabla, registros_originales, accion_realizada, verificado49FROM (VALUES50 ('clients', 1, 'ANONIMIZADO', TRUE),51 ('transactions', 4122, 'FK ANONIMIZADA', TRUE),52 ('kyc_documents', 3, 'BORRADO FÍSICO', TRUE),53 ('communications', 847, 'NULLIFICADO', TRUE),54 ('support_tickets', 12, 'NULLIFICADO', TRUE),55 ('fraud_alerts', 3, 'FK ANONIMIZADA', TRUE)56) AS t(tabla, registros_originales, accion_realizada, verificado);
Queries de verificación -- demostrar que no queda PII
NUNCA ejecutes una anonimización GDPR sin verificación posterior. Si dejas un solo registro sin anonimizar y hay una auditoría de la AEPD, la multa puede ser del 4% de la facturación anual. La verificación no es opcional -- es parte integral del proceso y debe quedar documentada en el compliance report.
En la práctica, las solicitudes GDPR vienen en oleadas. Cuando un banco grande publica que respeta el derecho al olvido, otros clientes lo piden también por principio. Automatizar AHORA te ahorra cientos de horas en los próximos meses. Sara lo sabe -- por eso insiste en el runbook.
## ejercicios
Inventario de PII automatizado
Implementa una función que dado un account_id, consulte todas las tablas simuladas y devuelva un inventario completo con registros afectados, campos PII y nivel de riesgo.
Implementar GDPRAnonymizer completo
Implementa la clase GDPRAnonymizer con hash SHA-256+salt efímero, audit trail inmutable y generación de compliance report para Sara.
Queries de verificación post-anonimización
Escribe las queries SQL que verifican que no queda PII en ninguna tabla después de la anonimización. Deben ser exhaustivas para pasar una auditoría de la AEPD.
Regístrate para guardar tu progreso.
## comentarios
Reporta erratas, ayuda a otros o comparte tu opinión. Sé constructivo.
Inicia sesión para comentar y responder.
cargando comentarios...