Executive Summary — TL;DR
OpenSearch Document Level Security (DLS) is one of the most operationally critical and underutilized features in enterprise SIEM deployments built on Wazuh.
Most organizations configure role-based access at the index level — which is insufficient for multi-tenant architectures where multiple clients, departments, or SOC analysts share the same OpenSearch cluster.
4 Key Insights:
- Index-level RBAC alone is not enough — DLS enforces row-level filtering so each user only retrieves documents matching their authorized scope (e.g.,
agent.id,agent.name, or custom tags). - Wazuh’s OpenSearch Security plugin supports DLS natively via JSON query filters embedded directly in role definitions — no external proxy or filter layer required.
- Production DLS implementation requires careful alignment between Wazuh agent metadata, OpenSearch index mappings, and the DLS query filter syntax.
- Multi-tenancy in OpenSearch with Wazuh goes beyond tenant namespaces — true isolation demands DLS policies applied per role, per tenant, enforced at query time by the security engine.
What you will gain: A production-ready understanding of DLS architecture, hands-on configuration steps from a real SOC environment, diagrams to visualize the access control flow.
Understanding OpenSearch Document Level Security Architecture
What DLS Is and Why Index-Level RBAC Falls Short
OpenSearch, the open-source fork of Elasticsearch maintained by AWS and the community, ships with a powerful Security plugin that provides authentication, authorization, audit logging, and fine-grained access control.
Role-Based Access Control (RBAC) at the index level grants or restricts access to entire indices.
In a Wazuh deployment, all agent logs land in the same index pattern — typically wazuh-alerts-* and wazuh-archives-*.
Granting a SOC analyst read access to wazuh-alerts-* exposes every alert from every monitored agent — regardless of which client or department owns those agents.
This is architecturally unacceptable in shared SOC environments.
Document Level Security solves this by embedding a query filter inside the role definition itself.
When a user with that role executes any search — whether from Wazuh Dashboard, a Kibana Lens visualization, or a direct API call — OpenSearch intercepts the query and applies the DLS filter as a mandatory condition.
The user never sees documents that don’t match their authorized filter.
This happens transparently, at the search engine level, with no application-side logic required.
DLS Internal Mechanics in OpenSearch Security Plugin
The Security plugin intercepts every search request at the Transport layer before it reaches the shard-level query engine.
The DLS filter is parsed as a standard OpenSearch query DSL object and injected as a bool.filter clause into the user’s actual query.
This means:
- The merge is performed server-side — the client cannot bypass it.
- It works with all search APIs:
_search,_msearch,_async_search, scroll, and point-in-time. - Field Level Security (FLS) can be combined with DLS to also restrict which fields are returned per document.
The role definition in roles.yml (or via REST API) looks like this:
yaml
my_tenant_role:
cluster_permissions:
- 'cluster_composite_ops_ro'
index_permissions:
- index_patterns:
- 'wazuh-alerts-*'
dls: '{"term": {"agent.id": "001"}}'
allowed_actions:
- 'read'
- 'indices:data/read/search'
- 'indices:data/read/msearch'The dls value is a raw JSON string representing any valid OpenSearch query DSL.
For multi-tenant deployments, this value is typically parameterized using DLS templating — a feature that allows ${user.name} or custom attribute substitution at query time.
DLS Templating for Dynamic Multi-Tenancy
Static DLS is sufficient for fixed role-to-agent mappings.
But in large SOC environments where the agent roster changes or users are dynamically provisioned, DLS templating is the correct approach.
OpenSearch Security supports substituting user attributes — defined in the backend role, the internal user definition, or fetched from an LDAP/Active Directory attribute — directly into the DLS query at runtime.
Example using extensions user attributes:
yaml
# internal_users.yml
analyst_client_a:
hash: "..."
attributes:
tenant_id: "client_a"yaml
# roles.yml
role_client_a:
index_permissions:
- index_patterns:
- 'wazuh-alerts-*'
dls: '{"term": {"agent.labels.tenant": "${attr.internal.tenant_id}"}}'
allowed_actions:
- 'read'At query time, ${attr.internal.tenant_id} is resolved to client_a — the DLS filter becomes {"term": {"agent.labels.tenant": "client_a"}}.
This pattern scales to hundreds of tenants without creating one static role per client.

