Opensearch 3.2.0 configure SSO with SAML on Keycloak

Versions (relevant - OpenSearch/Dashboard/Server OS/Browser):
Opensearch and dashboards docker 3.2.0,
Keycloak - Version 26.3.2

Describe the issue:
I want to setup SingleSign on to Opensearch with keycloak and SAML, however when I click on “Log in with single-sign on” browser got redirected to https://opensearch.example.com:5601/auth/saml/login?redirectHash=false and I get an error message: “{“statusCode”:500,“error”:“Internal Server Error”,“message”:“Internal Error”}”

Configuration:
opensearch.yml:

_meta:
  type: "config"
  config_version: 2
config:
  dynamic:
    http:
      anonymous_auth_enabled: false
    authc:
      basic_internal_auth_domain:
        order: 0
        description: "HTTP basic authentication using the internal user database"
        http_enabled: true
        transport_enabled: true
        http_authenticator:
          type: basic
          challenge: false
        authentication_backend:
          type: internal
      saml_auth_domain:
        order: 1
        description: "SAML provider"
        http_enabled: true
        transport_enabled: true
        http_authenticator:
          type: saml
          challenge: true
          config:
            idp:
              metadata_url: http://keycloak:8080/realms/boundary/protocol/saml/descriptor
            sp:
              entity_id: opennsearch
            kibana_url: https://opensearch-dashboards:5601
            roles_key: groups
            exchange_key: eb96d42cd351aced3b90b2be44c990c2e4534644e5c098cbd83297f3a175170f  #openssl rand -hex 32
        authentication_backend:
          type: noop

opensearch-dashboards.yml

server.name: opensearch-dashboards
server.host: "0.0.0.0"

server.ssl.enabled: true
server.ssl.certificate: "/opt/certs/server.crt"
server.ssl.key: "/opt/certs/server.key"
opensearch.ssl.certificateAuthorities: ["/opt/certs/rootCA.crt"]
opensearch.ssl.verificationMode: none

opensearch.username: kibanaserver
opensearch.password: kibanaserver
opensearch.requestHeadersWhitelist: ["securitytenant", "security_tenant", "Authorization"]

opensearch_security.multitenancy.enabled: true
opensearch_security.multitenancy.tenants.preferred: ["Private", "Global"]
opensearch_security.readonly_mode.roles: ["kibana_read_only"]
opensearch_security.cookie.secure: false
opensearch_security.auth.type: ["basicauth","saml"]
opensearch_security.auth.multiple_auth_enabled: true


server.xsrf.allowlist:  [   "/_opendistro/_security/saml/acs/idpinitiated",  "/_opendistro/_security/saml/acs",  "/_opendistro/_security/saml/logout",  "/_plugins/_security/saml/acs/idpinitiated",  "/_plugins/_security/saml/acs",  "/_plugins/_security/saml/logout",  "/_opendistro/_security/saml/acs" ]

docker-compose.yml

services:
  opensearch1:
    image: opensearchproject/opensearch:3.2.0
    environment:
      - cluster.name=opensearch-cluster
      - node.name=opensearch1
      - discovery.seed_hosts=opensearch1,opensearch2
      - cluster.initial_master_nodes=opensearch1,opensearch2
      - bootstrap.memory_lock=true
      - "OPENSEARCH_JAVA_OPTS=-Xms512m -Xmx512m"
      - OPENSEARCH_INITIAL_ADMIN_PASSWORD=gqYeDIzbEwTTYmB7
    ulimits:
      memlock:
        soft: -1
        hard: -1
      nofile:
        soft: 65536
        hard: 65536
    volumes:
      - opensearch-data1:/usr/share/opensearch/data
      - ./configs/opensearch.yml:/usr/share/opensearch/plugins/opensearch-security/securityconfig/config.yml
    ports:
      - 9200:9200
      - 9600:9600
    networks:
      - opensearch-net
  opensearch2:
    image: opensearchproject/opensearch:3.2.0
    environment:
      - cluster.name=opensearch-cluster
      - node.name=opensearch2
      - discovery.seed_hosts=opensearch1,opensearch2
      - cluster.initial_master_nodes=opensearch1,opensearch2
      - bootstrap.memory_lock=true
      - "OPENSEARCH_JAVA_OPTS=-Xms512m -Xmx512m"
      - OPENSEARCH_INITIAL_ADMIN_PASSWORD=gqYeDIzbEwTTYmB7
    ulimits:
      memlock:
        soft: -1
        hard: -1
      nofile:
        soft: 65536
        hard: 65536
    volumes:
      - opensearch-data2:/usr/share/opensearch/data
      - ./configs/opensearch.yml:/usr/share/opensearch/plugins/opensearch-security/securityconfig/config.yml
    networks:
      - opensearch-net

  opensearch-dashboards:
    image: opensearchproject/opensearch-dashboards:3.2.0
    ports:
      - 5601:5601
    expose:
      - "5601"
    environment:
      OPENSEARCH_HOSTS: '["https://opensearch1:9200","https://opensearch2:9200"]'
    volumes:
      - ./configs/opensearch-dashboards.yml:/usr/share/opensearch-dashboards/config/opensearch_dashboards.yml
      - ./configs/certs:/opt/certs
    networks:
      - opensearch-net

