AgileSec ServiceNow VR: OpenSearch OAuth2 Setup

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.

  1. Go to Azure Portal → Microsoft Entra ID → App registrations → New registration

  2. Set a name (e.g. agilesec-api-access)

  3. Set Supported account types to Accounts in this organizational directory only

  4. Leave Redirect URI blank

  5. Click Register

  6. 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.

  1. In the resource app registration, go to Manifest

  2. Find "requestedAccessTokenVersion" and change the value from null to 2

  3. 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

  1. In the resource app registration, go to Expose an API

  2. Click Add next to Application ID URI, accept the default (api://<app-id>), and save

  3. 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

  4. Click Add scope

1.4 Create an App Role

  1. Still in the resource app, go to App roles → Create app role

  2. 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

  3. Click Apply

1.5 Create Client App

This application will request tokens and present them to OpenSearch.

  1. Go to App registrations → New registration

  2. Set a name (e.g. agilesec-api-client)

  3. Set Supported account types to Accounts in this organizational directory only

  4. Leave Redirect URI blank

  5. Click Register

  6. Note the Application (client) ID

1.6 Create a Client Secret

  1. In the client app registration, go to Certificates & secrets → New client secret

  2. Set a description and expiry

  3. Click Add

  4. Copy the secret Value immediately – it will not be shown again

1.7 Assign the App Role to the Client App

  1. Still in the client app registration, go to API permissions → Add a permission

  2. Select APIs my organization uses

  3. Search for and select agilesec-api-access (the resource app from step 1.1)

  4. Choose Application permissions

  5. Check agilesec-alert-reader

  6. Click Add permissions

  7. 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_id is the client app ID. The scope points to the resource app ID. These must be different apps. If they match, the roles claim 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 the roles array from the JWT and uses those values as backend roles for mapping

  • subject_key: "sub" – the sub claim becomes the OpenSearch username

  • challenge: false – suppresses the WWW-Authenticate challenge 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.comexample_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.sh on port 9200.

Common script flags:

Flag

Meaning

-f

Push a single file

-t

Config type (roles, rolesmapping, config, internalusers, actiongroups)

-backup

Export current live config to a directory

-icl

Ignore cluster name

-nhnv

No hostname verification

-cacert

Root CA that signed the node certs

-cert

Admin client certificate

-key

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

  • scopeapi:// + 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

  • roles claim is missing from the token: Confirm you have two separate app registrations (client and resource). If client_idand the app ID in scope are the same, the roles claim will not appear. Also confirm admin consent was granted.

  • ver is 1.0 instead of 2.0: Follow step 1.2 to set "requestedAccessTokenVersion": 2 in the Manifest of the resource app registration.

  • Authentication passes but role isn't mapped: Run /_plugins/_security/authinfo and check backend_roles. The value must exactly match the string in roles_mapping.yml. Case-sensitive.

  • No OpenIDConnect metadata fetched: OpenSearch must be able to reach https://login.microsoftonline.com at startup. Check egress rules and confirm the tenant ID in the URL is correct.

  • JWT clock skew errors: Increase jwt_clock_skew_tolerance_seconds or ensure OpenSearch node time is NTP-synchronized.

  • config.yml rejected with Unrecognized field or YAML parsing error: The oauth2_access_token_domain block 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 under authc:. – Confirm you are connecting to port 9200 and that the hostname backend-1.kf-agilesec.internal is reachable.

  • Permission denied on alert indexes: Confirm index_patterns includes the dot prefix and wildcards are correct. Check that system index protection is not blocking access.