Reliability & Consequence 18 September 2026 8 min read 1,757 words

The rule ships with the key

On 3 September the Apache Iceberg REST specification gained read restrictions — row filters and column masks that a catalog returns to whatever is loading a table, in the same JSON object as the credential to that table's files. Every obligation in it is written as something the reader must do.

The argument

Iceberg returns row filters and column masks in the same response as the storage credential, so lakehouse access control is now a property of the reader that the catalog can neither negotiate beforehand nor record afterwards.

There is now a worked example of a redacted email address in the Apache Iceberg REST catalog specification. It reads "iceberg16112018@apache.org" -> "xxxxxxxnnnnnnnn@xxxxxx.xxx", and it is exact: digits become n, six named punctuation characters survive, everything else becomes x. Beside it sit eight more transformations — replace with null, show first four, show last four, truncate to year, truncate to month, mask to a fixed value, and two flavours of SHA-256. A table format has grown a redaction vocabulary, specified to the Unicode code point.

It arrived on 3 September, in a change called Spec: Add finer grained read restrictions as part of loadTable. Read the schema and a second thing becomes apparent, which is that every sentence in it is an instruction to somebody else. "For each column listed, the reader must apply the specified action before returning values for that column." "A reader must discard any row for which the filter evaluates to FALSE." And, covering the case where the reader has met something it does not understand: "If a reader that supports read-restrictions cannot apply any returned restriction (a filter expression or an action), it must fail the query and must not silently return raw, partial, or empty results."

This is a security control written entirely in the imperative mood, addressed to the party it constrains.

Everything below is read out of the Iceberg project's own repositories and those of three catalog and client implementations. There is no outside reporting to corroborate it, because there is none yet.

LoadTableResult is the object a catalog returns when an engine opens a table. It carries the table metadata, a config map, a remote-signing-config, and — as of this month — a read-restrictions field holding required-column-projections and a required-row-filter. The craft in it is real. Columns are named by field-id rather than by name, so a mask survives a rename. A server must not mask a map's key, because "applying an action to keys can produce duplicate or null keys, which readers silently coalesce or reject, causing data loss." Restrictions apply to every read made from that response, including subsequent server-side scan planning, and "the response should not be cached outside of the authenticated scope."

Three fields above read-restrictions, in the same object, is storage-credentials.

That adjacency is the whole subject. The catalog hands over, in one JSON body, the credential that opens the table's files in object storage and the rules about which rows and columns must never be shown. The credential is a fact — it either works against S3 or it does not. The rules are a request. The party asked to honour them is the party now holding the key to the unredacted bytes.

The phrase "a reader that supports read restrictions" appears twice in the specification. Nowhere does it say how anyone finds out.

This is not because the document is naive about capability negotiation; it is fluent in it. A client announces what it can do with the X-Iceberg-Access-Delegation header, described as an "optional signal to the server that the client supports delegated access", listing vended-credentials, remote-signing, or both. A server announces its own capabilities in the other direction: idempotency-key-lifetime, where "presence of this field indicates the server supports Idempotency-Key semantics for mutation endpoints. If absent, clients MUST assume idempotency is not supported." There is scan-planning-mode, which "communicates to clients the supported planning mode" precisely so that a client can "fail fast if the supported scanning mode is not available on the client."

So the spec negotiates idempotency. It negotiates who plans the scan. It negotiates whether the client can accept a vended credential at all. It does not negotiate whether the reader about to receive that credential will apply a row filter, and there is no defined way for it to. A catalog returning read-restrictions to a library that has never heard of the field gets back exactly what it gets from a conforming engine: a 200, and silence.

The proposal is candid about this. Its rationale says the expectation is that the rules are applied by the client, which brings an implicit requirement to have a "trusted partner", and it puts the establishment of that trust — mTLS, OAuth, whatever a deployment chooses — explicitly out of scope. That is an honest design note. It is also the only place the idea appears. In a specification of six thousand lines, the word trust does not occur. Neither does the word audit.

Two reference clients suggest how the asymmetry will land. In PyIceberg, the type that parses the load-table response has a field for storage-credentials and no field for read restrictions. In iceberg-rust, on which at least one production catalog is built, the same response type has storage_credentials: Option<Vec<StorageCredential>> and, again, nothing for restrictions.

The fair reading of that is not an accusation. The change is fifteen days old, specifications are supposed to lead implementations, and both projects will get to it. The point is what happens during the interval, and it is structural rather than temporary: there is no mechanism by which the interval ends visibly. A catalog cannot tell that it spent six months returning masks to a client that discarded them. The failure mode of this control is not an error. It is a correct-looking result with the real values in it.

The strongest argument against all of the above is that client-side enforcement is not a compromise here — it is the only design that keeps the lakehouse open.

