How to secure OpenSearch Cluster with Keycloak OIDC

How to secure OpenSearch Cluster with Keycloak OIDC

opensearch_with_keycloak_auth_demo

Here in this article we will try to secure OpenSearch cluster using Keycloak Open ID Connect Authentication and Authorization service provider.

Test Environment

  • Fedora Server 41
  • Docker v27.5.1
  • Docker Compose v2.32.4

What is OpenSearch

OpenSearch is an open-source search and analytics engine, designed for a variety of use cases such as implementing a search box on a website to analyzing security data for threat detection, log analytics, real-time application monitoring, and full-text search.

OpenSearch supports several authentication types, including basic authentication (username and password), SAML and OpenID Connect for single sign-on, client certificates, and JWT (JSON Web Tokens). You can also integrate with identity providers like Amazon Cognito and AWS IAM Identity Center. The Security plugin handles these by configuring various authentication backends, such as the internal user database, LDAP, or external SAML providers.

What is Keycloak

Keycloak is an open-source Identity and Access Management (IAM) solution developed by Red Hat. It allows you to centralize authentication and authorization for your applications and services, offering features like single sign-on (SSO), social login, user federation, and more. Keycloak simplifies securing applications by handling user authentication and authorization, eliminating the need for developers to build these functionalities from scratch.

If you are interested in watching the video. Here is the YouTube video on the same step by step procedure outlined below.

Procedure

Step1: Ensure Docker and Docker compose installed and running

As a first step ensure that you have docker and docker-compose installed on your system. You can following the official documentation from docker to install these tools.

admin@linuxser:~$ docker -v
Docker version 27.5.1, build 9f9e405

admin@linuxser:~$ docker compose version
Docker Compose version v2.32.4

Start up the docker services if its not already started.

admin@linuxser:~$ sudo systemctl start docker.service 

Also let us create a working directory and switch into that folder.

admin@linuxser:~$ mkdir opensearch_with_openid
admin@linuxser:~$ cd opensearch_with_openid/

Step2: Ensure OpenSearch Cluster and Keycloak Service up and running

Here is the docker compose file for OpenSearch, OpenSearch Dashboard and Keycloak service setup.

Ensure that we have the OpenSearch “admin” user password initialized in the environment file before starting up the OpenSearch cluster.

