[LDAP] Users lose permissions during session

Versions (relevant - OpenSearch/Dashboard/Server OS/Browser): 2.19.2

Describe the issue:

After the following error:
[2025-09-25T17:35:38.801][ERROR][o.o.s.a.BackendRegistry ] Unable to retrieve roles for user [name=, backend_roles=, requestedTenant=null] from ldap due to OpenSearchSecurityException[java.lang.NullPointerException: Unable to call ‘org.ldaptive.LdapEntry.getAttribute(String)’ because ‘e0’ is null]; nested: NullPointerException[Unable to call “org.ldaptive.LdapEntry.getAttribute(String)” because “e0” is null];

[25/09/2025 17:35:38.803][DEBUG][o.o.s.a.BackendRegistry ] Rest user “User [name=, backend_roles=, requestedTenant=null]” is authenticated
[2025-09-25T17:35:38,807][DEBUG][o.o.s.a.BackendRegistry ] Rest user “User [name=, backend_roles=, requestedTenant=null]” is authenticated
[2025-09-25T17:35:38,808][WARN ][o.o.s.c.PrivilegesInterceptorImpl] Tenant global_tenant is not allowed for user

Users lose permissions to view indexes and do not regain them even after logging out and logging back in.

However, the user has the assigned roles on ‘view roles and identities’ (see screen).

Configuration:

What configurations do you require?
We communicate with LDAP via port 389 in HTTP. Would switching to HTTPS resolve the issue?

Relevant Logs or Screenshots:

Thanks,

Alessio

@apinna Could you share your config.yml?


_meta:
  type: "config"
  config_version: 2

config:
  dynamic:
    # Set filtered_alias_mode to 'disallow' to forbid more than 2 filtered aliases per index
    # Set filtered_alias_mode to 'warn' to allow more than 2 filtered aliases per index but warns about it (default)
    # Set filtered_alias_mode to 'nowarn' to allow more than 2 filtered aliases per index silently
    #filtered_alias_mode: warn
    #do_not_fail_on_forbidden: false
    #kibana:
    # Kibana multitenancy
    #multitenancy_enabled: true
    #private_tenant_enabled: true
    #default_tenant: ""
    #server_username: kibanaserver
    #index: '.kibana'
    http:
      anonymous_auth_enabled: false
      xff:
        enabled: false
        internalProxies: '192\.168\.0\.10|192\.168\.0\.11' # regex pattern
        #internalProxies: '.*' # trust all internal proxies, regex pattern
        #remoteIpHeader:  'x-forwarded-for'
        ###### see https://docs.oracle.com/javase/7/docs/api/java/util/regex/Pattern.html for regex help
        ###### more information about XFF https://en.wikipedia.org/wiki/X-Forwarded-For
        ###### and here https://tools.ietf.org/html/rfc7239
        ###### and https://tomcat.apache.org/tomcat-8.0-doc/config/valve.html#Remote_IP_Valve
    authc:
      kerberos_auth_domain:
        http_enabled: false
        transport_enabled: false
        order: 6
        http_authenticator:
          type: kerberos
          challenge: true
          config:
            # If true a lot of kerberos/security related debugging output will be logged to standard out
            krb_debug: false
            # If true then the realm will be stripped from the user name
            strip_realm_from_principal: true
        authentication_backend:
          type: noop
      basic_internal_auth_domain:
        description: "Authenticate via HTTP Basic against internal users database"
        http_enabled: true
        transport_enabled: true
        order: 4
        http_authenticator:
          type: basic
          challenge: true
        authentication_backend:
          type: intern
      proxy_auth_domain:
        description: "Authenticate via proxy"
        http_enabled: false
        transport_enabled: false
        order: 3
        http_authenticator:
          type: proxy
          challenge: false
          config:
            user_header: "x-proxy-user"
            roles_header: "x-proxy-roles"
        authentication_backend:
          type: noop
      jwt_auth_domain:
        description: "Authenticate via Json Web Token"
        http_enabled: false
        transport_enabled: false
        order: 0
        http_authenticator:
          type: jwt
          challenge: false
          config:
            signing_key: "base64 encoded HMAC key or public RSA/ECDSA pem key"
            jwt_header: "Authorization"
            jwt_url_parameter: null
            jwt_clock_skew_tolerance_seconds: 30
            roles_key: null
            subject_key: null
        authentication_backend:
          type: noop
      clientcert_auth_domain:
        description: "Authenticate via SSL client certificates"
        http_enabled: false
        transport_enabled: false
        order: 2
        http_authenticator:
          type: clientcert
          config:
            username_attribute: cn #optional, if omitted DN becomes username
          challenge: false
        authentication_backend:
          type: noop
      ldap:
        description: "Authenticate via LDAP or Active Directory"
        http_enabled: true
        transport_enabled: true
        order: 1
        http_authenticator:
          type: basic
          challenge: true
        authentication_backend:
          # LDAP authentication backend (authenticate users against a LDAP or Active Directory)
          type: ldap
          config:
            connect_timeout: 10000
            read_timeout: 15000
            # 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: false
            pemtrustedcas_filepath: "/opt/opensearch/opensearch/config/certificates/chain.pem"
            hosts:
            - <ip>:389
            bind_dn: "<user>"
            password: "<pass>"
            users:
              primary-userbase:
                base: "<query1>"
                search: '(sAMAccountName={0})'
              secondary-userbase:
                base: "<query2>"
                search: '(sAMAccountName={0})'
            # Filter to search for users (currently in the whole subtree beneath userbase)
            # {0} is substituted with the username
            #usersearch: '(sAMAccountName={0})'
            # Use this attribute from the user as username (if not set then DN is used)
            username_attribute: 'sAMAccountName'
    authz:
      roles_from_myldap:
        description: "Authorize via LDAP or Active Directory"
        http_enabled: true
        transport_enabled: true
        authorization_backend:
          # LDAP authorization backend (gather roles from a LDAP or Active Directory, you have to configure the above LDAP authentication backend settings too)
          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: false
            pemtrustedcas_filepath: "/opt/opensearch/opensearch/config/certificates/chain.pem"
            hosts:
            - <ip>:389
            bind_dn: "<user>"
            password: "<pass>"
            roles:
              primary-rolebase:
                base: "<query ldap>"
                search: '(member={0})'
              secondary-rolebase:
                base: "<query ldap>"
                search: '(member={0})'
            # Filter to search for roles (currently in the whole subtree beneath rolebase)
            # {0} is substituted with the DN of the user
            # {1} is substituted with the username
            # {2} is substituted with an attribute value from user's directory entry, of the authenticated user. Use userroleattribute to specify the name of the attribute
            #rolesearch: '(member={0})'
            #my_search: '(&(objectCategory=group)(|(cn=YAK199GR)(cn=G55)))'
            # Specify the name of the attribute which value should be substituted with {2} above
            userroleattribute: null
            # Roles as an attribute of the user entry
            #userrolename: disabled
            userrolename: memberOf
            # The attribute in a role entry containing the name of that role, Default is "name".
            # Can also be "dn" to use the full DN as rolename.
            rolename: dn
            # Resolve nested roles transitive (roles which are members of other roles and so on ...)
            resolve_nested_roles: true
              #userbase: 'ou=people,dc=example,dc=com'
            # Filter to search for users (currently in the whole subtree beneath userbase)
            # {0} is substituted with the username
            #usersearch: '(uid={0})'
            # Skip users matching a user name, a wildcard or a regex pattern
            skip_users:
              - admin
              - kibanaserver

