Most systems that need authorization weren't built with Cerbos in mind. Trino has an OPA plugin. Kafka has its own authorizer interface. Kubernetes has admission webhooks. Envoy has ext_authz. Each one speaks a different protocol, expects a different request format, and returns a different response shape.
Cerbos Synapse exists to solve that problem. It's an orchestration layer that sits between systems that don't speak Cerbos and the Cerbos PDP. It handles the protocol translation, enriches the request with data from external sources, evaluates the policy, and returns the response in whatever format the calling system expects.
Trino is a good example of what this looks like in practice, because Trino's authorization needs go beyond simple allow/deny. It needs row-level filtering and column masking, both of which require user attributes that Trino doesn't have and policy outputs that go beyond a boolean.
Above is a 3 minute walkthrough of the whole thing in action.
Trino's OPA plugin makes 3 types of HTTP calls during query execution:
| Trino operation | What it asks | What it expects back |
|---|---|---|
| Access control | "Can this user run this query on this table?" | {"result": true/false} |
| Row filtering | "What rows should this user see?" | A SQL WHERE clause |
| Column masking | "How should each column be displayed?" | SQL expressions per column |
Synapse handles all 3. It exposes endpoints that speak Trino's OPA protocol, translates each request into a Cerbos CheckResources call, and formats the Cerbos response back into what Trino expects. Trino doesn't know it's talking to Cerbos. It thinks it's talking to OPA.
But Synapse is doing more than protocol translation. Before the PDP evaluates, Synapse enriches the request with user attributes fetched from your identity provider. Trino only sends a username. Synapse turns that into a fully attributed principal with department, clearance level, role, whatever your IdP knows about that user. Those attributes then drive the policy decisions: which rows to filter, which columns to mask, and how.
Here's the flow when an analyst runs SELECT * FROM marketing.users:
Analyst runs query
│
â–¼
Trino Engine
│
├──▶ Access control check (SelectFromColumns)
│ │
│ ├── Synapse looks up user attributes (dept, clearance)
│ ├── Cerbos PDP evaluates: ALLOW
│ └── Returns {"result": true}
│
├──▶ Row filter check
│ │
│ ├── Looks up user attributes
│ ├── Cerbos PDP evaluates GetRowFilter
│ └── Returns: clearance IN ('public', 'internal')
│
└──▶ Column masking check
│
├── Checks each column against policy
├── email → partial mask (first 2 chars visible)
├── phone → last 4 digits only
├── clearance → [REDACTED] (analyst lacks confidential clearance)
└── other columns → pass through
The analyst gets 5 rows instead of 8 (confidential users filtered out), with emails showing al***@example.com, phone numbers showing ***-***-0101, and the clearance column replaced with [REDACTED]. The query looks normal. The results are shaped entirely by Cerbos policy.
Policies live in Cerbos Hub, versioned and distributed to every Synapse instance automatically. Start with who can query which tables.
apiVersion: api.cerbos.dev/v1
resourcePolicy:
resource: trino
version: default
importDerivedRoles:
- user_groups
rules:
- name: deny_destructive_writes
derivedRoles: [admin, viewer, analyst]
actions: [DeleteFromTable, DropTable]
effect: EFFECT_DENY
- name: allow_viewer_select_users
derivedRoles: [viewer]
actions: [SelectFromColumns]
condition:
match:
all:
of:
- expr: R.attr.resource.table.schemaName == "marketing"
- expr: R.attr.resource.table.tableName == "users"
effect: EFFECT_ALLOW
- name: allow_analyst_select
derivedRoles: [analyst]
actions: [SelectFromColumns]
condition:
match:
all:
of:
- expr: R.attr.resource.table.schemaName == "marketing"
- expr: R.attr.resource.table.tableName in ["users", "transactions"]
effect: EFFECT_ALLOW
- name: allow_admin_select
derivedRoles: [admin]
actions: [CreateTable, SelectFromColumns]
effect: EFFECT_ALLOW
The viewer can query marketing.users. The analyst can query marketing.users and marketing.transactions. The admin can query anything. Nobody can DROP TABLE or DELETE FROM.
When a viewer tries to query the transactions table, Trino gets a 403:
Query failed: OPA server returned status 403
Same Cerbos policy language you'd use for any other resource in your stack. The Trino-specific parts (table names, schema names, operations like SelectFromColumns) come from the protocol translation layer. The policy author doesn't need to know anything about Trino's OPA protocol.
Access control is binary: you can query the table or you can't. Row filtering is where Synapse's orchestration gets more interesting. Different users see different subsets of the same table, based on attributes that Synapse fetched from the identity provider.
GetRowFilter rules use policy outputs to return SQL WHERE clauses:
- name: row_filter_viewer_users
derivedRoles: [viewer]
actions: [GetRowFilter]
condition:
match:
all:
of:
- expr: R.attr.resource.table.tableName == "users"
effect: EFFECT_ALLOW
output:
when:
ruleActivated: |
"department = '" + P.attr.department + "'"
- name: row_filter_analyst_users
derivedRoles: [analyst]
actions: [GetRowFilter]
condition:
match:
all:
of:
- expr: R.attr.resource.table.tableName == "users"
effect: EFFECT_ALLOW
output:
when:
ruleActivated: |
"clearance IN ('public', 'internal')"
The viewer is in the marketing department, so their filter becomes department = 'marketing'. Out of 8 rows in the users table, they see 4. The analyst has internal clearance, so their filter excludes confidential rows: they see 5 out of 8. The admin has no GetRowFilter output, so no filter is applied. All 8.
The filter expression is built dynamically from principal attributes. P.attr.department is the value Synapse fetched from the identity provider. Change someone's department in the IdP, and their row filter changes on the next query. No policy update required.
Column masking controls what data looks like even when you're allowed to see the row. The policy returns SQL expressions that Trino wraps around column values before returning results.
- name: mask_email_for_viewer
derivedRoles: [viewer]
actions: [GetColumnMask]
condition:
match:
all:
of:
- expr: R.attr.resource.column.columnName == "email"
effect: EFFECT_ALLOW
output:
when:
ruleActivated: |
"'***@' || substring(email, position('@' in email) + 1)"
- name: mask_email_for_analyst
derivedRoles: [analyst]
actions: [GetColumnMask]
condition:
match:
all:
of:
- expr: R.attr.resource.column.columnName == "email"
effect: EFFECT_ALLOW
output:
when:
ruleActivated: |
"substr(email, 1, 2) || '***@' || substring(email, position('@' in email) + 1)"
Same column, different masks depending on role:
| User | Email column | Phone column |
|---|---|---|
| admin | alice@example.com |
555-0101 |
| analyst | al***@example.com |
***-***-0101 |
| viewer | ***@example.com |
***-***-0101 |
Role-based masking is the easy case though. The more interesting pattern is attribute-based masking.
The clearance column in the users table contains values like public, internal, and confidential. Whether you can see those values depends on your own clearance level.
- name: mask_clearance_for_non_confidential
derivedRoles: [viewer, analyst]
actions: [GetColumnMask]
condition:
match:
all:
of:
- expr: R.attr.resource.column.columnName == "clearance"
- expr: P.attr.clearance != "confidential"
effect: EFFECT_ALLOW
output:
when:
ruleActivated: |
"'[REDACTED]'"
The condition checks P.attr.clearance, an attribute Synapse enriched from the identity provider. If your clearance isn't confidential, the clearance column shows [REDACTED] regardless of your role.
This is where the orchestration pays off. The policy references an attribute (P.attr.clearance) that doesn't exist in Trino's request. Synapse fetched it from the IdP, attached it to the principal, and made it available to the PDP. The policy author writes P.attr.clearance != "confidential" and it just works, because Synapse handled the plumbing.
3 users, same query: SELECT id, name, email, phone, clearance FROM marketing.users
Admin (engineering, confidential clearance):
id | name | email | phone | clearance
----+---------+--------------------+----------+--------------
1 | Alice | alice@example.com | 555-0101 | confidential
2 | Bob | bob@example.com | 555-0102 | internal
3 | Charlie | charlie@example.com| 555-0103 | public
...
(8 rows)
Analyst (analytics, internal clearance):
id | name | email | phone | clearance
----+---------+------------------------+--------------+------------
2 | Bob | bo***@example.com | ***-***-0102 | [REDACTED]
3 | Charlie | ch***@example.com | ***-***-0103 | [REDACTED]
5 | Eve | ev***@example.com | ***-***-0105 | [REDACTED]
...
(5 rows, confidential users excluded)
Viewer (marketing, public clearance):
id | name | email | phone | clearance
----+---------+-------------------+--------------+------------
3 | Charlie | ***@example.com | ***-***-0103 | [REDACTED]
4 | Dana | ***@example.com | ***-***-0104 | [REDACTED]
7 | Grace | ***@example.com | ***-***-0107 | [REDACTED]
...
(4 rows, marketing department only)
Same table. Same query. 3 different views. Every difference is driven by Cerbos policy, evaluated by the PDP, orchestrated by Synapse.
Once this is running, the day-to-day is pretty clean.
Policies live in Cerbos Hub. Versioned, testable, and they go through the same review process as any other code change. Want to give the analyst access to a new table? PR the policy, get it reviewed, merge it, it's live across every Synapse instance. No Trino restart. No config pushes.
User attributes come from your identity provider. The data source is a connector, not a copy. When someone changes departments in Okta, their row filter changes on the next query. When someone's clearance level changes, their column masks change. No policy update needed. The policy already references P.attr.department and P.attr.clearance. The data is live.
Audit logs flow to Hub automatically. Every access control decision, every row filter applied, every column mask returned. Searchable, filterable, exportable. When the compliance team asks "what could the analytics team see last quarter," you pull a report.
Testing is built in. Cerbos Hub's test framework lets you write assertions against your Trino policies the same way you'd test any other Cerbos policy. Verify all 3 authorization layers before every deploy: which users can query which tables, what row filters are applied, and what column masks are returned.
| Area | Details |
|---|---|
| One policy language | Trino's OPA plugin expects Rego. Kafka's authorizer expects its own format. Envoy expects something else. With Synapse, you write Cerbos policies for all of them. Your team learns one policy language, uses one test framework, and manages one set of policies in Hub. |
| Data governance | Row filtering and column masking are defined in policy, not in views, application code, or custom Trino plugins. One policy file governs who sees what across every table. Changes are auditable, testable, and versioned. |
| Compliance | GDPR, HIPAA, SOC 2: they all want to know who accessed PII and what controls were in place. With Synapse, you have a timestamped log of every query authorization. Which user, which table, what filter was applied, what columns were masked, and what the policy decided. |
| Attribute-based access | Role-based masking is a start. Attribute-based masking is what real data governance requires. A user's clearance level, department, or risk score can all factor into what they see. Those attributes can change without touching the policy. Synapse fetches the current values on every request. |
Trino is one system. The pattern is the same for any system that calls out to an external policy server but doesn't speak Cerbos natively.
Synapse's route extension system can implement any HTTP-based authorization protocol. Trino speaks OPA's protocol. Kafka speaks its own. Kubernetes admission control speaks another. Envoy has ext_authz. For each one, Synapse handles the protocol translation. The policy engine, the policy language, and the audit trail stay the same.
The data source layer is shared across all of them too. The same user profile lookup that enriches Trino authorization decisions enriches every other system Synapse orchestrates. Write the connector once, use it everywhere.
If your data platform runs Trino for analytics, Kafka for streaming, and application services behind Envoy, you can govern all 3 from one policy store in Hub, with one audit trail, using one set of tools your team already knows. That's what Synapse orchestrates.
If you're running Trino and want to bring Cerbos policy decisions into your data layer, we'd like to show you how Synapse fits into your stack. Reach out to the Cerbos team, or explore Cerbos Hub.
Book a free Policy Workshop to discuss your requirements and get your first policy written by the Cerbos team




Join thousands of developers | Features and updates | 1x per month | No spam, just goodies.
What is Cerbos?
Cerbos is an end-to-end enterprise authorization software for Zero Trust environments and AI-powered systems. It enforces fine-grained, contextual, and continuous authorization across apps, APIs, AI agents, MCP servers, services, and workloads.
Cerbos consists of an open-source Policy Decision Point, Enforcement Point integrations, and a centrally managed Policy Administration Plane (Cerbos Hub) that coordinates unified policy-based authorization across your architecture. Enforce least privilege & maintain full visibility into access decisions with Cerbos authorization.