admin@linuxser:~/opensearch$ cat .env 
OPENSEARCH_INITIAL_ADMIN_PASSWORD=Se@rch@2025
admin@linuxser:~/opensearch_with_openid$ cat docker-compose.yml 
services:
  opensearch-node1: # This is also the hostname of the container within the Docker network (i.e. https://opensearch-node1/)
    image: opensearchproject/opensearch:3.3.0
    container_name: opensearch-node1
    environment:
      - cluster.name=opensearch-cluster # Name the cluster
      - node.name=opensearch-node1 # Name the node that will run in this container
      - discovery.seed_hosts=opensearch-node1,opensearch-node2 # Nodes to look for when discovering the cluster
      - cluster.initial_cluster_manager_nodes=opensearch-node1,opensearch-node2 # Nodes eligibile to serve as cluster manager
      - bootstrap.memory_lock=true # Disable JVM heap memory swapping
      - "OPENSEARCH_JAVA_OPTS=-Xms3072m -Xmx3072m" # Set min and max JVM heap sizes to at least 50% of system RAM
      - OPENSEARCH_INITIAL_ADMIN_PASSWORD=${OPENSEARCH_INITIAL_ADMIN_PASSWORD} # Sets the demo admin user password when using demo configuration (for OpenSearch 2.12 and later)
    ulimits:
      memlock:
        soft: -1 # Set memlock to unlimited (no soft or hard limit)
        hard: -1
      nofile:
        soft: 65536 # Maximum number of open files for the opensearch user - set to at least 65536
        hard: 65536
    volumes:
      - opensearch-data1:/usr/share/opensearch/data # Creates volume called opensearch-data1 and mounts it to the container
      #- ./config.yml:/usr/share/opensearch/config/opensearch-security/config.yml
    ports:
      - 9200:9200 # REST API
      - 9600:9600 # Performance Analyzer
    networks:
      - opensearch-net # All of the containers will join the same Docker bridge network
  opensearch-node2:
    image: opensearchproject/opensearch:3.3.0 # This should be the same image used for opensearch-node1 to avoid issues
    container_name: opensearch-node2
    environment:
      - cluster.name=opensearch-cluster
      - node.name=opensearch-node2
      - discovery.seed_hosts=opensearch-node1,opensearch-node2
      - cluster.initial_cluster_manager_nodes=opensearch-node1,opensearch-node2
      - bootstrap.memory_lock=true
      - "OPENSEARCH_JAVA_OPTS=-Xms3072m -Xmx3072m"
      - OPENSEARCH_INITIAL_ADMIN_PASSWORD=${OPENSEARCH_INITIAL_ADMIN_PASSWORD}
    ulimits:
      memlock:
        soft: -1
        hard: -1
      nofile:
        soft: 65536
        hard: 65536
    volumes:
      - opensearch-data2:/usr/share/opensearch/data
      #- ./config.yml:/usr/share/opensearch/config/opensearch-security/config.yml
    networks:
      - opensearch-net
  opensearch-dashboards:
    image: opensearchproject/opensearch-dashboards:3.3.0 # Make sure the version of opensearch-dashboards matches the version of opensearch installed on other nodes
    container_name: opensearch-dashboards
    ports:
      - 5601:5601 # Map host port 5601 to container port 5601
    expose:
      - "5601" # Expose port 5601 for web access to OpenSearch Dashboards
    #volumes:
    #  - ./opensearch_dashboards.yml:/usr/share/opensearch-dashboards/config/opensearch_dashboards.yml
    environment:
      OPENSEARCH_HOSTS: '["https://opensearch-node1:9200","https://opensearch-node2:9200"]' # Define the OpenSearch nodes that OpenSearch Dashboards will query
    networks:
      - opensearch-net
  keycloak:
    image: quay.io/keycloak/keycloak:26.1.4
    command: start-dev
    container_name: keycloak
    environment:
      KC_BOOTSTRAP_ADMIN_USERNAME: admin
      KC_BOOTSTRAP_ADMIN_PASSWORD: admin
    ports:
      - "8080:8080"
      - "8443:8443"
    networks:
      - opensearch-net
    healthcheck:
      test: ["CMD", "sh", "-c", "exec 3<>/dev/tcp/localhost/8080; echo 'HTTP/1.1' >&3; head -1 <&3"]
      interval: 30s
      timeout: 10s
      retries: 5


volumes:
  opensearch-data1:
  opensearch-data2:

networks:
  opensearch-net:
admin@linuxser:~/opensearch_with_openid$ docker compose up -d

OpenSearch service validation

admin@linuxser:~/opensearch$ curl https://linuxser.stack.com:9200 -ku admin:Se@rch@2025
{OPENSEARCH_INITIAL_ADMIN_PASSWORD=Se@rch@2025
  "name" : "opensearch-node1",
  "cluster_name" : "opensearch-cluster",
  "cluster_uuid" : "xcaIsJtWSE-F_iBXmzv9Kg",
  "version" : {
    "distribution" : "opensearch",
    "number" : "3.2.0",
    "build_type" : "tar",
    "build_hash" : "6adc0bf476e1624190564d7fbe4aba00ccf49ad8",
    "build_date" : "2025-08-12T03:55:01.226522683Z",
    "build_snapshot" : false,
    "lucene_version" : "10.2.2",
    "minimum_wire_compatibility_version" : "2.19.0",
    "minimum_index_compatibility_version" : "2.0.0"
  },
  "tagline" : "The OpenSearch Project: https://opensearch.org/"
}

OpenSearch Dashboard service validation

URL: http://linuxser.stack.com:5601/

Keycloak service validation

URL: http://linuxser.stack.com:8080/

Step3: Configure Keycloak

