{"id":2211,"date":"2026-06-05T22:29:17","date_gmt":"2026-06-05T20:29:17","guid":{"rendered":"https:\/\/cln.io\/blog\/?p=2211"},"modified":"2026-06-05T22:30:01","modified_gmt":"2026-06-05T20:30:01","slug":"misp-authentik-oidc","status":"publish","type":"post","link":"https:\/\/cln.io\/blog\/misp-authentik-oidc\/","title":{"rendered":"Hooking MISP up to Authentik (OIDC) + trusting an internal CA"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">Notes to future me: getting <a href=\"https:\/\/www.misp-project.org\/\">MISP<\/a> to do SSO against <a href=\"https:\/\/goauthentik.io\/\">Authentik<\/a> over OIDC, on an internal network where the IdP cert is signed by a corporate CA. Two halves \u2014 the OIDC wiring (with one env-var gotcha that cost me an afternoon) and getting MISP to trust the CA (a <code>cURL #60<\/code> that survives &#8220;but I mounted the cert&#8221;).<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Stack: official <code>ghcr.io\/misp\/misp-docker\/misp-core<\/code> (we run a mirror of <code>v2.5.32<\/code>), Authentik as the IdP, Traefik out front, Docker Swarm.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"875\" src=\"https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/06\/image-1024x875.png\" alt=\"\" class=\"wp-image-2222\" srcset=\"https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/06\/image-1024x875.png 1024w, https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/06\/image-300x256.png 300w, https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/06\/image-768x656.png 768w, https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/06\/image.png 1112w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">Authentik side<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Create an <strong>Application<\/strong> + an <strong>OAuth2\/OpenID Provider<\/strong> (confidential):<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Redirect URI<\/strong>: <code>https:\/\/misp.example\/users\/login<\/code> (MISP comes back here)<\/li>\n\n\n\n<li><strong>Scopes<\/strong>: <code>openid<\/code>, <code>profile<\/code>, <code>email<\/code>, and add the <strong>groups<\/strong> scope mapping so the <code>groups<\/code> claim is actually emitted \u2014 that\u2019s what MISP maps roles from<\/li>\n\n\n\n<li>Grab the <strong>Client ID<\/strong> and <strong>Client secret<\/strong><\/li>\n\n\n\n<li>Your <strong>issuer<\/strong> is <code>https:\/\/auth.example\/application\/o\/&lt;app-slug&gt;\/<\/code> (the slug is the application slug)<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">That\u2019s it on the IdP. Everything else is MISP env.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">MISP side: the env vars that actually matter<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The footgun: the official <code>misp-docker<\/code> image reads the <strong><code>OIDC_*<\/code><\/strong> namespace, gated behind <strong><code>OIDC_ENABLE=true<\/code><\/strong>. There\u2019s a <em>different<\/em> image build floating around that uses <code>SECURITY_AUTH: \"OidcAuth.Oidc\"<\/code> + <code>OIDCAUTH_*<\/code> \u2014 those are <strong>silently inert<\/strong> on the official image. No error, SSO just never engages.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">It comes down to one switch in the entrypoint:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"shell\"># core\/files\/configure_misp.sh -&gt; set_up_oidc()\nif [[ \"$OIDC_ENABLE\" == \"true\" ]]; then\n    ...\n    # writes OidcAuth.* into config.php via modify_config.php\n    echo \"... OIDC authentication enabled\"\nfi<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">No <code>OIDC_ENABLE=true<\/code> \u2192 that block never runs \u2192 <code>Security.auth<\/code> stays empty. So make sure you\u2019re on <code>OIDC_*<\/code>, not <code>OIDCAUTH_*<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A working block:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"yaml\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">OIDC_ENABLE: \"true\"\nOIDC_PROVIDER_URL: \"https:\/\/auth.example\/application\/o\/misp\/\"\nOIDC_ISSUER: \"https:\/\/auth.example\/application\/o\/misp\/\"\nOIDC_CLIENT_ID: \"misp\"\nOIDC_CLIENT_SECRET: \"&lt;from-authentik>\"\nOIDC_AUTH_METHOD: \"client_secret_post\"\nOIDC_CODE_CHALLENGE_METHOD: \"S256\"\nOIDC_ROLES_PROPERTY: \"groups\"\nOIDC_ROLES_MAPPING: '{\"SOC_Admins\": 1, \"SOC_Analysts\": 3}'\nOIDC_DEFAULT_ORG: \"ACME\"\nOIDC_MIXEDAUTH: \"true\"\nOIDC_SCOPES: '[\"openid\", \"profile\", \"email\", \"groups\"]'\nOIDC_REDIRECT_URI: \"https:\/\/misp.example\/users\/login\"\nOIDC_LOGOUT_URL: \"https:\/\/auth.example\/application\/o\/misp\/end-session\/\"<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><code>OIDC_MIXEDAUTH: \"true\"<\/code> keeps local admin login working alongside SSO (instead of force-redirecting to Authentik) \u2014 handy when you\u2019re still setting it up.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><code>OIDC_ROLES_MAPPING<\/code> is JSON, not <code>key=value<\/code><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The value is dropped <em>raw<\/em> into a JSON document:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"shell\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">\"role_mapper\": ${OIDC_ROLES_MAPPING},<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">So it must be a valid JSON object, group \u2192 MISP role id:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"json\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">{\"SOC_Admins\": 1, \"SOC_Analysts\": 3}<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">MISP role ids: <code>1<\/code> = admin (site admin), <code>2<\/code> = org admin, <code>3<\/code> = user, <code>4<\/code> = publisher. Feed it the <code>A=2,B=4<\/code> form from that other image and you just get broken JSON.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Trusting the internal CA<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">OIDC\u2019s wired, you click through, and:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"raw\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">[CertMichelin\\OpenIDConnectClientException] cURL error #60: SSL certificate problem: unable to get local issuer certificate\nRequest URL: \/users\/login?OidcAuth=enable<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The IdP cert is signed by our internal CA and MISP doesn\u2019t trust it. First question: <em>which<\/em> trust store does the OIDC client even use? It\u2019s <code>certmichelin\/openid-connect-php<\/code> \u2192 plain PHP cURL, and in the image:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"shell\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">php -i | grep -iE \"curl.cainfo|openssl.cafile\"\n# curl.cainfo  => no value\n# openssl.cafile => no value<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">No override, no <code>setCertPath()<\/code> in MISP\u2019s <code>Oidc.php<\/code> \u2014 so it\u2019s cURL\u2019s default = the <strong>system bundle <code>\/etc\/ssl\/certs\/ca-certificates.crt<\/code><\/strong>. (Not CakePHP\u2019s <code>cacert.pem<\/code>, so <code>DISABLE_CA_REFRESH<\/code> is a red herring.)<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">That bundle is rebuilt at boot by <code>update-ca-certificates<\/code>, which pulls <code>.crt<\/code> files from <code>\/usr\/local\/share\/ca-certificates\/<\/code>. The image runs it on startup, so just drop the CA in there:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"yaml\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">secrets:\n  INTERNAL_ROOT_CA:\n    external: true\n  INTERNAL_SUB_CA:\n    external: true\nservices:\n  misp:\n    secrets:\n      - source: INTERNAL_ROOT_CA\n        target: \/usr\/local\/share\/ca-certificates\/INTERNAL_ROOT_CA.crt\n      - source: INTERNAL_SUB_CA\n        target: \/usr\/local\/share\/ca-certificates\/INTERNAL_SUB_CA.crt<\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">One cert per file (this is the bit that bit me)<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">I first mounted a single concatenated bundle (root + sub-CA in one <code>.crt<\/code>). Still #60. A multi-cert file <em>does<\/em> get its certs into <code>ca-certificates.crt<\/code>, but <code>update-ca-certificates<\/code> warns and skips the rehash:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"raw\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">rehash: warning: skipping bundle.pem, it does not contain exactly one certificate or CRL\n1 added, 0 removed<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Splitting it into <strong>two separate <code>.crt<\/code> files \u2014 root and sub-CA each on their own \u2014 fixed it.<\/strong> The chain needs both the root <em>and<\/em> the issuing sub-CA trusted, and one-cert-per-file is what <code>update-ca-certificates<\/code> actually wants. &#x1f389;<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Confirm trust before blaming MISP:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"shell\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">MISP=$(docker ps -q --filter \"name=misp_misp\")\ndocker exec $MISP sh -c 'echo | openssl s_client -connect auth.example:443 -servername auth.example \\\n  -CAfile \/etc\/ssl\/certs\/ca-certificates.crt 2>\/dev\/null | grep -i \"Verify return code\"'\n# Verify return code: 0 (ok)   &lt;- you want this<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><code>0 (ok)<\/code> \u2192 the OIDC handshake works. If you still get &#8220;unable to get local issuer certificate&#8221; while your CAs are in the bundle, your server isn\u2019t sending the intermediate \u2014 which is exactly why you trust both the root <em>and<\/em> the sub-CA.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p class=\"has-text-color has-link-color wp-elements-4a0314882b367aa4d822ae1c2c3cd0c5 wp-block-paragraph\" style=\"color:#9ca3af;font-size:14px\">This post was written with the help of Claude (Opus 4), Anthropic\u2019s AI assistant.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Wiring MISP to Authentik over OIDC on the official misp-docker image (OIDC_* vs OIDCAUTH_*, JSON role mapping) and getting it to trust an internal CA \u2014 the cURL #60 fix is one cert per .crt file.<\/p>\n","protected":false},"author":1,"featured_media":2221,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[63,48,26],"tags":[],"class_list":["post-2211","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-authentication","category-security","category-it"],"_links":{"self":[{"href":"https:\/\/cln.io\/blog\/wp-json\/wp\/v2\/posts\/2211","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/cln.io\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/cln.io\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/cln.io\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/cln.io\/blog\/wp-json\/wp\/v2\/comments?post=2211"}],"version-history":[{"count":5,"href":"https:\/\/cln.io\/blog\/wp-json\/wp\/v2\/posts\/2211\/revisions"}],"predecessor-version":[{"id":2224,"href":"https:\/\/cln.io\/blog\/wp-json\/wp\/v2\/posts\/2211\/revisions\/2224"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/cln.io\/blog\/wp-json\/wp\/v2\/media\/2221"}],"wp:attachment":[{"href":"https:\/\/cln.io\/blog\/wp-json\/wp\/v2\/media?parent=2211"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/cln.io\/blog\/wp-json\/wp\/v2\/categories?post=2211"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/cln.io\/blog\/wp-json\/wp\/v2\/tags?post=2211"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}