How to secure OpenSearch Cluster using OpenLDAP Identity Provider

How to secure OpenSearch Cluster using OpenLDAP Identity Provider

opensearch_with_ldap_auth_demo

Here in this article we will try to secure OpenSearch Cluster using OpenLDAP based authentication and authorization.

Test Environment

  • Fedora Server 41
  • OpenSearch v3.3.0
  • OpenLDAP v2

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 OpenSearch Cluster and OpenLDAP running

As a first step we will try to setup OpenSearch cluster and OpenLDAP service using docker compose file. Here are the details below.

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

admin@linuxser:~/opensearch_with_openldap$ cat .env 
OPENSEARCH_INITIAL_ADMIN_PASSWORD=Se@rch@2025

Below is the custom LDAP directory structure LDIF file that we will be using to setup LDAP domain, groups and users for this demo. As you can see there are two types of users.

  • “osdev1” and “osdev2” part of “Developers” group
  • “osadmin1” user part of “Administrator” group
admin@linuxser:~/opensearch_with_openldap$ cat ldifs/custom.ldif 
dn: dc=stack,dc=com
objectClass: top
objectClass: domain
dc: stack

dn: ou=groups,dc=stack,dc=com
objectClass: organizationalUnit
objectClass: top
ou: groups

dn: ou=users,dc=stack,dc=com
objectClass: organizationalUnit
objectClass: top
ou: users

dn: cn=osdev1,ou=users,dc=stack,dc=com
objectClass: person
objectClass: inetOrgPerson
objectClass: organizationalPerson
objectClass: top
cn: osdev1
sn: osdev1
givenName: osdev1
mail: osdev1@stack.com
uid: 1001
userPassword: osdev1

dn: cn=osdev2,ou=users,dc=stack,dc=com
objectClass: person
objectClass: inetOrgPerson
objectClass: organizationalPerson
objectClass: top
cn: osdev2
sn: osdev2
givenName: osdev2
mail: osdev2@stack.com
uid: 1002
userPassword: osdev2

dn: cn=osadmin1,ou=users,dc=stack,dc=com
objectClass: person
objectClass: inetOrgPerson
objectClass: organizationalPerson
objectClass: top
cn: osadmin1
sn: osadmin1
givenName: osadmin1
mail: osadmin1@stack.com
uid: 1003
userPassword: osadmin1

dn: cn=Administrator,ou=groups,dc=stack,dc=com
objectClass: groupOfNames
objectClass: top
cn: Administrator
member: cn=osadmin1,ou=users,dc=stack,dc=com

dn: cn=Developers,ou=groups,dc=stack,dc=com
objectClass: groupOfNames
objectClass: top
cn: Developers
member: cn=osdev1,ou=users,dc=stack,dc=com
member: cn=osdev2,ou=users,dc=stack,dc=com

Here is the docker compose file to setup OpenSearch Cluster along with OpenLDAP service.

admin@linuxser:~/opensearch_with_openldap$ 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
    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
    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
    environment:
      OPENSEARCH_HOSTS: '["https://opensearch-node1:9200","https://opensearch-node2:9200"]' # Define the OpenSearch nodes that OpenSearch Dashboards will query
    networks:
      - opensearch-net

  openldap:
    image: bitnami/openldap:2
    container_name: openldap
    ports:
      - '1389:1389'
      - '1636:1636'
    environment:
      - LDAP_ADMIN_USERNAME=admin
      - LDAP_ADMIN_PASSWORD=admin@1234
      #- LDAP_USERS=devuser1,devuser2
      #- LDAP_PASSWORDS=devuser1,devuser2
      - LDAP_ROOT=dc=stack,dc=com
      - LDAP_ADMIN_DN=cn=admin,dc=stack,dc=com
      - LDAP_CUSTOM_LDIF_DIR=/ldifs         # Configuring LDAP to use custom LDIF file
    networks:
      - opensearch-net
    volumes:
      - 'openldap-data:/bitnami/openldap'
      - './ldifs:/ldifs'                    # Custom LDIF file directory loaded from host to container

volumes:
  opensearch-data1:
  opensearch-data2:
  openldap-data:

networks:
  opensearch-net:

Let’s now instantiate the OpenSearch cluster services.

admin@linuxser:~/opensearch_with_openldap$ docker compose -f opensearch/docker-compose.yml up -d

If you face any issue in pulling the OpenLDAP docker image you can build your own docker image for OpenLDAP using the below steps and try starting up the service.

admin@linuxser:~/openldapbuild$ git clone https://github.com/bitnami/containers.git
admin@linuxser:~/openldapbuild$ cd containers/bitnami/openldap/2.6/debian-12
admin@linuxser:~/openldapbuild/containers/bitnami/openldap/2.6/debian-12$ docker build -t bitnami/openldap:2 .

Validate your OpenSearch cluster is up and running.

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

Once the OpenLDAP services is running we can validate LDAP DIT tree structure using the below command.

admin@linuxser:~/opensearch_with_openldap$ ldapsearch -H ldap://linuxser.stack.com:1389 -x -b 'dc=stack,dc=com' -D 'cn=admin,dc=stack,dc=com' '(objectClass=*)' -W

NOTE: Install openldap-clients package for ldap client tools installation

Step2: Configure OpenSearch Cluster Security config with OpenLDAP

Here in this step we will configure the OpenSearch cluster to leverage Chained authentication feature to enabled authentication and authorization using both “basicauth” and “ldap” auth.

For internal users such as “admin” and “kibanaserver” we will leverage the “basicauth” with internal user database for authc and authz. For all other users, authc and authz will be carried out using the “ldap” identity provider.

