Cifrare i dati: Always Encrypted vs column-level encryption

Always Encrypted protegge i dati at rest, in transit e in use: la cifratura avviene lato client tramite il driver, quindi il motore SQL non vede mai il plaintext. Serve una column master key (CMK), custodita in un key store esterno come Azure Key Vault o il Windows Certificate Store, e una column encryption key (CEK), salvata cifrata nel database. Lo scenario tipico è la separation of duties: impedire a chi amministra il database (DBA, admin cloud) di leggere colonne sensibili come codici fiscali o numeri di carta.

Due modalità: la deterministic encryption produce sempre lo stesso ciphertext per lo stesso valore, quindi consente equality lookup, join e group by, ma è vulnerabile all’inferenza su colonne a bassa cardinalità; la randomized encryption è più sicura ma non ammette alcuna operazione lato server. Con i secure enclaves si abilitano range e pattern matching direttamente sui dati cifrati.

La column-level encryption è tutt’altra cosa: usa funzioni T-SQL con chiavi simmetriche, certificati o chiavi asimmetriche gestite nel database e cifra/decifra lato server. Il motore vede il plaintext durante l’elaborazione, quindi protegge i dati a riposo ma non dagli amministratori. Da non confondere con TDE, che cifra l’intero file a livello di storage in modo trasparente e non isola singole colonne.

Dynamic data masking e row-level security

Il dynamic data masking (DDM) offusca i valori nel result set restituito a utenti non privilegiati: i dati restano in chiaro nello storage, è solo un controllo di presentazione, non un confine di sicurezza. Le funzioni di masking (default, email, random sui numerici, partial sulle stringhe) nascondono, ma chi ha accesso alle query può dedurre i valori con filtri mirati (inference attack). Il permesso UNMASK rimuove il mascheramento. Usalo per ridurre l’esposizione, mai come sostituto della cifratura o del controllo accessi.

La row-level security (RLS) limita quali righe un utente può vedere o modificare, in base al principal che esegue la query. Si definisce una security policy con una predicate function (inline table-valued function). I filter predicate filtrano silenziosamente le righe in lettura, update e delete; i block predicate impediscono scritture che violerebbero la regola. È lo strumento naturale per il multi-tenant o per isolare i dati per reparto, spesso combinato con SESSION_CONTEXT o USER_NAME per identificare il chiamante.

Object permissions e least privilege

Il least privilege significa concedere solo i permessi minimi necessari. Sugli oggetti (tabelle, viste, stored procedure, funzioni, schema) si agisce con GRANT, DENY e REVOKE: ricorda che DENY prevale sempre su GRANT. Preferisci ruoli personalizzati ai ruoli fissi molto ampi come db_datareader o db_owner, che violano il principio concedendo più del necessario.

Uno strumento chiave è l’ownership chaining: se una stored procedure e la tabella hanno lo stesso owner, basta concedere EXECUTE sulla procedura senza dare accesso diretto alla tabella. Così esponi solo l’operazione voluta, mantenendo minima la superficie di attacco. In Azure SQL integra il tutto con l’autenticazione Microsoft Entra e principal di database contained.

Trappole tipiche d’esame

  • Nascondere i dati anche al DBA → Always Encrypted: la cifratura lato client fa sì che il motore non veda mai il plaintext; DDM e column-level encryption non proteggono dall’amministratore.
  • Serve equality search su colonna cifrata → deterministic encryption: la randomized è più sicura ma blocca qualsiasi operazione lato server; per range o LIKE servono i secure enclaves.
  • “Mascherare i numeri di carta ai call center” → dynamic data masking: è offuscamento di presentazione, non un controllo robusto; un permesso UNMASK lo annulla e i dati restano in chiaro nello storage.
  • Ogni cliente vede solo le proprie righe (multi-tenant) → row-level security: non filtrare a mano con viste; la security policy con block predicate impedisce anche le scritture fuori scope.
  • Dare accesso a una procedura senza esporre la tabella → ownership chaining + GRANT EXECUTE: evita il SELECT diretto e assegna ruoli custom invece dei ruoli fissi ampi.
  • GRANT e DENY in conflitto → vince DENY: un permesso ereditato da un ruolo viene comunque bloccato da un DENY esplicito sull’utente.