volumes:
  opensearch-data1:
  opensearch-data2:

networks:
  opensearch-net:
    external: true
    name: my_network #docker network create my_network

keycloak docker-compose.yml


services:
  keycloak:
    image: quay.io/keycloak/keycloak:26.3.2
    restart: always
    environment:
      KC_DB: postgres
      KC_DB_URL: jdbc:postgresql://pg:5432/keycloak
      KC_DB_USERNAME: keycloak
      KC_DB_PASSWORD: keycloak

      KC_HTTP_ENABLED: "true"
      KC_HTTPS_ENABLED: "true"
      KC_HOSTNAME_STRICT_HTTPS: "false"
      PROXY_ADDRESS_FORWARDING: "true"

      KC_HTTPS_CERTIFICATE_FILE: /opt/keycloak/certs/server.crt
      KC_HTTPS_CERTIFICATE_KEY_FILE: /opt/keycloak/certs/server.key

      KC_LOG_LEVEL: info
      KC_METRICS_ENABLED: true
      KC_HEALTH_ENABLED: true

      KEYCLOAK_ADMIN: admin
      KEYCLOAK_ADMIN_PASSWORD: admin
    entrypoint: ["/opt/keycloak/bin/kc.sh"]
    command: [
      "start-dev",
      "--features=token-exchange,organization,admin-fine-grained-authz,scripts,client-policies",
      "--http-port", "8080",
      "--https-port", "8443"
    ]
    ports:
      - 8080:8080
      - 8443:8443
    volumes:
      - ./certs:/opt/keycloak/certs
    healthcheck:
      test: [ "CMD", "curl", "-f", "http://localhost:8080" ]
      interval: 5s
      timeout: 5s
      retries: 3
      start_period: 5s
    networks:
    - "infra"

  pg:
    image: postgres:16.4
    container_name: pg
    ports:
      - 5432:5432
    environment:
      POSTGRES_DB: keycloak
      POSTGRES_USER: keycloak
      PGUSER: keycloak
      POSTGRES_PASSWORD: keycloak
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -d keycloak -U keycloak"]
      interval: 3s
      timeout: 5s
      retries: 5
    networks:
      - "infra"


networks:
  infra:
    external: true
    name: my_network #docker network create my_network

terraform keycloak

resource "keycloak_realm" "realm" {
  realm                          = "boundary"
  enabled                        = true
  registration_allowed           = false
  registration_email_as_username = false
  reset_password_allowed         = true
  remember_me                    = true
  verify_email                   = true
  login_with_email_allowed       = true
  duplicate_emails_allowed       = false
  access_token_lifespan          = "12h"
}

resource "keycloak_realm_events" "events" {
  realm_id = keycloak_realm.realm.id

  admin_events_enabled         = true
  admin_events_details_enabled = true

  events_listeners = ["jboss-logging"]

  events_enabled = true
}

resource "keycloak_oidc_google_identity_provider" "google" {
  provider_id                             = "google"
  realm                                   = keycloak_realm.realm.id
  client_id                               = "apps.googleusercontent.com"
  client_secret                           = "GOCS"
  request_refresh_token                   = true
  default_scopes                          = "openid profile email"
  accepts_prompt_none_forward_from_client = false
  trust_email                             = true
  link_only                               = false
}

resource "keycloak_group" "developers" {
  realm_id = keycloak_realm.realm.id
  name     = "developers"
}

resource "keycloak_saml_client" "app" {
  realm_id                  = keycloak_realm.realm.id
  client_id                 = "opennsearch"
  name                      = "opennsearch"
  client_signature_required = false
  sign_assertions           = false
  name_id_format            = "email"
  include_authn_statement   = false
  force_post_binding        = true
  force_name_id_format      = true
  sign_documents            = true
  signature_algorithm       = "RSA_SHA256"
  signature_key_name        = "NONE"
  valid_redirect_uris = [
    "http://127.0.0.1:8000/api/saml/callback",
    "https://127.0.0.1:8443/api/saml/callback",
    "http://127.0.0.1:5601/_opendistro/_security/saml/acs",
    "http://127.0.0.1:5601/_plugins/_security/saml/acs",
  ]
}

