Prácticas recomendadas para la implementación segura de pg_columnmask - Amazon Aurora

Prácticas recomendadas para la implementación segura de pg_columnmask

En la siguiente sección, se proporcionan las prácticas recomendadas de seguridad para la implementación de pg_columnmask en el entorno de Aurora PostgreSQL. Siga estas recomendaciones para:

  • Establecimiento de una arquitectura de control de acceso basada en roles segura

  • Desarrollo de funciones de enmascaramiento que eviten las vulnerabilidades de seguridad

  • Comprensión y control del comportamiento de los activadores con datos enmascarados

Arquitectura de seguridad basada en roles

Defina una jerarquía de roles para implementar controles de acceso en la base de datos. Aurora PostgreSQL pg_columnmask amplía estos controles al proporcionar una capa adicional para el enmascaramiento de datos detallado dentro de esos roles.

Cree roles dedicados que se alineen con funciones organizativas en lugar de conceder permisos a usuarios individuales. Este enfoque proporciona una mejor auditoría y simplifica la administración de permisos a medida que la estructura organizativa evoluciona.

ejemplo de creación de una jerarquía de roles organizativos

En el siguiente ejemplo, se crea una jerarquía de roles organizativos con roles específicos para distintas funciones y, a continuación, se asignan usuarios individuales a los roles correspondientes. En este ejemplo, primero se crean los roles organizativos (analyst_role, support_role) y, a continuación, se concede a los usuarios individuales la pertenencia a estos roles. Esta estructura le permite administrar los permisos por rol y no para cada usuario individual.

-- Create organizational role hierarchy CREATE ROLE data_admin_role; CREATE ROLE security_admin_role; CREATE ROLE analyst_role; CREATE ROLE support_role; CREATE ROLE developer_role; -- Specify security_admin_role as masking policy manager in the DB cluster parameter -- group pgcolumnmask.policy_admin_rolname = 'security_admin_role' -- Create specific users and assign to appropriate roles CREATE USER security_manager; CREATE USER data_analyst1, data_analyst2; CREATE USER support_agent1, support_agent2; GRANT security_admin_role TO security_manager; GRANT analyst_role TO data_analyst1, data_analyst2; GRANT support_role TO support_agent1, support_agent2;

Implemente el principio de privilegio mínimo mediante la concesión de solo los permisos mínimos necesarios para cada rol. Evite conceder permisos amplios que puedan aprovecharse si las credenciales se ven comprometidas.

-- Grant specific table permissions rather than schema-wide access GRANT SELECT ON sensitive_data.customers TO analyst_role; GRANT SELECT ON sensitive_data.transactions TO analyst_role; -- Do not grant: GRANT ALL ON SCHEMA sensitive_data TO analyst_role;

Los administradores de políticas requieren privilegios de USAGE en los esquemas en los que administran las políticas de enmascaramiento. Conceda estos privilegios de forma selectiva, siguiendo el principio de privilegio mínimo. Realice revisiones periódicas de los permisos de acceso al esquema para asegurarse de que solo el personal autorizado mantenga las capacidades de administración de políticas.

La configuración de los parámetros del rol de administrador de políticas está restringida solo a los administradores de bases de datos. Este parámetro no se puede modificar en la base de datos o sesión, lo que impide que los usuarios sin privilegios invaliden las asignaciones de administradores de políticas. Esta restricción garantiza que el control de la política de enmascaramiento permanezca centralizado y seguro.

Asigne el rol de administrador de políticas a individuos específicos en lugar de a grupos. Este enfoque específico garantiza el acceso selectivo a la administración de políticas de enmascaramiento, ya que los administradores de políticas tienen la capacidad de enmascarar todas las tablas de la base de datos.

Desarrollo seguro de la función de enmascaramiento

Desarrolle funciones de enmascaramiento utilizando una semántica de enlace temprana para garantizar un seguimiento adecuado de las dependencias y evitar vulnerabilidades de enlace tardío, como la modificación de la ruta de búsqueda durante el tiempo de ejecución. Se recomienda utilizar la sintaxis BEGIN ATOMIC de las funciones de SQL para permitir la validación en tiempo de compilación (es decir, la vinculación temprana) y la administración de las dependencias.

