Enabled JWT Authentication for Opensearch

Hello Everyone,

We want to iframe Opensearch Dashboards in website. However, it is redirecting to the login page. I have enabled anonumous_auth to bypass the Login Page for Opensearch Dashboards and display the dashboards directly. However, it didn’t work after making changes in the config.

Hence, I have enabled JWT authentication for Opensearch to pass the authorization in the URL . I am able to access the Opensearch Dashboard successfully, which bypasses the login page and redirects to the home page.
However, I am seeing error related to roles and not able to access some of the features in Opensearch Dashboard.

I’m trying to autenticate to OpenSearch using a JWT token and I keep getting the following error:

[security_exception] no permissions for [cluster:admin/opendistro/ism/policy/search] and User [name=opendistro_security_anonymous, backend_roles=[opendistro_security_anonymous_backendrole], requestedTenant=null]

Here are the config files:

1. Opensearch.yml


network.host: 0.0.0.0
discovery.type: single-node
#action.destructive_requires_name: true
plugins.security.disabled: false
######## Start OpenSearch Security Demo Configuration ########
# WARNING: revise all the lines below before you go into production
plugins.security.ssl.transport.pemcert_filepath: /home/aiml/opensearch-2.3.0/config/node1-2.pem
plugins.security.ssl.transport.pemkey_filepath:  /home/aiml/opensearch-2.3.0/config/node1-key-2.pem
plugins.security.ssl.transport.pemtrustedcas_filepath: /home/aiml/opensearch-2.3.0/config/root-ca-2.pem
plugins.security.ssl.transport.enforce_hostname_verification: false
plugins.security.ssl.transport.resolve_hostname: false
plugins.security.ssl.http.enabled: true
plugins.security.ssl.http.pemcert_filepath: /home/aiml/opensearch-2.3.0/config/node1-2.pem
plugins.security.ssl.http.pemkey_filepath:  /home/aiml/opensearch-2.3.0/config/node1-key-2.pem
plugins.security.ssl.http.pemtrustedcas_filepath: /home/aiml/opensearch-2.3.0/config/root-ca-2.pem
plugins.security.allow_unsafe_democertificates: true
plugins.security.allow_default_init_securityindex: true
plugins.security.authcz.admin_dn:
  - 'EMAILADDRESS=AA,CN=BB,OU=ZZ,O=XX,L=YY,ST=CC,C=LL'
plugins.security.nodes_dn:
  - 'EMAILADDRESS=AA,CN=BB,OU=ZZ,O=XX,L=YY,ST=CC,C=LL'
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: [".plugins-ml-model", ".plugins-ml-task", ".opendistro-alerting-config", ".opendistro-alerting-alert*", ".opendistro-anomaly-results*", ".opendistro-anomaly-detector*", ".opendistro-anomaly-checkpoints", ".opendistro-anomaly-detection-state", ".opendistro-reports-*", ".opensearch-notifications-*", ".opensearch-notebooks", ".opensearch-observability", ".opendistro-asynchronous-search-response*", ".replication-metadata-store"]
node.max_local_storage_nodes: 3
######## End OpenSearch Security Demo Configuration ########

  1. Config.yml
_meta:
  type: "config"
  config_version: 2

config:
  dynamic:
    http:
      anonymous_auth_enabled: true
      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: false
        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: true
        transport_enabled: true
        order: 0
        http_authenticator:
          type: jwt
          challenge: false
          config:
            signing_key: "kUPSF3ZLfr5wk/52hfubABCPENsGUAmPQRZEERFNDL2="
            jwt_header: "Authorization"
            jwt_url_parameter: "jwtToken"
            roles_key: "roles"
            subject_key: "sub"
        authentication_backend:
          type: noop

  1. Opensearch_Dashboard.yml