resource "keycloak_saml_client_default_scopes" "this" {
  realm_id       = keycloak_realm.realm.id
  client_id      = keycloak_saml_client.app.id
  default_scopes = []
}

resource "keycloak_generic_protocol_mapper" "groups" {
  realm_id        = keycloak_realm.realm.id
  client_id       = keycloak_saml_client.app.id
  name            = "groups"
  protocol        = "saml"
  protocol_mapper = "saml-group-membership-mapper"
  config = {
    "attribute.name"       = "groups"
    "attribute.nameformat" = "Basic"
    "friendly.name"        = "groups"
    "full.path"            = "false"
    "single"               = "true"
  }
}


resource "keycloak_saml_user_property_protocol_mapper" "email" {
  realm_id                   = keycloak_realm.realm.id
  client_id                  = keycloak_saml_client.app.id
  name                       = "email"
  friendly_name              = "email"
  user_property              = "email"
  saml_attribute_name        = "email"
  saml_attribute_name_format = "Basic"
}

Relevant Logs or Screenshots:

Hey @noel ,

With your deploy did you manage to validate keycloak is running and created the user and roles you want to use when you Log in?

Leeroy.

hi @Leeroy
Keycloak is running fine in Docker (we confirmed it’s reachable via the admin UI).

It’s deployed via Docker Compose alongside OpenSearch and OpenSearch Dashboards, and both services are on the same Docker network, so they can talk to each other

We’re not creating users directly in Keycloak; logins come through Google OIDC federation. We have provisioned the SAML client and the “developers” group via Terraform, and they appear correctly in Keycloak after terraform apply.

I’ve tested the login flow using OIDC → SAML and can confirm Keycloak sends group claims via the SAML mapper as expected.

1 Like

@noel can you please add the entity_id to your config.yml and see if this works for you.

saml_auth_domain:
        order: 1
        http_enabled: true
        transport_enabled: false
        http_authenticator:
          type: saml
          challenge: true
          config:
            idp:
              entity_id: "http://keycloak:8080/realms/boundary"
              metadata_url: "http://keycloak:8080/realms/boundary/protocol/saml/descriptor"
1 Like

@Anthony hi
same issue
and start to get error from dashboards

{"type":"error","@timestamp":"2025-09-05T08:10:40Z","tags":["connection","client","error"],"pid":1,"level":"
error","error":{"message":"4068A963D5790000:error:0A000416:SSL routines:ssl3_read_bytes:sslv3 alert certificate unknown:../deps/opens
sl/openssl/ssl/record/rec_layer_s3.c:1605:SSL alert number 46\n","name":"Error","stack":"Error: 4068A963D5790000:error:0A000416:SSL r
outines:ssl3_read_bytes:sslv3 alert certificate unknown:../deps/openssl/openssl/ssl/record/rec_layer_s3.c:1605:SSL alert number 46\n"
,"code":"ERR_SSL_SSLV3_ALERT_CERTIFICATE_UNKNOWN"},"message":"4068A963D5790000:error:0A000416:SSL routines:ssl3_read_bytes:sslv3 aler
t certificate unknown:../deps/openssl/openssl/ssl/record/rec_layer_s3.c:1605:SSL alert number 46\n"}

@noel can you remove the TLS from this for the time being to make sure this works as expected.

See my configuration below for reference.

provider.tf:

provider "keycloak" {
  url       = var.keycloak_url
  realm     = var.keycloak_realm
  client_id = "admin-cli"
  username  = var.keycloak_user
  password  = var.keycloak_password
}

variable "keycloak_url" {
  type    = string
  default = "http://localhost:8080"
}

variable "keycloak_realm" {
  type    = string
  default = "master"
}

variable "keycloak_user" {
  type    = string
  default = "admin"
}

variable "keycloak_password" {
  type    = string
  default = "admin"
}

main.tf:

resource "keycloak_realm" "realm" {
  realm                    = "boundary"
  enabled                  = true
  reset_password_allowed   = true
  remember_me              = true
  verify_email             = true
  login_with_email_allowed = true
  access_token_lifespan    = "12h"
}

resource "keycloak_realm_events" "events" {
  realm_id = keycloak_realm.realm.id
  admin_events_enabled          = true
  admin_events_details_enabled  = true
  events_enabled                = true
  events_listeners              = ["jboss-logging"]
}

