Apache Iceberg REST Catalog · Rust · Apache-2.0 OR MIT

A policy-first
Iceberg REST catalog

A whole catalog, not a proxy in front of one: an embedded file, or Postgres for HA. Engines connect once, with one identity. Rustberg authenticates the caller, authorizes every operation against Cedar policy, vends short-lived storage credentials scoped to what that policy allows, and records the decision.

$ rustberg --dev --insecure-http

WARN No API keys or OIDC configured — minted a temporary admin key.
WARN     X-API-Key: rb_9oroTuw3xDT5x1_wovV5IUDgx628Wj6hJuCh9H1ZKwE
WARN     curl -H 'X-API-Key: rb_9oro…' http://localhost:8000/v1/config

One command, one binary, no configuration file. Authentication stays on — it mints a key and prints it rather than defaulting to open.

23 µsauthorization, p99
< 1 msloadTable, p99
~15 MBidle memory
0required services

Storing table pointers is solved. Governing them is not.

Every Iceberg catalog can tell an engine where a table's metadata lives. The hard part is deciding who may read it, who may write it, and who gets handed a credential — across catalogs an organisation did not choose to have in the same place. That is what Rustberg is.

Policy is the product

Every operation is decided by Cedar, a formally verified policy engine that is a library rather than a service. Resources form a hierarchy, so one policy covers a whole namespace subtree — including tables that do not exist yet. Deny by default, including on evaluation error.

How authorization works →

Federation, under one identity

Mount several catalogs — your own redb or Postgres, or somebody else's Iceberg REST catalog — under one endpoint, routed by top-level namespace. The mount is invisible on the wire, so a cross-catalog join is ordinary SQL. Capabilities are negotiated per mount and published as an intersection.

How federation works →

Credentials that only narrow

AWS STS session policies, GCS credential access boundaries, Azure user-delegation SAS — each a real downscoping exchange, scoped to one table prefix. A vended credential is the intersection of the caller's policy and the mount's own ceiling, and never wider.

How vending works →

Row and column policy, enforced

A row filter or column mask travels to the engine as the Iceberg spec's read-restrictions — and is not merely advertised. Such a table is refused a broad credential, pinned to server-side planning, and handed pre-signed URLs for exactly the files the filter selected.

How restrictions work →

An audit trail, not a log line

Every decision — permit and deny alike — names the policy that made it and the version of the policy set it came from. An empty rule list on a denial is itself the answer: nothing forbade the request and nothing permitted it. When the sink fails, mutations fail with it.

What is recorded →

A binary, or a crate

The same operations are reachable in-process, through the same authorization guard and against the same catalog — no router, no socket, no second implementation to keep honest. The equivalence is tested as equivalence.

Use it as a library →

Nothing required underneath

A useful deployment is one binary and a filesystem. Add Postgres when you want several replicas, object storage when your tables live there. Pure Rust, no unsafe, no C dependencies in the default build, so it cross-compiles statically.

Catalog and warehouse →

Written as policy, enforced on every request

Roles become Cedar groups; namespaces, tables and views form a tree. A grant on a subtree reaches everything below it, and nothing above.

// Analysts read anything under one namespace subtree, and nothing else.
permit(
  principal in Rustberg::Group::"analysts",
  action    in [Rustberg::Action::"Read", Rustberg::Action::"List"],
  resource  in Rustberg::Namespace::"acme\u{1F}analytics"
);

// A pipeline writes to one namespace, outside business hours only.
permit(
  principal == Rustberg::User::"svc-etl",
  action    in [Rustberg::Action::"Create", Rustberg::Action::"Update"],
  resource  in Rustberg::Namespace::"acme\u{1F}analytics\u{1F}web"
) when { context.utc_hour < 6 || context.utc_hour > 20 };

// Nothing in production is reachable from outside the VPC.
forbid(
  principal, action,
  resource in Rustberg::Tenant::"prod"
) unless {
  context has source_ip && context.source_ip.isInRange(ip("10.0.0.0/8"))
};

Policies are validated against the schema at load, so a typo is a startup failure rather than a permit that silently never matches. Listings filter rather than deny, so a caller never learns what it cannot see.

Works with the engines you already run

PyIceberg

Read and write

Apache Spark

Read and write, including atomic CTAS

Trino

Read and write

DuckDB

Read

Conformance suites for PyIceberg, DuckDB and Trino run against the built binary on every change. Client configuration →

Pick your shape

Single binary

One process, an embedded redb catalog, a local or object-store warehouse, no external services. The default, and the shape that stays effortless.

rustberg \
  --catalog-url file:///var/lib/rustberg \
  --warehouse   s3://acme/warehouse

Replicated

Several stateless replicas over one Postgres catalog. Policies, keys and mount configuration live in the database, so every replica agrees.

rustberg \
  --catalog-url postgres://rustberg@db/rustberg \
  --warehouse   s3://acme/warehouse

Embedded

A Rust service depending on the crate, with policy and vending in-process and no network hop.

let session = app.as_principal(
    Principal::embedded("svc-etl", "acme")
        .with_role("writer")
        .build(),
);
let table = session.load_table(&events).await?;

Start with one command

An ephemeral catalog, an admin key printed at startup, and the curl that uses it.