server.ssl.enabled: true
server.ssl.certificate: /home/aiml/opensearch-2.3.0/config/node1-2.pem
server.ssl.key: /home/aiml/opensearch-2.3.0/config/node1-key-2.pem
opensearch.ssl.certificateAuthorities: [ "/home/aiml/opensearch-2.3.0/config/root-ca-2.pem" ]
opensearch.ssl.verificationMode: certificate
opensearchDashboards.defaultAppId: "home"
opensearch.hosts: [https://0.0.0.0:9200]
opensearch.username: "admin"
opensearch.password: "admin"
opensearch_security.auth.anonymous_auth_enabled: true
opensearch.requestHeadersAllowlist: [authorization, securitytenant, Authorization]
opensearch_security.auth.type: "jwt"
opensearch_security.jwt.url_param: "jwtToken"
opensearch_security.jwt.enabled: true
opensearch_security.multitenancy.enabled: true
opensearch_security.multitenancy.tenants.preferred: [Private, Global]
opensearch_security.readonly_mode.roles: [kibana_read_only]
opensearch_security.cookie.secure: true

How to map the existing opendistro_security_anonymous_backendrole to admin? Attached are the roles.yml and roles_mapping.yml files

Kindly suggest how to take this forward and change the role to admin.

@Pablo Please help

roles.yml

opendistro_security_anonymous:
  reserved: true
  cluster_permissions:
    - 'unlimited'
  index_permissions:
    - index_patterns:
        - '*'
      allowed_actions:
        - 'unlimited'
  tenant_permissions:
    - tenant_patterns:
        - "global_tenant"
      allowed_actions:
        - "kibana_all_write"
                                                                                                                   1,1           Top


roles_mapping.yml

---
opendistro_security_anonymous:
  reserved: true
  backend_roles:
  - "opendistro_security_anonymous_backendrole"

Hello,

I am trying to give anonymous user admin level privilege. I have updated the config.yml with http.anonymous_auth_enabled: true and also made required changes to role.yml for anonymous_backendrole role.

But I am still getting this error -

{"error":{"root_cause":[{"type":"security_exception","reason":"no permissions for [cluster:monitor/main] and User [name=opendistro_security_anonymous, backend_roles=[opendistro_security_anonymous_backendrole], requestedTenant=null]"}],"type":"security_exception","reason":"no permissions for [cluster:monitor/main] and User [name=opendistro_security_anonymous, backend_roles=[opendistro_security_anonymous_backendrole], requestedTenant=null]"},"status":403}

Looking for here if anyone could provide correct way to setup the anonymous auth with Opendistro.

@Pablo @Anthony

@Bindu Giving admin permissions to the anonymous user is not recommended as it allows admin access to anyone who will access either the OpenSearch or OpenSearch Dashboards URL without a password.

However, it is easier to use the mapping below to give admin permissions to any role.

roles_mapping.yml

---
opendistro_security_anonymous:
  reserved: true
  backend_roles:
  - "admin"

Thanks for the reply @Pablo.

I have mapped opendistro_security_anonymous to admin in the roles_mapping.yml as per your suggestion and executed ./securityadmin.sh for the changes to be applied with the certs argument.

./securityadmin.sh -f /home/aiml/opensearch-2.3.0/config/opensearch-security/config.yml -icl -nhnv -cert /home/aiml/opensearch-2.3.0/config/admin-2.pem -cacert /home/aiml/opensearch-2.3.0/config/root-ca-2.pem -key /home/aiml/opensearch-2.3.0/config/admin-key-2.pem -t config

However, In the response I see the error:

ERR: Seems you use a node certificate which is also an admin certificate
     That may have worked with older OpenSearch Security versions but it indicates
     a configuration error and is therefore forbidden now.
OpenSearch Version: 2.3.0
Contacting opensearch cluster 'opensearch' and wait for YELLOW clusterstate ...
Clustername: opensearch
Clusterstate: YELLOW
Number of nodes: 1
Number of data nodes: 1
.opendistro_security index already exists, so we do not need to create one.
Populate config from /home/aiml/opensearch-2.3.0/plugins/opensearch-security/tools
Force type: config
Will update '/config' with /home/aiml/opensearch-2.3.0/config/opensearch-security/config.yml
   SUCC: Configuration for 'config' created or updated
SUCC: Expected 1 config types for node {"updated_config_types":["config"],"updated_config_size":1,"message":null} is 1 (["config"]) due to: null
Done with success

curl https://localhost:9200 -k
{"error":{"root_cause":[{"type":"security_exception","reason":"no permissions for [cluster:monitor/main] and User [name=opendistro_security_anonymous, backend_roles=[opendistro_security_anonymous_backendrole], requestedTenant=null]"}],"type":"security_exception","reason":"no permissions for [cluster:monitor/main] and User [name=opendistro_security_anonymous, backend_roles=[opendistro_security_anonymous_backendrole], requestedTenant=null]"},"status":403}

Opensearch Dashboard logs

log   [10:02:59.180] [error][data][opensearch] [security_exception]: no permissions for [indices:data/read/get] and User [name=opendistro_security_anonymous, backend_roles=[opendistro_security_anonymous_backendrole], requestedTenant=null]
  log   [10:03:09.987] [error][data][opensearch] [security_exception]: no permissions for [indices:data/read/search] and User [name=opendistro_security_anonymous, backend_roles=[opendistro_security_anonymous_backendrole], requestedTenant=null]
  log   [10:03:24.570] [error][data][opensearch] [security_exception]: no permissions for [indices:data/read/get] and User [name=opendistro_security_anonymous, backend_roles=[opendistro_security_anonymous_backendrole], requestedTenant=null]
  log   [10:03:24.621] [error][data][opensearch] [security_exception]: no permissions for [indices:data/read/get] and User [name=opendistro_security_anonymous, backend_roles=[opendistro_security_anonymous_backendrole], requestedTenant=null]
  log   [10:03:24.686] [error][http] StatusCodeError: [security_exception] no permissions for [indices:admin/resolve/index] and User [name=opendistro_security_anonymous, backend_roles=[opendistro_security_anonymous_backendrole], requestedTenant=null]
    at respond (/home/aiml/opensearch-dashboards-2.3.0/node_modules/elasticsearch/src/lib/transport.js:349:15)
    at checkRespForFailure (/home/aiml/opensearch-dashboards-2.3.0/node_modules/elasticsearch/src/lib/transport.js:306:7)
    at HttpConnector.<anonymous> (/home/aiml/opensearch-dashboards-2.3.0/node_modules/elasticsearch/src/lib/connectors/http.js:173:7)
    at IncomingMessage.wrapper (/home/aiml/opensearch-dashboards-2.3.0/node_modules/lodash/lodash.js:4991:19)
    at IncomingMessage.emit (events.js:412:35)
    at endReadableNT (internal/streams/readable.js:1333:12)
    at processTicksAndRejections (internal/process/task_queues.js:82:21) {
  status: 403,
  displayName: 'AuthorizationException',
  path: '/_resolve/index/*',
  query: undefined,
  body: {
    error: {
      root_cause: [Array],
      type: 'security_exception',
      reason: 'no permissions for [indices:admin/resolve/index] and User [name=opendistro_security_anonymous, backend_roles=[opendistro_security_anonymous_backendrole], requestedTenant=null]'
    },
    status: 403
  },
  statusCode: 403,
  response: '{"error":{"root_cause":[{"type":"security_exception","reason":"no permissions for [indices:admin/resolve/index] and User [name=opendistro_security_anonymous, backend_roles=[opendistro_security_anonymous_backendrole], requestedTenant=null]"}],"type":"security_exception","reason":"no permissions for [indices:admin/resolve/index] and User [name=opendistro_security_anonymous, backend_roles=[opendistro_security_anonymous_backendrole], requestedTenant=null]"},"status":403}',
  toString: [Function (anonymous)],
  toJSON: [Function (anonymous)]

Does this error indicates any issue with the admin or node certs, because the changes I am trying to make in the roles.mapping.yml is not considered, but it is still pointing the backend role : “opendistro_security_anonymous_backendrole”

Hi @pablo

The below Topics are inter-related with each other.

We want display embedded Opensearch Dashboards within an iframe in our website. The iframe tag should be ssl enabled, hence we tried creating certs and followed the official documentation to configure TLS. However, the iframe was redirecting to Login page, to bypass that we enabled anonymous_auth, and enabled JWT Authentication to pass the token in the URL, which resolved the issue. But anonymous_auth doesn’t have required privileges to create index, dashboard or anything. I tried mapping the backend role to admin as well, which is again giving errors.

Kindly help.

if you’re using JWT then you shouldn’t need to use anonymous auth because your users are already authenticated. was there an issue with using JWT directly?

Hi @ralph,

I have disabled anonymous auth both in opensearch.yml and opensearch_dashboards.yml. When I use JWT directly, and try to access the dashboard, I am seeing 401 Unauthorized.

how did you configure JWT? by default it expects an HTTP header with Authorization: Bearer [TOKEN]. you need to configure that it should pick it from your URL instead.

i’m not familiar with OpenSearch Dashboards, but at least for the backend you can do so in the config with the jwt_url_parameter (see the docs - note that i also have no personal experience with this). i would presume that something similar exists for dashboards, but you’d have to check the documentation for that

@Bindu You have used double quotes with the token. Try this instead.

https://<opensearch_dashboards>:5601?jwtToken=eyJhbGciOiJIUzI1...kuPHer3IpU

Hi @Pablo,

I tried removing double quotes with the token, but still it is showing Unauthorized

From the logs, I see:

[2023-03-24T04:46:11,047][WARN ][o.o.s.h.HTTPBasicAuthenticator] [CNAS-AIML] No 'Basic Authorization' header, send 401 and 'WWW-Authenticate Basic'
[2023-03-24T04:46:11,047][WARN ][o.o.s.a.BackendRegistry  ] [CNAS-AIML] Authentication finally failed for null from 1.1.1.1:43808
[2023-03-24T04:46:14,029][WARN ][o.o.s.h.HTTPBasicAuthenticator] [CNAS-AIML] No 'Basic Authorization' header, send 401 and 'WWW-Authenticate Basic'
[2023-03-24T04:46:14,029][WARN ][o.o.s.a.BackendRegistry  ] [CNAS-AIML] Authentication finally failed for null from 1.1.1.1:43808

@Bindu Could you share your JWT token?

Hi @Pablo, I am able to access the Opensearch dashboard Web UI with new generated JWT token, I have set anonumous_auth to false in the config.yml as it does not provide required permissions to read/create indexes or dashboards.

Now the Web UI is having admin level privileges with New JWT token:

https://0.0.0.0:5601?jwtToken=eyJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6ImFkbWluIiwiaXNzIjoiaHR0cHM6Ly9sb2NhbGhvc3QiLCJzdWIiOiJhZG1pbiIsImV4cCI6MTY3OTkwMjA5M30.mogmYOFT7an38B5W5Bk-PNX2k5883LUNDnASHAyUOtA

However, the iframe embedded dashboards is still showing as {“statusCode”:401,“error”:“Unauthorized”,“message”:“Unauthorized”} in our website.

this is the iframe tag:

<iframe src="https://0.0.0.0:5601/app/dashboards#/view/2c596fa0-bc14-11ed-aee2-8b288182ed4d?embed=true&_g=(filters%3A!()%2CrefreshInterval%3A(pause%3A!f%2Cvalue%3A900000)%2Ctime%3A(from%3Anow-90d%2Cto%3Anow))&show-top-menu=true&show-query-input=true&show-time-filter=true" height="600" width="800"></iframe>

Kindly suggest.

@Bindu Your iframe URL doesn’t contain a JWT token.

It should be like this. I did my testing with Firefox Web Browser.

<iframe src="https://0.0.0.0:5601/app/dashboards?jwtToken=eyJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6ImFkbWluIiwiaXNzIjoiaHR0cHM6Ly9sb2NhbGhvc3QiLCJzdWIiOiJhZG1pbiIsImV4cCI6MTY3OTkwMjA5M30.mogmYOFT7an38B5W5Bk-PNX2k5883LUNDnASHAyUOtA#/view/2c596fa0-bc14-11ed-aee2-8b288182ed4d?embed=true&_g=(filters%3A!()%2CrefreshInterval%3A(pause%3A!f%2Cvalue%3A900000)%2Ctime%3A(from%3Anow-90d%2Cto%3Anow))&show-top-menu=true&show-query-input=true&show-time-filter=true" height="600" width="800"></iframe>

Hi @Pablo, thanks so much for your support.

I tried embedding the JWT Token in the iframe URL and it worked.

https://0.0.0.0:5601/app/dashboards?jwtToken=eyJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6ImFkbWluIiwiaXNzIjoiaHR0cHM6Ly9sb2NhbGhvc3QiLCJzdWIiOiJhZG1pbiIsImV4cCI6MTY3OTkxMjE1M30.jO4UMvcQbAJ5_4wq2yHSg54Pn3o0qBIcqO58OTLeEgQ#/view/2c596fa0-bc14-11ed-aee2-8b288182ed4d?embed=true&_g=(filters%3A!()%2CrefreshInterval%3A(pause%3A!f%2Cvalue%3A900000)%2Ctime%3A(from%3Anow-90d%2Cto%3Anow))

Now since the expiration is set in the token, below is the Java Code that generates JWTs valid for 25 hours.

import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.io.Encoders;
import io.jsonwebtoken.security.Keys;
import java.security.Key;
import java.util.Date;
import java.util.HashMap;

public class JWTTestTokens {
    public static void main(String[] args) {
        Key key = Keys.secretKeyFor(SignatureAlgorithm.HS256);
        Date exp = new Date(System.currentTimeMillis() + 90000000);
        HashMap<String,Object> hm = new HashMap<>();
        hm.put("roles","admin");
        String jws = Jwts.builder()
                .setClaims(hm)
                .setIssuer("https://localhost")
                .setSubject("admin")
                .setExpiration(exp)
                .signWith(key).compact();
        System.out.println("Token:");
        System.out.println(jws);
        if(Jwts.parser().setSigningKey(key).parseClaimsJws(jws).getBody().getSubject().equals("test")) {
            System.out.println("test");
        }
        String encoded = Encoders.BASE64.encode(key.getEncoded());
        System.out.println("Shared secret:");
        System.out.println(encoded);
    }
}

Is it possible to extend the validation of the token for infinite no. of days?

@Bindu I’m not aware of such an option. Maybe try to remove exp from the token or set it to some very high number i.e. 100 years.

Hi @Pablo, Hope you’re doing well.

I have removed the exp from the token and have a fixed JWT Token appended in the iframe url.

(https://0.0.0.0:5601/app/dashboards?jwtToken=eyJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6ImFkbWluIiwiaXNzIjoiaHR0cHM6Ly9sb2NhbGhvc3QiLCJzdWIiOiJhZG1pbiIsImV4cCI6MTY3OTkxMjE1M30.jO4UMvcQbAJ5_4wq2yHSg54Pn3o0qBIcqO58OTLeEgQ#/view/2c596fa0-bc14-11ed-aee2-8b288182ed4d?embed=true&_g=(filters%3A!()%2CrefreshInterval%3A(pause%3A!f%2Cvalue%3A900000)%2Ctime%3A(from%3Anow-90d%2Cto%3Anow))

The above iframe is working in the Web UI, however when I try to integrate in our application, I see that the iframe keeps loading for long time and doesn’t display the dashboard, it will be in the loading state itself. From the OpenSearch logs, I see these errors

What could be the issue here?

@Bindu Have you tried Firefox?

Hi @Pablo, Nope, I tried in Google Chrome Incognito Mode.

@Bindu It worked for me only in the Firefox. I think it has something to do with the browser.
In Windows, Firefox doesn’t use system’s Internet settings but its own.
I dont think this issue regards the security plugin.