Here let us first create a new realm named “stack” by navigating to top left side and clicking on the drop down to Create Realm. Once you create a realm you will be automatically switched into your new realm.

Now navigate to Clients on left navigation bar and create a new client named “opensearchsso” with the following details.

General Settings

Client Name : opensearchsso
Client ID   : opensearchsso

Capability Config

Client Authentication   : On
Authorization Flow      : Standard flow, Direct access grants and Implicit flow enabled

Login Settings

Valid Redirect URls     : http://linuxser.stack.com:5601/*
Web Origins             : http://linuxser.stack.com:5601

Once the Client is created you can navigate to “Credentials” tab and retrieve your Client Secret and keep it secure.

Client Secret   : HifH6BgxETJ0exeYTsrLB4nNb1YOBcw5

Once the client has been created, its now time to setup groups and users. Let us create groups named “osadministrators” and “osdevelopers“.

Navigate to Groups on left navigation bar and create the following groups.

Now let us create two users named “osadmin” and “osdev” part of groups “osadministrators” and “osdevelopers” respectively.

Username    : osadmin
Email       : osadmin@stack.com
First name  : osadmin
Last name   : osadminsso
Groups      : osadministrators
Username    : osdev
Email       : osdev@stack.com
First name  : osdev
Last name   : osdevsso
Groups      : osdevelopers

Navigate to “Credentials” tab and “Set Password” for each user and ensure Temporary option is Off so you don’t need to change the password again on first login.

There are two more important steps that we need to carry out. First “Create a client scope with group membership mapping” as shown below.

Navigate to Client scopes on the left navigation bar and click “Create client scope” on that page. Update the details and save the changes.

Now we need to go to “Mappers” tab for the Client scope “groups” and click on “Configure a new mapper” and select “Group Membership”. Just disable the “Full group path” as we are going to deal with top level groups only for this demo and create the mappers.

This is how your updated client scope mappers look like.

Now we need to map this client scope to our client “opensearchsso”. Navigate to your client and go to “Client scopes” tab and ensure you click on “groups” to map it to the client as shown below and set the Assigned type as “Default”.

NOTE: By default “Client” is mapped with the following scopes “email” and “profile” which we will be using during SSO communication.

Just to make sure your client is configured correctly and is able to authenticate and issue tokens with the required claims we can validate it using the implicit authentication flow wherein we provide the client id and client secret to get the access token as shown below.

