Le migliori pratiche per l'implementazione sicura di pg_columnmask - Amazon Aurora

Le traduzioni sono generate tramite traduzione automatica. In caso di conflitto tra il contenuto di una traduzione e la versione originale in Inglese, quest'ultima prevarrà.

Le migliori pratiche per l'implementazione sicura di pg_columnmask

La sezione seguente fornisce le best practice di sicurezza per l'implementazione pg_columnmask nell'ambiente Aurora PostgreSQL. Segui questi consigli per:

  • Stabilisci un'architettura di controllo degli accessi sicura basata sui ruoli

  • Sviluppa funzioni di mascheramento che impediscano le vulnerabilità di sicurezza

  • Comprendi e controlla il comportamento dei trigger con dati mascherati

Architettura di sicurezza basata sui ruoli

Definisci una gerarchia di ruoli per implementare i controlli di accesso nel tuo database. Aurora PostgreSQL pg_columnmask aumenta questi controlli fornendo un livello aggiuntivo per il mascheramento granulare dei dati all'interno di tali ruoli.

Crea ruoli dedicati in linea con le funzioni organizzative anziché concedere autorizzazioni ai singoli utenti. Questo approccio offre una migliore verificabilità e semplifica la gestione delle autorizzazioni man mano che la struttura organizzativa si evolve.

Esempio di creare una gerarchia dei ruoli organizzativi

L'esempio seguente crea una gerarchia di ruoli organizzativi con ruoli dedicati per diverse funzioni, quindi assegna i singoli utenti ai ruoli appropriati. In questo esempio, i ruoli organizzativi (analyst_role, support_role) vengono creati per primi, quindi ai singoli utenti viene concessa l'appartenenza a questi ruoli. Questa struttura consente di gestire le autorizzazioni a livello di ruolo anziché per ogni singolo utente.

-- 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;

Implementa il principio del privilegio minimo concedendo solo le autorizzazioni minime necessarie per ogni ruolo. Evita di concedere autorizzazioni ampie che potrebbero essere sfruttate se le credenziali vengono compromesse.

-- 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;

Gli amministratori delle politiche richiedono USAGE privilegi sugli schemi in cui gestiscono le politiche di mascheramento. Concedi questi privilegi in modo selettivo, seguendo il principio del privilegio minimo. Effettua revisioni periodiche delle autorizzazioni di accesso allo schema per garantire che solo il personale autorizzato mantenga le capacità di gestione delle politiche.

La configurazione dei parametri del ruolo di amministratore delle politiche è riservata ai soli amministratori del database. Questo parametro non può essere modificato a livello di database o di sessione, per evitare che gli utenti senza privilegi abbiano la precedenza sulle assegnazioni degli amministratori delle policy. Questa restrizione garantisce che il controllo delle policy di mascheramento rimanga centralizzato e sicuro.

Assegna il ruolo di amministratore delle politiche a individui specifici anziché a gruppi. Questo approccio mirato garantisce un accesso selettivo alla gestione delle policy di mascheramento, in quanto gli amministratori delle policy hanno la possibilità di mascherare tutte le tabelle all'interno del database.

Sviluppo sicuro della funzione di mascheramento