# SAML client — matches your OpenSearch SP entity_id: "opennsearch"
resource "keycloak_saml_client" "app" {
  realm_id                  = keycloak_realm.realm.id
  client_id                 = "opennsearch"
  name                      = "opennsearch"
  client_signature_required = false
  sign_assertions           = false
  sign_documents            = true
  name_id_format            = "email"
  include_authn_statement   = false
  force_post_binding        = true
  force_name_id_format      = true
  signature_algorithm       = "RSA_SHA256"
  signature_key_name        = "NONE"

  # Include both HTTP and HTTPS local URIs to match how you run Dashboards
  valid_redirect_uris = [
    "http://localhost:5601/_plugins/_security/saml/acs/",
    "http://localhost:5601/_opendistro/_security/saml/acs/",
    # keep the customer's extras if you want them too:
    "http://127.0.0.1:8000/api/saml/callback",
    "https://127.0.0.1:8443/api/saml/callback",
    "http://127.0.0.1:5601/_opendistro/_security/saml/acs",
    "http://localhost:5601/_opendistro/_security/saml/acs"
  ]
}

# Group membership -> 'groups' attribute (aligns with roles_key: groups)
resource "keycloak_generic_protocol_mapper" "groups" {
  realm_id        = keycloak_realm.realm.id
  client_id       = keycloak_saml_client.app.id
  name            = "groups"
  protocol        = "saml"
  protocol_mapper = "saml-group-membership-mapper"
  config = {
    "attribute.name"       = "groups"
    "attribute.nameformat" = "Basic"
    "friendly.name"        = "groups"
    "full.path"            = "false"
    "single"               = "true"
  }
}

resource "keycloak_saml_user_property_protocol_mapper" "email" {
  realm_id                   = keycloak_realm.realm.id
  client_id                  = keycloak_saml_client.app.id
  name                       = "email"
  friendly_name              = "email"
  user_property              = "email"
  saml_attribute_name        = "email"
  saml_attribute_name_format = "Basic"
}

opensearch_dashboards.yml

server.name: kibana
server.host: "0.0.0.0"
server.customResponseHeaders : { "Access-Control-Allow-Credentials" : "true" }


server.ssl.enabled: false

opensearch.ssl.verificationMode: none
opensearch.username: kibanaserver
opensearch.password: kibanaserver
opensearch.requestHeadersWhitelist: ["securitytenant","Authorization"]

opensearch_security.multitenancy.enabled: true
opensearch_security.multitenancy.tenants.preferred: ["Private"]
opensearch_security.readonly_mode.roles: ["kibana_read_only"]


opensearch_security.auth.type: ["basicauth","saml"]
opensearch_security.auth.multiple_auth_enabled: true

server.xsrf.whitelist:  
   - /_opendistro/_security/saml/acs
   - /_opendistro/_security/saml/acs/idpinitiated
   - /_opendistro/_security/saml/logout
   - /_plugins/_security/saml/acs
   - /_plugins/_security/saml/acs/idpinitiated
   - /_plugins/_security/saml/logout

opensearch_security.cookie.secure: false

config.yml:

---
_meta:
  type: "config"
  config_version: 2
config:
  dynamic:
    http:
      anonymous_auth_enabled: false
      xff:
        enabled: false
        #internalProxies: "192\\.168\\.0\\.10|192\\.168\\.0\\.11"
    do_not_fail_on_forbidden: true
    do_not_fail_on_forbidden_empty: true
    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: "intern"
      saml_auth_domain:
        order: 1
        http_enabled: true
        transport_enabled: false
        http_authenticator:
          type: saml
          challenge: true
          config:
            idp:
              #metadata_file: "descriptor.xml"
              entity_id: "http://keycloak:8080/realms/boundary"
              metadata_url: "http://keycloak:8080/realms/boundary/protocol/saml/descriptor"
            sp:
              entity_id: "opennsearch"       # MUST match TF client_id
            kibana_url: "http://localhost:5601"  # or https://localhost:5601 if you enable TLS in Dashboards
            roles_key: "groups"
            exchange_key: "9b4a1c4a3d6f0f1a2b3c4d5e6f7a8b9c9b4a1c4a3d6f0f1a2b3c4d5e6f7a8b9"
        authentication_backend:
          type: noop
1 Like

hi @Anthony
thank you for you response

change my environment according to recommendation above and got same error(

full env config files can be found here

also can’t find anything about saml in response

curl --insecure -u admin:sdfDSEqw234weQR -XGET https://127.0.0.1:9200/_plugins/_security/ap
i/securityconfig

thank you in advance

@noel This would indicate that the custom configuration that you are providing is being overwritten by demo configuration.

Once the containers are up, can you connect to opensearch container and examine the content of config.yml, if the saml details are there, you can load it to security index manually using securityadmin.sh with -cd option using the generated admin certificate.

./plugins/opensearch-security/tools/securityadmin.sh -cacert config/root-ca.pem -cert config/kirk.pem -key config/kirk-key.pem -cd config/opensearch-security/ -nhnv -icl

I would recommend to extract the contents on the opensearch.yml file and use volumes to map it going forward.

hi @Anthony
yep, demo config ruin everything
working example can be found in here

Did you get this working then?

yes

1 Like