admin@linuxser:~/opensearch_with_openid$ curl -k -d "grant_type=password" -d "scope=openid" -d "client_id=opensearchsso" -d "client_secret=HifH6BgxETJ0exeYTsrLB4nNb1YOBcw5" -d "username=osadmin" -d "password=osadmin" http://linuxser.stack.com:8080/realms/stack/protocol/openid-connect/token | jq .

  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  3538  100  3401  100   137  20954    844 --:--:-- --:--:-- --:--:-- 22112
{
  "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICIxcWV1eGEwS3pLcGdRVmR1N3dmWldpZmF1QXQzUHpQOW9ESk1aeFl6WWljIn0.eyJleHAiOjE3NjE5OTM4ODYsImlhdCI6MTc2MTk5MzU4NiwianRpIjoiMzZiODE2MDMtMmY2OS00NTE2LTllZGEtYjc5YjQwZTFjNDYzIiwiaXNzIjoiaHR0cDovL2xpbnV4c2VyLnN0YWNrLmNvbTo4MDgwL3JlYWxtcy9zdGFjayIsImF1ZCI6ImFjY291bnQiLCJzdWIiOiI4Y2M4OWJmOS1lNTE1LTQ1ZmQtYmNjZS03N2FiOTZkMmFjNGIiLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJvcGVuc2VhcmNoc3NvIiwic2lkIjoiOGQwNmIzMzQtNTI2Zi00NGQ0LWE1Y2QtNzkxYzNjODU1ZTZjIiwiYWNyIjoiMSIsImFsbG93ZWQtb3JpZ2lucyI6WyJodHRwOi8vbG9jYWxob3N0OjU2MDEiXSwicmVhbG1fYWNjZXNzIjp7InJvbGVzIjpbImRlZmF1bHQtcm9sZXMtc3RhY2siLCJvZmZsaW5lX2FjY2VzcyIsInVtYV9hdXRob3JpemF0aW9uIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsiYWNjb3VudCI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCIsIm1hbmFnZS1hY2NvdW50LWxpbmtzIiwidmlldy1wcm9maWxlIl19fSwic2NvcGUiOiJvcGVuaWQgZW1haWwgcHJvZmlsZSIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwibmFtZSI6Im9zYWRtaW4gb3NhZG1pbnNzbyIsInByZWZlcnJlZF91c2VybmFtZSI6Im9zYWRtaW4iLCJnaXZlbl9uYW1lIjoib3NhZG1pbiIsImZhbWlseV9uYW1lIjoib3NhZG1pbnNzbyIsImVtYWlsIjoib3NhZG1pbkBzdGFjay5jb20ifQ.UU8vL2At9JnSfD_00Imwdwth1K0tJcR8osb8guqCd66QCyZ7tXc8CHnCVIB1PwRHWzdiJDm0t0WDkHj4VtWyBjp89BzbVr7Aa3E_N1m0Z9Vy4EmaLF-BoKWenoFm3FbG8AtmqyRj4QTF4FNaTfN2a7lT3eRxw44oI1VcSrMow5I0vZkHDzcqiWeMr95s0raXiCzaQgSPBvYsZQewcMgtgXFRO1D4UoEgsI1XLNdhLaPnOGPuOKJ88VpXLtvXTpIXgX03BChb6NOm-XbPkLsOYImdzVC87lDtI_7QnFilSyW_NH8StRsC-kKLM5rUJKQVPzQOT60dvfN2l1BtgreRZQ",
  "expires_in": 300,
  "refresh_expires_in": 1800,
  "refresh_token": "eyJhbGciOiJIUzUxMiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICI5N2ZjN2ViMS03ZmJkLTQwMjEtYWQzOS0wMWY4NDkwN2UwNmEifQ.eyJleHAiOjE3NjE5OTUzODYsImlhdCI6MTc2MTk5MzU4NiwianRpIjoiZTA4NTkxZTEtMTg3Zi00MjFiLWI5OGEtMDA1M2M1OGVlMzIwIiwiaXNzIjoiaHR0cDovL2xpbnV4c2VyLnN0YWNrLmNvbTo4MDgwL3JlYWxtcy9zdGFjayIsImF1ZCI6Imh0dHA6Ly9saW51eHNlci5zdGFjay5jb206ODA4MC9yZWFsbXMvc3RhY2siLCJzdWIiOiI4Y2M4OWJmOS1lNTE1LTQ1ZmQtYmNjZS03N2FiOTZkMmFjNGIiLCJ0eXAiOiJSZWZyZXNoIiwiYXpwIjoib3BlbnNlYXJjaHNzbyIsInNpZCI6IjhkMDZiMzM0LTUyNmYtNDRkNC1hNWNkLTc5MWMzYzg1NWU2YyIsInNjb3BlIjoib3BlbmlkIGVtYWlsIHJvbGVzIGJhc2ljIHdlYi1vcmlnaW5zIHByb2ZpbGUgYWNyIn0.JMt-L2iwRxoZYTFbng6VI0NNOV1TAim0ImdnbcPJqais5ppHkGzzdOqM0MlY2z5hc-R3pzQFnASAwzXFORL5ug",
  "token_type": "Bearer",
  "id_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICIxcWV1eGEwS3pLcGdRVmR1N3dmWldpZmF1QXQzUHpQOW9ESk1aeFl6WWljIn0.eyJleHAiOjE3NjE5OTM4ODYsImlhdCI6MTc2MTk5MzU4NiwianRpIjoiOTMxZjgxYjktMTQyZS00ZWViLTg5NWItNmE2NDVjZjNlMWE0IiwiaXNzIjoiaHR0cDovL2xpbnV4c2VyLnN0YWNrLmNvbTo4MDgwL3JlYWxtcy9zdGFjayIsImF1ZCI6Im9wZW5zZWFyY2hzc28iLCJzdWIiOiI4Y2M4OWJmOS1lNTE1LTQ1ZmQtYmNjZS03N2FiOTZkMmFjNGIiLCJ0eXAiOiJJRCIsImF6cCI6Im9wZW5zZWFyY2hzc28iLCJzaWQiOiI4ZDA2YjMzNC01MjZmLTQ0ZDQtYTVjZC03OTFjM2M4NTVlNmMiLCJhdF9oYXNoIjoiX0o2UWt6dVFxS0VISFFXdUc3bWp6dyIsImFjciI6IjEiLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsIm5hbWUiOiJvc2FkbWluIG9zYWRtaW5zc28iLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJvc2FkbWluIiwiZ2l2ZW5fbmFtZSI6Im9zYWRtaW4iLCJmYW1pbHlfbmFtZSI6Im9zYWRtaW5zc28iLCJlbWFpbCI6Im9zYWRtaW5Ac3RhY2suY29tIn0.lG4Zyr6nXcxL5VrUuj21-1M4TmEouqP3IBhPOWjTVWIuX0oApvu0JDy2d8H19S1CPzeXV0wGO3rY4yjNnCDX-R7pQsFtGFhv9VskQM6LhiArT_ySHOFWzafRm-NAXTBroLnESk4ybKjaYOdRECax0v7NnzaYVuKJN-6wL0SvV8znakKYtWa0RXpk3eVOAdjkLOUo6CXrE5gg2FnAWOqNcLWBgXAS-2AqUdszh-gwoKMlhtfsVB7_rvv9k8IPi9N0qZr9oOrCxDhYK55q_IHO8fSsNqMc-hmVVTWWmX31RGKCCVM6xgC4j0oUQ8h-sm4WSoeGmT8pzblihpWYHd-v6g",
  "not-before-policy": 0,
  "session_state": "8d06b334-526f-44d4-a5cd-791c3c855e6c",
  "scope": "openid email profile"
}