-- Example - Secure masking function with early binding CREATE OR REPLACE FUNCTION secure_mask_ssn(input_ssn TEXT) RETURNS TEXT LANGUAGE SQL IMMUTABLE PARALLEL SAFE STRICT BEGIN ATOMIC SELECT CASE WHEN input_ssn IS NULL THEN NULL WHEN length(input_ssn) < 4 THEN repeat('X', length(input_ssn)) ELSE repeat('X', length(input_ssn) - 4) || right(input_ssn, 4) END; END;

Como alternativa, cree funciones que sean inmunes a los cambios en la ruta de búsqueda mediante un esquema explícito que califique todas las referencias a los objetos para garantizar un comportamiento coherente en las distintas sesiones de usuario.

-- Function immune to search path changes CREATE OR REPLACE FUNCTION data_masking.secure_phone_mask(phone_number TEXT) RETURNS TEXT LANGUAGE SQL IMMUTABLE PARALLEL SAFE STRICT AS $$ SELECT CASE WHEN phone_number IS NULL THEN NULL WHEN public.length(public.regexp_replace(phone_number, '[^0-9]', '', 'g')) < 10 THEN 'XXX-XXX-XXXX' ELSE public.regexp_replace( phone_number, '([0-9]{3})[0-9]{3}([0-9]{4})', public.concat('\1-XXX-\2') ) END; $$;

Implemente la validación de entradas en las funciones de enmascaramiento para gestionar los casos de periferia y evitar comportamientos inesperados. Incluya siempre el manejo NULL y valide los formatos de entrada para garantizar un comportamiento de enmascaramiento coherente.

-- Robust masking function with comprehensive input validation CREATE OR REPLACE FUNCTION secure_mask_phone(phone_number TEXT) RETURNS TEXT LANGUAGE SQL IMMUTABLE PARALLEL SAFE STRICT BEGIN ATOMIC SELECT CASE WHEN phone_number IS NULL THEN NULL WHEN length(trim(phone_number)) = 0 THEN phone_number WHEN length(regexp_replace(phone_number, '[^0-9]', '', 'g')) < 10 THEN 'XXX-XXX-XXXX' ELSE regexp_replace(phone_number, '([0-9]{3})[0-9]{3}([0-9]{4})', '\1-XXX-\2') END; END;

Comportamiento de activadores de DML con pg_columnmask

Para los activadores de tablas, las tablas de transición se desenmascararán por completo. Para los activadores de visualización (IOT), las tablas de transición se enmascararán de acuerdo con los permisos de visualización del usuario actual.

Activadores de tablas con pg_columnmask

A los activadores se les pasa una tabla de transición que contiene la versión antigua y la nueva de las filas modificadas por la consulta de DML de activación. Según el momento en que se active el activador, Aurora PostgreSQL rellena las filas antiguas y nuevas. Por ejemplo, un activador BEFORE INSERT solo tiene versiones nuevas de las filas y versiones antiguas vacías porque no hay ninguna versión anterior a la que hacer referencia.

pg_columnmask no enmascara las tablas de transición dentro de los activadores de las tablas. Los activadores pueden usar columnas enmascaradas dentro de su cuerpo y ve los datos desenmascarados. El creador del activador debe asegurarse de cómo se ejecuta el activador para un usuario. El siguiente ejemplo funciona correctamente en este caso.

