---
title: "Row-level security for Apache Trino, powered by Cerbos Synapse"
description: "Add row-level security, column masking, and table-level access control to Apache Trino using Cerbos policies. No Rego, no custom plugins. Covers policy examples, attribute-based filtering from your IdP, per-role masking rules, and compliance audit logging through Cerbos Hub."
author: "Alex Olivier"
date: "2026-04-07T12:34:34.539Z"
canonical: "https://www.cerbos.dev/blog/row-level-security-for-apache-trino"
image: "https://stylish-appliance-1c1cc1c30d.media.strapiapp.com/Row_level_security_for_Apache_Trino_powered_by_Cerbos_Synapse_e2a447bc57.png"
tags: ["engineering","documentation","guide"]
source: "https://www.cerbos.dev/blog/row-level-security-for-apache-trino"
---

# Row-level security for Apache Trino, powered by Cerbos Synapse

[Watch on YouTube](https://www.youtube.com/watch?v=Eil3b8Iz6ws)

Most systems that need authorization weren't built with [Cerbos](http://cerbos.dev) 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](https://www.cerbos.dev/product-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.

<p>&nbsp;</p>

## What Cerbos Synapse orchestrates

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.

<p>&nbsp;</p>

## Table-level access control

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.

<p>&nbsp;</p>

## Row-level filtering

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.

<p>&nbsp;</p>

## Column masking

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.

<p>&nbsp;</p>

## 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.

<p>&nbsp;</p>

## What the results look like

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.

<p>&nbsp;</p>

## The operational model

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.

<p>&nbsp;</p>

## Why this matters

| 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. |

<p>&nbsp;</p>

## The broader pattern

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.

<p>&nbsp;</p>

## Get started

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](https://cerbos.dev/contact), or explore [Cerbos Hub](https://hub.cerbos.cloud/).

## FAQ

### Does Trino need any modifications to work with Cerbos Synapse?

No. Trino's built-in OPA plugin is the integration point. Synapse implements the same HTTP protocol that OPA exposes. You change 3 URLs in Trino's config and it talks to Synapse without knowing the difference.

### How does attribute-based column masking differ from role-based masking?

Role-based masking applies the same mask to everyone in a role. Attribute-based masking makes the decision conditional on the user's own attributes. The clearance column is masked for anyone whose own clearance level is below confidential, regardless of whether they're a viewer or an analyst. The mask depends on a property of the user, not just their role.

### What happens to query performance?

Synapse runs in-process with the Cerbos PDP, so there's no network hop for policy evaluation. Data source lookups include a configurable cache layer with TTLs per source. After the first query, user attribute lookups are served from cache. The authorization overhead per query is typically sub-millisecond for cached lookups.

### Can I use the same Cerbos Synapse instance for Trino and other systems?

Yes. The same Synapse instance can handle Trino, Kafka, Envoy, and any other system. They all share the same PDP, the same policies from Hub, and the same data sources.

### How does row-level security work in Apache Trino with Cerbos?

Row-level security in Apache Trino with Cerbos works by evaluating a policy for every query and returning a SQL WHERE clause that Trino applies as a filter. Cerbos Synapse sits between Trino's built-in OPA plugin and the Cerbos PDP, translating the protocol and enriching each request with user attributes fetched from your identity provider. The policy references those attributes to determine which rows each user can see. For example, a marketing department viewer only sees rows matching their department, while an analyst with internal clearance sees everything except confidential rows.

### What is the difference between role-based and attribute-based column masking in Trino?

The difference between [role-based](https://www.cerbos.dev/blog/what-is-role-based-access-control-and-when-to-use-it) and [attribute-based](https://www.cerbos.dev/blog/rbac-vs-abac) column masking in Trino is what drives the masking decision. Role-based masking applies the same mask to everyone in a given role, so all analysts see the same partial email and all viewers see fully redacted emails. Attribute-based masking makes the decision conditional on a specific property of the user, like their clearance level. A column can be visible to anyone with confidential clearance and redacted for everyone else, regardless of whether they are a viewer or an analyst.

### Can Cerbos Synapse replace OPA for Trino authorization?

Cerbos Synapse can replace OPA for Trino authorization without any modifications to Trino itself. Synapse implements the same HTTP protocol that Trino's OPA plugin expects, so the integration is a configuration change of three URLs. The difference is that policies are written in Cerbos's YAML and CEL format instead of Rego, and Synapse enriches each request with user attributes from your identity provider before the policy evaluates. Trino doesn't know it's no longer talking to OPA.

### How does Trino query performance change with Cerbos Synapse?

Trino query performance with Cerbos Synapse adds minimal overhead because Synapse runs in-process with the Cerbos PDP, eliminating the network hop for policy evaluation. Data source lookups for user attributes include a configurable cache layer with TTLs per source, so after the first query in a session, attribute lookups are served from cache. The authorization overhead per query is typically sub-millisecond for cached lookups.

### How do you audit Trino access control decisions for compliance?

Auditing Trino access control decisions for compliance is handled automatically through Cerbos Hub's [audit logging](https://www.cerbos.dev/blog/why-audit-logs-are-important). Every access control decision, row filter applied, and column mask returned is logged with a timestamp, the user identity, the table queried, and the policy outcome. These logs are searchable, filterable, and exportable, which maps directly to what GDPR, HIPAA, and SOC 2 auditors ask for when they want to know who accessed PII and what controls were in place.
