Fine-Grained Access Control in OpenSearch: Roles, Users, and Field-Level Security
OpenSearch's security plugin provides index-level permissions, field-level exclusions, and field masking out of the box. All of these are configurable through the Security REST API, which means you can automate role provisioning in CI/CD pipelines without touching the Dashboards UI. This post documents two concrete access control scenarios tested on a live OpenSearch 2.19.1 cluster managed by FoundryDB, with real HTTP responses showing enforcement in action.
All commands use YOUR_OPENSEARCH_HOST and YOUR_PASSWORD as placeholders. Note that the Security API uses the _plugins/_security prefix.
Prerequisites
- A running FoundryDB OpenSearch cluster.
- An index with data (a
productsalias works well for the examples below).
Scenario 1: Read-Only Access to a Single Index
Step 1: Create the Role
curl -u app_user:YOUR_PASSWORD -k \
-X PUT "https://YOUR_OPENSEARCH_HOST:9200/_plugins/_security/api/roles/products_reader" \
-H "Content-Type: application/json" \
-d '{
"cluster_permissions": ["cluster_composite_ops_ro"],
"index_permissions": [
{
"index_patterns": ["products*"],
"allowed_actions": [
"read",
"indices:data/read/search",
"indices:data/read/get"
]
}
]
}'
cluster_composite_ops_ro allows the user to run searches (which internally require a cluster-level check). Without it, even correctly granted index permissions will fail at the cluster level.
Step 2: Create the User
curl -u app_user:YOUR_PASSWORD -k \
-X PUT "https://YOUR_OPENSEARCH_HOST:9200/_plugins/_security/api/internalusers/catalog_reader" \
-H "Content-Type: application/json" \
-d '{
"password": "CatalogReader2026!",
"backend_roles": [],
"attributes": {}
}'
Step 3: Map the User to the Role
curl -u app_user:YOUR_PASSWORD -k \
-X PUT "https://YOUR_OPENSEARCH_HOST:9200/_plugins/_security/api/rolesmapping/products_reader" \
-H "Content-Type: application/json" \
-d '{
"users": ["catalog_reader"]
}'
Step 4: Verify Enforcement
Search on the allowed index:
curl -u catalog_reader:CatalogReader2026! -k \
"https://YOUR_OPENSEARCH_HOST:9200/products/_search" | jq '.hits.total.value'
Returns 4. Access permitted.
Write attempt on the allowed index:
curl -u catalog_reader:CatalogReader2026! -k \
-X POST "https://YOUR_OPENSEARCH_HOST:9200/products/_doc" \
-H "Content-Type: application/json" \
-d '{"sku": "HACK", "name": "Unauthorized write", "price": 0}'
{"status": 403, "error": "security_exception"}
Access attempt on a different index:
curl -u catalog_reader:CatalogReader2026! -k \
"https://YOUR_OPENSEARCH_HOST:9200/logs-app/_search"
{"status": 403, "error": "security_exception"}
All three enforcement points behave as expected: reads on products* succeed, writes are blocked, and access to unrelated indices is blocked.
Scenario 2: Field-Level Security and Field Masking
Some fields contain sensitive data that should not be returned in API responses. OpenSearch supports two mechanisms: field exclusion (the field is absent from the response entirely) and field masking (the field is present but the value is replaced with a SHA-256 hash).
Step 1: Create the Masked Role
curl -u app_user:YOUR_PASSWORD -k \
-X PUT "https://YOUR_OPENSEARCH_HOST:9200/_plugins/_security/api/roles/products_masked" \
-H "Content-Type: application/json" \
-d '{
"cluster_permissions": ["cluster_composite_ops_ro"],
"index_permissions": [
{
"index_patterns": ["products*"],
"allowed_actions": ["read", "indices:data/read/search", "indices:data/read/get"],
"fls": ["~price"],
"masked_fields": ["sku"]
}
]
}'
fls (field-level security) accepts a list of field names. The ~ prefix means exclude. masked_fields replaces the value with a SHA-256 hash before returning it.
Step 2: Create the User and Map the Role
curl -u app_user:YOUR_PASSWORD -k \
-X PUT "https://YOUR_OPENSEARCH_HOST:9200/_plugins/_security/api/internalusers/catalog_masked" \
-H "Content-Type: application/json" \
-d '{"password": "CatalogMasked2026!"}'
curl -u app_user:YOUR_PASSWORD -k \
-X PUT "https://YOUR_OPENSEARCH_HOST:9200/_plugins/_security/api/rolesmapping/products_masked" \
-H "Content-Type: application/json" \
-d '{"users": ["catalog_masked"]}'
Step 3: Verify the Response
curl -u catalog_masked:CatalogMasked2026! -k \
"https://YOUR_OPENSEARCH_HOST:9200/products/_doc/1" | jq '._source'
Result from the test (document for "Wireless Keyboard"):
{
"name": "Wireless Keyboard",
"sku": "c4ce000966d57111dd1ca5cea5512c0e8d9b58daceec909a97f090d0660eba6f"
}
The price field is completely absent. The sku field is present but the original value (KB-001) has been replaced with its SHA-256 hash. An application that logs all API responses will see the hash rather than the real SKU. The field name is preserved, which means consuming code does not break on the absence of the key.
Listing and Auditing Roles
# List all custom roles
curl -u app_user:YOUR_PASSWORD -k \
"https://YOUR_OPENSEARCH_HOST:9200/_plugins/_security/api/roles" | \
jq 'keys | map(select(startswith("products")))'
# Check the mapping for a specific role
curl -u app_user:YOUR_PASSWORD -k \
"https://YOUR_OPENSEARCH_HOST:9200/_plugins/_security/api/rolesmapping/products_reader"
Because the Security API is fully REST-scriptable, role provisioning fits naturally into a Terraform module, a GitHub Actions workflow, or any script that runs after cluster creation. FoundryDB exposes the cluster endpoint and credentials through the API, so the entire security configuration can be automated from your infrastructure pipeline.
Field Masking vs Field Exclusion
| Mechanism | Syntax | Effect | Use case |
|---|---|---|---|
| Field exclusion | "fls": ["~fieldname"] | Field absent from response | Completely hide sensitive data |
| Field masking | "masked_fields": ["fieldname"] | Field present, value is SHA-256 hash | Audit log compatibility, field must be present |
Use exclusion when downstream code should not see the field at all. Use masking when the consuming code expects the field key to be present (for example, a schema-validated response) but you do not want the raw value exposed.
What's Next
- Combine fine-grained access control with index lifecycle management to restrict which users can manage lifecycle policies.
- Access control applies to snapshot repositories too: scope backup permissions the same way you scope query permissions.
- Restrict PPL (Piped Processing Language) queries to specific log indices per team using the same role and mapping pattern shown here.
Provision a secure OpenSearch cluster at foundrydb.com. Documentation at docs.foundrydb.com.