In-flight processing
Masking
Hide sensitive values before they reach a target with partial masks, keyed hashes or constant replacements, with worked examples of each.
A mask replaces a column's value before it is written to the target. The source keeps the real value; the target only ever receives the masked one, in the initial load and in every change.
| Mask | Example | Source value | Target receives |
|---|---|---|---|
| Partial mask | PARTIAL_MASK(email, 1, 3) | ada@example.com | a***********com |
| Keyed hash | HASH_MASK(email, 'customer') | ada@example.com | 64 hexadecimal characters, the same for the same input and key |
| Constant | CAST('[redacted]' AS VARCHAR(32)) | ada@example.com | [redacted] |
Before you begin
- See Validation and limits for the source settings that rules need.
- For a keyed hash, you need access to the agent host to store a secret key.
Mask a column in one table
- Open Configure for the target and select the table.
- Tick Choose output columns and list the columns to keep. See Output and computed columns.
- For the column to mask, enter the mask as its Source expression, and keep the column's name as its Output name.
For example, to send dbo.Customers with its contact details masked:
| Output name | Source expression |
|---|---|
customer_id | customer_id |
full_name | full_name |
email | HASH_MASK(email, 'customer') |
phone | PARTIAL_MASK(phone, 0, 4) |
tax_id | CAST('[redacted]' AS VARCHAR(32)) |
Mask a column in every table
- Open Configure and select All tables.
- Open Output columns and masks for all tables (optional) and tick Mask columns globally.
- Select Add column mask. Enter the column's name in Existing output name and the mask in Source expression.
Each global mask replaces the named column in every table that has it, and leaves the other columns unchanged. Names match without regard to case. A table that chooses its own output columns ignores global masks, so put the mask in its own list.
For example, these global masks hash every email column and mask every phone column:
| Existing output name | Source expression |
|---|---|
email | HASH_MASK(email, 'customer') |
phone | PARTIAL_MASK(phone, 0, 4) |
A global mask doesn't skip a key column. If email is a primary key, unique index or foreign key column in one of the tables, the start is refused with protected-column-missing-or-transformed; give that table its own output columns.
Partial mask
PARTIAL_MASK(value, keep_start, keep_end) keeps the first keep_start and the last keep_end characters of a text value and replaces each character in between with *. The length of the value doesn't change.
valuemust be text. Cast a number to text first:PARTIAL_MASK(account_number, 0, 4)on a number column is refused withstring-type.keep_startandkeep_endmust be whole-number literals.- When
keep_startpluskeep_endis at least the value's length, the value is unchanged, so choose counts that hide enough of your shortest values. NULLstaysNULL.
| Source expression | Source value | Target receives |
|---|---|---|
PARTIAL_MASK(email, 1, 3) | ada@example.com | a***********com |
PARTIAL_MASK(phone, 0, 4) | +1 555 0100 | *******0100 |
PARTIAL_MASK(card_number, 0, 4) | 4111111111111111 | ************1111 |
PARTIAL_MASK(CAST(account_number AS VARCHAR(20)), 0, 4) | 1234567890 (a bigint) | ******7890 |
PARTIAL_MASK(last_name, 1, 3) | Ng | Ng (unchanged: the value is shorter than 1 + 3) |
Keyed hash
HASH_MASK(value, 'alias') replaces a value with an HMAC-SHA256 of the value under a secret key, written as 64 lowercase hexadecimal characters. The same value always gives the same hash under the same key and version, so masked columns can still be joined, grouped and counted at the target. Without the key, the original can't be recovered or guessed by hashing candidate values.
valuecan be text or binary. Cast a number to text first:HASH_MASK(national_id, 'customer')on a number column is refused withmask-input-type.'alias'is a text literal naming the key, using letters, digits, dots, hyphens or underscores, at most 128 characters.NULLstaysNULL.
The key itself never leaves the agent host. The rule holds only a reference to it.
The hash is exact: values that differ in case or spaces hash differently. Normalize them first if they should match:
| Source value | HASH_MASK(email, 'customer') | HASH_MASK(LOWER(TRIM(email)), 'customer') |
|---|---|---|
ada@example.com | Hash A | Hash A |
Ada@Example.com | Hash B | Hash A |
ada@example.com (trailing space) | Hash C | Hash A |
HASH_MASK(LOWER(TRIM(email)), 'customer')
HASH_MASK(CAST(national_id AS VARCHAR(20)), 'customer')
HASH_MASK(document_fingerprint, 'customer')Use the same alias wherever hashes must match across tables, for example customer on both dbo.Customers.email and dbo.Orders.email, and a different alias where they must not match, for example HASH_MASK(tax_id, 'finance').
Provide the key
Generate a random key of 32 to 4,096 bytes, encoded as base64.
$bytes = New-Object byte[] 32 [System.Security.Cryptography.RandomNumberGenerator]::Create().GetBytes($bytes) [Convert]::ToBase64String($bytes)Store the base64 text on each agent that runs the pipeline. With relay, store it on both the capture agent and the apply agent. Use either:
- the environment variable
SQLFLOW_SECRET_<name>of the agent service, or - the file
secrets/<name>under the agent's data directory. A trailing newline is ignored.
- the environment variable
In Configure, select All tables. For each alias your rules use, a pair of fields appears. Enter an immutable version in the field labeled with the alias and key version, such as customer: key version, for example
v1, and the reference in Operator secret reference, such asenv:CUSTOMER_MASK_V1orfile:CUSTOMER_MASK_V1.
Enter the reference, never the key. See Database credentials and secret references for the reference format.
For example, rules that use the aliases customer and finance need two keys:
| Alias | Key version | Operator secret reference | Stored on each agent as |
|---|---|---|---|
customer | v1 | env:CUSTOMER_MASK_V1 | The environment variable SQLFLOW_SECRET_CUSTOMER_MASK_V1 |
finance | v1 | file:FINANCE_MASK_V1 | The file secrets/FINANCE_MASK_V1 under the agent's data directory |
The alias and version are part of every hash, so the same key gives different hashes under a different alias or version. Treat a version as fixed. To rotate a key, store the new key under a new name, enter a new version and reference, and save the rules. Saving reloads the pipeline, so every row is hashed again with the new key.
| Problem | Refusal |
|---|---|
| An alias has no version or reference | <alias>: enter the masking key version and operator secret reference. |
| The agent can't read the referenced secret | mask-key-unavailable |
| The secret isn't base64, or contains spaces or line breaks | mask-key-encoding |
| The key is shorter than 32 or longer than 4,096 bytes | mask-key-size |
| More than 64 keys | Use at most 64 masking key references. |
Constant replacement
To replace every value with the same text, cast a literal to the type you want at the target, or use REDACT with a text literal:
CAST('[redacted]' AS VARCHAR(32))
REDACT('[redacted]')REDACT accepts only a text literal: REDACT(tax_id) is refused with redact-constant-required, or mask-constant-count-required in a global list. The cast form also sets the target column's type.
A constant replaces NULL values too. To keep NULL where the source has no value, test for it with CASE:
CASE WHEN tax_id IS NULL THEN NULL ELSE '***-**-****' ENDWhere masks can't be used
- Row conditions. A condition reads the real source values.
HASH_MASKin a condition is refused withmask-in-predicate, andPARTIAL_MASKwithprocessing_source_predicate_unsupported. - Key columns. Primary key, unique index and foreign key columns must pass through unchanged. See Columns that must pass through unchanged.
- Non-literal arguments.
PARTIAL_MASKcounts and theHASH_MASKalias must be literals.PARTIAL_MASK(email, prefix_length, 3)is refused withmask-constant-count-required, andHASH_MASK(email, key_name)withmask-key-alias-constant-required. - Other hash functions.
HASHandKEYED_HASHare refused withimmutable-mask-key-version-required. UseHASH_MASK.
