Steps To Reproduce
- Self-host Bitwarden 2026.7.0 (standard
bitwarden.sh Docker deployment) with SMTP configured against Microsoft 365 / Exchange Online Protection (<tenant>.mail.protection.outlook.com, port 25, startTls=true).
- Attempt any action that sends mail (e.g. admin portal passwordless login).
Expected Result
Email is sent; admin portal login link arrives.
Actual Result
MailKit.Security.SslHandshakeException on every send:
The server's SSL certificate could not be validated for the following reasons:
• The server certificate has the following errors:
• unable to get certificate CRL
• An intermediate certificate has the following errors:
• unable to get certificate CRL
• The root certificate has the following errors:
• self-signed certificate in certificate chain
Additionally, on the admin portal the failed login POST redirects to /login (missing the /admin path base) which returns 404, masking the underlying SMTP error entirely (see "Related cosmetic bug" below).
Root cause
Not a Bitwarden code bug, but a trust-store regression in the shipped images plus two hardening gaps that made it very hard to diagnose:
- On 15 April 2026 Mozilla and Chrome removed the DigiCert Global Root CA (G1) from their trust stores (industry-wide G1 distrust). Alpine inherits Mozilla's bundle, so the Alpine-based images introduced in newer releases ship a CA bundle without this root (verified: 119 roots in the bundle, G1 absent).
- Microsoft's EOP MX endpoints (
*.mail.protection.outlook.com) still serve a chain anchored to that distrusted G1 root (leaf → DigiCert SHA2 Secure Server CA → DigiCert Global Root CA). See the mozilla dev-security-policy thread "Microsoft MX servers use certificate chaining to obsolete DigiCert Global Root CA" and Microsoft advisory MC1282565.
- Result: every Alpine-image self-host instance relaying via M365 fails outbound SMTP TLS validation with
UntrustedRoot. Reproduced outside Bitwarden entirely: a minimal .NET 10 SslStream client on mcr.microsoft.com/dotnet/sdk:10.0-alpine fails identically against the same relay; adding the G1 root to the trust store makes the same handshake succeed with SslPolicyErrors.None (revocation Online).
Workaround (works, documented here for other affected users)
Place the DigiCert Global Root CA in PEM form into bwdata/ca-certificates and restart:
curl -s http://cacerts.digicert.com/DigiCertGlobalRootCA.crt \
| openssl x509 -inform DER -out ./bwdata/ca-certificates/digicert-global-root-ca.crt
./bitwarden.sh restart
Note the DER→PEM conversion is required — see gap 2 below.
Suggested actions for the Bitwarden team
- Release notes / awareness: when a base-image bump changes the shipped CA bundle (especially removals such as the April 2026 G1 distrust), call it out in release notes. This change silently broke outbound SMTP for M365 relays with no configuration change on the user's side.
bwdata/ca-certificates silently ignores non-PEM files. The .NET certificate-directory loader (OpenSslCachedSystemStoreProvider) reads PEM only. DigiCert (and other CAs) serve roots from their AIA URLs in DER, so the obvious curl of the cert into that directory produces a file that is silently skipped — no log line, no error. The previous root-entrypoint flow (cp ... && update-ca-certificates) tolerated this; the current non-root entrypoint performs no processing at all. Suggestion: at container start, attempt to parse each file in /etc/bitwarden/ca-certificates and log a clear warning for any file that yields no certificates (or auto-convert DER). This would have reduced a multi-hour diagnosis to a one-line log read.
- Docs: the Certificate Options page only covers inbound NGINX TLS. The outbound-SMTP trust mechanism (
bwdata/ca-certificates, PEM requirement, which containers consume it) is undocumented.
- Related cosmetic bug: when
LoginController.Index throws (any unhandled mail exception), the admin portal ends up redirecting to /login without the /admin path base, producing a 404 that hides the real error. The 404 sent this investigation down a reverse-proxy rabbit hole before the SMTP exception was found in container logs.
Environment
- Self-hosted 2026.7.0 (standard Docker /
bitwarden.sh deployment)
- SMTP:
<tenant>.mail.protection.outlook.com:25, startTls=true (M365/EOP direct-send)
- Reproduced independently on
mcr.microsoft.com/dotnet/sdk:10.0-alpine with a minimal SslStream client
References
Troubleshooting and issue write-up assisted by Claude Code (Anthropic). Root cause isolated by reproducing the failure on a clean mcr.microsoft.com/dotnet/sdk:10.0-alpine image with a minimal SslStream client, then bisecting trust-store configurations against the live EOP relay.
Steps To Reproduce
bitwarden.shDocker deployment) with SMTP configured against Microsoft 365 / Exchange Online Protection (<tenant>.mail.protection.outlook.com, port 25,startTls=true).Expected Result
Email is sent; admin portal login link arrives.
Actual Result
MailKit.Security.SslHandshakeExceptionon every send:Additionally, on the admin portal the failed login POST redirects to
/login(missing the/adminpath base) which returns 404, masking the underlying SMTP error entirely (see "Related cosmetic bug" below).Root cause
Not a Bitwarden code bug, but a trust-store regression in the shipped images plus two hardening gaps that made it very hard to diagnose:
*.mail.protection.outlook.com) still serve a chain anchored to that distrusted G1 root (leaf →DigiCert SHA2 Secure Server CA→DigiCert Global Root CA). See the mozilla dev-security-policy thread "Microsoft MX servers use certificate chaining to obsolete DigiCert Global Root CA" and Microsoft advisory MC1282565.UntrustedRoot. Reproduced outside Bitwarden entirely: a minimal .NET 10SslStreamclient onmcr.microsoft.com/dotnet/sdk:10.0-alpinefails identically against the same relay; adding the G1 root to the trust store makes the same handshake succeed withSslPolicyErrors.None(revocation Online).Workaround (works, documented here for other affected users)
Place the DigiCert Global Root CA in PEM form into
bwdata/ca-certificatesand restart:curl -s http://cacerts.digicert.com/DigiCertGlobalRootCA.crt \ | openssl x509 -inform DER -out ./bwdata/ca-certificates/digicert-global-root-ca.crt ./bitwarden.sh restartNote the DER→PEM conversion is required — see gap 2 below.
Suggested actions for the Bitwarden team
bwdata/ca-certificatessilently ignores non-PEM files. The .NET certificate-directory loader (OpenSslCachedSystemStoreProvider) reads PEM only. DigiCert (and other CAs) serve roots from their AIA URLs in DER, so the obviouscurlof the cert into that directory produces a file that is silently skipped — no log line, no error. The previous root-entrypoint flow (cp ... && update-ca-certificates) tolerated this; the current non-root entrypoint performs no processing at all. Suggestion: at container start, attempt to parse each file in/etc/bitwarden/ca-certificatesand log a clear warning for any file that yields no certificates (or auto-convert DER). This would have reduced a multi-hour diagnosis to a one-line log read.bwdata/ca-certificates, PEM requirement, which containers consume it) is undocumented.LoginController.Indexthrows (any unhandled mail exception), the admin portal ends up redirecting to/loginwithout the/adminpath base, producing a 404 that hides the real error. The 404 sent this investigation down a reverse-proxy rabbit hole before the SMTP exception was found in container logs.Environment
bitwarden.shdeployment)<tenant>.mail.protection.outlook.com:25,startTls=true(M365/EOP direct-send)mcr.microsoft.com/dotnet/sdk:10.0-alpinewith a minimalSslStreamclientReferences
Troubleshooting and issue write-up assisted by Claude Code (Anthropic). Root cause isolated by reproducing the failure on a clean
mcr.microsoft.com/dotnet/sdk:10.0-alpineimage with a minimalSslStreamclient, then bisecting trust-store configurations against the live EOP relay.