Hi all,
I have been configuring Wazuh to use Keycloak as an identity provider. Both of 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 setting ‘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.
The guide references ‘<WAZUH_DASHBOARD_URL>’, which Wazuh says to replace with the corresponding URL of the Wazuh dashboard instance. For me, this is ‘https://localhost:31000’, but I’m unsure of its inclusion in the Wazuh manifest due to Wazuh and Keycloak existing on an airgapped server – https://localhost:31000 is just what my browser uses on my local machine to access the UI.
All (4) Wazuh pods are in the READY state, but the wazuh-dashboard pod has the following logs inside:
{"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\n 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"]
These are my idp.metadata.xml and sp.metadata.xml files, downloaded from the Wazuh UI after configuring it per the Wazuh guide mentioned above. Since the Wazuh Dashboard URL is located at https://localhost:31000, idp.metadata.xml originally has ‘localhost:31000’ in every spot that has ‘keycloak.keycloak:8443’; I changed the original ‘localhost’ URL to the ‘keycloak’ URL because I thought it made sense for Wazuh and Keycloak to communicate with each other:
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>EL4xyYqoA-qpzxJ7O8PCu1gJNOQz-UaFo3QYKls4mCw</ds:KeyName>
<ds:X509Data>
<ds:X509Certificate>MIICmTCCAYECBgGbvfECHDANBgkqhkiG9w0BAQsFADAQMQ4wDAYDVQQDDAVXYXp1aDAeFw0yNjAxMTQxOTE1NDdaFw0zNjAxMTQxOTE3MjdaMBAxDjAMBgNVBAMMBVdhenVoMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAryufq09IzkdYZmTHx113vjeOYA8pfAgCmgsEEGiGuyoEqwO3QZS2JUoL7qG/BOdoDFtyGGX/HHR/xJT73ajKDpBeQVWGxn65sp5Kjnqo6CGDmV1EiK8yE6pA5JutXQibrsc6nSQ5RJlcke8w2zHWIR9v7qbPBfVJhN0z+uRq8rKMDqXeKiKphUeU5ylz8BwfubBGZ6G90+lO8a5x7FFPoDm8kpbB3dNoj6kM1hPsR83290p9ED+YoNzjKO4dNWsAYbdB2ipNylyWRali3j/XM2+lPa6DT8ZbYKm3Z3xN/EeQoYXufrq3k21nuVnxWx8GO2KOdyyuuj4+1l1R/HAFowIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQB96CWF1T/moB3b3/Q1zlBl/LrMt44JjtJ+5T5K1rSK0A9XG/6xQFmEwyrR3MX7iWF8k+5LazeOz6JSCs3r/rgBeN7oc8bN5IJCmN0eenMjKhiu2RAPsVDKPsY3n5YghNY3zDnrfzEaLyXebJJBW36MDWJMLlpyFAiVsJBDof3EFequOxQpQUSy9cfU5WnoVGRJHB758hdwrUy/Y3XFOR08vgijrevLlfSokrxxpE+xNgkyc7H77Rv/D5hXr9kXv/u5MAY9X0EYWSMteArZ7/9bDPM5ZX58PZdkX2GzHEi5SBLsg3sywAqMWvx2gyM3DOu97om3GkdYsLgRezOhWuQR</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_461f5781-5850-4268-97cb-013a2bdeb378">
<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="https://wazuh-indexer:9200/_plugins/_security/saml/logout"/>
<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://wazuh-indexer:9200/_plugins/_security/saml/acs" isDefault="true" index="1"/>
</md:SPSSODescriptor>
</md:EntityDescriptor>
Here is what I added to my opensearch-security/config.yml file:
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
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