Implementing DLS in a Production Wazuh OpenSearch Deployment
Prerequisites and Environment Alignment
Before implementing DLS, the following conditions must be validated in your Wazuh environment:
1. Agent metadata must be indexed consistently.
The field you use as a DLS filter key — typically agent.id, agent.name, or a custom label — must exist as a keyword field in the OpenSearch index mapping.
Text fields with standard analyzers will not work correctly with term queries used in DLS filters.
Verify the mapping:
bash
curl -k -u admin:<REDACTED> \
-X GET "https://localhost:9200/wazuh-alerts-4.x-*/_mapping?pretty" \
| grep -A3 '"agent"'Expected output confirming keyword type:
json
"agent" : {
"properties" : {
"id" : {
"type" : "keyword"
},
"name" : {
"type" : "keyword"
}
}
}2. OpenSearch Security plugin must be active.
bash
curl -k -u admin:<REDACTED> \
https://localhost:9200/_cat/plugins?v | grep securityopensearch-node1 opensearch-security 2.x.x.x3. The securityadmin.sh tool must be accessible for applying configuration changes if you manage security via flat files.
Creating DLS Roles via REST API
The REST API approach is recommended for production — it avoids needing to restart the security configuration and supports automation via Ansible or Terraform.
Create a tenant-scoped role:
bash
curl -k -u admin:<REDACTED> \
-X PUT "https://localhost:9200/_plugins/_security/api/roles/role_tenant_alpha" \
-H 'Content-Type: application/json' \
-d '{
"cluster_permissions": ["cluster_composite_ops_ro"],
"index_permissions": [
{
"index_patterns": ["wazuh-alerts-*", "wazuh-archives-*"],
"dls": "{\"term\": {\"agent.id\": \"005\"}}",
"allowed_actions": [
"read",
"indices:data/read/search",
"indices:data/read/msearch",
"indices:data/read/scroll"
]
}
],
"tenant_permissions": [
{
"tenant_patterns": ["tenant_alpha"],
"allowed_actions": ["kibana_all_read"]
}
]
}'Expected response:
json
{
"status": "CREATED",
"message": "'role_tenant_alpha' created."
}Map the role to a user or backend role:
bash
curl -k -u admin:<REDACTED> \
-X PUT "https://localhost:9200/_plugins/_security/api/rolesmapping/role_tenant_alpha" \
-H 'Content-Type: application/json' \
-d '{
"users": ["analyst_alpha"],
"backend_roles": []
}'json
{
"status": "CREATED",
"message": "'role_tenant_alpha' created."
}Validating DLS Enforcement in Production
After applying the role and mapping, validate DLS behavior by running a search as the restricted user.
Test as the tenant analyst:
bash
curl -k -u analyst_alpha:<REDACTED> \
-X GET "https://localhost:9200/wazuh-alerts-*/_search?pretty" \
-H 'Content-Type: application/json' \
-d '{"query": {"match_all": {}}}'What to verify:
hits.total.valueshould only reflect documents whereagent.id == "005".- Attempting to add a
termfilter for a differentagent.idin the query body must return zero results — the DLS filter overrides the client-side query.
Real terminal output from our SOC validation (sanitized):
json
{
"_shards": {
"total": 5,
"successful": 5,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 1423,
"relation": "eq"
},
"hits": [
{
"_index": "wazuh-alerts-4.x-2025.05.18",
"_source": {
"agent": {
"id": "005",
"name": "[REDACTED]"
},
"rule": {
"level": 7,
"description": "SSH brute force - Multiple failed logins"
}
}
}
]
}
}All 1,423 returned documents belong exclusively to agent.id: 005 — DLS enforcement is confirmed.

