lección 7
Protección de datos para ingenieros
Qué es un dato personal y cuál es sensible, guardar solo lo que hace falta, por qué un hash con hashlib no basta y cómo se hace un código con clave secreta (HMAC), y cómo se borra a quien lo pide de todas partes y se comprueba que se hizo.
⏱ 45 min
## El usuario 4417749
El 4 de agosto de 2006, el departamento de investigación de AOL publicó en internet unos 20 millones de búsquedas hechas por unas 650.000 personas durante tres meses. Querían ayudar a los investigadores. Antes de publicarlas hicieron lo que a casi todo el mundo le parece suficiente: quitaron el nombre de usuario y pusieron un número en su lugar.
Cinco días después, dos periodistas del New York Times llamaron a la puerta de Thelma Arnold, una viuda de 62 años de Lilburn, en Georgia. Era el usuario 4417749. Bastó con leer sus búsquedas juntas: «jardineros en Lilburn», varias personas apellidadas Arnold, dolencias de una mujer de su edad. Cada búsqueda sola no decía nada; todas juntas, con el mismo número, eran una persona con nombre y dirección. AOL retiró los datos, despidió a dos personas y su directora de tecnología dejó la empresa.
Esta lección va de no ser AOL. Un contrato de datos te dice qué columnas llegan; no te dice cuáles deberías dejar entrar. Aquí aprendes lo que se hace en el pipeline, no en un despacho de abogados. Son cuatro cosas: reconocer un dato personal y uno sensible, guardar solo lo que hace falta, convertir un email en un código con clave secreta, y borrar a quien lo pide de todas partes, sabiendo demostrar que se hizo.
## Qué es un dato personal
En Europa la regla es el Reglamento General de Protección de Datos (RGPD), que se aplica desde el 25 de mayo de 2018; en España lo completa la Ley Orgánica 3/2018 (LOPDGDD). Su definición es más amplia de lo que se suele pensar: un dato personal es toda información sobre una persona física identificada o identificable. Identificable significa que se puede llegar a saber quién es, directa o indirectamente, con medios razonables.
La palabra que importa es «identificable». El nombre y el email son obvios. Lo que no es obvio es que una combinación de datos inocentes también identifica. En el año 2000, la investigadora Latanya Sweeney mostró que el código postal, la fecha de nacimiento y el sexo bastaban para señalar a una sola persona en el 87 % de la población de Estados Unidos. Ninguna de las tres columnas es un nombre. Las tres juntas casi siempre lo son.
| Columna (tienda online de bicicletas) | ¿Dato personal? | Por qué |
|---|---|---|
| email, nombre, teléfono | Sí | Identifican directamente |
| dirección de envío | Sí | Lleva a una casa y a quien vive en ella |
| IP de la última visita | Sí | El Tribunal de Justicia de la UE lo confirmó en 2016 (caso Breyer): con los registros del operador se llega a la persona |
| fecha de nacimiento + código postal | Sí | Juntas reducen a muy pocas personas |
| talla de cuadro, en la fila de un cliente | Sí | Cualquier dato que cuelga de una persona identificable es personal |
| nota libre de atención al cliente | Casi siempre | Quien la escribe pone nombres, emails, teléfonos y hasta dolencias, y ninguna columna lo avisa |
| precio del modelo, en el catálogo | No | Describe una bicicleta, no a una persona |
| stock del almacén de Zaragoza | No | Describe un almacén |
## Datos sensibles: las categorías especiales
Dentro de los datos personales hay un grupo con protección reforzada. El artículo 9 del RGPD los llama categorías especiales y en el día a día se les dice «datos sensibles»: origen étnico o racial, opiniones políticas, convicciones religiosas o filosóficas, afiliación sindical, datos genéticos, datos biométricos usados para identificar a alguien (una huella para abrir un torniquete), datos de salud y datos sobre la vida sexual o la orientación sexual. Tratarlos está prohibido por defecto, salvo las excepciones que enumera el propio artículo, como el consentimiento explícito o la asistencia sanitaria que presta un profesional obligado a guardar secreto.
El peligro no suele estar en la columna que se llama «diagnóstico», sino en lo que se deduce. En 2012, el New York Times contó cómo una cadena de grandes almacenes de Estados Unidos calculaba una «puntuación de embarazo» a partir de compras como crema sin perfume o suplementos de calcio. Ninguna columna decía «embarazada». Un dato de salud deducido sigue siendo un dato de salud.
Una tabla de compras de una farmacia online es, en la práctica, una tabla de salud: las tiras de glucosa dicen «diabetes» aunque ninguna columna lo diga. Antes de cruzar compras con personas, pregúntate qué se puede deducir del cruce, no solo qué dicen las columnas.
Hay datos que el artículo 9 no incluye y que aun así se tratan como si lo hicieran, porque su fuga hace mucho daño: el DNI, el IBAN, la ubicación precisa del móvil, los datos de menores o las contraseñas.
## Minimizar: guardar solo lo que hace falta para la pregunta
Piensa en el portero de una discoteca. Su pregunta es «¿tiene dieciocho años?». Mira el DNI y te lo devuelve. No lo fotocopia, no apunta tu dirección y no se queda con tu número de soporte «por si acaso». Necesita un sí o un no, y se queda con un sí o un no.
El RGPD lo llama minimización (artículo 5): los datos tienen que ser adecuados, pertinentes y limitados a lo necesario para el fin. En un pipeline se traduce en una costumbre muy concreta: se parte de la pregunta de negocio y se baja hasta las columnas, nunca al revés. Si la pregunta es «cuánto se vende por provincia y tramo de edad», no necesitas el email, ni el nombre, ni la fecha de nacimiento exacta, ni la calle.
- Elige las columnas a mano. Una lista explícita de columnas en la ingesta, no «todo lo que venga». Lo que no está en la lista no entra.
- Generaliza. La fecha de nacimiento se queda en tramo de edad; el código postal, en provincia (en España, las dos primeras cifras); la IP, en país; la hora exacta, en día.
- Limita el tiempo. Minimizar también es no guardar para siempre. El mismo artículo 5 pide conservar los datos solo el tiempo necesario: decide cuánto y bórralo después.
1import pandas as pd23# Lo que manda la tienda: todo lo que tiene4pedidos_crudos = pd.DataFrame({5 "pedido": [101, 102, 103],6 "email": ["ana@correo.es", "luis@correo.es", "eva@correo.es"],7 "nombre": ["Ana Ruiz", "Luis Gil", "Eva Sanz"],8 "fecha_nacimiento": ["1991-04-12", "1978-11-30", "2001-07-02"],9 "codigo_postal": ["28012", "08024", "46005"],10 "ip": ["83.44.10.7", "88.12.200.3", "95.20.1.18"],11 "importe": [349.0, 1290.0, 89.9],12 "fecha": ["2026-10-01", "2026-10-01", "2026-10-02"],13})1415# La pregunta: ¿cuánto se vende por provincia y tramo de edad?16COLUMNAS_NECESARIAS = ["pedido", "fecha_nacimiento", "codigo_postal", "importe", "fecha"]17pedidos = pedidos_crudos[COLUMNAS_NECESARIAS].copy()1819# Generalizar: la pregunta no necesita el día exacto ni la calle20edad = 2026 - pd.to_datetime(pedidos["fecha_nacimiento"]).dt.year21pedidos["tramo_edad"] = pd.cut(edad, bins=[0, 29, 44, 64, 120],22 labels=["<30", "30-44", "45-64", "65+"])23pedidos["provincia"] = pedidos["codigo_postal"].str[:2]24pedidos = pedidos.drop(columns=["fecha_nacimiento", "codigo_postal"])2526print(pedidos.to_string(index=False))27# pedido importe fecha tramo_edad provincia28# 101 349.0 2026-10-01 30-44 2829# 102 1290.0 2026-10-01 45-64 0830# 103 89.9 2026-10-02 <30 46
De ocho columnas a cinco, y dos de ellas generalizadas. La tabla responde a la misma pregunta y ya no dice quién compró.
Consejo de senior: lo que no guardas no se te puede filtrar, no te lo pueden pedir borrar y no tienes que explicar por qué lo tenías. Cada vez que alguien diga «guárdalo por si acaso», pregunta para qué pregunta. Si nadie sabe contestar, no entra.
## Seudonimizar no es anonimizar
Lo que hizo AOL tiene nombre: seudonimizar. Es sustituir lo que identifica (el nombre de usuario) por un código (el número), de forma que sin información adicional, guardada aparte, no se sepa quién es. Anonimizar es otra cosa: transformar los datos de manera que ya nadie, con medios razonables, pueda volver a la persona. La diferencia no es de matiz. La Agencia Española de Protección de Datos (AEPD) lo dice sin rodeos: un dato seudonimizado sigue siendo un dato personal y se le sigue aplicando el RGPD entero. Solo el dato anonimizado de verdad queda fuera.
¿Por qué el seudónimo no basta para salir? Porque el código es siempre el mismo para la misma persona, así que junta todas sus filas. Y las filas juntas cuentan una vida: le pasó a AOL, y en 2008 al concurso de Netflix, cuyos usuarios «anónimos» se reidentificaron cruzando sus valoraciones con reseñas públicas en IMDb. Y porque quien tiene la información adicional (la tabla de correspondencias, la clave) puede deshacerlo.
Entonces, ¿para qué seudonimizar? Para reducir el riesgo, que no es poco: quien analiza cuenta clientes y cruza tablas sin ver jamás un email, y una tabla de códigos filtrada hace mucho menos daño que una de emails. El propio RGPD la cita como medida de seguridad. Solo hay que ser honesto con lo que es.
Nunca escribas «tabla anonimizada» en la documentación de una tabla seudonimizada. Las palabras tienen consecuencias legales: quien lea «anónimo» puede decidir compartirla con un proveedor o guardarla para siempre, y ninguna de las dos cosas está permitida con un dato personal.
## Por qué un hash simple no basta
Una función hash convierte cualquier texto en una huella de longitud fija. SHA-256, publicada por el NIST de Estados Unidos en 2001, convierte «ana@correo.es» en 64 caracteres hexadecimales. El mismo texto da siempre la misma huella, dos textos distintos dan huellas distintas y no hay fórmula para volver de la huella al texto. En Python viene de serie, en el módulo `hashlib` de la biblioteca estándar: `hashlib.sha256("ana@correo.es".encode()).hexdigest()`, sin instalar nada. Es tan cómodo que tanta gente concluye: «si no se puede invertir, mi email está protegido».
El fallo es que nadie necesita invertirla: basta con adivinar. Quien tiene una lista de emails o de móviles candidatos calcula el hash de cada uno y compara. Los emails siguen patrones (nombre.apellido@proveedor) y los móviles españoles son nueve cifras que empiezan casi siempre por 6 o 7; un ordenador normal calcula millones de SHA-256 por segundo. La AEPD y el Supervisor Europeo de Protección de Datos dedicaron un documento entero a esto: el hash solo seudonimiza si el atacante no puede recorrer los valores posibles.
1import hashlib2import time34# En la tabla «seudonimizada» aparece este hash de un móvil5hash_filtrado = hashlib.sha256("612345678".encode()).hexdigest()67# El atacante no invierte nada: prueba. Supongamos que sabe el prefijo8# (6123...) y le faltan 5 cifras: 100.000 candidatos.9inicio = time.perf_counter()10for resto in range(100_000):11 candidato = f"6123{resto:05d}"12 if hashlib.sha256(candidato.encode()).hexdigest() == hash_filtrado:13 print("Encontrado:", candidato)14 break15print(f"Intentos: {resto + 1:,}".replace(",", "."))16print(f"Tiempo: {time.perf_counter() - inicio:.2f} s")17# Encontrado: 61234567818# Intentos: 45.67919# Tiempo: 0.04 s
Cuarenta y cinco mil intentos en unas centésimas de segundo. Con los 200 millones de móviles posibles, un portátil tarda minutos; una tarjeta gráfica, menos de un segundo.
La reacción habitual es añadir una «sal», un texto que se pega al email antes del hash. Si es aleatoria y distinta por fila, el mismo email da códigos distintos y ya no puedes cruzar tablas ni contar personas, que era el objetivo. Si es fija y está escrita en el código, es una clave mal guardada: la tiene cualquiera con acceso al repositorio. Lo que necesitas es una clave, bien hecha y bien guardada. Muchos equipos la siguen llamando «sal». El nombre da igual; lo que importa es que no esté en el código.
## HMAC: un código con clave secreta
En 1996, Mihir Bellare, Ran Canetti y Hugo Krawczyk propusieron HMAC, que en 1997 se publicó como estándar de internet (RFC 2104). Nació para demostrar que un mensaje viene de alguien que conoce una clave y que nadie lo ha tocado, pero su forma es justo la que buscamos: código = HMAC(clave, email). Mismo email y misma clave, mismo código; sin la clave, no se puede calcular. En Python está en otro módulo de la biblioteca estándar, `hmac`, que usa por dentro la función hash que le digas. Con `hashlib.sha256` el resultado se llama HMAC-SHA256, el nombre que vas a ver escrito en casi todas partes.
Piénsalo como un molde. Un hash simple es un molde que vende cualquier ferretería: el atacante se compra uno, prueba emails y compara. HMAC es un molde que solo tiene tu empresa, guardado en una caja fuerte. El atacante puede tener la lista de emails más completa del mundo; sin el molde, sus huellas no se parecen en nada a las tuyas.
1import hashlib2import hmac3import os45clave = os.environ.get("CLAVE_SEUDONIMO", "").encode("utf-8")6if not clave:7 raise RuntimeError("Falta CLAVE_SEUDONIMO: la carga no sigue sin ella")8910def normalizar(email: str) -> str:11 return email.strip().lower()121314def codigo(email: str) -> str:15 mensaje = normalizar(email).encode("utf-8")16 return hmac.new(clave, mensaje, hashlib.sha256).hexdigest()171819print(codigo("ana@correo.es")[:16])20print(codigo(" Ana@Correo.es ")[:16]) # el mismo: se normaliza antes
Las tres piezas: la clave se lee de fuera del código, el email se normaliza y el código es un HMAC-SHA256 en hexadecimal (64 caracteres).
La clave vive fuera del código: en una variable de entorno que el sistema pone antes de arrancar la carga o, mejor, en el gestor de secretos de tu proveedor de nube, que la entrega solo al proceso que la necesita y deja registro de quién la ha pedido. Nunca en el repositorio: lo que entra en el historial de Git se queda ahí aunque lo borres en el siguiente commit. Para generar una clave buena y ponerla en tu terminal:
1# Generar una clave aleatoria de 32 bytes (igual en Windows y Mac)2python -c "import secrets; print(secrets.token_hex(32))"34# Windows (PowerShell): solo para esta sesión de la terminal5$env:CLAVE_SEUDONIMO = "pega-aqui-la-clave-generada"67# Mac (Terminal/zsh): solo para esta sesión de la terminal8export CLAVE_SEUDONIMO="pega-aqui-la-clave-generada"
En tu máquina, para probar. En producción la pone el orquestador o el gestor de secretos, nunca una persona a mano.
Tres errores que he visto en producción. Uno: leer la variable con un valor por defecto, como «clave123», por si no está; el día que alguien olvide configurarla, esa será la clave real y nadie se enterará. Si falta la clave, la carga se para. Dos: imprimir la clave en el log «para depurar»; los logs se copian a muchos sitios. Tres: dar la clave a quien analiza: con la clave y una lista de emails se deshacen los códigos.
Cambiar la clave cambia todos los códigos, y rompe todos los cruces con datos históricos igual que renombrar una columna en un contrato de datos. Se planifica, se avisa y se recalcula todo a la vez, como explica «Contratos de datos: definir qué esperas ANTES de que lleguen», de este mismo módulo. Y usa una clave distinta en cada entorno: si la copia de pruebas usa la de producción, un código visto en pruebas reconoce a la persona en producción.
## Normalizar antes del código
HMAC es exacto hasta el último byte. Un espacio al final o una mayúscula producen un código completamente distinto, no uno parecido: se llama efecto avalancha y es una virtud cuando proteges un mensaje. Cuando lo que quieres es reconocer a la misma persona en dos sistemas, es un problema. El formulario de la web guarda «ana@correo.es»; el CRM, que lo tecleó alguien en una tienda, guarda « Ana@Correo.es». Es la misma persona y, sin normalizar, son dos códigos.
La normalización mínima es quitar los espacios de los extremos y pasar a minúsculas. El estándar del correo permite en teoría distinguir mayúsculas antes de la arroba, pero ningún proveedor grande lo hace; la decisión se deja escrita. Lo que no conviene es inventar reglas de un proveedor, como quitar los puntos en Gmail: si te equivocas, juntas a dos personas en un mismo código, que es peor que separar a una.
Y la normalización es un contrato en sí misma: todos los procesos que calculan códigos usan exactamente la misma función, que vive en un único módulo compartido y lleva sus pruebas automáticas. Una de ellas comprueba que el mismo email escrito de dos formas da el mismo código.
## El derecho a borrar
En 2010, Mario Costeja se quejó ante la AEPD: al buscar su nombre en Google salía un anuncio de 1998 sobre la subasta de un inmueble suyo por unas deudas que ya había saldado. El Tribunal de Justicia de la Unión Europea le dio la razón el 13 de mayo de 2014. Se le llamó «derecho al olvido», y el RGPD lo recogió en su artículo 17 como derecho de supresión. Una persona puede pedir que borres sus datos cuando ya no son necesarios, cuando retira su consentimiento o cuando se trataron sin base legal.
El plazo es corto y está escrito: sin dilación y como mucho en un mes desde que llega la petición. Se puede ampliar dos meses más si es compleja o hay muchas, avisando a la persona dentro del primer mes y explicándole por qué. Planifica para un mes, y que ese mes lo consuma la gestión, no buscar dónde están los datos.
## Antes de borrar: el inventario
La persona no vive en una tabla. Vive en la base de origen, en la zona raw del lago, en el almacén, en el extracto que se mandó a marketing el martes, en el entorno de pruebas que alguien llenó con una copia de producción y en las copias de seguridad. Y vive en sitios sin columna de email: la nota de atención al cliente, el log que imprimió la fila entera. Por eso el primer paso no es un DELETE: es un inventario. Es la lista de cada sitio con datos personales, cómo se reconoce en él a la persona (email, código, dentro de un texto) y qué se hace. Se mantiene cada vez que nace una tabla, y el día de la petición se recorre, no se improvisa.
Consejo de senior: diseña para el borrado desde el primer día. Si cada tabla que toca a personas lleva la misma columna de código, el inventario es corto y borrar es un filtro repetido en un bucle. Si cada equipo identificó a la gente a su manera (email aquí, teléfono allá, un id interno en el tercer sitio), mal asunto. El día de la primera petición vas a pasar una semana buscando, y el mes corre.
## Borrar, anonimizar, nullificar o bloquear
«Suprimir» no es una sola operación. Son cuatro, y cada sitio del inventario se lleva la suya según para qué sirven sus datos sin la persona:
- Borrar: la fila desaparece. Para lo que no vale nada sin la persona: su ficha, la foto de su DNI, el extracto que se hizo para una campaña.
- Anonimizar: la fila se queda y pierde para siempre el vínculo con la persona. El código pasa a vacío y lo demasiado exacto se generaliza (la hora al día, el importe redondeado). Para lo que sigue sirviendo como estadística: las ventas del mes, las visitas de cada día.
- Nullificar: una columna de texto libre se vacía entera (NULL). No se resume ni se tachan nombres. «La clienta, profesora en el instituto del barrio, que vive con su madre» señala a alguien sin nombrarlo, y ningún filtro caza todas las maneras de contarlo.
- Bloquear: la fila se queda tal cual, pero apartada. Para lo que una ley obliga a conservar, como las facturas.
| Sitio (tienda de bicicletas) | Cómo aparece la persona | Qué se hace |
|---|---|---|
| clientes | email y código | Borrar |
| pedidos | código del cliente | Anonimizar: el código a vacío, y las ventas por mes siguen cuadrando |
| facturas | código, nombre y NIF | Bloquear hasta que venza el plazo legal |
| tickets de atención al cliente | dentro del texto | Nullificar la columna del texto |
| extracto de marketing del martes | Borrar, y que marketing borre su copia | |
| copias de seguridad | todo lo anterior | Dejar que caduquen; si se restaura una, repasar la lista |
Bloquear es la que más se confunde. Llega alguien con buena intención: «si vais a anonimizar a ese cliente, pasadme sus datos para el estudio». No se anonimiza nada. Una factura anonimizada ya no se puede atribuir a nadie y deja de servir para lo que la ley pide conservarla, así que anonimizarla es incumplir la obligación. Y bloqueado no es «apartado pero disponible para estadística»: solo se usa para esa obligación, ante Hacienda o un juez (en España lo regula el artículo 32 de la LOPDGDD). En el pipeline es una fecha en la fila (`bloqueado_desde`) y unas vistas del día a día que la excluyen. Solo un rol con base legal puede leerla, y una carga la borra o la anonimiza cuando vence el plazo.
Qué se conserva, por qué ley y hasta cuándo no lo decide el pipeline ni quien lo programa. Lo decide la persona responsable de protección de datos, y el pipeline lo aplica. Y quien pidió el borrado tiene derecho a saberlo: se le contesta por escrito qué se ha borrado, qué se conserva, por qué ley y hasta cuándo.
Así se monta en un pipeline. La petición entra en una lista de supresión que guarda el código, no el email. Guardar el email de quien pide que lo olvides sería crear otra tabla de datos personales. Cada noche, la carga recorre el inventario, aplica a cada sitio su acción y escribe un registro con el código, las fechas y cuántas filas trató. El email solo hace falta durante la carga, para buscarlo en los textos libres; por eso tantas ingestas no dejan pasar la columna de notas al almacén.
Las copias de seguridad no se abren para borrar una fila: se dejan caducar. Su política de conservación (por ejemplo, 35 días) dice qué día habrá desaparecido la persona también de ahí, y esa fecha va al registro. Mientras tanto no se usan, y si hay que restaurar una, lo primero es volver a pasar la lista de supresión, o restaurar sería resucitar a quien pidió irse. Los logs son una tabla más: conservación corta y escrita. Las versiones antiguas de las tablas con historial se purgan aparte; lo ves en «Time travel y versionado: volver al pasado de tus datos», del módulo «Data Lakes y formatos modernos».
## Comprobar que se hizo
Un borrado que nadie comprueba es una promesa. La comprobación es una tabla con una fila por sitio del inventario: lo esperado, lo obtenido al volver a buscar y OK o FALLO. Donde se borró, quedan tantas filas menos como se trataron; donde se anonimizó, nullificó o bloqueó, las mismas que antes, porque si faltan has borrado de más. Y en todas, cero filas que sigan señalando a la persona: ni su código, ni su email dentro de un texto, ni una fila suya sin la marca de bloqueo. Con un solo FALLO, la carga se marca como fallida y avisa a alguien.
Busca con los datos copiados de la petición y del registro, nunca escritos de memoria ni sacados del ticket. Un email mal tecleado devuelve cero filas, y cero filas parece un éxito: es la forma más fácil de certificar un borrado que no se hizo.
## Lo que te llevas
- 01.Un dato personal es cualquier información sobre alguien identificable, y las combinaciones identifican. Las categorías especiales (salud, biometría, ideología…) llevan protección reforzada, también cuando se deducen.
- 02.Minimizar es partir de la pregunta: columnas elegidas a mano, datos generalizados y un plazo para borrarlos.
- 03.Seudonimizar no es anonimizar: el código sigue siendo un dato personal.
- 04.Un hash simple con `hashlib` se adivina probando candidatos. Un HMAC-SHA256 con `hmac` y una clave guardada fuera del código no, mientras la clave no salga de donde está.
- 05.Normaliza antes de calcular el código, siempre con la misma función y con pruebas que lo demuestren.
- 06.Quien pide que lo borren desaparece de todas partes en menos de un mes. Inventario, una acción por sitio (borrar, anonimizar, nullificar o bloquear), copias que caducan y una comprobación que lo demuestra, sin volver a guardar su email.
Todo esto corre de noche, sin nadie mirando. Una carga que debía borrar a tres personas y borró a cero no da ningún error: simplemente no hace nada. Y la comprobación que escribe FALLO no sirve de mucho si nadie la lee. Por eso lo siguiente que necesita un pipeline serio no es otra regla, sino alguien que se entere cuando algo no ha pasado.
## Fuentes
## comentarios
Reporta erratas, ayuda a otros o comparte tu opinión. Sé constructivo.
Inicia sesión para comentar y responder.
cargando comentarios...
Regístrate para guardar tu progreso.