You can validate your issued token at JWT token decoder. Here is the JSON token in my case.

{
  "exp": 1761994040,
  "iat": 1761993740,
  "jti": "a3cce949-d5c4-4cd9-8252-fa29f52e1fbc",
  "iss": "http://linuxser.stack.com:8080/realms/stack",
  "aud": "account",
  "sub": "8cc89bf9-e515-45fd-bcce-77ab96d2ac4b",
  "typ": "Bearer",
  "azp": "opensearchsso",
  "sid": "5340c450-a2e6-4be2-8f38-833ebf1a33a0",
  "acr": "1",
  "allowed-origins": [
    "http://localhost:5601"
  ],
  "realm_access": {
    "roles": [
      "default-roles-stack",
      "offline_access",
      "uma_authorization"
    ]
  },
  "resource_access": {
    "account": {
      "roles": [
        "manage-account",
        "manage-account-links",
        "view-profile"
      ]
    }
  },
  "scope": "openid email profile",
  "email_verified": false,
  "name": "osadmin osadminsso",
  "groups": [
    "osadministrators"
  ],
  "preferred_username": "osadmin",
  "given_name": "osadmin",
  "family_name": "osadminsso",
  "email": "osadmin@stack.com"
}

With this we have now completed our Realm, Clients, Client Scopes, Users and Groups settings.

Step4: Configure OpenSearch Security

Here we are going to update config.yml to enable both “basic_internal_auth_domain” and “oidc_auth_domain” domains.