-- Example for table trigger uses masked column in its definition -- Create a table and insert some rows CREATE TABLE public.credit_card_table ( name TEXT, credit_card_no VARCHAR(16), is_fraud BOOL ); INSERT INTO public.credit_card_table (name, credit_card_no, is_fraud) VALUES ('John Doe', '4532015112830366', false), ('Jane Smith', '5410000000000000', true), ('Brad Smith', '1234567891234567', true); -- Create a role which will see masked data and grant it privileges CREATE ROLE intern_user; GRANT SELECT, DELETE ON public.credit_card_table TO intern_user; -- Trigger which will silenty skip delete of non fraudelent credit cards CREATE OR REPLACE FUNCTION prevent_non_fraud_delete() RETURNS TRIGGER AS $$ BEGIN IF OLD.is_fraud = false THEN RETURN NULL; END IF; RETURN OLD; END; $$ LANGUAGE plpgsql; CREATE TRIGGER prevent_non_fraud_delete BEFORE DELETE ON credit_card_table FOR EACH ROW EXECUTE FUNCTION prevent_non_fraud_delete(); CREATE OR REPLACE FUNCTION public.return_false() RETURNS BOOLEAN LANGUAGE SQL IMMUTABLE PARALLEL SAFE STRICT BEGIN ATOMIC SELECT false; END; -- A masking policy that masks both credit card number and is_fraud column. -- If we apply masking inside trigger then prevent_non_fraud_delete trigger will -- allow deleting more rows to masked user (even non fraud ones). CALL pgcolumnmask.create_masking_policy( 'mask_credit_card_no_&_is_fraud'::NAME, 'public.credit_card_table'::REGCLASS, JSON_BUILD_OBJECT('credit_card_no', 'pgcolumnmask.mask_text(credit_card_no)', 'is_fraud', 'public.return_false()')::JSONB, ARRAY['intern_user']::NAME[], 10::INT ); -- Test trigger behaviour using intern_user BEGIN; SET ROLE intern_user; -- credit card number & is_fraud is completely masked from intern_user SELECT * FROM public.credit_card_table; name | credit_card_no | is_fraud ------------+------------------+---------- John Doe | XXXXXXXXXXXXXXXX | f Jane Smith | XXXXXXXXXXXXXXXX | f Brad Smith | XXXXXXXXXXXXXXXX | f (3 rows) -- The delete trigger lets the intern user delete rows for Jane and Brad even though -- intern_user sees their is_fraud = false, but the table trigger works with original -- unmasked value DELETE FROM public.credit_card_table RETURNING *; name | credit_card_no | is_fraud ------------+------------------+---------- Jane Smith | XXXXXXXXXXXXXXXX | f Brad Smith | XXXXXXXXXXXXXXXX | f (2 rows) COMMIT;

El creador del activador filtra datos desenmascarados al usuario si no tiene cuidado con las instrucciones que utiliza en el cuerpo del activador. Por ejemplo, el uso de RAISE NOTICE ‘%’, masked_column; imprime la columna al usuario actual.

-- Example showing table trigger leaking column value to current user CREATE OR REPLACE FUNCTION leaky_trigger_func() RETURNS TRIGGER AS $$ BEGIN RAISE NOTICE 'Old credit card number was: %', OLD.credit_card_no; RAISE NOTICE 'New credit card number is %', NEW.credit_card_no; RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TRIGGER leaky_trigger AFTER UPDATE ON public.credit_card_table FOR EACH ROW EXECUTE FUNCTION leaky_trigger_func(); -- Grant update on column is_fraud to auditor role -- auditor will NOT HAVE PERMISSION TO READ DATA CREATE ROLE auditor; GRANT UPDATE (is_fraud) ON public.credit_card_table TO auditor; -- Also add auditor role to existing masking policy on credit card table CALL pgcolumnmask.alter_masking_policy( 'mask_credit_card_no_&_is_fraud'::NAME, 'public.credit_card_table'::REGCLASS, NULL::JSONB, ARRAY['intern_user', 'auditor']::NAME[], NULL::INT ); -- Log in as auditor -- [auditor] -- Update will fail if trying to read data from the table UPDATE public.credit_card_table SET is_fraud = true WHERE credit_card_no = '4532015112830366'; ERROR: permission denied for table cc_table -- [auditor] -- But leaky update trigger will still print the entire row even though -- current user does not have permission to select from public.credit_card_table UPDATE public.credit_card_table SET is_fraud = true; NOTICE: Old credit_card_no was: 4532015112830366 NOTICE: New credit_card_no is 4532015112830366
Se activa en las vistas con pg_columnmask (en lugar de activadores)

Los activadores solo se pueden crear en las vistas de PostgreSQL. Se utilizan para ejecutar instrucciones DML en vistas que no se pueden actualizar. Las tablas de tránsito siempre están enmascaradas en el interior en lugar de activarlas (IOT), ya que la vista y las tablas base utilizadas dentro de la consulta de vista pueden tener propietarios diferentes. En ese caso, las tablas base pueden tener algunas políticas de enmascaramiento aplicables al propietario de la vista y el propietario de la vista siempre debe ver los datos enmascarados de las tablas base dentro de sus activadores. Esto es diferente de los activadores de las tablas porque, en ese caso, el creador del activador y los datos contenidos en las tablas son propiedad del mismo usuario, lo que no es el caso aquí.

