Elasticsearch access governance — every query reviewed before it runs
Last updated
AccessFlow governs Elasticsearch through the same submit, analyse, approve, execute pipeline as a SQL database. Queries are JSON rather than statements, so the scripting ban is a scan of the whole request tree, and row-level security is applied by wrapping the user query in a bool filter — a rewrite that provably cannot widen what the original query matched.
- Family
- Search
- Query language
- JSON Query DSL
- Runs as
- Engine plugin (SHA-256 pinned)
- Default port
9200- Default SSL mode
REQUIRE- Install
- One-click from the connector catalog
How AccessFlow connects
It is an engine plugin rather than an in-process JDBC datasource: a standalone shaded JAR, pinned in the connector catalog by URL and SHA-256, downloaded on first use, hash-verified, and loaded into an isolated classloader. If the hash does not match, it does not load. In an air-gapped install the JAR can be pre-seeded into the driver cache and the runtime told never to reach the network. The plugin works at the low-level REST client so it controls every header, which means it sends no product-check or version-compatibility media type of its own.
The base URL is built from host and port (9200 by default) with the scheme from the SSL mode, or supplied verbatim as an override. Authentication is HTTP basic, or an API key stored in its own encrypted field and sent as an ApiKey authorization header. One REST client is cached per datasource.
What AccessFlow understands
A query is a JSON envelope: a single object whose first recognised command key names the operation, and whose value is the target index or pattern. search and count are reads — and get and mget are lowered into a search over an ids query, so there is exactly one row-security path rather than three. index and bulk are inserts, update_by_query is an update, delete_by_query is a delete, and index and mapping management is DDL.
Grants target the lowercased index name or pattern.
What it refuses
Painless scripting is refused anywhere in the request tree, not merely at the top level: script, script_fields, script_score, scripted_metric and runtime_mappings. Cluster-level APIs are refused, and so is any index name beginning with an underscore or a dot, which is how system indices are addressed.
A bulk request may carry only index actions. Update and delete actions inside a bulk are rejected so that the submission classifies cleanly as a single insert, rather than being one reviewable unit that quietly performs three different kinds of write.
Row-level security and masking
The user's query is wrapped, never merged: it becomes the must clause of a new boolean query whose filter carries the policy clauses. Wrapping is what makes the rewrite provably non-widening — merging into an existing boolean query could change how the original clauses combine.
Equality becomes a term clause, inequality a negated term, ranges become range clauses, and set membership becomes terms. An empty value list becomes a negated match-all, which matches nothing — fail-closed by construction. A write into a policied index is rejected, since a write cannot be filtered.
One honest caveat: term clauses match exact keyword fields. A policy pointed at an analysed text field will match tokens rather than the literal value, which is why introspection surfaces field types — the search analogue of Cassandra's key-column limit, and worth checking when you write the policy.
Masking recurses by dot-path, so a rule on user.email redacts the nested leaf while the rest of the object stays visible, and the top-level column is flagged restricted when it or any descendant has a rule.
What the rewrite actually does
A policy restricting the orders index to the caller's region wraps the submitted query rather than merging into it:
// submitted
{ "search": "orders", "query": { "match": { "status": "OPEN" } } }
// executed — the original query becomes must, the policy becomes filter
{ "search": "orders",
"query": { "bool": { "must": [ { "match": { "status": "OPEN" } } ],
"filter": [ { "term": { "region": "EU" } } ] } } }
Wrapping is what makes the rewrite provably non-widening — merging clauses into an existing boolean query could change how the original ones combine. An empty policy value list becomes a negated match-all, which matches nothing.
Introspection and execution limits
The connection test issues a cluster-info request. Introspection reads the index mappings, surfacing every non-system index as a table and each mapped field, flattened to a dot-path, as a column, with a synthetic id column as the primary key — the only stable identity the engine exposes.
Searches page to one more than the row cap so truncation is detectable, clamped so the offset and size never exceed the index result window. A search that reports itself timed out — which Elasticsearch returns with an HTTP 200 — and a bulk that reports errors are both translated into execution failures, so a partial result never masquerades as success.