The problem this decision solves

A single role check in a controller works until it doesn't: the second team that queries the table, the read replica the analytics job hits, the export job that forgot the filter. Masking that lives in application code is only as strong as the last engineer who remembered to add it.

The column masking PoC on this site tests the narrow version of this problem: one table, three roles, one query shape. This note is the scaled-up version: hundreds of columns, dozens of roles and partner integrations, multiple services reading the same tables, and a compliance team that needs to answer "who could have seen this field on this date" without reading source code.

Two mechanisms, one decision

01

Masking

The underlying value never leaves the database in readable form for an unauthorized caller. A dynamic view, a proxy, or a row/column security policy substitutes a redacted, truncated, or synthetic value at read time. Masking is usually one-directional. There is no path back to the original from the masked output.

02

Tokenization

The original value is replaced with a token that is meaningless outside a vault, and the mapping is stored separately from every other system that uses the token. An authorized caller can detokenize; everyone else only ever holds a token. Tokenization is reversible by design, which is exactly why the detokenization path needs its own policy decision.

They are not competing choices. A payment field is usually tokenized so a support agent can reference "card ending 4242" without ever seeing the PAN, while a diagnosis field is usually masked outright for anyone outside clinical care, because there is no legitimate reason for billing to reconstruct it.

Evaluation criteria

01

Policy expressiveness

Can the engine express attribute-based rules (role, data classification, purpose of use, time of day, case status) rather than a static role-to-table map?

02

Decision latency

Is the authorization check cheap enough to run per-column, per-request, without becoming the slowest part of the query path?

03

Auditability

Can every permit or deny be reconstructed later, including which policy version, which principal attributes, and which resource, without replaying application logs?

04

Reversibility control

Is detokenization itself a policy-gated action with its own audit trail, rather than an implicit side effect of holding a database credential?

05

Blast radius of a bug

If a policy is wrong, does it fail open or closed? A default-deny engine turns a missing rule into an incident caught in staging instead of a silent leak.

06

Integration surface

Does the enforcement point sit at the database boundary, the API gateway, or both, and does that match where the data actually fans out to other services?

Where Cedar fits

Cedar is an open-source, schema-validated authorization language originally built for Amazon Verified Permissions and released independently of it. It is deliberately narrow: it does not run business logic; it answers one question, can this principal take this action on this resource, given this context? The answer is always Allow, Deny, or an explicit reason. Policies are permit or forbid statements, evaluation is default-deny, and any matching forbid overrides every permit. That last rule is what makes Cedar a good fit for masking: the default state of a new column is invisible, and a forbid for a sensitive field can't be quietly outvoted by a broad role grant written somewhere else.

Cedar policies can be unit-tested and schema-validated in CI the same way a type system catches a bad call: a policy that references a role or a resource attribute the schema doesn't define fails to compile, before it ever reaches production.

Field-level entitlement, in policy

The pattern is to model each protected column as a resource the principal is requesting to view, not to try to encode masking rules inside SQL. Enforcement then asks Cedar one question per field, or a batch of them, and only reveals the fields that came back Allow.

Cedar conceptMaps to
PrincipalThe authenticated caller — User::"agent-victor" — with role and department attributes
ActionAction::"viewField", one per read, or Action::"detokenize" for the reversible path
ResourceA specific column instance — Field::"patient_record.diagnosis" — tagged with a data classification
ContextRequest-time facts the policy can condition on — case status, time of day, whether MFA was used this session

Cedar policy set (excerpt): field-level entitlement

// Schema (excerpt): what a Field resource carries
entity Field {
  table: String,
  classification: String, // "clinical" | "billing" | "support" | "restricted"
};

entity User in [Role] {
  department: String,
};

entity Role;

// Default posture: nothing is visible unless a policy says otherwise.

// Clinical staff see clinical-classified fields for records tied to their department.
permit (
  principal in Role::"Clinical",
  action == Action::"viewField",
  resource
)
when { resource.classification == "clinical" };

// Billing sees billing fields, and tokenized payment references, never raw PANs.
permit (
  principal in Role::"Billing",
  action == Action::"viewField",
  resource
)
when { resource.classification == "billing" };

// Support sees coordination fields only — explicitly walled off from restricted data,
// even if a future role grant would otherwise have covered it.
forbid (
  principal in Role::"PatientSupport",
  action == Action::"viewField",
  resource
)
when { resource.classification == "restricted" };

// Detokenizing a payment token is its own action, gated separately from
// viewing the tokenized field, and only inside an open, unresolved case.
permit (
  principal in Role::"Billing",
  action == Action::"detokenize",
  resource
)
when {
  resource.classification == "billing" &&
  context.case.status == "open"
};

Notice what the application never does: it never asks "is this user Billing." It asks Cedar "can this principal view this field, right now, given this context," and renders whatever comes back. The role-to-field mapping lives in one reviewable place instead of being reconstructed from scattered conditionals.

Reference architecture

Cedar makes the decision; something else enforces it. Production systems need both, kept as separate services so a masking bug and an authorization bug fail independently and get diagnosed independently.

Client or service request
API gateway
Cedar policy decision point
Per-field decision: allow · deny · detokenize
Masking view / column proxy
Token vault (Vault Transit, KMS-backed, or a managed tokenization service)
Audit and decision log

The database itself should never be the last line of defense on its own. The masking view or column proxy enforces what Cedar decided, and the token vault is the only place that can map a token back to a value; a compromised application credential should only ever yield tokens and masked values.

Side by side

ApproachReversibleBest fitPrimary concern
Static masking (ETL-time)NoNon-production copies, analytics sandboxesSource of truth still holds raw data
Dynamic masking (views / RLS)NoRead paths where the raw value is never needed by that callerEvery read path must go through the view, not the base table
Tokenization (vaulted)Yes, via a gated detokenize actionPayment data, identifiers referenced across many systemsVault becomes a second system of record to secure and operate
Format-preserving encryptionYes, with the keyFields where downstream systems validate shape (length, digits)Key management carries the entire security guarantee

Production checklist