Sviluppa funzioni di mascheramento utilizzando la semantica di associazione precoce per garantire il corretto tracciamento delle dipendenze e prevenire vulnerabilità di associazione tardiva, come la modifica del percorso di ricerca durante il runtime. Si consiglia di utilizzare la BEGIN ATOMIC sintassi per le funzioni SQL per abilitare la convalida in fase di compilazione (ovvero l'associazione anticipata) e la gestione delle dipendenze.

-- 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;

In alternativa, è possibile creare funzioni immuni alle modifiche del percorso di ricerca qualificando in modo esplicito lo schema di tutti i riferimenti agli oggetti, garantendo un comportamento coerente tra le diverse sessioni utente.

-- 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; $$;

Implementa la convalida degli input all'interno delle funzioni di mascheramento per gestire i casi limite e prevenire comportamenti imprevisti. Includi sempre la gestione dei NULL e convalida i formati di input per garantire un comportamento di mascheramento coerente.

-- 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;

DML attiva il comportamento con pg_columnmask

Per i trigger delle tabelle, le tabelle di transizione verranno completamente smascherate. Per i trigger di visualizzazione (IOT), le tabelle di transizione verranno mascherate in base alle autorizzazioni di visualizzazione dell'utente corrente.

Trigger di tabella con pg_columnmask

Ai trigger viene passata una tabella di transizione che contiene la vecchia e la nuova versione delle righe modificate dalla query DML di avvio. A seconda di quando viene attivato il trigger, Aurora PostgreSQL popola le righe vecchie e nuove. Ad esempio, un BEFORE INSERT trigger contiene solo nuove versioni delle righe e le vecchie versioni vuote perché non esiste una vecchia versione a cui fare riferimento.

pg_columnmasknon maschera le tabelle di transizione all'interno dei trigger delle tabelle. I trigger possono utilizzare colonne mascherate all'interno del loro corpo e vedono i dati non mascherati. Il creatore del trigger deve assicurarsi che il trigger venga eseguito per un utente. L'esempio seguente funziona correttamente in questo 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;

Il creatore di Trigger comunica i dati non mascherati all'utente se non presta attenzione alle istruzioni che utilizza nel corpo del trigger. Ad esempio, l'utilizzo di a RAISE NOTICE ‘%’, masked_column; stampa la colonna per l'utente corrente.

-- 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
Trigger sulle viste con pg_columnmask (anziché trigger)

I trigger possono essere creati solo nelle viste in PostgreSQL. Vengono utilizzati per eseguire istruzioni DML su viste che non sono aggiornabili. Le tabelle di transito sono sempre mascherate all'interno anziché al trigger (IOT), poiché la vista e le tabelle di base utilizzate all'interno della query di visualizzazione potrebbero avere proprietari diversi. In tal caso, le tabelle di base potrebbero avere alcune politiche di mascheramento applicabili al proprietario della vista e il proprietario della vista deve sempre visualizzare i dati mascherati delle tabelle di base all'interno dei suoi trigger. Questo è diverso dai trigger sulle tabelle perché in tal caso il creatore del trigger e i dati all'interno delle tabelle sono di proprietà dello stesso utente, cosa che non avviene in questo caso.

-- 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;
Livello GuCs di database/utente per controllare il comportamento dei trigger

Due parametri di configurazione controllano il comportamento di esecuzione dei trigger per gli utenti con politiche di mascheramento applicabili. Utilizzate questi parametri per impedire l'esecuzione dei trigger su tabelle o viste mascherate quando sono necessarie restrizioni di sicurezza aggiuntive. Entrambi i parametri sono disabilitati per impostazione predefinita, per consentire ai trigger di essere eseguiti normalmente.

Primo GUC: restrizione di attivazione dei trigger sulle tabelle mascherate

Specifiche:

  • Valore: pgcolumnmask.restrict_dml_triggers_for_masked_users

  • Tipo: boolean

  • Impostazione predefinita: false (i trigger possono essere eseguiti)

Impedisce l'esecuzione dei trigger sulle tabelle mascherate per gli utenti mascherati se impostata su TRUE. pg_columnmaskesegue l'errore.

Secondo GUC: attiva la restrizione di attivazione delle viste con tabelle mascherate

Specifiche:

  • Valore: pgcolumnmask.restrict_iot_triggers_for_masked_users

  • Tipo: boolean

  • Impostazione predefinita: false (i trigger possono essere eseguiti)

Impedisce l'esecuzione dei trigger nelle viste che includono tabelle mascherate nella loro definizione per gli utenti mascherati se impostata su TRUE.

Questi parametri funzionano in modo indipendente e sono configurabili come i parametri di configurazione standard del database.