Hace un tiempo os contaba de qué iba esto de Dynamic Data Masking y cómo trabajar con esta característica en un motor SQL Server. Podéis hacer memoria revisando este artículo. Pues bien, en este tutorial os voy a contar cómo utilizar esta técnica de enmascaramiento de información en un motor PostgreSQL.
Para ello recurro a la extensión Anonymizer. Os dejo en este repositorio un documento explicando cómo instalar esta extensión en PostgreSQL en versión 17, instalada en una máquina Ubuntu Server 24.04 LTS.
¡Vamos al lío!
¿ Qué es Anonymizer y qué nos permite hacer ?
La extensión PostgreSQL Anonymizer es un módulo para PostgreSQL que permite enmascarar o reemplazar información identificable personalmente (PII – Personally Identifiable Information) o datos sensibles en una base de datos.
Esta extensión introduce funcionalidades para poder definir políticas de anonimización o enmascaramiento y aplicarlas de forma declarativa. Esto la hace útil en contextos de ingeniería de datos, gobernanza del dato, cumplimiento de normativas (por ejemplo GDPR) o cuando se desean entornos de pruebas/clones de producción libres de datos personales.
La forma de generar estas reglas de enmascaramiento se hace de forma declarativa a través de una ampliación de DDL (Data Definition Language) en Postgres.
Nos ofrece varios métodos de enmascaramiento:
- Anonymous Dumps: exportaciones de datos (por ejemplo con pg_dump) que ya incorporan las reglas de enmascaramiento para que el dump sea seguro, sin exponer datos sensibles.
- Static Masking: se aplica y reemplazan los datos originales (irreversible) según las reglas definidas. Ideal para entornos de pruebas o cuando se va a compartir o archivar datos.
- Dynamic Masking: los datos originales se mantienen, pero para ciertos roles/usuarios que están etiquetados como “enmascarados”, la vista que se les presenta está enmascarada automáticamente, sin necesidad de duplicar tablas.
- Masking Views: crear vistas dedicadas que presentan los datos enmascarados para determinados usuarios o roles.
- Masking Data Wrappers: aplicar las reglas de enmascaramiento a datos externos o vía wrappers de datos.
Esta extensión también nos ofrece una serie de funciones de enmascaramiento diferentes, algunos de los tipos de funciones que se pueden utilizar:
- Sustitución/falseficación (fake data): por ejemplo generación de nombres, correos, etc.
- Aleatorización (randomization): para valores numéricos o de otro tipo.
- Pseudoanonimización: valores consistentes que permiten, por ejemplo, mantener la integridad de relaciones pero sin exponer el dato real.
- Raspado parcial (partial scrambling): por ejemplo conservar dos caracteres, sustituir el resto por asteriscos u otro placeholder.
- Barajado (shuffling): mezclar los valores dentro de un conjunto.
- Adición de ruido (noise): para datos numéricos mantener distribución pero alterar valores.
- Generalización: agrupar valores en rangos o categorías más amplias (por ejemplo edades en rangos de 10 años) como técnica para anonimización.
- Reemplazo por valor fijo o nulos (‘NULL’ing) si se desea eliminar completamente la información sensible.
En las referencias os he dejado un enlace en donde podéis ver todos los grupos de funciones y el detalle de las funciones incluidas en cada grupo.
Las reglas que se crean se integran directamente con los roles de PostgreSQL, para el caso de Dynamic Data Masking se hace mediante una sentencia del estilo:
SECURITY LABEL FOR ANON ON ROLE <role_name> IS 'MASKED'. Haciendo que las reglas estén definidas en el esquema mediante reglas de seguridad, se aprovecha de la infraestructura de PostgreSQL para permisos, visibilidad y roles.
Escenario propuesto
Para mostrar las capacidades de esta extensión me he planteado un escenario en donde se parte de una tabla con información de trabajadores de una empresa. Existirán tres perfiles de personal de Recursos Humanos con distintos permisos de acceso a la información:
- Un perfil rrhh_admin que podrá ver toda la información.
- Un perfil rrhh_consulta que podrá ver solo algunos campos sin enmascarar.
- Un perfil rrhh_financiero que tampoco podrá ver todos los datos desenmascarados como el perfil anterior, con la salvedad de que sí podrán ver la información del salario de los trabajadores.
A medida que avance en la explicación os daré los detalles de cada regla y qué se enmascara y qué no para cada perfil. La configuración que se puede ver en ejemplos simplifica el enmascaramiento marcando algo muy binario: el usuario ve o no ve los datos enmascarados; pero he querido complicarlo un poco creando distintos perfiles incluyendo esta diferencia entre el perfil rrhh_consulta y el perfil rrhh_financiero. Tendremos algo más de juego con esta casuística.
¡ Vamos al lío !
Todos los pasos que voy a mostrar están recogidos en este script SQL. Todos los pasos que se muestran se harán con el usuario postgres, salvo que se indique lo contrario. Os explico el detalle. Lo primero que necesitamos, una vez instalada la extensión, es crear la base de datos, conectarnos a ella e instalar la extensión en la base de datos:
CREATE DATABASE rrhh;
\c rrhh
CREATE EXTENSION anon CASCADE;Esto instala la extensión pero no podemos trabajar con ella directamente, tenemos que inicializarla para que se genere un esquema dentro de la base de datos, el esquema anon, en donde se generan una serie de componentes que utiliza la extensión para poder trabajar con todas las características de las que he hablado en el punto anterior. La forma de inicializarla es con la sentencia:
SELECT anon.init();Lo siguiente es modificar la base de datos con un par de sentencias:
ALTER DATABASE rrhh SET session_preload_libraries = 'anon';
ALTER DATABASE rrhh SET anon.transparent_dynamic_masking = 'on';La primera sentencia obliga a que se precarguen las librerías de la extensión de anonimización al crear una sesión de usuario. Es lo que permite que se le apliquen las reglas definidas en el momento que un usuario se conecte a la base de datos. La segunda instrucción habilita la capacidad de Dynamic Data Masking en la base de datos.
La siguiente instancia que se lanza, es la que nos crea diferentes perfiles para las reglas de anonimización. Solo es necesario lanzarla en caso de que se trabajen con distintos perfiles, si vamos a trabajar solamente con la opción de que haya usuarios que visualicen toda la información, o que la vean enmascarada, y todos vean la misma información enmascarada, no sería necesario, se usaría el perfil anon que es el perfil por defecto. Para crear los perfiles lanzamos:
ALTER DATABASE rrhh SET anon.masking_policies TO 'rrhh, financiero';Tras esto, debemos desconectarnos y volver a conectarnos debido a que se han hecho cambios que afectan a la sesión y no se aplicará hasta el momento de que nos reconectemos. Nos daría error los comandos que lancemos para crear las reglas, por ejemplo, porque en nuestra sesión todavía no estarán cargadas las librerías de la extensión.
Tras reconectarnos, creamos la tabla empleados y cargamos los datos en ella:
CREATE TABLE empleados (
id SERIAL PRIMARY KEY,
nombre VARCHAR(100),
apellidos VARCHAR(150),
dni VARCHAR(15),
email VARCHAR(150),
telefono VARCHAR(20),
direccion VARCHAR(200),
provincia VARCHAR(100),
salario NUMERIC(10,2)
);
COPY empleados(id,nombre,apellidos,dni,email,telefono,direccion,provincia,salario)
FROM '/DATASETS/empleados.csv' DELIMITER ',' CSV HEADER;Crearemos también los tres roles asociados a los tres perfiles de usuarios que os he comentado en el detalle del escenario que os propongo, y les damos permisos para poder realizar consultas sobre la tabla creada:
CREATE ROLE rrhh_admin LOGIN PASSWORD 'AdminPass123';
CREATE ROLE rrhh_consulta LOGIN PASSWORD 'UserPass123';
CREATE ROLE rrhh_financiero LOGIN PASSWORD 'FinPass123';
GRANT CONNECT ON DATABASE rrhh TO rrhh_consulta, rrhh_admin, rrhh_financiero;
GRANT USAGE ON SCHEMA public TO rrhh_consulta, rrhh_admin, rrhh_financiero;
GRANT SELECT ON empleados TO rrhh_consulta, rrhh_admin, rrhh_financiero;Y ahora empezamos con lo bueno, con lo divertido. Vamos a crear las reglas para el perfil de rrhh_consulta. Para ello lanzamos lo siguiente:
SECURITY LABEL FOR rrhh ON COLUMN empleados.dni
IS 'MASKED WITH FUNCTION anon.partial(dni, 2, ''******'', 0)';
SECURITY LABEL FOR rrhh ON COLUMN empleados.email
IS 'MASKED WITH FUNCTION anon.fake_email()';
SECURITY LABEL FOR rrhh ON COLUMN empleados.salario
IS 'MASKED WITH VALUE 0';
SECURITY LABEL FOR rrhh ON COLUMN empleados.telefono
IS 'MASKED WITH FUNCTION anon.partial(telefono, 2, ''*******'', 2)';Lo que se hace en cada una de ellas es crear una etiqueta asociada al perfil rrhh, en donde se define la función de enmascaramiento para distintas columnas. El detalle:
- En la primera instrucción se hace un enmascaramiento parcial sobre la columna dni, en donde se mostrarán los dos primeros dígitos y ningún dígito del final y el resto se mostrarán asteriscos.
- En la segunda instrucción se mostrarán mails fake de los trabajadores, no por nada en particular, podría bastar con mostrar asteriscos o un Null, pero quería probar una función de generación de datos fake, y le tocó al campo email.
- La tercera instrucción mostrará el valor 0 para el salario de todos los trabajadores, los hemos convertido en voluntarios, no les pagamos.
- La última instrucción de nuevo hace un enmascaramiento parcial del teléfono, pero esta vez muestra los dos primeros dígitos y los dos últimos.
Lo siguiente es crear las reglas para el perfil rrhh_financiero, serán las mismas, con la excepción de que no se enmascarará la columna del salario, y se asociarán las reglas al perfil de financiero, en lugar de unirlo al perfil rrhh. Las instrucciones a ejecutar:
SECURITY LABEL FOR financiero ON COLUMN empleados.dni
IS 'MASKED WITH FUNCTION anon.partial(dni, 2, ''******'', 0)';
SECURITY LABEL FOR financiero ON COLUMN empleados.email
IS 'MASKED WITH FUNCTION anon.fake_email()';
SECURITY LABEL FOR financiero ON COLUMN empleados.telefono
IS 'MASKED WITH FUNCTION anon.partial(telefono, 2, ''*******'', 2)';Ahora toca aplicar las reglas a los roles de usuarios. Para ello se crean etiquetas que vinculan el perfil de anonimización al role del usuario al que se le aplicarán al entrar a consultar la base de datos:
SECURITY LABEL FOR rrhh ON ROLE rrhh_consulta IS 'MASKED';
SECURITY LABEL FOR financiero ON ROLE rrhh_financiero IS 'MASKED';Se puede ver que se aplica una etiqueta con el nombre del perfil de reglas a cada uno de los usuarios. Pero no llega solo con esto, tenemos que habilitar todo esto para cada usuario. Toca lanzar las siguientes instrucciones:
ALTER ROLE rrhh_admin SET anon.masking = off;
ALTER ROLE rrhh_consulta SET anon.masking = on;
ALTER ROLE rrhh_financiero SET anon.masking = on;En primer lugar reforzamos que no se le apliquen reglas de enmascaramiento al usuario rrhh_admin, la teoría dice que no es necesario hacer esto, pero nunca está de más. Para los otros usuarios, se lanza el ALTER para habilitar el enmascaramiento.
Y aún tenemos que lanzar alguna cosa más, tenemos la extensión instalada y configurada en la base de datos, las reglas para cada perfil, el mapeo de reglas a los roles de usuario, y habilitado el servicio de data masking en los usuarios. Nos queda arrancar todo esto, para eso se lanza la sentencia siguiente:
SELECT anon.start_dynamic_masking();Esta sentencia arrancará el servicio aplicando todas las reglas definidas. Bueno, lo haría si usáramos la configuración estandar usando el perfil por defecto, el perfil anon. Si usamos el perfil por defecto, este perfil tiene acceso a todas las funciones de la extensión, pero hemos creado perfiles y con eso se pierde el acceso a estas funciones. Me tocó pelarme e investigar bastante esto, y al final encontré la manera de poder utilizar las funciones de partial y de fake_email. Se tiene que marcar esas funciones como TRUSTED. Se debe lanzar lo siguiente:
ALTER FUNCTION anon.fake_email() SECURITY DEFINER SET search_path = anon, public;
ALTER FUNCTION anon.partial(text, integer, text, integer) SECURITY DEFINER SET search_path = anon, public;Ahora ya podemos conectarnos con cada uno de los usuarios y validar que se ven los datos enmascarados. Por ejemplo, lo que obtendremos al entrar con el usuario rrhh_consulta y hacer una consulta sobre la tabla empleados:
id|nombre |dni |email |telefono |salario|
--+---------+--------+---------------------------+-----------+-------+
1|Aristides|25******|jonathaneverett@example.org|+3*******19| 0.00|
2|Clímaco |39******|brittany69@example.com |+3*******29| 0.00|
3|Encarna |42******|hriley@example.com |+3*******44| 0.00|
4|Augusto |46******|colleen65@example.com |+3*******42| 0.00|
5|Pelayo |58******|joshuahodges@example.com |+3*******81| 0.00|
6|Vidal |88******|glennjason@example.com |+3*******12| 0.00|
7|Coral |24******|susansutton@example.net |+3*******76| 0.00|
8|Maximino |92******|thompsonjessica@example.com|+3*******13| 0.00|
9|Jonatan |19******|aharris@example.org |+3*******17| 0.00| Si queréis revisar las reglas creadas, podéis lanzar las siguientes consultas:
SELECT * FROM pg_seclabel WHERE provider = 'rrhh';
SELECT * FROM pg_seclabel WHERE provider = 'financiero'; Veremos algo como esto:
objoid|classoid|objsubid|provider|label |
------+--------+--------+--------+------------------------------------------------------------+
28948| 1259| 4|rrhh |MASKED WITH FUNCTION anon.partial(dni, 2, '******', 0) |
28948| 1259| 5|rrhh |MASKED WITH FUNCTION anon.fake_email() |
28948| 1259| 9|rrhh |MASKED WITH VALUE 0 |
28948| 1259| 6|rrhh |MASKED WITH FUNCTION anon.partial(telefono, 2, '*******', 2)| Ahora os toca a vosotros jugar con esta extensión. Espero os haya resultado útil este how-To.
Referencias
- Extensión anonimyzer: https://postgresql-anonymizer.readthedocs.io/en/stable/
- Tutorial de instalación de la extensión: https://github.com/tblproject/tbl_postgres/blob/main/anonymizer/install.md
- Script SQL con los pasos mostrados: https://github.com/tblproject/tbl_postgres/blob/main/anonymizer/lab.sql
- Funciones de enmascaramiento: https://postgresql-anonymizer.readthedocs.io/en/stable/masking_functions/
- Código para generar datos sintéticos del lab: https://github.com/tblproject/tbl_postgres/blob/main/tools/genera_datos_csv_anonymizer.py