Operational Challenges and Best Practices for DLS at Scale
Performance Implications of DLS
DLS adds a query-time overhead because every search is rewritten with an additional filter clause before reaching the shards.
In practice, for most Wazuh deployments, this overhead is negligible — term queries on keyword fields are resolved via inverted index lookups, which are O(1) operations.
The critical risk is poorly constructed DLS queries.
Avoid using wildcard, regex, or script queries inside DLS definitions.
These trigger full shard scans and will degrade performance dramatically at scale.
Recommended DLS query patterns (fast):
json
// Single agent
{"term": {"agent.id": "005"}}
// Multiple agents for one tenant
{"terms": {"agent.id": ["005", "006", "007"]}}
// Label-based (requires agent label indexing in Wazuh)
{"term": {"agent.labels.client": "alpha"}}Patterns to avoid in DLS:
json
// AVOID — triggers full shard scan
{"wildcard": {"agent.name": "client-alpha-*"}}
// AVOID — script queries are blocked by default and expensive
{"script": {"script": "doc['agent.id'].value.startsWith('0')"}}Combining DLS with Field Level Security and Tenant Namespaces
OpenSearch Security supports layering multiple access control mechanisms simultaneously:
DLS — filters which documents are visible.
FLS — filters which fields within a document are returned.
Tenant namespaces — isolate Dashboards (saved objects, visualizations, index patterns) per tenant.
A complete production role definition combining all three:
bash
curl -k -u admin:<REDACTED> \
-X PUT "https://localhost:9200/_plugins/_security/api/roles/role_tenant_beta_full" \
-H 'Content-Type: application/json' \
-d '{
"cluster_permissions": ["cluster_composite_ops_ro"],
"index_permissions": [
{
"index_patterns": ["wazuh-alerts-*"],
"dls": "{\"term\": {\"agent.id\": \"012\"}}",
"fls": [
"rule.*",
"agent.*",
"timestamp",
"data.*"
],
"allowed_actions": ["read"]
}
],
"tenant_permissions": [
{
"tenant_patterns": ["tenant_beta"],
"allowed_actions": ["kibana_all_write"]
}
]
}'This role:
- Restricts documents to
agent.id: 012via DLS. - Returns only
rule.*,agent.*,timestamp, anddata.*fields via FLS — sensitive fields likefull_logor internal system fields are excluded. - Grants full read-write access to the
tenant_betaDashboard namespace.
Automating DLS Role Provisioning with Python
In environments where tenants are onboarded programmatically, manual role creation via curl is insufficient.
The following Python script automates DLS role creation and role mapping for new Wazuh tenants:
python
import requests
import json
import urllib3
urllib3.disable_warnings()
OPENSEARCH_URL = "https://localhost:9200"
ADMIN_USER = "admin"
ADMIN_PASS = "<REDACTED>"
def create_dls_role(tenant_name: str, agent_ids: list[str]):
role_name = f"role_{tenant_name}"
dls_filter = json.dumps({"terms": {"agent.id": agent_ids}})
role_payload = {
"cluster_permissions": ["cluster_composite_ops_ro"],
"index_permissions": [
{
"index_patterns": ["wazuh-alerts-*", "wazuh-archives-*"],
"dls": dls_filter,
"allowed_actions": ["read"]
}
],
"tenant_permissions": [
{
"tenant_patterns": [tenant_name],
"allowed_actions": ["kibana_all_read"]
}
]
}
response = requests.put(
f"{OPENSEARCH_URL}/_plugins/_security/api/roles/{role_name}",
auth=(ADMIN_USER, ADMIN_PASS),
headers={"Content-Type": "application/json"},
json=role_payload,
verify=False
)
print(f"[ROLE] {role_name}: {response.status_code} — {response.json()}")
return role_name
def map_role_to_user(role_name: str, username: str):
mapping_payload = {
"users": [username],
"backend_roles": []
}
response = requests.put(
f"{OPENSEARCH_URL}/_plugins/_security/api/rolesmapping/{role_name}",
auth=(ADMIN_USER, ADMIN_PASS),
headers={"Content-Type": "application/json"},
json=mapping_payload,
verify=False
)
print(f"[MAPPING] {role_name} -> {username}: {response.status_code} — {response.json()}")
# Example usage — onboard a new tenant
role = create_dls_role("client_delta", ["022", "023", "024"])
map_role_to_user(role, "analyst_delta")Terminal output from onboarding run (sanitized):
[ROLE] role_client_delta: 201 — {'status': 'CREATED', 'message': "'role_client_delta' created."}
[MAPPING] role_client_delta -> analyst_delta: 201 — {'status': 'CREATED', 'message': "'role_client_delta' created."}This script is production-ready and can be integrated into a Wazuh onboarding pipeline triggered via API gateway or internal ticketing system.
OpenSearch DLS in the Context of AWS OpenSearch Service and Hybrid Deployments
DLS on AWS OpenSearch Service
AWS OpenSearch Service (formerly Amazon Elasticsearch Service) supports Fine-Grained Access Control (FGAC) — AWS’s implementation of the OpenSearch Security plugin DLS/FLS capabilities.
The configuration approach differs slightly from self-managed deployments:
- DLS roles are managed via the OpenSearch Dashboards Security UI or the REST API —
securityadmin.shis not available. - IAM roles can be mapped to OpenSearch internal roles — enabling AWS-native identity integration.
- Master user credentials are required for initial role provisioning.
For Wazuh deployments targeting AWS OpenSearch Service, the same DLS query syntax applies — but agent data must be forwarded to the AWS-managed cluster via Wazuh’s Filebeat output module.
yaml
# Wazuh Filebeat output targeting AWS OpenSearch Service
output.elasticsearch:
hosts:
- "https://[REDACTED-DOMAIN].eu-west-1.es.amazonaws.com:443"
ssl.certificate_authorities:
- /etc/ssl/certs/ca-certificates.crt
username: "wazuh-indexer"
password: "<REDACTED>"DLS role definitions on AWS remain identical in syntax — only the administration method changes.
Hybrid SOC Architecture with Self-Managed and Cloud OpenSearch
Large enterprises often operate hybrid SOC architectures — a self-managed Wazuh cluster for on-premise assets, and an AWS OpenSearch Service cluster for cloud workloads.
In this model:
- On-premise Wazuh agents write to a self-managed OpenSearch cluster — DLS configured via
securityadmin.shor REST API. - Cloud workload agents (EC2, ECS, Lambda logs via Wazuh agent or CloudWatch integration) write to AWS OpenSearch Service — DLS configured via Fine-Grained Access Control.
- Both clusters are exposed to SOC analysts via a unified Wazuh Dashboard pointing to their respective OpenSearch endpoints.
DLS policies must be mirrored across both clusters to maintain consistent tenant isolation.
Automation via the Python provisioning script above — parameterized for both endpoints — is the recommended approach