For “basic_internal_auth_domain” we are going to use the default settings. For “oidc_auth_domain” here are the details of the updated fields.

  • openid_connect_url: The URL of your IdP where the Security plugin can find the OpenID Connect metadata/configuration settings. This URL differs between IdPs. Required when using OpenID Connect as your backend.
  • subject_key: The key in the JSON payload that stores the user’s name. If not defined, the subject registered claim is used. Most IdP providers use the preferred_username claim. To extract a username from nested JWT claims, you can configure subject_key as a list. Optional.
  • roles_key: The key in the JSON payload that stores the user’s roles. The value must be a comma-separated list of roles. This key is required only if you want to use roles in the JWT. You can configure roles_key as a list to extract roles from nested JWT claims.
  • client_id and client_secret: These are the credentials assigned by Keycloak OIDC when “opensearchsso” client was registered.

For brevity, i have removed all the comment sections and only provided the required settings. An authentication with lower order value is given first priority for authentication and if that fails authentication fallbacks to basic authentication.

admin@linuxser:~/opensearch_with_openid$ cat config.yml 
_meta:
  type: "config"
  config_version: 2

config:
  dynamic:
    http:
      anonymous_auth_enabled: false
    authc:
      basic_internal_auth_domain:
        description: "Authenticate via HTTP Basic against internal users database"
        http_enabled: true
        transport_enabled: true
        order: 0
        http_authenticator:
          type: "basic"
          challenge: false
        authentication_backend:
          type: "internal"

      oidc_auth_domain:
        description: "Authenticate via OIDC (OpenID Connect)"
        http_enabled: true
        transport_enabled: true
        order: 1
        http_authenticator:
          type: "openid"
          challenge: true
          config:
            subject_key: "preferred_username"
            roles_key: "groups"
            openid_connect_url: "http://linuxser.stack.com:8080/realms/stack/.well-known/openid-configuration"
            client_id: "opensearchsso"
            client_secret: "HifH6BgxETJ0exeYTsrLB4nNb1YOBcw5" # replace with your actual client secret
        authentication_backend:
          type: "noop"

As OpenID connect itself can be used for user authentication and authorization, there is no additional need of authentication_backend. We can set this value to noop.

Step5: Configure OpenSearch Dashboard Security

In order to enable OpenSearch Dashboard to support multiple authentication types we need to add “opensearch_security.auth.multiple_auth_enabled” as “true” and update the list of authentication types to be supported using the following setting “opensearch_security.auth.type” with values “basicauth” and “openid”.

Additionally we are configuring the OpenSearch Dashboard with the following settings to identify and communicate with the Keycloak OIDC provider.

  • opensearch_security.openid.connect_url: The URL where the IdP publishes the OpenID metadata. Required.
  • opensearch_security.openid.client_id: The ID of the OpenID Connect client configured in your IdP. Required.
  • opensearch_security.openid.client_secret: The client secret of the OpenID Connect client configured in your IdP. Required.
  • opensearch_security.openid.base_redirect_url: The base of the redirect URL that will be sent to your IdP. Optional.
  • opensearch_security.openid.scope: The scope of the identity token issued by the IdP. Optional. Default is openid profile email address phone.
