Práticas recomendadas para a implementação segura de pg_columnmask
A seção a seguir fornece as práticas recomendadas de segurança para implementação de pg_columnmask em seu ambiente Aurora PostgreSQL. Siga estas recomendações para:
Estabelecer uma arquitetura de controle de acesso com base em perfil seguro
Desenvolver funções de mascaramento que evitem vulnerabilidades de segurança
Entender e controlar o comportamento do gatilho com dados mascarados
Arquitetura de segurança baseada em perfis
Defina uma hierarquia de perfis para implementar controles de acesso em seu banco de dados. O Aurora PostgreSQL pg_columnmask aumenta esses controles fornecendo uma camada adicional para mascaramento de dados refinado dentro desses perfis.
Criar perfis dedicados que se alinhem às funções organizacionais em vez de conceder permissões a usuários individuais. Essa abordagem fornece melhor capacidade de auditoria e simplifica o gerenciamento de permissões à medida que sua estrutura organizacional evolui.
exemplo de criar uma hierarquia de perfis organizacionais
O exemplo a seguir cria uma hierarquia de perfis organizacionais com perfis dedicados para diferentes funções e, depois, atribui usuários individuais aos perfis apropriados. Neste exemplo, os perfis organizacionais (analyst_role, support_role) são criados primeiro e, depois, os usuários individuais recebem a associação a esses perfis. Essa estrutura permite que você gerencie as permissões em nível de perfil, e não de cada usuário 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 o princípio de privilégio mínimo concedendo apenas o mínimo de permissões necessárias para cada perfil. Evite conceder permissões amplas que poderiam ser exploradas se as credenciais fossem 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;
Os administradores de políticas exigem privilégios USAGE nos esquemas em que gerenciam políticas de mascaramento. Conceda esses privilégios de modo seletivo, seguindo o princípio do privilégio mínimo. Realize análises regulares das permissões de acesso ao esquema para garantir que somente o pessoal autorizado mantenha os recursos de gerenciamento de políticas.
A configuração dos parâmetros do perfil de administrador da política é restrita somente aos administradores do banco de dados. Esse parâmetro não pode ser modificado em nível de banco de dados ou de sessão, impedindo que usuários sem privilégios substituam as atribuições do administrador da política. Essa restrição garante que o controle da política de mascaramento permaneça centralizado e seguro.
Atribua o perfil de administrador de políticas a indivíduos específicos em vez de grupos. Essa abordagem direcionada garante acesso seletivo ao gerenciamento de políticas de mascaramento, pois os administradores de políticas têm a capacidade de mascarar todas as tabelas no banco de dados.
Desenvolvimento seguro da função de mascaramento
Desenvolva funções de mascaramento usando semântica de vinculação antecipada para garantir o monitoramento adequado de dependências e evitar vulnerabilidades de vinculação tardia, como modificação do caminho de pesquisa durante o runtime. É recomendável usar a sintaxe BEGIN ATOMIC para funções SQL a fim de permitir a validação em tempo de compilação (ou seja, vinculação antecipada) e o gerenciamento de dependências.
-- 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, crie funções imunes às alterações do caminho de pesquisa qualificando explicitamente o esquema de todas as referências de objetos, garantindo um comportamento consistente em diferentes sessões de usuário.
-- 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 a validação de entrada nas funções de mascaramento para lidar com casos de borda e evitar comportamentos inesperados. Sempre inclua o tratamento de NULL e valide os formatos de entrada para garantir um comportamento consistente de mascaramento.
-- 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;
Comportamento dos gatilhos do DML com pg_columnmask
Para gatilhos de tabela, as tabelas de transição serão totalmente sem máscara. Para gatilhos de visualização (IOT), as tabelas de transição serão mascaradas de acordo com as permissões de visualização do usuário atual.
- Gatilhos de tabela com pg_columnmask
Os gatilhos recebem uma tabela de transição que contém a versão antiga e a nova das linhas modificadas pela consulta do DML de acionamento. Dependendo de quando o gatilho é acionado, o Aurora PostgreSQL preenche as linhas antiga e nova. Por exemplo, um gatilho
BEFORE INSERTsó tem novas versões das linhas e versões antigas vazias porque não há uma versão antiga para consultar.O
pg_columnmasknão mascara as tabelas de transição dentro dos gatilhos das tabelas. Os gatilhos podem usar colunas mascaradas dentro de seu corpo e ver dados não mascarados. O criador do gatilho deve se certificar de como ele é executado por um usuário. O exemplo a seguir funciona corretamente nesse 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;O criador do gatilho vazará dados não mascarados para o usuário se ele não tiver cuidado com as declarações que usa no corpo do gatilho. Por exemplo, usar
RAISE NOTICE ‘%’, masked_column;imprime a coluna para o usuário atual.-- 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- Gatilhos em visualizações com pg_columnmask (em vez de gatilhos)
Os gatilhos só podem ser criados em visualizações no PostgreSQL. Eles são usados para executar instruções do DML em visualizações que não são atualizáveis. As tabelas de trânsito são sempre mascaradas internamente em vez de no gatilho (IOT), porque a visualização e as tabelas base usadas na consulta de visualização podem ter proprietários diferentes. Nesse caso, as tabelas base podem ter algumas políticas de mascaramento aplicáveis ao proprietário da visualização e este deve sempre ver os dados mascarados das tabelas base dentro dos respectivos gatilhos. Isso é diferente dos gatilhos nas tabelas porque, nesse caso, o criador do gatilho e os dados dentro das tabelas pertencem ao mesmo usuário, o que não é o caso aqui.
-- 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;- GuCs em nível de banco de dados/usuário para controlar o comportamento dos gatilhos
Dois parâmetros de configuração controlam o comportamento de execução do gatilho para usuários com políticas de mascaramento aplicáveis. Use esses parâmetros para evitar que os gatilhos sejam executados em tabelas ou visualizações mascaradas quando restrições de segurança adicionais forem necessárias. Ambos os parâmetros são desabilitados por padrão, permitindo que os gatilhos sejam executados normalmente.
Primeiro GUC: restrição de disparo de gatilho em tabelas mascaradas.
Especificações:
Nome: :
pgcolumnmask.restrict_dml_triggers_for_masked_usersTipo::
booleanPadrão:
false(os gatilhos podem ser executados).
Impede a execução do gatilho em tabelas mascaradas para usuários mascarados quando definido como TRUE.
pg_columnmaské executado ocasionando um erro.Segundo GUC: restrição de acionamento do gatilho em visualizações com tabelas mascaradas.
Especificações:
Nome: :
pgcolumnmask.restrict_iot_triggers_for_masked_usersTipo::
booleanPadrão:
false(os gatilhos podem ser executados).
Impede a execução de gatilhos em visualizações que incluem tabelas mascaradas em sua definição para usuários mascarados quando definidas como TRUE.
Esses parâmetros funcionam de forma independente e são configuráveis, como os parâmetros padrão de configuração do banco de dados.