Most clusters we audit have one of two access models: everything runs as elastic, or every app shares one broad service account. Both work until a leaked credential, a runaway dashboard user, or a compliance question shows up. This post covers least-privilege access with roles, API keys, and document- and field-level security. It applies to Elasticsearch with the Elastic Stack security features and, with different syntax in places, to OpenSearch.
The failure modes
- A shared superuser credential in a Logstash config, so one leaked pipeline file exposes every index.
- A Kibana user with
allon*, who can delete a data stream with one click. - An application key that can write to indices it only reads.
- No way to rotate a credential without a deploy of every consumer.
Step 1: Write roles per workload, not per person
Define one role per workload: an ingest writer, a search reader, a dashboard viewer. Grant the minimum index privileges on named patterns.
PUT _security/role/logs_writer
{
"cluster": ["monitor"],
"indices": [
{
"names": ["logs-myapp-*"],
"privileges": ["create_doc", "auto_configure"]
}
]
}
create_doc allows indexing new documents only, with no updates and no deletes. For data streams that is usually all a shipper needs. auto_configure lets it create the data stream on first write. Drop it if your templates and streams are provisioned ahead of time.
A read-only search role:
PUT _security/role/catalog_reader
{
"indices": [
{
"names": ["products-*"],
"privileges": ["read", "view_index_metadata"]
}
]
}
Grant against aliases where you can. products-* as a pattern breaks quietly when someone names an unrelated index products-tmp-export.
Step 2: Use API keys, with expiry
API keys can be created with a role descriptor that is a subset of the creator's permissions, and they can be invalidated individually.
POST _security/api_key
{
"name": "myapp-log-shipper-2025q3",
"expiration": "90d",
"role_descriptors": {
"writer": {
"cluster": ["monitor"],
"indices": [
{ "names": ["logs-myapp-*"], "privileges": ["create_doc"] }
]
}
},
"metadata": { "owner": "platform-team", "ticket": "OPS-1234" }
}
Put the owner in metadata. When you find an unfamiliar key 14 months later, that field is the difference between revoking it and guessing.
Things to check:
GET _security/api_key?active_only=truefor a list of live keys. Anything without an expiry is a finding.- Rotation: create the new key, deploy it, confirm the old key shows no recent use in your audit or proxy logs, then
DELETE _security/api_keywith the old id. - Keys created by a user inherit that user's privileges at most. If the user's role shrinks, the key's effective permissions shrink with it.
Use cross-cluster API keys for cross-cluster search or replication rather than sharing a user's credentials between clusters.
Step 3: Field- and document-level security
Use these when one index serves several audiences. For example, support staff can see orders but not card metadata, and each regional team sees only its region.
PUT _security/role/support_orders
{
"indices": [
{
"names": ["orders"],
"privileges": ["read"],
"field_security": {
"grant": ["*"],
"except": ["payment.*", "customer.email"]
},
"query": "{\"term\": {\"region\": \"emea\"}}"
}
]
}
Trade-offs, stated plainly:
- Document-level security adds the role query to every search. A cheap
termfilter costs little. A complex or scripted query on a hot index can measurably raise p99, so measure before and after. - Both features are license-gated in Elastic's distribution (check your subscription tier). OpenSearch includes them in its security plugin, configured through
roles.ymlor the security API instead. - If the audiences are very different, separate indices are simpler to reason about than a clever role query.
Step 4: Kibana spaces and feature privileges
Index privileges alone do not control what a user can do in Kibana. Add Kibana feature privileges to the role, scoped to a space:
PUT _security/role/dash_viewer
{
"indices": [
{ "names": ["logs-*"], "privileges": ["read", "view_index_metadata"] }
],
"applications": [
{
"application": "kibana-.kibana",
"privileges": ["feature_dashboard.read", "feature_discover.read"],
"resources": ["space:ops"]
}
]
}
Step 5: Audit what you did
Run these checks against the cluster:
GET _security/_authenticateusing each credential to confirm the roles it resolves to.POST _security/user/_has_privilegeswith the actions you expect to be denied, such asdelete_indexon a production pattern.- Try the write the key should not be able to do. A 403 is the passing result.
- Enable audit logging for
access_deniedandauthentication_failedevents at minimum. Full audit logging on a busy cluster produces a lot of volume, so filter by event type and index it with an ILM policy.
OpenSearch notes
- Roles live in the security plugin:
PUT _plugins/_security/api/roles/logs_writerwithindex_permissionsand action groups such ascrudorcreate_index. - Tenants play the role Kibana spaces play in OpenSearch Dashboards.
- API key support differs from Elasticsearch. Check what your OpenSearch version offers; many teams use internal users or roles mapped from SAML/OIDC backend roles instead.
What to do this week
- List every credential your pipelines and apps use. Count how many are superusers.
- Replace each with a workload-specific role and an expiring API key.
- Record an owner for each key in metadata.
- Verify denials, not only successes.
If the access model is tangled with an upcoming migration or upgrade, a Cluster & Cost Review can include a security pass. Send us the cluster situation through the contact form.