This guide defines the least-privilege roles for the Elastic Security MCP app on stateful (self-managed and Elastic Cloud Hosted) deployments. Two paths:
- Quickstart — assign a built-in Kibana role plus a small index-privileges add-on. Recommended for most users.
- Advanced — Custom roles — fully scripted role JSON. Use when you need to provision via API/IaC, or want finer-grained control than the built-ins offer.
Space ID: Kibana index patterns include a
<space-id>segment (e.g.,.alerts-security.alerts-<space-id>). For most deployments this isdefault. Replace<space-id>with your actual space ID throughout this guide. The app currently targets thedefaultspace.
Serverless: This guide targets stateful deployments. For Elastic Cloud Serverless Security projects, see permissions-serverless.md.
If you just want a working role and aren't picky about going through the UI or using built-ins, paste these three commands into Stack Management → Dev Tools (as elastic or any user with manage_security). Replace <space-id> with your space (typically default) and pick a strong password.
PUT /_security/role/mcp_app_full
{
"cluster": ["monitor"],
"indices": [
{
"names": [
".alerts-security.alerts-<space-id>",
".alerts-security.attack.discovery.alerts-<space-id>",
".adhoc.alerts-security.attack.discovery.alerts-<space-id>",
".internal.alerts-security.alerts-<space-id>-*",
".internal.alerts-security.attack.discovery.alerts-<space-id>-*",
".internal.adhoc.alerts-security.attack.discovery.alerts-<space-id>-*",
"logs-*",
"risk-score.risk-score-latest-*"
],
"privileges": ["read", "write", "monitor", "view_index_metadata"]
}
],
"applications": [
{
"application": "kibana-.kibana",
"privileges": [
"feature_siemV5.all",
"feature_securitySolutionCasesV3.all",
"feature_securitySolutionTimeline.all",
"feature_securitySolutionNotes.all",
"feature_securitySolutionRulesV4.all",
"feature_securitySolutionAlertsV1.all",
"feature_securitySolutionAssistant.all",
"feature_securitySolutionAttackDiscovery.all",
"feature_actions.all"
],
"resources": ["space:<space-id>"]
}
]
}
PUT /_security/user/mcp_app_user
{
"password": "<choose-a-strong-password>",
"roles": ["mcp_app_full"]
}
POST /_security/api_key/grant
{
"grant_type": "password",
"username": "mcp_app_user",
"password": "<the password you chose above>",
"api_key": { "name": "mcp-app-full" }
}
Use the encoded field from the last response as the elasticsearchApiKey for your cluster entry in CLUSTERS_JSON (or CLUSTERS_FILE) — see Cluster configuration.
Targets 9.4+. For 9.3 or 9.0–9.2, swap the Kibana feature names — see the version-specific tables. For a strict read-only key, see the Read-Only Role.
Prefer built-in Kibana roles (editor/viewer) instead? See the Quickstart below.
Two pre-built Kibana roles cover the entire Kibana feature surface the MCP app needs (Security, Cases, Timeline, Notes, Rules, Alerts, AI Assistant, Attack Discovery, Actions and Connectors). You only need to add a small block of Elasticsearch index privileges on top — Kibana feature privileges require no toggling.
1. Create a companion role for index privileges
Stack Management → Roles → Create role → name it mcp_app_indexes_full, grant Cluster privilege monitor, and grant the following Index privileges:
| Index pattern | Privileges |
|---|---|
.alerts-security.alerts-<space-id> |
read, write, monitor, view_index_metadata |
.alerts-security.attack.discovery.alerts-<space-id> |
read, write, monitor, view_index_metadata |
.adhoc.alerts-security.attack.discovery.alerts-<space-id> |
read, write, monitor, view_index_metadata |
.internal.alerts-security.alerts-<space-id>-* |
read, write, monitor, view_index_metadata |
.internal.alerts-security.attack.discovery.alerts-<space-id>-* |
read, write, monitor, view_index_metadata |
.internal.adhoc.alerts-security.attack.discovery.alerts-<space-id>-* |
read, write, monitor, view_index_metadata |
logs-* |
read, write, monitor, view_index_metadata |
risk-score.risk-score-latest-* |
read, monitor, view_index_metadata |
Why cluster
monitor, index-levelmonitor, andview_index_metadata:_cat/indices/<pattern>(Threat Hunt's index list) needs clustermonitor(to enumerate indices at all) plus index-levelmonitor(which coversindices:monitor/statsandindices:monitor/settings/get)._mapping(Threat Hunt's field-mapping picker) needs index-levelview_index_metadata— that's a separate privilege frommonitor, despite the overlap in name. Neithereditornorviewergrants clustermonitor, so it has to come from this companion role;vieweralso doesn't grantview_index_metadataon the.internal.alerts-security.*backing indices, which is why the companion grants it explicitly.
No Kibana-feature or application privileges in this companion role — those come from editor.
2. Assign roles to a user
Stack Management → Users → Create user (or edit an existing one) → assign both roles:
editor(built-in) — covers all Kibana features for read/write Security workflows.mcp_app_indexes_full(the companion role created above).
3. Create an API key for that user
As an admin (e.g. logged in as elastic), open Dev Tools and grant a key on the user's behalf:
POST /_security/api_key/grant
{
"grant_type": "password",
"username": "<the user you created>",
"password": "<their password>",
"api_key": { "name": "mcp-app-full" }
}
The response includes an encoded field — use that as the elasticsearchApiKey for your cluster entry in CLUSTERS_JSON (or CLUSTERS_FILE). The key inherits the user's combined privileges from editor + the companion role.
1. Create a companion role for index privileges
Stack Management → Roles → Create role → name it mcp_app_indexes_readonly, grant Cluster privilege monitor, and grant the following Index privileges:
| Index pattern | Privileges |
|---|---|
.alerts-security.alerts-<space-id> |
read, monitor, view_index_metadata |
.alerts-security.attack.discovery.alerts-<space-id> |
read, monitor, view_index_metadata |
.adhoc.alerts-security.attack.discovery.alerts-<space-id> |
read, monitor, view_index_metadata |
.internal.alerts-security.alerts-<space-id>-* |
read, monitor, view_index_metadata |
.internal.alerts-security.attack.discovery.alerts-<space-id>-* |
read, monitor, view_index_metadata |
.internal.adhoc.alerts-security.attack.discovery.alerts-<space-id>-* |
read, monitor, view_index_metadata |
logs-* |
read, monitor, view_index_metadata |
risk-score.risk-score-latest-* |
read, monitor, view_index_metadata |
2. Assign roles to a user
Assign both viewer (built-in) and mcp_app_indexes_readonly to the user.
3. Create an API key
Same as above — as an admin, run POST /_security/api_key/grant with the read-only user's credentials and an api_key.name of mcp-app-readonly.
| Surface | editor |
viewer |
|---|---|---|
| All Kibana features (SIEM, Cases, Timeline, Notes, Rules, Alerts, AI Assistant, Attack Discovery, Actions/Connectors) | All | Read |
| Alert acknowledgment, case CRUD, rule CRUD | Yes (via Kibana APIs) | No |
| Sample-data generation | Needs companion write on logs-* (covered above) |
Disabled |
| Threat Hunt index listing | Needs companion monitor on target indices (covered above) |
Same |
The built-ins eliminate the need to specify per-feature privileges like feature_siemV5.all or feature_securitySolutionRulesV4.all. Those names change between minor versions; the built-ins absorb the churn.
- API key is invalid, expired, or malformed
- Verify with:
curl -s -H "Authorization: ApiKey <your-key>" <ELASTICSEARCH_URL>/_security/_authenticate
- Missing Kibana feature privileges — the role needs application privileges on
kibana-.kibana. The Quickstart'seditor/viewercovers these by default. - Check which features are missing: the 403 response body usually names the required privilege.
- Common cause: forgetting to grant privileges in the correct Kibana space.
- If you've followed the documented privileges and still see 403 on rules-read endpoints (e.g.
GET /api/detection_engine/rules/_find), addfeature_siemV4.readto your role's read privileges (andfeature_siemV4.allfor the full-featured role) as a transitional workaround. - The Quickstart path may already include this via the built-in
editor/viewerroles.
- The companion role is missing index-level
monitoron the target index pattern. _cat/indices/<pattern>requires index-levelmonitor; cluster-levelmonitoralone is not sufficient.
- Built-in
editorcovers the AI Assistant, Attack discovery, and Actions/Connectors feature privileges. If you scoped the key narrower thaneditor, restore those grants — Attack Discovery requires all three plus Rules and Alerts.
- Companion role is missing
writeonlogs-*and the alert backing indices. Theeditorbuilt-in does not grant raw indexwrite; it must come from the companion role.
- Index patterns use the Kibana space ID (e.g.,
.alerts-security.alerts-default) - If using a non-default space, update all index patterns in the companion role
- The app currently targets the
defaultspace
- The companion role's index privileges may not cover the alert index for your space
- Check the space ID in the index pattern matches your Kibana space
Use this path when:
- You provision roles via API, Terraform, or other IaC and want a single self-contained role definition.
- You need finer-grained restriction than the built-ins offer (e.g. Cases-only, no Threat Hunt).
- Your users' built-in role assignments are managed elsewhere and you can't add
editor/viewerto them.
Each role below is a single self-contained definition — no companion role required.
| Privilege | Why |
|---|---|
monitor |
_cat/indices for Threat Hunt, AI Assistant prerequisite |
System / alert indices (read, write, monitor, view_index_metadata):
| Index pattern | Used by |
|---|---|
.alerts-security.alerts-<space-id> |
Alert Triage, Attack Discovery, Detection Rules, Case Management, Sample Data cleanup |
.alerts-security.attack.discovery.alerts-<space-id> |
Attack Discovery |
.adhoc.alerts-security.attack.discovery.alerts-<space-id> |
Attack Discovery (ad-hoc generated) |
.internal.alerts-security.alerts-<space-id>-* |
Backing indices for the alert data stream |
.internal.alerts-security.attack.discovery.alerts-<space-id>-* |
Backing indices for the attack-discovery data stream |
.internal.adhoc.alerts-security.attack.discovery.alerts-<space-id>-* |
Backing indices for the ad-hoc attack-discovery data stream |
Data indices (read, write, monitor, view_index_metadata):
| Index pattern | Used by |
|---|---|
logs-* |
Threat Hunt, Alert Triage (enrichment), Sample Data (write + cleanup) |
risk-score.risk-score-latest-* |
Attack Discovery (entity risk scoring) |
9.4+ (recommended)
| Feature | Privilege | Why |
|---|---|---|
| Security > Security | All | Base access gate |
| Security > Cases | All | Case CRUD |
| Security > Timeline | All | Investigation timelines |
| Security > Notes | All | Timeline notes |
| Security > Rules and Exceptions | All | Rule CRUD, Attack Discovery prerequisite |
| Security > Alerts | All | Alert triage, Attack Discovery prerequisite |
| Security > Elastic AI Assistant | All | Anonymization fields for Attack Discovery |
| Security > Attack discovery | All | Generate/view/acknowledge discoveries |
| Management > Actions and Connectors | All | AI connector execution for Attack Discovery |
9.3 (diff from 9.4+)
- "Rules and Exceptions" and "Alerts" are combined into a single "Rules, Alerts, and Exceptions" feature — grant All
- All other features remain the same
9.0 - 9.2 (diff from 9.4+)
- Rules, Alerts, and Exceptions are all part of the base "Security" feature — grant All
- No separate Rules or Alerts features exist
- All other features remain the same
PUT /_security/role/mcp_app_full
{
"cluster": ["monitor"],
"indices": [
{
"names": [
".alerts-security.alerts-<space-id>",
".alerts-security.attack.discovery.alerts-<space-id>",
".adhoc.alerts-security.attack.discovery.alerts-<space-id>",
".internal.alerts-security.alerts-<space-id>-*",
".internal.alerts-security.attack.discovery.alerts-<space-id>-*",
".internal.adhoc.alerts-security.attack.discovery.alerts-<space-id>-*",
"logs-*",
"risk-score.risk-score-latest-*"
],
"privileges": ["read", "write", "monitor", "view_index_metadata"]
}
],
"applications": [
{
"application": "kibana-.kibana",
"privileges": [
"feature_siemV5.all",
"feature_securitySolutionCasesV3.all",
"feature_securitySolutionTimeline.all",
"feature_securitySolutionNotes.all",
"feature_securitySolutionRulesV4.all",
"feature_securitySolutionAlertsV1.all",
"feature_securitySolutionAssistant.all",
"feature_securitySolutionAttackDiscovery.all",
"feature_actions.all"
],
"resources": ["space:<space-id>"]
}
]
}
Replace
<space-id>with your Kibana space ID (typicallydefault).
Two-step recipe — assign the role to a user, then have an admin grant an API key that inherits the user's privileges:
PUT /_security/user/mcp_app_user
{
"password": "<choose-a-strong-password>",
"roles": ["mcp_app_full"]
}
Then, authenticated as an admin (e.g. elastic):
POST /_security/api_key/grant
{
"grant_type": "password",
"username": "mcp_app_user",
"password": "<the password you chose above>",
"api_key": { "name": "mcp-app-full" }
}
The response includes an encoded field — use that as the elasticsearchApiKey for your cluster entry in CLUSTERS_JSON (or CLUSTERS_FILE). The key inherits the user's role privileges directly.
A strict read-only role: view everything, change nothing.
Need to acknowledge alerts or create cases? Use the full-featured role, or build a custom role using the per-tool privilege breakdown.
| Privilege | Why |
|---|---|
monitor |
_cat/indices for Threat Hunt |
All index patterns (read, monitor, view_index_metadata):
| Index pattern | Used by |
|---|---|
.alerts-security.alerts-<space-id> |
Alert Triage, Detection Rules, Case viewing |
.alerts-security.attack.discovery.alerts-<space-id> |
Attack Discovery viewing |
.adhoc.alerts-security.attack.discovery.alerts-<space-id> |
Attack Discovery viewing |
.internal.alerts-security.alerts-<space-id>-* |
Backing indices for the alert data stream |
.internal.alerts-security.attack.discovery.alerts-<space-id>-* |
Backing indices for the attack-discovery data stream |
.internal.adhoc.alerts-security.attack.discovery.alerts-<space-id>-* |
Backing indices for the ad-hoc attack-discovery data stream |
logs-* |
Threat Hunt, Alert Triage (enrichment) |
risk-score.risk-score-latest-* |
Attack Discovery (entity risk scoring) |
| Feature | Privilege | Why |
|---|---|---|
| Security > Security | Read | Base access gate |
| Security > Cases | Read | View cases |
| Security > Timeline | Read | View timelines |
| Security > Notes | Read | View notes |
| Security > Rules and Exceptions | Read | View rules |
| Security > Alerts | Read | View alerts |
| Security > Elastic AI Assistant | None | Not available in read-only |
| Security > Attack discovery | None | Not available in read-only |
| Management > Actions and Connectors | Read | List configured AI connectors (no execute) |
PUT /_security/role/mcp_app_readonly
{
"cluster": ["monitor"],
"indices": [
{
"names": [
".alerts-security.alerts-<space-id>",
".alerts-security.attack.discovery.alerts-<space-id>",
".adhoc.alerts-security.attack.discovery.alerts-<space-id>",
".internal.alerts-security.alerts-<space-id>-*",
".internal.alerts-security.attack.discovery.alerts-<space-id>-*",
".internal.adhoc.alerts-security.attack.discovery.alerts-<space-id>-*",
"logs-*",
"risk-score.risk-score-latest-*"
],
"privileges": ["read", "monitor", "view_index_metadata"]
}
],
"applications": [
{
"application": "kibana-.kibana",
"privileges": [
"feature_siemV5.read",
"feature_securitySolutionCasesV3.read",
"feature_securitySolutionTimeline.read",
"feature_securitySolutionNotes.read",
"feature_securitySolutionRulesV4.read",
"feature_securitySolutionAlertsV1.read",
"feature_actions.read"
],
"resources": ["space:<space-id>"]
}
]
}
PUT /_security/user/mcp_app_readonly_user
{
"password": "<choose-a-strong-password>",
"roles": ["mcp_app_readonly"]
}
Then, authenticated as an admin (e.g. elastic):
POST /_security/api_key/grant
{
"grant_type": "password",
"username": "mcp_app_readonly_user",
"password": "<the password you chose above>",
"api_key": { "name": "mcp-app-readonly" }
}
Use this appendix to build custom roles that enable only specific tools.
| Operation | Cluster | Index privileges | Kibana features |
|---|---|---|---|
| Search alerts | monitor |
read on .alerts-security.alerts-<space-id> |
Security (Read), Alerts (Read) |
| View alert context | monitor |
read on .alerts-security.alerts-<space-id>, logs-endpoint.events.process-*, logs-endpoint.events.network-* |
Security (Read), Alerts (Read) |
| Acknowledge alerts | monitor |
read, write on .alerts-security.alerts-<space-id> and .internal.alerts-security.alerts-<space-id>-* |
Security (All), Alerts (All) |
| Operation | Cluster | Index privileges | Kibana features |
|---|---|---|---|
| View discoveries | monitor |
read on .alerts-security.attack.discovery.alerts-<space-id>, .adhoc.alerts-security.attack.discovery.alerts-<space-id> |
Security (Read), Attack discovery (Read) |
| Assess confidence | monitor |
read on .alerts-security.alerts-<space-id>, risk-score.risk-score-latest-* |
Security (Read), Attack discovery (Read) |
| Generate discoveries | monitor |
read on .alerts-security.alerts-<space-id> |
Security (All), Rules and Exceptions (All), Alerts (All), Elastic AI Assistant (All), Attack discovery (All), Actions and Connectors (All) |
| Acknowledge discoveries | monitor |
read, write on .alerts-security.attack.discovery.alerts-<space-id>, .adhoc.alerts-security.attack.discovery.alerts-<space-id> and the matching .internal.*-* backing-index patterns |
Security (All), Attack discovery (All) |
| Operation | Cluster | Index privileges | Kibana features |
|---|---|---|---|
| List/view cases | — | — | Security (Read), Cases (Read) |
| View case alerts | — | read on .alerts-security.alerts-<space-id> |
Security (Read), Cases (Read), Alerts (Read) |
| Create/update cases | — | — | Security (All), Cases (All) |
| Add comments | — | — | Security (All), Cases (All) |
| Attach alerts | — | — | Security (All), Cases (All) |
| Operation | Cluster | Index privileges | Kibana features |
|---|---|---|---|
| List/view rules | — | — | Security (Read), Rules and Exceptions (Read) |
| Validate queries | — | read on .alerts-security.alerts-<space-id> |
Security (Read) |
| Noisy rules report | — | read on .alerts-security.alerts-<space-id> |
Security (Read), Rules and Exceptions (Read), Alerts (Read) |
| Create/patch/delete rules | — | — | Security (All), Rules and Exceptions (All) |
Bulk action on rules (_bulk_action) |
— | — | Security (All), Rules and Exceptions (All) |
| Toggle rules | — | — | Security (All), Rules and Exceptions (All) |
| List exceptions | — | — | Security (Read), Rules and Exceptions (Read) |
| Add exceptions | — | — | Security (All), Rules and Exceptions (All) |
| Operation | Cluster | Index privileges | Kibana features |
|---|---|---|---|
| ES|QL queries | monitor |
read on target indices (e.g., logs-*) |
Security (Read) |
| List indices | monitor |
read, monitor on target indices |
Security (Read) |
| Field mappings | monitor |
read, view_index_metadata on target indices |
Security (Read) |
| Entity detail | monitor |
read on logs-endpoint.events.process-*, logs-endpoint.events.network-*, .alerts-security.alerts-<space-id> |
Security (Read), Alerts (Read) |
| Operation | Cluster | Index privileges | Kibana features |
|---|---|---|---|
| Check existing data | monitor |
read on .alerts-security.alerts-<space-id>, logs-* |
Security (Read) |
| Generate sample data | monitor |
read, write on logs-*, .alerts-security.alerts-<space-id> and .internal.alerts-security.alerts-<space-id>-* |
Security (All), Rules and Exceptions (All), Alerts (All) |
| Cleanup sample data | monitor |
read, write on logs-*, .alerts-security.alerts-<space-id> and .internal.alerts-security.alerts-<space-id>-* |
Security (All), Rules and Exceptions (All), Alerts (All) |
If your cluster was upgraded from Elastic Security 8.0 or earlier, detection alerts may still exist in .siem-signals-<space-id> instead of .alerts-security.alerts-<space-id>. Add read (and write for acknowledgment) privileges on .siem-signals-<space-id> if you need to access these legacy alerts.