Access control with Lakekeeper

Add an identity provider and fine-grained, role-based permissions to your catalog, so the catalog decides who can read and write which warehouse, namespace, and table.

Runnable source: https://github.com/lakekeeper/lakekeeper/tree/main/examples/access-control-simple.

Vending short-lived credentials keeps keys out of your engines — but it doesn’t yet decide who is allowed to get them. That’s the job of Lakekeeper’s authorization layer. In this tutorial you’ll run a stack with a real identity provider and a policy engine, then watch two users get different answers from the same catalog.

See it running: the access-control-simple example wires up Keycloak, OpenFGA, and three engines against one Lakekeeper catalog.

The model: authentication vs. authorization

Lakekeeper separates the two cleanly, and it’s worth internalizing the split:

  • Authentication (who are you?) — Lakekeeper is not an identity provider. Identities come from an external OIDC provider (Keycloak here, but any OIDC IdP works; it can also authenticate Kubernetes service accounts natively).
  • Authorization (what may you do?) — handled by a pluggable Authorizer. This example uses OpenFGA; Lakekeeper also supports Cedar and Open Policy Agent (OPA).

The engine sends a token, Lakekeeper verifies it against the IdP, then asks the authorizer whether that identity may perform the action on that object.

The permission hierarchy

Permissions flow down a hierarchy of objects:

Server → Project → Warehouse → Namespace → Table / View

Grants are inherited: give a role read on a namespace and it can read every table beneath it. Roles live on the project, so a role can be reused across every warehouse in that project — you define “analyst” once, not per-warehouse.

1. Start the stack

cd lakekeeper/examples/access-control-simple
docker compose up

This brings up Lakekeeper, Keycloak (identity provider, pre-loaded with an iceberg realm), OpenFGA (authorization backend), SeaweedFS for storage, and Jupyter with Spark, PyIceberg, Trino, and StarRocks.

Endpoints:

2. Bootstrap and meet the users

Run the 01-Bootstrap notebook in Jupyter. The example ships three identities in Keycloak:

User Login Starting access
Peter peter / iceberg Full access (bootstrap admin)
Anna anna / iceberg No permissions yet
Admin admin / admin Keycloak administrator

Peter bootstraps the server and creates a warehouse. Anna exists but can’t see anything — that’s the point.

3. Grant, then verify

Connect as Anna and try to list namespaces or read a table. The request is authenticated (her token is valid) but denied — the authorizer has no grant for her.

Now, as Peter (or via the UI), grant Anna a role with read access to a namespace. The next request Anna makes succeeds — and only for what she was granted. No engine restart, no key change, no config edit on Anna’s side: the decision lives entirely in the catalog.

This is the whole loop: one identity, one policy decision, enforced no matter which engine asks. Anna gets the same answer whether she connects with Spark, Trino, or StarRocks.

Why centralize authorization here

Because it composes with everything else the catalog does:

  • One place to reason about access instead of per-engine configs and coarse cloud IAM.
  • Instant revocation — remove the grant and the next request is denied everywhere.
  • Least privilege that engines inherit automatically — combined with vended credentials, an identity only ever receives short-lived credentials scoped to what its role allows.

Where to go next