Wazuh using Keycloak as an Identity Provider

118 views
Skip to first unread message

Jacob Molland

unread,
Jan 19, 2026, 1:52:04 PMJan 19
to Wazuh | Mailing List

Hi all,

I have been configuring Wazuh to use Keycloak as an identity provider (or IdP). Both the Wazuh and Keycloak deployments are kubernetes-based, existing on an air-gapped server. I access the UIs for these apps via ‘kubectl port-forward’ to forward Wazuh from the air-gapped server to my local machine on localhost:31000 and Keycloak to localhost:30000.

I have been following along with this Keycloak integration guide from Wazuh: https://documentation.wazuh.com/current/user-manual/user-administration/single-sign-on/administrator/keycloak.html. However, after adding ‘opensearch_security.auth.type: "saml"’ in the opensearch_dashboards.yml file and navigating to https://localhost:31000 in my browser to access the Wazuh UI, I get the following message:

{"statusCode":500,"error":"Internal Server Error","message":"Internal Error"}

When ‘opensearch_security.auth.type: "saml"’ is commented out, the dashboard is accessible.

All (4) Wazuh pods are in the READY state, but the wazuh-dashboard pod has the following logs inside (bolding and coloring the parts that stand out to me):

{"type":"log","@timestamp":"2026-01-14T23:23:19Z","tags":["error","plugins","securityDashboards"],"pid":55,"message":"Failed to get saml header: Authentication Exception :: {\"path\":\"/_plugins/_security/authinfo\",\"query\":{\"auth_type\":\"saml\"},\"statusCode\":401,\"response\":\"Authentication finally failed\"}"}

{"type":"error","@timestamp":"2026-01-14T23:23:19Z","tags":[],"pid":55,"level":"error","error":{"message":"Internal Server Error","name":"Error","stack":"Error: Internal Server Error at HapiResponseAdapter.toError (/usr/share/wazuh-dashboard/src/core/server/http/router/response_adapter.js:127:19)\n    at HapiResponseAdapter.toHapiResponse (/usr/share/wazuh-dashboard/src/core/server/http/router/response_adapter.js:83:19)\n    at HapiResponseAdapter.handle (/usr/share/wazuh-dashboard/src/core/server/http/router/response_adapter.js:79:17)\n    at Router.handle (/usr/share/wazuh-dashboard/src/core/server/http/router/router.js:175:34)\n at processTicksAndRejections (node:internal/process/task_queues:95:5)\n at handler (/usr/share/wazuh-dashboard/src/core/server/http/router/router.js:140:50)\n at exports.Manager.execute (/usr/share/wazuh-dashboard/node_modules/@hapi/hapi/lib/toolkit.js:60:28)\n at Object.internals.handler (/usr/share/wazuh-dashboard/node_modules/@hapi/hapi/lib/handler.js:46:20)\n    at exports.execute (/usr/share/wazuh-dashboard/node_modules/@hapi/hapi/lib/handler.js:31:20)\n at Request._lifecycle (/usr/share/wazuh-dashboard/node_modules/@hapi/hapi/lib/request.js:371:32)\n    at Request._execute (/usr/share/wazuh-dashboard/node_modules/@hapi/hapi/lib/request.js:281:9)"},"url":"https://localhost:31000/auth/saml/login?redirectHash=false&nextUrl=%2F","message":"Internal Server Error"}


I have the following configured inside opensearch_dashboards.yml, mounted from a ConfigMap (I added the last (3) lines per Wazuh’s guide for Keycloak integration); when ‘opensearch_security.auth.type: "saml"’ is commented out, everything works fine:

opensearch_dashboards.yml: |2-

    server.host: 0.0.0.0

    server.port: 5601

    opensearch.hosts: https://indexer:9200

    opensearch.ssl.verificationMode: none

    opensearch.requestHeadersWhitelist: [ authorization,securitytenant ]

    opensearch_security.multitenancy.enabled: false

    opensearch_security.readonly_mode.roles: ["kibana_read_only"]

    server.ssl.enabled: true

    server.ssl.key: "/usr/share/wazuh-dashboard/certs/key.pem"

    server.ssl.certificate: "/usr/share/wazuh-dashboard/certs/cert.pem"

    opensearch.ssl.certificateAuthorities: ["/usr/share/wazuh-dashboard/certs/root-ca.pem"]

    uiSettings.overrides.defaultRoute: /app/wz-home

    # Session expiration settings

    opensearch_security.cookie.ttl: 900000

    opensearch_security.session.ttl: 900000

    opensearch_security.session.keepalive: false

    opensearch_security.auth.type: "saml"

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