admin@linuxser:~/opensearch_with_openldap$ 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: true
        authentication_backend:
          type: intern
      ldap:
        description: "Authenticate via LDAP or Active Directory"
        http_enabled: true
        transport_enabled: true
        order: 1
        http_authenticator:
          type: basic
          challenge: false
        authentication_backend:
          # LDAP authentication backend (authenticate users against a LDAP or Active Directory)
          type: ldap
          config:
            # enable ldaps
            enable_ssl: false
            # enable start tls, enable_ssl should be false
            enable_start_tls: false
            # send client certificate
            enable_ssl_client_auth: false
            # verify ldap hostname
            verify_hostnames: true
            hosts:
            - linuxser.stack.com:1389
            bind_dn: cn=admin,dc=stack,dc=com
            password: admin@1234
            userbase: 'ou=users,dc=stack,dc=com'
            # Filter to search for users (currently in the whole subtree beneath userbase)
            # {0} is substituted with the username
            usersearch: '(cn={0})'
            # Use this attribute from the user as username (if not set then DN is used)
            username_attribute: cn
    authz:
      roles_from_myldap:
        description: "Authorize using LDAP"
        http_enabled: true
        transport_enabled: true
        authorization_backend:
          type: ldap
          config:
            enable_ssl: false
            enable_start_tls: false
            enable_ssl_client_auth: false
            verify_hostnames: true
            hosts:
            - linuxser.stack.com:1389
            bind_dn: cn=admin,dc=stack,dc=com
            password: admin@1234
            userbase: ou=users,dc=stack,dc=com
            usersearch: (cn={0})
            username_attribute: cn
            skip_users:
              - admin
              - kibanaserver
            rolebase: ou=groups,dc=stack,dc=com
            rolesearch: (member={0})
            userroleattribute: null
            userrolename: disabled
            rolename: cn
            resolve_nested_roles: false

Step3: Update Docker compose file

Let’s now update the docker compose file to volume mount the OpenSearch security “config.yml” file to each cluster node as shown below.

admin@linuxser:~/opensearch_with_openldap$ 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

  openldap:
    image: bitnami/openldap:2
    container_name: openldap
    ports:
      - '1389:1389'
      - '1636:1636'
    environment:
      - LDAP_ADMIN_USERNAME=admin
      - LDAP_ADMIN_PASSWORD=admin@1234
      #- LDAP_USERS=devuser1,devuser2
      #- LDAP_PASSWORDS=devuser1,devuser2
      - LDAP_ROOT=dc=stack,dc=com
      - LDAP_ADMIN_DN=cn=admin,dc=stack,dc=com
      - LDAP_CUSTOM_LDIF_DIR=/ldifs
      #- LDAP_DEBUG_LEVEL=-1
    networks:
      - opensearch-net
    volumes:
      - 'openldap-data:/bitnami/openldap'
      - './ldifs:/ldifs'

volumes:
  opensearch-data1:
  opensearch-data2:
  openldap-data:

networks:
  opensearch-net:

Once we have the config file and docker compose file ready we can restart our OpenSearch cluster for the changes to take effect.

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

Once the cluster is up and running, we can verify the security config that is currently enabled within the cluster as shown below.

### Get cluster security config
GET https://linuxser.stack.com:9200/_plugins/_security/api/securityconfig?pretty
Authorization: Basic admin:Se@rch@2025
Accept: application/json
Content-Type: application/json

Step4: Create Role

Here we will create two roles “Developers” and “Administrator” with a set of permissions to manager the OpenSearch cluster.

  • cluster_composite_ops: cluster_composite_ops is a default role in OpenSearch that grants permissions to perform actions related to cluster-level composite operations, such as managing data in multiple indices at once. This can include tasks like bulk indexing or large-scale data manipulation.
  • cluster_monitor: cluster_monitor is a feature within OpenSearch used to continuously check the health and performance of a cluster and take action when certain conditions are met.
PUT https://linuxser.stack.com:9200/_plugins/_security/api/roles/Developers
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"
    ]
  }]
}
PUT https://linuxser.stack.com:9200/_plugins/_security/api/roles/Administrator
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"
    ]
  }]
}

Step5: Create Role Mapping

Here in this step we will map the roles that were created in our previous step to backend ldap roles “Developers” and “Administrator” as shown below.

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

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

{
  "backend_roles" : [ "Administrator" ]
}

Step6: Validate access using LDAP users

Now its time to access our OpenSearch cluster service using the LDAP users as shown below.

Search all documents within the indicies

The following request succeeds with “osdev1” user as per the permissions granted.

GET https://linuxser.stack.com:9200/_search
Authorization: Basic osdev1:osdev1
Accept: application/json
Content-Type: application/json

{
  "query": {
    "match_all": {}
  }
}

Get OpenSearch cluster state

The following request fails as “osdev1” user does not have “cluster_monitor” permissions as per the “Developers” role which is assigned.

GET https://linuxser.stack.com:9200/_cluster/state
Authorization: Basic osdev1:osdev1
Accept: application/json
Content-Type: application/json

Search and Get OpenSearch cluster state

The same two requests will succeed for user “osadmin1” as the assigned role “Administrator” has the required permissions to search and monitor the cluster status.

GET https://linuxser.stack.com:9200/_search
Authorization: Basic osadmin1:osadmin1
Accept: application/json
Content-Type: application/json

{
  "query": {
    "match_all": {}
  }
}
GET https://linuxser.stack.com:9200/_cluster/state
Authorization: Basic osadmin1:osadmin1
Accept: application/json
Content-Type: application/json

You can also access the OpenSearch Dashboard with LDAP users which would leverage “ldap” identity provider.

Here are the screenshots below for “osdev1” and “osadmin1” users.

For “admin” user accessing the OpenSearch Dashboard it will use the “basicauth” for authentication and authorization.

Hope you enjoyed reading this article. Thank you..