Set up OpenSearch Security to accept OAuth2 Bearer tokens issued by Microsoft Entra ID and map those tokens to an OpenSearch role with permissions scoped to alert indexes.
Overview
This guide sets up OpenSearch Security to accept OAuth2 Bearer tokens and maps those tokens to an OpenSearch role with permissions scoped to alert indexes. mTLS via securityadmin.sh is used for all security config pushes.
Note: The OpenSearch configuration in this guide works with any OAuth2-compatible Identity Provider that issues JWT tokens. Microsoft Entra ID is used as the example, but the same approach applies to Okta, Auth0, Keycloak, or any other OIDC-compliant IdP – only the IdP-specific setup in Part 1 will differ.
Authentication Flow
The authentication flow is:
Client app → requests Bearer token from Entra (client credentials flow)
→ presents Bearer token to OpenSearch
→ OpenSearch validates token against Entra JWKS endpoint
→ extracts `roles` claim from token
→ maps claim value to OpenSearch role
→ role grants permissions on alert indexes
Components
Two separate Entra (or other IdP) app registrations are required:
-
Resource app represents OpenSearch; defines the app role
-
Client app represents the application requesting tokens; assigned the defined app role
Step 1: Microsoft Entra ID Configuration
1.1 Create Resource App (OpenSearch)
This app represents OpenSearch and defines the role appearing in tokens.
-
Go to Azure Portal → Microsoft Entra ID → App registrations → New registration
-
Set a name (e.g.
agilesec-api-access) -
Set Supported account types to Accounts in this organizational directory only
-
Leave Redirect URI blank
-
Click Register
-
Note the Application (client) ID and Directory (tenant) ID
1.2 Set Token Version to v2.0
This ensures tokens issued for this resource are v2.0, which is required for the roles claim to appear correctly.
-
In the resource app registration, go to Manifest
-
Find
"requestedAccessTokenVersion"and change the value fromnullto2 -
Click Save
AAD Graph App Manifest Note:
If you don’t see "requestedAccessTokenVersion", it’s likely the older version of the manifest file is being used. (AAD Graph App Manifest)
Modify “accessTokenAcceptedVersion” to 2 instead.
If you have both old and new options available to update, update both.
1.3 Expose an API
-
In the resource app registration, go to Expose an API
-
Click Add next to Application ID URI, accept the default (
api://<app-id>), and save -
Click Add a scope and fill in:
-
Scope name:
access_as_application -
Who can consent: Admins only
-
Admin consent display name: Access OpenSearch
-
Admin consent description: Allows the application to access OpenSearch
-
State: Enabled
-
-
Click Add scope
1.4 Create an App Role
-
Still in the resource app, go to App roles → Create app role
-
Fill in:
-
Display name:
AgileSec Alert Reader -
Allowed member types:
Applications -
Value:
agilesec-alert-reader← this is the string OpenSearch will read from the token -
Description: Read access to OpenSearch alert indexes
-
-
Click Apply
1.5 Create Client App
This application will request tokens and present them to OpenSearch.
-
Go to App registrations → New registration
-
Set a name (e.g.
agilesec-api-client) -
Set Supported account types to Accounts in this organizational directory only
-
Leave Redirect URI blank
-
Click Register
-
Note the Application (client) ID
1.6 Create a Client Secret
-
In the client app registration, go to Certificates & secrets → New client secret
-
Set a description and expiry
-
Click Add
-
Copy the secret Value immediately – it will not be shown again
1.7 Assign the App Role to the Client App
-
Still in the client app registration, go to API permissions → Add a permission
-
Select APIs my organization uses
-
Search for and select
agilesec-api-access(the resource app from step 1.1) -
Choose Application permissions
-
Check
agilesec-alert-reader -
Click Add permissions
-
Click Grant admin consent for [your org] and confirm
1.8 Verify the Token
Request a token and decode it to confirm the roles claim is present:
curl -s -X POST \
"https://login.microsoftonline.com/<tenant-id>/oauth2/v2.0/token" \
-d "grant_type=client_credentials" \
-d "client_id=<agilesec-api-client-id>" \
-d "client_secret=<client-secret>" \
-d "scope=api://<agilesec-api-access-app-id>/.default" \
| jq -r '.access_token' \
| cut -d. -f2 \
| tr '_-' '/+' \
| awk '{ n=length($0)%4; if(n==2) print $0"=="; else if(n==3) print $0"="; else print $0 }' \
| base64 -d \
| jq .
Confirm the output contains:
{
"ver": "2.0",
"roles": ["agilesec-alert-reader"],
...
}
Note:
client_idis the client app ID. Thescopepoints to the resource app ID. These must be different apps. If they match, therolesclaim will not appear in the token.
Step 2: OpenSearch Security Configuration
All config files live in config/opensearch-security/. Changes are pushed with securityadmin.sh over mTLS.
2.1 config.yml – Enable the OpenID Authenticator
The oauth2_access_token_domain block is already present in config/opensearch-security/config.yml but commented out. To enable OpenID Authenticator, remove the # (pound space) prefix from each line of the block.
Important: Do not remove just the #. The space must be removed too, otherwise the indentation will be off and the YAML will fail to parse.
Once uncommented, update the openid_connect_url with your tenant ID as shown below:
config:
dynamic:
authc:
basic_internal_auth_domain:
description: "HTTP basic auth against internal users"
http_enabled: true
transport_enabled: true
order: 0
http_authenticator:
type: basic
challenge: false
authentication_backend:
type: internal
oauth2_access_token_domain:
description: "Entra ID OAuth2 Bearer token authentication"
http_enabled: true
transport_enabled: false
order: 1
http_authenticator:
type: openid
challenge: false
config:
openid_connect_url: "https://login.microsoftonline.com/<tenant-id>/v2.0/.well-known/openid-configuration"
subject_key: "sub"
roles_key: "roles"
jwt_clock_skew_tolerance_seconds: 30
authentication_backend:
type: noop
Replace <tenant-id> with your Directory (tenant) ID.
Key config notes:
-
roles_key: "roles"– OpenSearch reads therolesarray from the JWT and uses those values as backend roles for mapping -
subject_key: "sub"– thesubclaim becomes the OpenSearch username -
challenge: false– suppresses theWWW-Authenticatechallenge header; correct for API clients -
transport_enabled: false– tokens are only accepted on HTTP, not internal node-to-node transport
2.2 roles.yml – Define the Alert Index Role
In roles.yml, replace <org_domain> with your organization's domain, with . replaced by _ (e.g. example.com → example_com).
alert_reader_role:
description: "Read access to AgileSec alerting indexes"
cluster_permissions:
- "cluster:monitor/main"
index_permissions:
- index_patterns:
- "agilesec.<org_domain>.v3.alert-*"
allowed_actions:
- "read"
2.3 roles_mapping.yml – Map the Entra Role to the OpenSearch Role
The string in backend_roles must exactly match the Value field set in the Entra App Role (step 1.4). This value is case-sensitive.
alert_reader_role:
reserved: false
backend_roles:
- "agilesec-alert-reader" # must exactly match the App Role Value set in Entra
hosts: []
users: []
Step 3: Applying Config with securityadmin.sh (mTLS)
Set these variables before proceeding to apply the configuration:
export JAVA_HOME=<agilesec install dir>/bin/java
OPENSEARCH_HOME=<agilesec install dir>/services/opensearch
SECURITY_CONF=$OPENSEARCH_HOME/config/opensearch-security
ADMIN_CERT=<agilesec install dir>/certificates/kf-agilesec.internal/admin-user-cert.pem
ADMIN_KEY=<agilesec install dir>/certificates/kf-agilesec.internal/admin-user-key.pem
ROOT_CA=<agilesec install dir>/certificates/ca/agilesec-rootca-cert.pem
3.1 Backup Current Config
Before making any changes, back up the current live security config. This captures any roles or mappings created via the UI that would otherwise be overwritten.
$OPENSEARCH_HOME/plugins/opensearch-security/tools/securityadmin.sh \
-backup "$SECURITY_CONF/backup" \
-icl -nhnv \
-cacert "$ROOT_CA" -cert "$ADMIN_CERT" -key "$ADMIN_KEY" \
-h backend-1.kf-agilesec.internal -p 9200
Review $SECURITY_CONF/backup/roles.yml and $SECURITY_CONF/backup/roles_mapping.yml and merge any existing entries into your updated files before pushing in the next step.
3.2 Push Config Files Individually
Each file is pushed separately with securityadmin.sh to avoid touching other configs (internal users, action groups, tenants, etc.).
# config.yml
$OPENSEARCH_HOME/plugins/opensearch-security/tools/securityadmin.sh \
-f "$SECURITY_CONF/config.yml" \
-t config \
-icl -nhnv \
-cacert "$ROOT_CA" -cert "$ADMIN_CERT" -key "$ADMIN_KEY" \
-h backend-1.kf-agilesec.internal -p 9200
# roles.yml
$OPENSEARCH_HOME/plugins/opensearch-security/tools/securityadmin.sh \
-f "$SECURITY_CONF/roles.yml" \
-t roles \
-icl -nhnv \
-cacert "$ROOT_CA" -cert "$ADMIN_CERT" -key "$ADMIN_KEY" \
-h backend-1.kf-agilesec.internal -p 9200
# roles_mapping.yml
$OPENSEARCH_HOME/plugins/opensearch-security/tools/securityadmin.sh \
-f "$SECURITY_CONF/roles_mapping.yml" \
-t rolesmapping \
-icl -nhnv \
-cacert "$ROOT_CA" -cert "$ADMIN_CERT" -key "$ADMIN_KEY" \
-h backend-1.kf-agilesec.internal -p 9200
Note: AgileSec runs
securityadmin.shon port 9200.
Common script flags:
|
Flag |
Meaning |
|---|---|
|
|
Push a single file |
|
|
Config type ( |
|
|
Export current live config to a directory |
|
|
Ignore cluster name |
|
|
No hostname verification |
|
|
Root CA that signed the node certs |
|
|
Admin client certificate |
|
|
Admin client key |
Step 4: Testing the End-to-End Flow
Test the new OAuth setup flow with the following steps.
4.1 Get a Token
Run the following to get a token.
TOKEN=$(curl -s -X POST \
"https://login.microsoftonline.com/<tenant-id>/oauth2/v2.0/token" \
-d "grant_type=client_credentials" \
-d "client_id=<agilesec-api-client-id>" \
-d "client_secret=<client-secret>" \
-d "scope=api://<agilesec-api-access-app-id>/.default" \
| jq -r '.access_token')
The following variables are used:
-
tenant-id– Azure Portal → Microsoft Entra ID → Overview → Directory (tenant) ID -
client_id– App registrations → agilesec-api-client → Overview → Application (client) ID -
client_secret– the secret value created in step 1.6 for agilesec-api-client -
scope–api://+ App registrations → agilesec-api-access → Overview → Application (client) ID +/.default
4.2 Check Auth Info
Run the following to check the token’s authorization information.
curl -sk -H "Authorization: Bearer $TOKEN" \
"https://backend-1.kf-agilesec.internal:9200/_plugins/_security/authinfo" | jq .
Note: backend-1.kf-agilesec.internal is the default AgileSec backend hostname. If you have modified your deployment, replace this with your actual OpenSearch host.
Expected response:
{
"user_name": "<sub-value>",
"backend_roles": ["agilesec-alert-reader"],
"roles": ["alert_reader_role"],
...
}
Confirm backend_roles contains agilesec-alert-reader and roles contains alert_reader_role.
4.3 Query an Alert Index
Run the following to query an alert index and ensure the token is working.
curl -sk -H "Authorization: Bearer $TOKEN" \
"https://backend-1.kf-agilesec.internal:9200/agilesec.<org-domain>.v3.alert-*/_search?pretty" \
-H "Content-Type: application/json" \
-d '{"query": {"match_all": {}}, "size": 5}'
Troubleshooting
-
rolesclaim is missing from the token: Confirm you have two separate app registrations (client and resource). Ifclient_idand the app ID inscopeare the same, therolesclaim will not appear. Also confirm admin consent was granted. -
veris1.0instead of2.0: Follow step 1.2 to set"requestedAccessTokenVersion": 2in the Manifest of the resource app registration. -
Authentication passes but role isn't mapped: Run
/_plugins/_security/authinfoand checkbackend_roles. The value must exactly match the string inroles_mapping.yml. Case-sensitive. -
No OpenIDConnect metadata fetched: OpenSearch must be able to reachhttps://login.microsoftonline.comat startup. Check egress rules and confirm the tenant ID in the URL is correct. -
JWT clock skewerrors: Increasejwt_clock_skew_tolerance_secondsor ensure OpenSearch node time is NTP-synchronized. -
config.ymlrejected withUnrecognized fieldor YAML parsing error: Theoauth2_access_token_domainblock must be at the same indentation level as other auth domains (e.g.jwt_auth_domain,basic_internal_auth_domain). If it is indented one level deeper it will be parsed as a nested field inside the preceding domain rather than a sibling. Verify the indentation is consistent across all blocks underauthc:. – Confirm you are connecting to port 9200 and that the hostnamebackend-1.kf-agilesec.internalis reachable. -
Permission denied on alert indexes: Confirm
index_patternsincludes the dot prefix and wildcards are correct. Check that system index protection is not blocking access.