Next are my idp.metadata.xml and sp.metadata.xml files, downloaded from the Wazuh UI after configuring it per the Wazuh guide mentioned in the intro paragraph. Since the Keycloak UI is accessed at https://localhost:30000, idp.metadata.xml originally had ‘localhost:30000’ in every spot that currently has ‘keycloak.keycloak:8443’ (seen below); I changed the original ‘https://localhost:30000' to ‘https://keycloak.keycloak:8443’ because curl’ing ‘https://keycloak.keycloak:8443’ from the wazuh-dashboard pod worked while curl’ing ‘https://localhost:30000’ did not. Here are those idp.metadata.xml and sp.metadata.xml files:

  idp.metadata.xml: |

    <md:EntityDescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" entityID="https://keycloak.keycloak:8443/realms/Wazuh">

    <md:IDPSSODescriptor WantAuthnRequestsSigned="true" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">

    <md:KeyDescriptor use="signing">

    <ds:KeyInfo>

    <ds:KeyName>YkR563d6gPyxysXUUDyWkTDkHTat2BOlKj8-ryKWffk</ds:KeyName>

    <ds:X509Data>

    <ds:X509Certificate>MIICmTCCAYECBgGb1w2MiTANBgkqhkiG9w0BAQsFADAQMQ4wDAYDVQQDDAVXYXp1aDAeFw0yNjAxMTkxNjE3MjhaFw0zNjAxMTkxNjE5MDhaMBAxDjAMBgNVBAMMBVdhenVoMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAgptqoADX7Ay3kUtJBYYX9f54zQeFPFUdWE2EwV4wffGdhC48r3uV5L0TFm/eTB1RdON0YcpDOX7Et8gy2t7u+L9CS7bSl1jRRZeXFmtcJpzLmIVLww3MfT1XZoiTXiQmnRymjVTofVAZTFvZ146zaDvWZchB9yEQYBXStJORYkOEX8JlMm6N69OWbx1I0aAZnsy0orndGjrswHAp+ZW+MlAuvPHxs7JdpS8dWTFlo1obYMBblpCvBDaK5KccvGGli4+TBdZU5SvNJfnqAMVQg9oMid23PWE1Yw4nzblEGhokxlNwarOqa8dvxW4yWAzq9Aofpnd+MzvVqf5N8Z5qewIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQBIIXFZgyEY0rBoqmOOth6au6VpYFYkmTqniSzo653MbfZNu4l56NsKc6R4iWv2cjfl3InMMDp8dVycXmYVdY7m9sC30+v9vI/hKvWy/6nC5kFGmRhvJJHcSZ09kjjL+U33OZywW8/bHSFrvCaq71U/Ysl9isO23Dp0Ah5o5ULBz8ToR9DBx/Dn1HlzHvClpD28YJhNXK3g+imLlcatOiLloIjLjfpOv2J2w1n/qSx7BnEAsjIqXv2LcP0We0M954iXFpC4eC49UtmNFxiDqiM/dG25JbV5+uOOrg0r1JNq42/cjjpcdNdMvz7YhfyDzrfk8ke2UAAJNw5YdE8U09Y0</ds:X509Certificate>

    </ds:X509Data>

    </ds:KeyInfo>

    </md:KeyDescriptor>

    <md:ArtifactResolutionService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP" Location="https://keycloak.keycloak:8443/realms/Wazuh/protocol/saml/resolve" index="0"/>

    <md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://keycloak.keycloak:8443/realms/Wazuh/protocol/saml"/>

    <md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://keycloak.keycloak:8443/realms/Wazuh/protocol/saml"/>

    <md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact" Location="https://keycloak.keycloak:8443/realms/Wazuh/protocol/saml"/>

    <md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP" Location="https://keycloak.keycloak:8443/realms/Wazuh/protocol/saml"/>

    <md:NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:persistent</md:NameIDFormat>

    <md:NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</md:NameIDFormat>

    <md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified</md:NameIDFormat>

    <md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress</md:NameIDFormat>

    <md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://keycloak.keycloak:8443/realms/Wazuh/protocol/saml"/>

    <md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://keycloak.keycloak:8443/realms/Wazuh/protocol/saml"/>

    <md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP" Location="https://keycloak.keycloak:8443/realms/Wazuh/protocol/saml"/>

    <md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact" Location="https://keycloak.keycloak:8443/realms/Wazuh/protocol/saml"/>

    </md:IDPSSODescriptor>

    </md:EntityDescriptor>

 

  sp.metadata.xml: |

    <md:EntityDescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" entityID="wazuh-saml" ID="ID_578315be-55d3-4b9f-a340-3505343ca84d">

    <md:SPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol" AuthnRequestsSigned="false" WantAssertionsSigned="true">

    <md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="ERROR:ENDPOINT_NOT_SET"/>

    <md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified</md:NameIDFormat>

    <md:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://dashboard.wazuh:443/_opendistro/_security/saml/acs/idpinitiated" isDefault="true" index="1"/>

    </md:SPSSODescriptor>

    </md:EntityDescriptor>

