Data Masking
Data masking transforms sensitive column values in SELECT results before they reach the developer. Masking is column-level and read-side only — writes (INSERT, UPDATE) pass through unaffected. Masking is orthogonal to table grants: a user with SELECT on a table sees masked values for any masked columns, while a user without SELECT cannot query the table at all.
Overview
Masking rules are defined as part of policies. Each rule maps a column match pattern to a masking preset. When a query result includes a matched column, the agent replaces the raw value with a masked version before forwarding it to the client.
- Column-level — masking targets individual columns, not entire rows or tables.
- Read-side only — INSERT and UPDATE statements pass through unaffected. Only SELECT results are masked.
- Transparent — developers see masked values in their query results without any changes to their queries or client code.
- Policy-driven — masking rules are configured in the console as part of a policy and pushed to agents automatically.
Masking never prevents access to a table. To control which tables a user can query, use table grants. Masking controls what the user sees in the columns they are allowed to read.
Masking Presets
DAAM provides seven built-in masking presets. Each preset is designed for a specific data type and preserves enough structure to confirm the data format while hiding the sensitive content.
| Preset | Input | Output | What Leaks |
|---|---|---|---|
email | [email protected] | j***@e***.com | First char of local part, first char of domain, TLD |
phone | 415-555-1234 | ***-***-1234 | Last 4 digits |
ssn | 123-45-6789 | ***-**-6789 | Last 4 digits |
credit_card | 4111-1111-1111-1111 | ****-****-****-1111 | Last 4 digits |
name | Alice Johnson | A*** J*** | First character of each word |
redact | anything | *** | Nothing — full redaction |
null | anything | NULL | Nothing — value replaced with SQL NULL |
Masked values are returned as strings regardless of the original column type.
If the original column value is NULL, it passes through unchanged. NULL is already the absence of data, so no masking is needed.
Adding Masking Rules
Masking rules are configured as part of a policy in the console. To add a masking rule:
- Navigate to the database and open the policy you want to edit.
- In the Masking Rules section, click Add Rule.
- Enter a match pattern (e.g.
public.users.email) or use the column dropdown to select from discovered columns. - Choose a masking preset from the dropdown.
- Save the policy.
The column dropdown is populated from the agent's schema introspection. The agent automatically discovers your database's tables and columns and reports them to the control plane, so the dropdown always reflects your actual schema.
Once saved, the updated policy is pushed to the agent immediately. New masking rules take effect on subsequent queries — there is no delay or restart required.
You can also type wildcard patterns directly into the match pattern field. Wildcards are useful for applying the same masking preset across multiple tables or schemas without creating a rule for each column individually.
The masking rules editor below shows five rules using different presets. Click a column pattern field to open the searchable dropdown populated from schema introspection data.
Masking Rules
Standard masking transforms query output — developers see masked values but can reference the column freely in SQL.
Strict masking additionally blocks the column from being used in WHERE, GROUP BY, JOIN, HAVING, and other query clauses. This prevents inference attacks where a developer could deduce masked values through query predicates.
Strict-masked columns can only appear in SELECT output, ORDER BY, and RETURNING clauses.
| Pattern (schema.table.column) | Type | Strict | |
|---|---|---|---|
Match Patterns
Masking rules use three-part match patterns in the format schema.table.column. Each segment can be a literal name or a wildcard (*). Matching is case-sensitive.
| Pattern | Matches |
|---|---|
public.users.email | The email column on the users table in the public schema |
public.users.* | All columns on the users table in the public schema |
public.*.email | Any column named email in any table in the public schema |
*.users.email | The email column on the users table in any schema |
*.*.email | Any column named email in any table in any schema |
*.*.* | Every column in every table (use with caution) |
Wildcard patterns let you enforce masking broadly. For example, *.*.email with the email preset ensures that any column named "email" is masked across all tables, including tables added in the future.
Pattern Specificity
When multiple masking rules within a single policy match the same column, more specific patterns override wildcards. For example, public.users.email takes priority over *.*.email when resolving the public.users.email column, while the wildcard still applies to email columns in all other tables.
Strict Masking
By default, masking transforms a column's value in the result set but still allows the column to be used elsewhere in a query — for example in a WHERE clause. That leaves a gap: a determined user could narrow down a masked value indirectly, for instance by filtering on it (WHERE ssn = '123-45-6789') and observing which rows return.
Enabling Strict on a masking rule closes that gap. When a rule is strict, the masked column may only appear in output positions of a query — the SELECT list, ORDER BY, and RETURNING. Any query that references the column where its underlying value could be inferred — WHERE, JOIN conditions, GROUP BY, HAVING, or function arguments — is rejected with an error rather than executed. The developer still sees the masked value in their results; they simply cannot use the column to probe the real one.
Strict is a per-rule option: enable it on the columns where inference is a concern (national IDs, exact salaries, medical codes) and leave it off where ordinary masking is sufficient. Set it with the Strict checkbox when adding or editing a masking rule.
Strict masking trades query flexibility for stronger confidentiality. If developers report queries being rejected on a strict column, that is expected behavior — the column cannot be filtered or grouped on. Consider whether the workflow can read the column for display only, or whether standard (non-strict) masking is acceptable for that column.
Overlapping Policies
When a user is assigned to multiple policies on the same database and those policies define masking rules for the same column, the most restrictive preset wins. This ensures that sensitive data is never exposed through a less-restrictive policy.
For example, if one policy shows a column unmasked and another redacts it, the column is redacted. The null and redact presets are the most restrictive since they reveal the least information.
If a masking rule exists in one policy but not another, the rule is included in the effective policy. The absence of a masking rule in one policy does not cancel a rule from another.
Most-restrictive-wins applies to a user's standing policies. There is one deliberate exception: an approved access request can temporarily unmask a specific column, overriding a standing mask for the duration of the grant. This lets an admin authorize a narrow, time-boxed exception without weakening the permanent policy. See the Access Requests documentation for details.
Example: Policy A masks public.customers.email with the "email" preset. Policy B masks the same column with "redact". A user assigned to both policies sees public.customers.email masked with "redact" because it is more restrictive (rank 2 vs rank 4).
Computed Columns and Aliases
Masking works with aliased columns. If you write SELECT email AS e FROM users, DAAM resolves the original column name from the database metadata and applies masking rules that match public.users.email, not the alias.
For computed columns (expressions, aggregates, literals), the agent cannot resolve a physical column identity. Only wildcard rules that match the alias name apply. For example, *.*.cnt would match SELECT COUNT(*) AS cnt, but public.users.cnt would not.
Validation
The following rules are enforced when saving masking rules in a policy:
- Match patterns must have exactly three dot-separated segments (schema.table.column).
- Each segment must be a valid name or the wildcard
*. - The masking type must be a known preset (email, phone, ssn, credit_card, name, redact, or null).
- No duplicate match patterns within the same policy.
- Strict is an optional per-rule flag. When enabled, the column is restricted to output positions only (see Strict Masking above).
Invalid masking rules are rejected on save with an error message indicating which rule failed validation.
Masking fails closed. If the agent cannot positively resolve a column's identity (for example when upstream metadata is temporarily unavailable, or for binary-format result columns that cannot be safely rewritten), it replaces the value with NULL or rejects the query rather than risk leaking an unmasked value. Operations that would bypass result masking entirely, such as COPY ... TO STDOUT, are blocked on connections that carry masking rules.