Hi Pablo thank you, The strange thing is that users can browse through the indexes without any problems, but at a certain point they suddenly lose their permissions.

1 Like

Is there any reason for these timeouts? If so, why is not repeated for authz section

It was a test carried out when we thought the problem was possibly slow communication with the LDAP servers.

If necessary, I will repeat it in authz, otherwise I will remove it.

We also tried increasing the cache period to 10 hours, thinking that the problem was in the regeneration of the cache once it expired. However, we are waiting for the user load to arrive for this test. At the moment, 2/3 users have worked on it, and we are expecting at least a dozen.

I am also sharing the master’s opensearch.yaml with you if it could be useful.

cluster.name: opensearch-prod
cluster.initial_cluster_manager_nodes: ["<master>"]
node.name: <master>
node.roles: [master]
#bootstrap.memory_lock: true
network.host: _ens4_
node.attr.zone: "gcp-turin"
discovery.seed_hosts: ["<master>"]

# Path settings
path.data: /opt/opensearch/data
path.logs: /opt/opensearch/log


plugins.security.disabled: false

# Security Configuration PRODUZIONE
plugins.security.ssl.transport.pemcert_filepath: /opt/opensearch/opensearch/config/certificates/node.pem
plugins.security.ssl.transport.pemkey_filepath: /opt/opensearch/opensearch/config/certificates/node-key.pem
plugins.security.ssl.transport.pemtrustedcas_filepath: /opt/opensearch/opensearch/config/certificates/root-ca.pem
plugins.security.ssl.transport.enforce_hostname_verification: false

plugins.security.ssl.http.enabled: true
plugins.security.ssl.http.pemcert_filepath: /opt/opensearch/opensearch/config/certificates/node.pem
plugins.security.ssl.http.pemkey_filepath: /opt/opensearch/opensearch/config/certificates/node-key.pem
plugins.security.ssl.http.pemtrustedcas_filepath: /opt/opensearch/opensearch/config/certificates/root-ca.pem

plugins.security.authcz.admin_dn:
  - "<query>"

plugins.security.nodes_dn:
  - "<query>"

plugins.security.audit.type: internal_opensearch
plugins.security.enable_snapshot_restore_privilege: true
plugins.security.check_snapshot_restore_write_privileges: true
plugins.security.restapi.roles_enabled: ["all_access", "security_rest_api_access"]

plugins.security.system_indices.enabled: true
plugins.security.system_indices.indices:
  [
    ".opendistro-alerting-config",
    ".opendistro-alerting-alert*",
    ".opendistro-anomaly-results*",
    ".opendistro-anomaly-detector*",
    ".opendistro-anomaly-checkpoints",
    ".opendistro-anomaly-detection-state",
    ".opendistro-reports-*",
    ".opendistro-notifications-*",
    ".opendistro-notebooks",
    ".opendistro-asynchronous-search-response*",
  ]

plugins.security.allow_default_init_securityindex: true

plugins.security.cache.ttl_minutes: 600

Thanks you.

1 Like

Hi,

we solved setting this:
resolve_nested_roles: false