Advanced FAQ: OpenSearch Document Level Security for IT Architects and SOC Engineers
Q1: Can DLS filters reference multiple fields simultaneously?
Yes — any valid OpenSearch query DSL is accepted inside the dls field.
You can use bool.must to combine conditions:
json
{
"bool": {
"must": [
{"term": {"agent.id": "005"}},
{"term": {"agent.labels.environment": "production"}}
]
}
}This restricts the user to documents from agent.id: 005 that are also tagged as production environment.
Q2: Does DLS work with OpenSearch aggregations and Wazuh Dashboard visualizations?
Yes — DLS filters are applied to all search operations, including aggregations.
When a Wazuh Dashboard visualization performs a terms aggregation over the wazuh-alerts-* index, the underlying search request passes through the Security plugin — the DLS filter is injected before the aggregation is computed.
The user will only see aggregated data derived from their authorized document scope.
Q3: How do you handle DLS for Wazuh agents that change teams or clients?
The cleanest approach is label-based DLS rather than agent.id-based DLS.
Assign a label to each Wazuh agent in ossec.conf:
xml
<labels>
<label key="client">alpha</label>
<label key="environment">production</label>
</labels>These labels are indexed under agent.labels.* in OpenSearch.
The DLS filter then becomes:
json
{"term": {"agent.labels.client": "alpha"}}When an agent changes client assignment, only the label needs updating — the DLS role definition remains unchanged.
Q4: What happens if the DLS query references a field that doesn’t exist in the index mapping?
The DLS filter will still execute — but will return zero documents, since no documents match a field that doesn’t exist.
This silently breaks the user’s visibility without producing an error — which is a dangerous misconfiguration.
Always validate field existence in the mapping before deploying a DLS role in production.
Q5: Is DLS compatible with Wazuh’s threat intelligence and vulnerability modules?
Yes — the wazuh-alerts-* index pattern covers alerts generated by all Wazuh modules, including:
- Vulnerability Detection (
rule.groups: vulnerability-detector) - Threat Intelligence / VirusTotal integration
- CIS Benchmark compliance alerts
DLS filtering by agent.id or agent.labels.* applies uniformly across all these alert types — a tenant analyst sees vulnerability alerts only for their authorized agents.
Q6: What should IT architects consider before deploying DLS in a large-scale Wazuh SOC?
Three critical architectural decisions:
- Field consistency — Establish a standard for DLS filter fields (
agent.id,agent.name, oragent.labels.*) and enforce it across all Wazuh agents before deploying DLS roles. - Role provisioning automation — Manual DLS role management does not scale. Implement a programmatic provisioning pipeline from day one.
- Audit logging — Enable OpenSearch Security audit logging to verify DLS is enforcing correctly and to maintain a compliance audit trail of which users accessed which document scopes.
OpenSearch Document Level Security is not a niche configuration option — it is a fundamental requirement for any Wazuh deployment serving multiple clients, departments, or regulated environments.
The combination of DLS with Field Level Security and tenant namespace isolation gives you a complete, layered access control model that enforces data sovereignty at the document level — directly inside OpenSearch — without requiring application-side filtering logic.
From agent label standardization to Python-automated role provisioning and hybrid cloud alignment, the patterns documented here reflect production-grade SOC engineering.
Whether your cluster is self-managed or deployed on AWS OpenSearch Service, opensearch DLS gives your architecture the data isolation guarantees that enterprise multi-tenancy demands.
Discover more from Solide Info | The Engineer’s Authority on Cyber Defense
Subscribe to get the latest posts sent to your email.



