Hooking MISP up to Authentik (OIDC) + trusting an internal CA
Notes to future me: getting MISP to do SSO against Authentik over OIDC, on an internal network where the IdP cert is signed by a corporate CA. Two halves — the OIDC wiring (with one env-var gotcha that cost me an afternoon) and getting MISP to trust the CA (a cURL #60 that survives “but I mounted the cert”).
Stack: official ghcr.io/misp/misp-docker/misp-core (we run a mirror of v2.5.32), Authentik as the IdP, Traefik out front, Docker Swarm.

Authentik side
Create an Application + an OAuth2/OpenID Provider (confidential):
- Redirect URI:
https://misp.example/users/login(MISP comes back here) - Scopes:
openid,profile,email, and add the groups scope mapping so thegroupsclaim is actually emitted — that’s what MISP maps roles from - Grab the Client ID and Client secret
- Your issuer is
https://auth.example/application/o/<app-slug>/(the slug is the application slug)
That’s it on the IdP. Everything else is MISP env.
MISP side: the env vars that actually matter
The footgun: the official misp-docker image reads the OIDC_* namespace, gated behind OIDC_ENABLE=true. There’s a different image build floating around that uses SECURITY_AUTH: "OidcAuth.Oidc" + OIDCAUTH_* — those are silently inert on the official image. No error, SSO just never engages.
It comes down to one switch in the entrypoint:
# core/files/configure_misp.sh -> set_up_oidc()
if [[ "$OIDC_ENABLE" == "true" ]]; then
...
# writes OidcAuth.* into config.php via modify_config.php
echo "... OIDC authentication enabled"
fi
No OIDC_ENABLE=true → that block never runs → Security.auth stays empty. So make sure you’re on OIDC_*, not OIDCAUTH_*.
A working block:
OIDC_ENABLE: "true"
OIDC_PROVIDER_URL: "https://auth.example/application/o/misp/"
OIDC_ISSUER: "https://auth.example/application/o/misp/"
OIDC_CLIENT_ID: "misp"
OIDC_CLIENT_SECRET: "<from-authentik>"
OIDC_AUTH_METHOD: "client_secret_post"
OIDC_CODE_CHALLENGE_METHOD: "S256"
OIDC_ROLES_PROPERTY: "groups"
OIDC_ROLES_MAPPING: '{"SOC_Admins": 1, "SOC_Analysts": 3}'
OIDC_DEFAULT_ORG: "ACME"
OIDC_MIXEDAUTH: "true"
OIDC_SCOPES: '["openid", "profile", "email", "groups"]'
OIDC_REDIRECT_URI: "https://misp.example/users/login"
OIDC_LOGOUT_URL: "https://auth.example/application/o/misp/end-session/"
OIDC_MIXEDAUTH: "true" keeps local admin login working alongside SSO (instead of force-redirecting to Authentik) — handy when you’re still setting it up.
OIDC_ROLES_MAPPING is JSON, not key=value
The value is dropped raw into a JSON document:
"role_mapper": ${OIDC_ROLES_MAPPING},
So it must be a valid JSON object, group → MISP role id:
{"SOC_Admins": 1, "SOC_Analysts": 3}
MISP role ids: 1 = admin (site admin), 2 = org admin, 3 = user, 4 = publisher. Feed it the A=2,B=4 form from that other image and you just get broken JSON.
Trusting the internal CA
OIDC’s wired, you click through, and:
[CertMichelin\OpenIDConnectClientException] cURL error #60: SSL certificate problem: unable to get local issuer certificate Request URL: /users/login?OidcAuth=enable
The IdP cert is signed by our internal CA and MISP doesn’t trust it. First question: which trust store does the OIDC client even use? It’s certmichelin/openid-connect-php → plain PHP cURL, and in the image:
php -i | grep -iE "curl.cainfo|openssl.cafile" # curl.cainfo => no value # openssl.cafile => no value
No override, no setCertPath() in MISP’s Oidc.php — so it’s cURL’s default = the system bundle /etc/ssl/certs/ca-certificates.crt. (Not CakePHP’s cacert.pem, so DISABLE_CA_REFRESH is a red herring.)
That bundle is rebuilt at boot by update-ca-certificates, which pulls .crt files from /usr/local/share/ca-certificates/. The image runs it on startup, so just drop the CA in there:
secrets:
INTERNAL_ROOT_CA:
external: true
INTERNAL_SUB_CA:
external: true
services:
misp:
secrets:
- source: INTERNAL_ROOT_CA
target: /usr/local/share/ca-certificates/INTERNAL_ROOT_CA.crt
- source: INTERNAL_SUB_CA
target: /usr/local/share/ca-certificates/INTERNAL_SUB_CA.crt
One cert per file (this is the bit that bit me)
I first mounted a single concatenated bundle (root + sub-CA in one .crt). Still #60. A multi-cert file does get its certs into ca-certificates.crt, but update-ca-certificates warns and skips the rehash:
rehash: warning: skipping bundle.pem, it does not contain exactly one certificate or CRL 1 added, 0 removed
Splitting it into two separate .crt files — root and sub-CA each on their own — fixed it. The chain needs both the root and the issuing sub-CA trusted, and one-cert-per-file is what update-ca-certificates actually wants. 🎉
Confirm trust before blaming MISP:
MISP=$(docker ps -q --filter "name=misp_misp") docker exec $MISP sh -c 'echo | openssl s_client -connect auth.example:443 -servername auth.example \ -CAfile /etc/ssl/certs/ca-certificates.crt 2>/dev/null | grep -i "Verify return code"' # Verify return code: 0 (ok) <- you want this
0 (ok) → the OIDC handshake works. If you still get “unable to get local issuer certificate” while your CAs are in the bundle, your server isn’t sending the intermediate — which is exactly why you trust both the root and the sub-CA.
This post was written with the help of Claude (Opus 4), Anthropic’s AI assistant.