The alternative is for the catalog to read the data, which means the catalog becomes a query engine, which is the closed, single-vendor architecture that the Iceberg REST protocol exists to prevent. The spec does offer a genuinely stronger option in remote signing, formalised in July, where the client never receives a storage token and each request is signed by the catalog; it buys tighter control and pays for it in a round trip per file. Short of that, the honest observation is that vended credentials already rest on client behaviour. A Spark executor holding a scoped S3 token could always have read the raw Parquet and ignored whatever the catalog wished.

And the readers really have been a list you could write down. Apache Polaris names its interoperability targets in its README: Doris, Flink, Spark, Dremio OSS, StarRocks, Trino. Six systems, each with an organisation behind it and a contract in front of it. In that world, "trusted partner" is not a euphemism. It is a description. Against the realistic alternative — no masking at all, or masking reimplemented per engine in six dialects that drift — a single vocabulary the catalog can state once is a substantial gain. Lakekeeper makes exactly this pitch: "Define access control once, in the catalog, and enforce it across every compute engine — no rules duplicated per engine. Every request is checked against your policy before any data is read, and recorded."

I think that is right about the design and wrong about the bookkeeping, and the same repository shows the seam. Lakekeeper also ships an Open Policy Agent bridge, whose description is that it "exposes Lakekeeper permissions via Open Policy Agent so engines like Trino enforce them." Both sentences are true, and the second one explains the first. The catalog checks the request and records that it checked. The engine enforces the answer, or does not, elsewhere, unrecorded.

Lakekeeper's one-line description of itself is "access control, credential vending and audit for every engine and AI agent." That phrase is where the trusted-partner model runs out.

There are several independent MCP servers for Iceberg catalogs on GitHub. One of them advertises "Claude Desktop as your Iceberg Data Lake Catalog", is built on PyIceberg, translates natural language to SELECT and INSERT, and is configured through a JSON block in the desktop app containing ICEBERG_CATALOG_URI, AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY. Static bucket keys, on a laptop. That client will never see a vended credential, never mind a read restriction, and nothing about it is rogue — it is a reasonable weekend project, installable with one command, and it is what "every engine and AI agent" means in practice. A model that depends on being able to name your readers does not survive a reader population anyone can add to.

There is a quieter version of the same problem for readers that do comply perfectly. Two of the nine actions are hashes. sha-256-global is "deterministic across all queries and readers: the same input always produces the same output" — a stable pseudonym that can be joined across tables, and across anyone else holding the same masked column. sha-256-query-local salts per query and cannot. On a governance slide both read as hashed. Which one a column gets is set in a catalog configuration by whoever wrote the policy, and the analyst cannot tell the difference by looking. Even mask-alphanum preserves the length of the string, the positions of its digits, and its punctuation; xxxxxxxnnnnnnnn@xxxxxx.xxx is not much information, but it is not none.

None of this makes read restrictions a bad addition. A precise, portable vocabulary for masking is better than nine proprietary ones, and it is a good deal better than the row-level security that exists today in most lakehouses, which is a document. It just is not a boundary, and the specification's own silences say so: no negotiation, no attestation, no record. The thing an architect should carry out of this is narrow and practical. Treat read-restrictions as a strong default applied by cooperating software, put the real boundary where the credential is — separate prefixes, remote signing for the regulated tables, a serving layer for anything a subject could sue you over — and ask, in the meeting where the catalog is chosen, the question that has no field in any schema: which of our readers is a program somebody wrote last week?

Afterwards, the catalog's log will show that it vended a credential and attached a restriction to it. That is all it can show. Whether the mask was applied is a fact about a process on someone else's machine, and the only artefact that ever held the answer was a query result nobody kept.

What this is argued from

Reporting and primary material the piece rests on, dated at the time of writing. The interpretation is mine; the facts belong to these.

  1. Iceberg REST Catalog OpenAPI specification Apache Iceberg · 2026-09-18
  2. Spec — Add finer grained read restrictions as part of loadTable (#13879) Apache Iceberg · 2026-09-03
  3. Commit history for open-api/rest-catalog-open-api.yaml Apache Iceberg · 2026-09-18
  4. PyIceberg REST catalog client Apache Iceberg (PyIceberg) · 2026-09-18
  5. iceberg-rust REST catalog types Apache Iceberg (iceberg-rust) · 2026-09-18
  6. Lakekeeper — Apache Iceberg REST Catalog in Rust Lakekeeper · 2026-09-18
  7. Apache Polaris README Apache Polaris · 2026-09-18
  8. MCP Iceberg Catalog server ahodroj · 2026-09-18

Editorials on this site are written to be argued with. If you think the reading is wrong, it probably is in some particular way, and that is the useful part.

lakehouseaccess controlcatalogsdata governanceagents