admin@linuxser:~/opensearch_with_openid$ cat opensearch_dashboards.yml 
---
opensearch.hosts: [https://localhost:9200]
opensearch.ssl.verificationMode: none
opensearch.username: kibanaserver
opensearch.password: kibanaserver
opensearch.requestHeadersWhitelist: [authorization, securitytenant]

opensearch_security.multitenancy.enabled: true
opensearch_security.multitenancy.tenants.preferred: [Private, Global]
opensearch_security.readonly_mode.roles: [kibana_read_only]
# Use this setting if you are running opensearch-dashboards without https
opensearch_security.cookie.secure: false
server.host: '0.0.0.0'

#OpenID authentication - keycloak
opensearch_security.auth.multiple_auth_enabled: true
opensearch_security.auth.type: ["basicauth","openid"]
opensearch_security.openid.connect_url: "http://linuxser.stack.com:8080/realms/stack/.well-known/openid-configuration"
opensearch_security.openid.client_id: "opensearchsso"
opensearch_security.openid.client_secret: "HifH6BgxETJ0exeYTsrLB4nNb1YOBcw5"
opensearch_security.openid.base_redirect_url: "http://linuxser.stack.com:5601"
opensearch_security.openid.scope: "openid profile email groups"

Step6: Update Docker Compose file

Once we have the OpenSearch config.yml file and OpenSearch Dashboard config file ready with the updated settings, we can volume mount them into the docker container services as shown below.

admin@linuxser:~/opensearch_with_openid$ cat docker-compose.yml 
services:
  opensearch-node1: # This is also the hostname of the container within the Docker network (i.e. https://opensearch-node1/)
    image: opensearchproject/opensearch:3.3.0
    container_name: opensearch-node1
    environment:
      - cluster.name=opensearch-cluster # Name the cluster
      - node.name=opensearch-node1 # Name the node that will run in this container
      - discovery.seed_hosts=opensearch-node1,opensearch-node2 # Nodes to look for when discovering the cluster
      - cluster.initial_cluster_manager_nodes=opensearch-node1,opensearch-node2 # Nodes eligibile to serve as cluster manager
      - bootstrap.memory_lock=true # Disable JVM heap memory swapping
      - "OPENSEARCH_JAVA_OPTS=-Xms3072m -Xmx3072m" # Set min and max JVM heap sizes to at least 50% of system RAM
      - OPENSEARCH_INITIAL_ADMIN_PASSWORD=${OPENSEARCH_INITIAL_ADMIN_PASSWORD} # Sets the demo admin user password when using demo configuration (for OpenSearch 2.12 and later)
    ulimits:
      memlock:
        soft: -1 # Set memlock to unlimited (no soft or hard limit)
        hard: -1
      nofile:
        soft: 65536 # Maximum number of open files for the opensearch user - set to at least 65536
        hard: 65536
    volumes:
      - opensearch-data1:/usr/share/opensearch/data # Creates volume called opensearch-data1 and mounts it to the container
      - ./config.yml:/usr/share/opensearch/config/opensearch-security/config.yml
    ports:
      - 9200:9200 # REST API
      - 9600:9600 # Performance Analyzer
    networks:
      - opensearch-net # All of the containers will join the same Docker bridge network
  opensearch-node2:
    image: opensearchproject/opensearch:3.3.0 # This should be the same image used for opensearch-node1 to avoid issues
    container_name: opensearch-node2
    environment:
      - cluster.name=opensearch-cluster
      - node.name=opensearch-node2
      - discovery.seed_hosts=opensearch-node1,opensearch-node2
      - cluster.initial_cluster_manager_nodes=opensearch-node1,opensearch-node2
      - bootstrap.memory_lock=true
      - "OPENSEARCH_JAVA_OPTS=-Xms3072m -Xmx3072m"
      - OPENSEARCH_INITIAL_ADMIN_PASSWORD=${OPENSEARCH_INITIAL_ADMIN_PASSWORD}
    ulimits:
      memlock:
        soft: -1
        hard: -1
      nofile:
        soft: 65536
        hard: 65536
    volumes:
      - opensearch-data2:/usr/share/opensearch/data
      - ./config.yml:/usr/share/opensearch/config/opensearch-security/config.yml
    networks:
      - opensearch-net
  opensearch-dashboards:
    image: opensearchproject/opensearch-dashboards:3.3.0 # Make sure the version of opensearch-dashboards matches the version of opensearch installed on other nodes
    container_name: opensearch-dashboards
    ports:
      - 5601:5601 # Map host port 5601 to container port 5601
    expose:
      - "5601" # Expose port 5601 for web access to OpenSearch Dashboards
    volumes:
      - ./opensearch_dashboards.yml:/usr/share/opensearch-dashboards/config/opensearch_dashboards.yml
    environment:
      OPENSEARCH_HOSTS: '["https://opensearch-node1:9200","https://opensearch-node2:9200"]' # Define the OpenSearch nodes that OpenSearch Dashboards will query
    networks:
      - opensearch-net
  keycloak:
    image: quay.io/keycloak/keycloak:26.1.4
    command: start-dev
    container_name: keycloak
    environment:
      KC_BOOTSTRAP_ADMIN_USERNAME: admin
      KC_BOOTSTRAP_ADMIN_PASSWORD: admin
    ports:
      - "8080:8080"
      - "8443:8443"
    networks:
      - opensearch-net
    healthcheck:
      test: ["CMD", "sh", "-c", "exec 3<>/dev/tcp/localhost/8080; echo 'HTTP/1.1' >&3; head -1 <&3"]
      interval: 30s
      timeout: 10s
      retries: 5


volumes:
  opensearch-data1:
  opensearch-data2:

networks:
  opensearch-net:

Restart OpenSearch and OpenSearch Dashboard services for the changes to take effect.

admin@linuxser:~/opensearch_with_openid$ docker compose down opensearch-node1 -v
admin@linuxser:~/opensearch_with_openid$ docker compose down opensearch-node2 -v
admin@linuxser:~/opensearch_with_openid$ docker compose down opensearch-dashboards -v
admin@linuxser:~/opensearch_with_openid$ docker compose up -d opensearch-node1
admin@linuxser:~/opensearch_with_openid$ docker compose up -d opensearch-node2
admin@linuxser:~/opensearch_with_openid$ docker compose up -d opensearch-dashboards 

Step7: Create Role

Let’s now create a new role named “osdevelopers” with cluster and index level permissions as defined below.

The cluster_composite_ops action group in OpenSearch grants a set of cluster-level permissions primarily related to bulk operations and alias management.

PUT https://linuxser.stack.com:9200/_plugins/_security/api/roles/osdevelopers
Authorization: Basic admin:Se@rch@2025
Accept: application/json
Content-Type: application/x-ndjson

{
  "cluster_permissions": [
    "cluster_composite_ops"
  ],
  "index_permissions": [{
    "index_patterns": [
      "*"
    ],
    "allowed_actions": [
      "read",
      "search"
    ]
  }]
}

Here will create another role named “osadministrators” with additional permission to monitor the cluster.

PUT https://linuxser.stack.com:9200/_plugins/_security/api/roles/osadministrators
Authorization: Basic admin:Se@rch@2025
Accept: application/json
Content-Type: application/x-ndjson

{
  "cluster_permissions": [
    "cluster_composite_ops",
    "cluster_monitor"
  ],
  "index_permissions": [{
    "index_patterns": [
      "*"
    ],
    "allowed_actions": [
      "read",
      "search"
    ]
  }]
}

Step8: Create role mapping

Once we have the role ready, we need to map the roles “osdevelopers” and “osadministrators” to backend roles “osdevelopers” and “osadministrators” respectively. These backend roles are the groups which are assigned to the users in the realm “stack”.

### Create a role mapping
PUT https://linuxser.stack.com:9200/_plugins/_security/api/rolesmapping/osdevelopers
Authorization: Basic admin:Se@rch@2025
Accept: application/json
Content-Type: application/json

{
  "backend_roles" : [ "osdevelopers" ]
}
### Create a role mapping
PUT https://linuxser.stack.com:9200/_plugins/_security/api/rolesmapping/osadministrators
Authorization: Basic admin:Se@rch@2025
Accept: application/json
Content-Type: application/json

{
  "backend_roles" : [ "osadministrators" ]
}

Step9: Validate Access with OIDC

Now, if you try to launch the OpenSearch Dashboard portal, it will have both “basic” auth and “login with single sign-on” options enabled as shown below.

Now if you click the “login with single sign-on” button you will be redirected to keycloak sign-in page where in you can login with the “osdev” user created in the realm “stack”.

If everything goes fine, you should be able to login with “osdev” user into the OpenSearch Dashboard with the permissions granted under the role “osdevelopers”.

As the OpenSearch cluster still supports the “basic” auth with backend interal user database for authorization, admin user would still be able to login with basic auth and manage the cluster.

You can as well try to login with “osadmin” user and try to see if you able to monitor the cluster by running some of the queries relaled to cluster monitoring.

Hope you enjoyed reading this article. Thank you..