Data Masking, Ledger, and Row-Level Security
Implement dynamic data masking to hide sensitive data, Azure SQL Ledger for tamper-proof records, and row-level security for fine-grained access control.
Protecting sensitive data
Three ways to control who sees what:
Dynamic data masking is like putting a privacy screen on a monitor — the data is still there, but casual observers see blurred values. A support agent sees “XXX-XX-1234” instead of the full SSN.
Ledger is like a tamper-proof receipt book — every change is recorded and cryptographically sealed. If someone alters a record, the ledger proves it was tampered with.
Row-level security is like personalised search results — everyone queries the same table, but each person only sees the rows they’re allowed to see. A branch manager sees only their branch’s data.
Dynamic data masking
Masking hides sensitive data from non-privileged users at query time — the actual data in the table is unchanged.
Masking functions
| Function | What It Does | Example Input → Output |
|---|---|---|
| Default | Full mask based on data type | ”Hello” → “XXXX”, 12345 → 0 |
| Shows first letter + domain | ”amara@hospital.com” → “aXXX@XXXX.com” | |
| Partial | Custom prefix/suffix with padding | SSN “123-45-6789” → “XXX-XX-6789” |
| Random | Random number in range | 50000 → random between 1-100 |
-- Add masking when creating a table
CREATE TABLE Patients (
PatientID INT PRIMARY KEY,
Name NVARCHAR(100),
Email NVARCHAR(100) MASKED WITH (FUNCTION = 'email()'),
SSN NVARCHAR(11) MASKED WITH (FUNCTION = 'partial(0,"XXX-XX-",4)'),
Salary DECIMAL(10,2) MASKED WITH (FUNCTION = 'random(1, 100)')
);
-- Add masking to an existing column
ALTER TABLE Patients ALTER COLUMN Email ADD MASKED WITH (FUNCTION = 'email()');
-- Grant a user permission to see unmasked data
GRANT UNMASK TO [dr_chen@harbourhealth.com];
-- Granular unmask (SQL Server 2022+ / Azure SQL)
GRANT UNMASK ON dbo.Patients(SSN) TO [billing@harbourhealth.com];
Key points for the exam:
- Masking is not encryption — data is stored unmasked, only displayed masked
- Users with
UNMASKpermission see the real data db_owneralways sees unmasked data- Masking doesn’t protect against direct table access via T-SQL tricks (e.g., WHERE clauses can still filter on masked values)
Exam tip: masking limitations
Dynamic masking is a presentation-layer control, not a security boundary:
- It protects against casual exposure (support screens, reports)
- It does NOT protect against determined attackers who can infer values through queries
- For true protection against privileged users, use Always Encrypted instead
- The exam loves combining masking with Always Encrypted: “masking for support staff, Always Encrypted for the data itself”
Azure SQL Ledger
Ledger provides tamper-evident storage — cryptographic proof that data hasn’t been altered without authorisation.
How ledger works
- Every change to a ledger table is recorded in a transaction log hash chain
- Each block of transactions is hashed and linked to the previous block (blockchain-style)
- The hash chain is stored in database digests — write these to tamper-proof storage (Azure Confidential Ledger or Azure Blob Storage with immutability)
- Verification compares database content against the digest — any tampering is detected
Two types of ledger tables
| Type | Use Case | How It Works |
|---|---|---|
| Updatable ledger table | Tables with normal DML (INSERT, UPDATE, DELETE) | Creates a history table that records all changes. Main table + history table + ledger view. |
| Append-only ledger table | Insert-only scenarios (audit logs, event records) | No updates or deletes allowed. Every row is permanent. |
-- Create an updatable ledger table
CREATE TABLE dbo.AccountBalances (
AccountID INT PRIMARY KEY,
Balance DECIMAL(18,2),
LastUpdated DATETIME2
) WITH (SYSTEM_VERSIONING = ON, LEDGER = ON);
-- Create an append-only ledger table
CREATE TABLE dbo.AuditLog (
EventID INT IDENTITY PRIMARY KEY,
EventType NVARCHAR(50),
EventData NVARCHAR(MAX),
EventTime DATETIME2 DEFAULT GETUTCDATE()
) WITH (LEDGER = ON (APPEND_ONLY = ON));
-- Verify ledger integrity
EXECUTE sp_verify_database_ledger;
Amara’s use case: Harbour Health uses an updatable ledger table for medication administration records. If a regulator asks “was this record modified after the fact?”, the ledger verification proves data integrity.
Enabling database-level ledger
You can enable ledger at the database level — ALL new tables become ledger tables by default:
ALTER DATABASE HarbourHealthDB SET LEDGER = ON;
Row-level security (RLS)
RLS lets you control which rows a user can see — enforced at the database engine level.
How it works
- Create a filter predicate function — returns 1 (allow) or 0 (deny) for each row
- Create a security policy that binds the function to a table
- Every query on the table automatically filters rows based on the predicate
-- Step 1: Create a schema for security objects
CREATE SCHEMA Security;
-- Step 2: Create the predicate function
CREATE FUNCTION Security.fn_BranchFilter(@BranchID INT)
RETURNS TABLE
WITH SCHEMABINDING
AS
RETURN SELECT 1 AS result
WHERE @BranchID = CAST(SESSION_CONTEXT(N'BranchID') AS INT)
OR IS_MEMBER('db_owner') = 1;
-- Step 3: Create the security policy
CREATE SECURITY POLICY BranchFilter
ADD FILTER PREDICATE Security.fn_BranchFilter(BranchID) ON dbo.Orders
WITH (STATE = ON);
-- Step 4: Set the session context for each connection
EXEC sp_set_session_context @key = N'BranchID', @value = 42;
-- Now: SELECT * FROM Orders only returns rows where BranchID = 42
Filter vs block predicates
| Predicate | Controls | Behaviour |
|---|---|---|
| FILTER | SELECT, UPDATE, DELETE | Silently filters out rows — user doesn’t know hidden rows exist |
| BLOCK | INSERT, UPDATE (AFTER/BEFORE) | Prevents DML that would violate the policy — raises an error |
Priya’s multi-tenant use case: ScaleWave uses RLS to isolate tenant data. All tenants share the same table, but each only sees their own rows. A filter predicate checks TenantID against the authenticated user’s tenant.
RLS performance considerations
- The predicate function runs for every row the query touches — ensure it’s efficient
- Use
SCHEMABINDINGon the function (required by security policy) - Index the column used in the predicate (e.g., BranchID, TenantID)
- Test with execution plans to verify the filter is applied early (predicate pushdown)
- RLS doesn’t prevent users from seeing row counts or aggregate information about hidden rows (e.g., COUNT(*) may still reflect all rows depending on the predicate)
Harbour Health's support team needs to look up patient records but should NOT see full SSN numbers. Dr. Chen (CISO) needs to see everything. What should Amara implement?
Kenji needs to prove to auditors that no one tampered with NorthStar's financial transaction records after they were written. Which feature should he implement?
ScaleWave's SaaS application stores all customer data in shared tables. Each query should only return data belonging to the authenticated tenant. What should Priya implement?
🎬 Video coming soon
You’ve completed Domain 2! You can now secure Azure SQL with Entra auth, permissions, encryption, network controls, classification, auditing, masking, ledger, and RLS.
Next up: Performance Baselines and Monitoring Tools — build an operational performance baseline and learn the monitoring toolkit.