-- Create a view over credit card table CREATE OR REPLACE VIEW public.credit_card_view AS SELECT * FROM public.credit_card_table; -- Truncate credit card table and insert fresh data TRUNCATE TABLE public.credit_card_table; INSERT INTO public.credit_card_table (name, credit_card_no, is_fraud) VALUES ('John Doe', '4532015112830366', false), ('Jane Smith', '5410000000000000', true), ('Brad Smith', '1234567891234567', true); CREATE OR REPLACE FUNCTION public.print_changes() RETURNS TRIGGER AS $$ BEGIN RAISE NOTICE 'Old row: name=%, credit card number=%, is fraud=%', OLD.name, OLD.credit_card_no, OLD.is_fraud; RAISE NOTICE 'New row: name=%, credit card number=%, is fraud=%', NEW.name, NEW.credit_card_no, NEW.is_fraud; RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TRIGGER print_changes_trigger INSTEAD OF UPDATE ON public.credit_card_view FOR EACH ROW EXECUTE FUNCTION public.print_changes(); GRANT SELECT, UPDATE ON public.credit_card_view TO auditor; -- [auditor] -- Login as auditor role BEGIN; -- Any data coming out from the table will be masked in instead of triggers -- according to masking policies applicable to current user UPDATE public.credit_card_view SET name = CONCAT(name, '_new_name') RETURNING *; NOTICE: Old row: name=John Doe, credit card number=XXXXXXXXXXXXXXXX, is fraud=f NOTICE: New row: name=John Doe_new_name, credit card number=XXXXXXXXXXXXXXXX, is fraud=f NOTICE: Old row: name=Jane Smith, credit card number=XXXXXXXXXXXXXXXX, is fraud=f NOTICE: New row: name=Jane Smith_new_name, credit card number=XXXXXXXXXXXXXXXX, is fraud=f NOTICE: Old row: name=Brad Smith, credit card number=XXXXXXXXXXXXXXXX, is fraud=f NOTICE: New row: name=Brad Smith_new_name, credit card number=XXXXXXXXXXXXXXXX, is fraud=f name | credit_card_no | is_fraud ---------------------+------------------+---------- John Doe_new_name | XXXXXXXXXXXXXXXX | f Jane Smith_new_name | XXXXXXXXXXXXXXXX | f Brad Smith_new_name | XXXXXXXXXXXXXXXX | f -- Any new data going into the table using INSERT or UPDATE command will be unmasked UPDATE public.credit_card_view SET credit_card_no = '9876987698769876' RETURNING *; NOTICE: Old row: name=John Doe, credit card number=XXXXXXXXXXXXXXXX, is fraud=f NOTICE: New row: name=John Doe, credit card number=9876987698769876, is fraud=f NOTICE: Old row: name=Jane Smith, credit card number=XXXXXXXXXXXXXXXX, is fraud=f NOTICE: New row: name=Jane Smith, credit card number=9876987698769876, is fraud=f NOTICE: Old row: name=Brad Smith, credit card number=XXXXXXXXXXXXXXXX, is fraud=f NOTICE: New row: name=Brad Smith, credit card number=9876987698769876, is fraud=f name | credit_card_no | is_fraud ------------+------------------+---------- John Doe | 9876987698769876 | f Jane Smith | 9876987698769876 | f Brad Smith | 9876987698769876 | f COMMIT;
Los GUC por base de datos o usuario para controlar el comportamiento de los activadores

Dos parámetros de configuración controlan el comportamiento de ejecución de los activadores para los usuarios con políticas de enmascaramiento aplicables. Utilice estos parámetros para evitar que los activadores se ejecuten en tablas o vistas enmascaradas cuando se requieran restricciones de seguridad adicionales. Ambos parámetros están desactivados de forma predeterminada, lo que permite que los activadores se ejecuten con normalidad.

Primer GUC: desencadene la restricción de activación en tablas enmascaradas

Especificaciones:

  • Nombre: pgcolumnmask.restrict_dml_triggers_for_masked_users

  • Tipo:: boolean

  • Predeterminado: false (se permite la ejecución de los activadores)

Impide la ejecución del activador en tablas enmascaradas para los usuarios enmascarados cuando se establece en TRUE. pg_columnmask corrige el error.

Segundo GUC: desencadene la restricción de activación en vistas con tablas enmascaradas

Especificaciones:

  • Nombre: pgcolumnmask.restrict_iot_triggers_for_masked_users

  • Tipo:: boolean

  • Predeterminado: false (se permite la ejecución de los activadores)

Impide la ejecución del activador en vistas que incluyen tablas enmascaradas en su definición para los usuarios enmascarados cuando se establece en TRUE.

Estos parámetros funcionan de forma independiente y se pueden configurar como los parámetros de configuración de una base de datos estándar.