sp.metadata.xml is the result of downloading it after I created the 'wazuh-saml' client, setting its 'Assertion Consumer Service POST Binding URL' to 'https://dashboard.wazuh:443/_opendistro/_security/saml/acs/idpinitiated' and 'Assertion Consumer Service Redirect Binding URL' to 'https://dashboard.wazuh:443', along with everything else recommended by Wazuh's documentation. The guide references ‘<WAZUH_DASHBOARD_URL>’, which Wazuh says to replace with the corresponding URL of the Wazuh dashboard instance. From my local machine, this would be ‘https://localhost:31000’, but I didn't use this value due to Wazuh and Keycloak existing on an air-gapped server – https://localhost:31000 is just what my browser uses on my local machine to access the UI, so I used 'https://dashboard.wazuh:443'. From the wazuh-dashboard pod, 'curl -k https://localhost:31000' does not work ('Could not connect to server'). However, 'curl -k https://dashboard.wazuh:443' doesn't work either, returning the following error:

{"statusCode":401,"error":"Unauthorized","message":"Authentication required"}


Here is what I added to my opensearch-security/config.yml file (again, per Wazuh's documentation):

        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: 0

            http_authenticator:

              type: basic

              challenge: false

            authentication_backend:

              type: intern

          saml_auth_domain:

            http_enabled: true

            transport_enabled: false

            order: 1

            http_authenticator:

              type: saml

              challenge: true

              config:

                idp:

                  metadata_file: '/usr/share/wazuh-indexer/opensearch-security/idp.metadata.xml'

                  entity_id: 'https://keycloak.keycloak:8443/realms/Wazuh'

                sp:

                  metadata_file: /usr/share/wazuh-indexer/opensearch-security/sp.metadata.xml

                  entity_id: wazuh-saml

                kibana_url: https://localhost:31000 # **Maybe this should be ‘https://dashboard.wazuh:443’?

                roles_key: Roles

                exchange_key: 'c32a1565000ddc483fa0f22c14462b950aeb0dcda3be3b0b451f67852320d9aa'

            authentication_backend:

              type: noop

 

I added the following to my roles_mapping.yml file:

all_access:

  reserved: false

  hidden: false

  backend_roles:

  - "admin"


I also confirmed that the value of ‘run_as’ in the config/wazuh.yml file is set to ‘false’.

Closing questions:
- What should <WAZUH_DASHBOARD_URL> be when Wazuh is running on an air-gapped server but the UI is running on a local machine on localhost:31000?
- Is a SAML block for OpensSearch Security need to be created in the Wazuh manifest?
- Does Wazuh's root-ca.pem cert have to be configured to trust Keycloak's cert? Keycloak is configured with self-signed tls.key and tls.crt secrets BTW.
- Anything else I might be missing while attempting to configure Keycloak as an identity provider for Wazuh?


Any help on this would be greatly appreciated. I've been working this for days and can't seem to figure out what's wrong here, and the LLMs are just throwing me in circles.

Best,

Jacob

Jorge Eduardo Silva Jackson

unread,
Jan 19, 2026, 4:21:11 PMJan 19
to Wazuh | Mailing List
Hi Jacob,

This looks like an URL/endpoint consistency issue. The key piece is <WAZUH_DASHBOARD_URL>: it must be a *real, reachable URL in your current network*, exactly as the Wazuh documentation example implies (i.e., the address users will actually open in their browsers).

1) <WAZUH_DASHBOARD_URL> should NOT be “whatever works from inside the pod”.
   It must be the browser-facing URL of Wazuh Dashboards, reachable from the clients doing SSO.
   - Good: https://192.168.1.100:31000 (or an internal DNS name), where 192.168.1.100 is the host you port-forward to / or a reverse proxy endpoint.
   - Using https://localhost:31000 works only for the same machine initiating the browser session. “localhost” always points to the caller itself, so it will limit access and often breaks SAML redirects for other clients/users.

2) With kubectl port-forward, “localhost:31000” is only valid on your local machine. It is expected that `curl https://localhost:31000` from the wazuh-dashboard pod fails.

3) Ensure the Indexer SAML config uses a single, correct kibana_url that matches <WAZUH_DASHBOARD_URL> (and fix typos).
   - `kibana_url` must be ONE value, e.g. https://192.168.1.100:31000
   - Then Keycloak must also allow this exact value in its redirect/ACS settings.

4) ACS endpoint alignment:
   For SP-initiated login (Dashboards -> /auth/saml/login), Keycloak should include the standard ACS endpoint used by OpenSearch Dashboards:
   https://<WAZUH_DASHBOARD_URL>/_opendistro/_security/saml/acs
   (You can keep the idpinitiated ACS too if you need IdP-initiated flows, but don’t rely on it only.)

5) After editing OpenSearch Security files (config.yml / roles_mapping.yml), confirm you actually applied the changes (securityadmin.sh or your Kubernetes equivalent). If not applied, you’ll keep getting 401 from `/_plugins/_security/authinfo?auth_type=saml` and Dashboards will surface it as 500.

If you share the exact final <WAZUH_DASHBOARD_URL> you want to support on your LAN (IP/DNS + port), plus your Keycloak client ACS/redirect URIs and the final kibana_url value, we can validate them line-by-line.

Jorge Eduardo Silva Jackson

unread,
Jan 22, 2026, 3:45:07 PMJan 22
to Wazuh | Mailing List
Hi Jacob, any updates ?
Reply all
Reply to author
Forward
0 new messages