Securing ISM policies with role-based access control

Lock down who can create, attach, and trigger OpenSearch Index State Management policies so that an unscoped credential can never fire a premature tier migration, delete indices on a schedule, or stall a replication pipeline.

Automated lifecycle transitions run with real destructive power: a single over-privileged token can force a hot → cold migration during peak ingest, break a follower’s write block, or lock an index read-only. This procedure maps the OpenSearch Security plugin permission model onto ISM-specific cluster and index actions, then walks through creating least-privilege roles, binding them to machine identities, and validating the boundary end to end. It builds directly on the Security & Access Boundaries baseline and the endpoints introduced in Index Lifecycle Basics.

Prerequisites

The ISM permission surface

The Security plugin intercepts every ISM REST call and every background scheduler task. Enforcement splits across two scopes: cluster-level permissions govern policy CRUD and scheduler execution, while index-level permissions govern policy attachment, state queries, and manual transitions. A scope mismatch surfaces as a security_exception mid-transition — often long after the role was created, when the scheduler first tries to act.

ISM RBAC least-privilege boundary: architect versus operator Two role cards sit above two endpoint panels. The Policy architect role (human, high-trust) holds cluster:admin ism/policy/write, /delete and ism/change, and its grant arrow points to the destructive control-plane panel containing PUT and DELETE _plugins/_ism/policies/{id} and POST _plugins/_ism/change_policy. The ISM operator role (service account, scoped to logs-* and metrics-*) holds only indices:admin ism/policy/attach, /detach, /explain and cluster:monitor ism/policy/get, and its grant arrow points to the scoped data-plane panel containing POST _plugins/_ism/add, POST _plugins/_ism/remove and GET _plugins/_ism/explain. A dashed arrow from the operator toward the control-plane panel is interrupted by a no-entry symbol labelled 403, showing that the operator cannot cross the least-privilege boundary into destructive actions. Separate the author from the operator · one boundary between them holds · may call holds · may call 403 · blocked Policy architect human · small high-trust group cluster:admin/…/ism/policy/write cluster:admin/…/ism/policy/delete cluster:admin/…/ism/change ISM operator service account · scoped to logs-* metrics-* indices:admin/…/ism/policy/attach indices:admin/…/ism/policy/detach indices:admin/…/ism/policy/explain cluster:monitor/…/ism/policy/get Control plane · destructive PUT _plugins/_ism/policies/{id} DELETE _plugins/_ism/policies/{id} POST _plugins/_ism/change_policy Data plane · scoped to patterns POST _plugins/_ism/add/{index} POST _plugins/_ism/remove/{index} GET _plugins/_ism/explain/{index}
Scope Action What it authorizes
cluster cluster:admin/opendistro/ism/policy/* Create, update, delete, and enumerate ISM policies
cluster cluster:monitor/opendistro/ism/policy/* Read policy metadata, execution history, scheduler health
cluster cluster:admin/opendistro/ism/change Force manual state transitions across target indices
index indices:admin/opendistro/ism/policy/* Attach, detach, or explain policies against explicit patterns
index indices:admin/opendistro/ism/* Execute ISM-managed operations (rollover, shrink, force_merge)

Least-privilege design separates policy architects from policy consumers. Architects — a small human group — hold cluster:admin/opendistro/ism/policy/*. Consumers — log ingestion pipelines, monitoring agents, CI/CD deployers — hold only indices:admin/opendistro/ism/policy/* scoped to explicit index patterns, and never cluster-level write or change actions.

Step-by-step: scoping RBAC for ISM policies

1. Define the least-privilege operator role

Create the role that automation will actually use. It can attach, detach, and explain policies on logs-* and metrics-*, and read scheduler state — but cannot create or delete a policy definition.

Shell
curl -sS -X PUT "https://<cluster-endpoint>:9200/_plugins/_security/api/roles/ism_operator" \
  -H "Content-Type: application/json" \
  --cacert /etc/opensearch/certs/ca.pem \
  -u 'security_admin:REPLACE_ME' \
  -d '{
    "cluster_permissions": [
      "cluster:monitor/opendistro/ism/policy/get"
    ],
    "index_permissions": [
      {
        "index_patterns": ["logs-*", "metrics-*"],
        "allowed_actions": [
          "indices:admin/opendistro/ism/policy/attach",
          "indices:admin/opendistro/ism/policy/detach",
          "indices:admin/opendistro/ism/policy/explain"
        ]
      }
    ]
  }'

Expected response:

JSON
{ "status": "CREATED", "message": "'ism_operator' created." }

Gotcha: index_patterns are matched literally against the concrete index name at action time, not against the alias. A rollover write alias like logs-app produces backing indices such as logs-app-000001, which logs-* covers — but a bare pattern of logs-app (no wildcard) will not, and the attach silently 403s on the next backing index.

2. Map the role to a machine identity, not a human

Bind the role to the dedicated service account or an automation group via rolesmapping. Mapping to machine identities keeps credential rotation automatic and stops role scope from drifting when people change teams.

Shell
curl -sS -X PUT "https://<cluster-endpoint>:9200/_plugins/_security/api/rolesmapping/ism_operator" \
  -H "Content-Type: application/json" \
  --cacert /etc/opensearch/certs/ca.pem \
  -u 'security_admin:REPLACE_ME' \
  -d '{
    "users": ["ism_automation_svc"],
    "backend_roles": ["ism-automation"]
  }'

Expected response:

JSON
{ "status": "OK", "message": "'ism_operator' updated." }

Gotcha: if the same principal is also mapped to a broad role such as all_access, OpenSearch takes the union of permissions. Least privilege only holds if the service account carries no other high-privilege mapping — verify with the check in step 5 before trusting the boundary.

3. Create a separate policy-architect role

Policy authorship is a distinct, higher-trust job. Keep it in its own role held by humans, so the automation credential can never rewrite or delete a policy definition.

Shell
curl -sS -X PUT "https://<cluster-endpoint>:9200/_plugins/_security/api/roles/ism_architect" \
  -H "Content-Type: application/json" \
  --cacert /etc/opensearch/certs/ca.pem \
  -u 'security_admin:REPLACE_ME' \
  -d '{
    "cluster_permissions": [
      "cluster:admin/opendistro/ism/policy/write",
      "cluster:admin/opendistro/ism/policy/get",
      "cluster:admin/opendistro/ism/policy/delete",
      "cluster:admin/opendistro/ism/change"
    ],
    "index_permissions": [
      {
        "index_patterns": ["logs-*", "metrics-*"],
        "allowed_actions": ["indices:admin/opendistro/ism/*"]
      }
    ]
  }'

Gotcha: cluster:admin/opendistro/ism/change is what backs POST _plugins/_ism/change_policy — the ability to swap a running index onto a different state machine. Grant it only to architects; in the wrong hands it re-routes lifecycle mid-flight without touching the policy document, so it leaves almost no audit signal.

4. Attach a policy from the scoped service account

With the boundary in place, automation attaches policies over the _plugins/_ism/add/<index> endpoint using the operator credential. This Python client injects the credential from a vault, verifies TLS, and backs off on throttling — the same pattern used across the Python automation for dynamic ISM policy updates workflows.

Python
import time
import requests
from requests.auth import HTTPBasicAuth

CLUSTER_URL = "https://<cluster-endpoint>:9200"
POLICY_ID = "hot_warm_cold_policy"
CA_BUNDLE = "/etc/opensearch/certs/ca.pem"
AUTH = HTTPBasicAuth("ism_automation_svc", "REPLACE_WITH_VAULT_TOKEN")


def attach_ism_policy(index_name: str, policy_id: str) -> dict:
    """Attach an ISM policy to one index, honouring RBAC and 429 backoff."""
    endpoint = f"{CLUSTER_URL}/_plugins/_ism/add/{index_name}"
    payload = {"policy_id": policy_id}

    for attempt in range(3):
        response = requests.post(
            endpoint, json=payload, auth=AUTH,
            verify=CA_BUNDLE, timeout=10,
        )
        if response.status_code == 403:
            # RBAC denial — do not retry, the role is misscoped
            raise PermissionError(f"RBAC denied: {response.text}")
        if response.status_code == 429:
            time.sleep(2 ** attempt)  # scheduler saturated, back off
            continue
        response.raise_for_status()
        return response.json()
    raise RuntimeError("attach throttled after 3 attempts")


if __name__ == "__main__":
    print(attach_ism_policy("logs-app-2026.07.04", POLICY_ID))

Expected response body on success:

JSON
{ "updated_indices": 1, "failures": false, "failed_indices": [] }

Gotcha: a 403 here is a role problem, never a transient one — the retry loop deliberately raises instead of retrying. See the official requests documentation for session pooling if you attach across many indices per run.

5. Scope Cross-Cluster Replication follower permissions

When ISM governs indices participating in replication, the follower cluster enforces its own boundary. A follower index needs indices:admin/opendistro/ism/policy/attach on the follower cluster to run local allocation or tier routing, and the replication account needs cluster:monitor/opendistro/ism/policy/* to poll scheduler progress. Add the follower grant to the operator role on that cluster:

Shell
curl -sS -X PATCH "https://<follower-endpoint>:9200/_plugins/_security/api/roles/ism_operator" \
  -H "Content-Type: application/json" \
  --cacert /etc/opensearch/certs/ca.pem \
  -u 'security_admin:REPLACE_ME' \
  -d '[{ "op": "add", "path": "/cluster_permissions/-",
        "value": "cluster:monitor/opendistro/ism/policy/get" }]'

Gotcha: followers run under a write block, so rollover, force_merge, and shrink will fail even with the right permissions. Attach a read-optimized policy to followers and let those write actions happen on the leader — the same split covered in Index Lifecycle Basics. A misconfigured follower role otherwise produces a silent replication halt rather than a clean error.

Verification commands

Confirm the boundary is exactly what you intended before wiring automation into production.

Read back the active operator role and diff it against your intent:

Shell
curl -sS -X GET "https://<cluster-endpoint>:9200/_plugins/_security/api/roles/ism_operator" \
  --cacert /etc/opensearch/certs/ca.pem -u 'security_admin:REPLACE_ME'

The allowed_actions array must contain only the three policy/{attach,detach,explain} actions — no cluster:admin/opendistro/ism/* should appear.

Resolve the service account’s effective permissions (the union across every mapping), which is the real boundary:

Shell
curl -sS -X GET "https://<cluster-endpoint>:9200/_plugins/_security/api/account" \
  --cacert /etc/opensearch/certs/ca.pem -u 'ism_automation_svc:REPLACE_ME'

Expected roles field lists ism_operator and nothing privileged. If all_access or a *_admin role appears, least privilege is broken.

Confirm a managed index reports the expected policy and scheduler state:

Shell
curl -sS -X GET "https://<cluster-endpoint>:9200/_plugins/_ism/explain/logs-app-2026.07.04" \
  --cacert /etc/opensearch/certs/ca.pem -u 'ism_automation_svc:REPLACE_ME'

A healthy response shows "policy_id": "hot_warm_cold_policy" and a populated state object. A bare {} or a security_exception means the account cannot read scheduler state — grant cluster:monitor/opendistro/ism/policy/get.

Common failures

Symptom Root cause Fix command
403 on _ism/add index_patterns do not match the concrete backing index name GET _plugins/_security/api/roles/ism_operator → widen pattern to logs-*
Rollover stalls, no error Scheduler cannot read state; missing monitor permission PATCH .../roles/ism_operator add cluster:monitor/opendistro/ism/policy/get
security_exception on explain Account holds index attach but not cluster monitor scope add cluster:monitor/opendistro/ism/policy/* and retry
Follower replication halts on force_merge Write action attempted on a write-blocked follower index detach write policy: POST _plugins/_ism/remove/<follower-index>; attach read-only policy
Least privilege “not working” Service account also mapped to all_access (union of grants) GET .../api/account → remove the broad rolesmapping

Frequently asked questions

Why do ISM actions use opendistro in the permission strings?

The action names predate the OpenSearch fork and were never renamed for compatibility. cluster:admin/opendistro/ism/policy/write and indices:admin/opendistro/ism/policy/attach are the current, correct strings on modern OpenSearch — do not substitute an opensearch namespace, it will not match.

Should I map ISM roles to users or to backend roles?

Prefer backend roles (LDAP/OIDC groups or IAM-derived roles) so access follows the identity provider and rotates with it. Map the internal users list only for a break-glass service account whose secret lives in a vault. Mapping a human’s internal user directly is the pattern that rots first.

Does attaching a policy require cluster-level write permission?

No. Attaching (_ism/add) and detaching (_ism/remove) are index-level actions — indices:admin/opendistro/ism/policy/attach and /detach. Only authoring the policy document needs cluster:admin/opendistro/ism/policy/write. That split is exactly what lets an operator account attach without ever being able to rewrite a policy.

Why did a transition run with more privilege than the operator role?

ISM executes a policy under the security context captured when the policy was attached, not the operator’s. If a broad admin attached it, later transitions inherit that scope. Re-attach from the scoped ism_operator account so the running context matches least privilege, as detailed in Security & Access Boundaries.

How do I unblock an index stuck because of an RBAC denial?

Fix the role scope first, then re-drive the failed step with POST _plugins/_ism/retry/<index>. Retrying before correcting the permission just re-hits the same 403. The retry mechanics are covered in Implementing retry logic for stuck ISM transitions.

Up: Security & Access Boundaries · OpenSearch ISM Architecture & Fundamentals