FlatQL
Sign in Start free

Blog

Per-index access control for Elasticsearch without creating cluster users

Most teams query Elasticsearch with one admin credential pasted into a shared tool. FlatQL keeps a single credential on the cluster side and puts the per-person, per-index rules in front of it.

2026-11-04 · FlatQL · 6 min read

How a team ends up sharing one admin key#

It starts reasonably. Someone sets up the cluster, creates an API key with enough rights to do everything, and puts it in the one tool the team uses. A second engineer needs to look at orders, so the key goes into a shared vault entry. A contractor needs logs-* for a week. A product manager wants to count signups. Each of them gets the same key, because minting a narrower one means someone with cluster admin rights has to stop and do it.

Six months later the key is in four laptops' keychains, two Slack threads and a notebook. Nobody can say who ran the DELETE on the staging index, because every request looked like the same principal. Rotating the key means finding every copy.

What cluster-native roles cost#

Elasticsearch and OpenSearch both have proper security models: users, roles with index privileges, API keys with restricted descriptors. They are the right place for machine identities and for the boundary between services. For people, they carry an administration cost that is easy to underestimate.

Every new teammate is a cluster change, made by someone with security privileges, on every cluster the team touches. Every pattern change, "also give her orders-eu-*", is another. Index privileges live on the cluster, so there is no single screen that shows who can read what across production, staging and the analytics cluster. Hosted services add their own layer: identity providers, fine-grained access control, IAM mappings. The result is that the admin key stays in circulation because the alternative is a ticket.

FlatQL does not replace that model. It adds a layer that is cheaper to operate for the people who write queries.

FlatQL's model#

One connection, one credential, never shown#

A team connection holds the cluster URL and one set of credentials: basic auth, an API key or a bearer token, plus optional secret headers for a proxy or SSO gateway. The secrets are encrypted at rest with Fernet in FlatQL's vault; the key comes from a file on the server, the OS keychain on the desktop app, or memory only for a session vault. The API that returns a connection to the browser never includes a secret. It returns booleans, password: true, api_key: false, so a member can see that a credential is configured and nothing else. There is no reveal button. The security page covers the vault in more detail.

Roles with read or write per index pattern#

A workspace has an owner, admins and members. Owners and admins see every index on every team connection. Members see nothing until they are granted something. A grant is three things: a connection, a list of index patterns, and a write flag.

Grants come from two places and are unioned. An admin can attach rules directly to a member: orders, orders-* and organizations on the production connection, read only. Or an admin can create a named role, "Data analyst", with its own grants per connection and assign it to several members at once. A member with both direct rules and roles gets the union of all patterns, and may write if any grant allows it.

Patterns are matched with ordinary shell wildcards, so logs-* covers logs-2026.11.04 and * covers everything on that connection. A connection with no grant for a member is not shown to them at all.

Enforcement happens before the cluster#

Every request a member makes goes through FlatQL's backend, and every data endpoint checks the member's scope before anything is sent to the cluster. The sequence is the same each time:

  1. Resolve the connection and compute the caller's scope: full access for owners and admins, the union of grants for members, nothing for a disabled member.
  2. Check every index the request names. For a browse or export request that is the index parameter. For SQL, FlatQL parses the statement and collects every index in FROM and JOIN, skipping CTE names, and checks each one.
  3. Check the write flag for anything that modifies data.

Patterns in SQL are handled carefully. A member granted logs-* may query logs-* or logs-2026*, because the grant covers the whole pattern. A member granted logs-2026* may not query logs-*, because that pattern could resolve to indices outside the grant; the request is refused with "Your access does not cover the pattern logs-*". Concrete index names are matched against the grants one by one.

Index listings are filtered the same way, so autocomplete and the schema tree show only what the member may read.

-- member granted: orders, organizations (read) on production
SELECT o.order_id, g.name
FROM orders o
JOIN organizations g ON g.primary_domain = o.customer_id
WHERE o.status = 'paid'
LIMIT 20;
-- runs: both indices are in the grant

SELECT * FROM suppliers LIMIT 20;
-- refused before any request leaves FlatQL: "You have no access to suppliers."

Because the check runs against the parsed statement, there is no way to smuggle an index name past it in a comment or a string; only tables in FROM and JOIN count, and all of them must pass.

Read-only connections#

Separately from member grants, a connection has its own safety settings. A read-only connection accepts GET and HEAD requests and the POST endpoints that only read: _search, _count, _msearch, _sql, _field_caps, _pit, _explain and the rest of that family. Anything else, a PUT mapping, a _delete_by_query, a _bulk, is refused with a read-only error. The lock can be lifted on a connection explicitly, and that change is written to the audit log.

Production connections can start read-only: a guard-rail setting makes every new connection tagged production read-only unless the person creating it chooses otherwise. Two more guard-rails apply to all connections: wildcard and multi-index deletes are blocked, and destructive requests such as deleting an index, closing it, reindexing or changing cluster settings require an explicit confirmation.

Audit log and access graph#

Every change to who can do what is written to an audit log with the acting user, the action, the target and a detail string: invitations, role changes, member removals and disabling, direct access rule changes with the full rule set, named role creation and edits, role assignments. Exports and bulk actions are logged too, with the statement or index and the format. Owners and admins can read the log from the Team page.

Alongside the audit log, every statement anyone runs lands in query history with the user, connection, duration and row count. Members see their own history; owners and admins see everyone's. The two together answer the question the shared key could not: who ran what, on which indices, and who gave them access.

The access graph draws the same data as nodes and edges: the three system roles, the named roles with their grant counts, and the members connected to each. When someone asks why a contractor can read orders, the graph shows the role that grants it.

What the cluster sees, and what it does not#

Be clear about the boundary. The cluster authenticates one principal: the credential stored on the FlatQL connection. Every query from every member arrives as that principal. Cluster-side audit logs will show FlatQL, not the member. FlatQL's own history and audit log carry the per-person record.

That has two consequences. First, the connection credential should be scoped on the cluster to what the team as a whole needs: a read-only API key for a connection that is read-only in FlatQL, index privileges limited to the indices the team's grants mention. Cluster security remains the last line. Second, FlatQL's rules apply to requests that go through FlatQL. Someone holding the raw credential bypasses them, which is why the credential is never shown.

This is a complement to cluster security, not a replacement. The cluster decides what the connection may do; FlatQL decides which person may use which part of that, and records it.

Setting it up#

Create a team connection with a credential scoped for the team. Invite members. For each one, either attach rules directly or create a named role and assign it. Mark production connections read-only. Check the access graph once; from then on, the audit log tells you when it changes. The teams reference walks through each screen, and the index-level access page compares this with the cluster-native approach.

A workspace starts on the Free plan with one connection. Create an account, add the cluster and invite the first teammate.

Start on Free. It stays free.

Connect a cluster and run a join in five minutes. No card, no expiry.

Guides

How FlatQL handles the jobs Elasticsearch makes hard.