Repository navigation
fix(express): rebuild image to drop eMudhra TLS trust - #10035
ralph-bitgo[bot] wants to merge 1 commit into
Conversation
bitgo/express:17.6.0 validates TLS only against Node's embedded root store and ships no system CA bundle, so TLS-sensitive workloads behind Express accept eMudhra-rooted chains (October 2026 CA compromise). The base already pins node:22.23.4 (WCN-3185), but installing Debian's ca-certificates alone would swap one eMudhra trust path for another: the distro package (20250419~deb12u2) still ships three emSign roots. - Install ca-certificates in the runtime stage so a usable system CA bundle exists, and strip the eMudhra/emSign anchors Debian 12 still ships until the distro removes them. - Set NODE_OPTIONS=--use-openssl-ca and SSL_CERT_FILE so outbound TLS validates against that scrubbed bundle instead of Node's embedded roots, keeping stale embedded trust data inert. - Add scripts/verify-express-image-trust.sh and run it in the CI docker-build job. It asserts the bundle exists and carries no eMudhra trust, and that verification follows the configured store: a probe CA is accepted when bundled and rejected when not. Ticket: WCN-3730 Session-Id: 68feda0a-1840-4f71-b0f7-bc7c55ab292d Task-Id: 32f2e4ec-11d8-4286-bae3-386a657625e7
| # bottom), so ca-certificates must be installed. Debian's package still trusts | ||
| # eMudhra roots after the October 2026 CA compromise, so they are stripped here | ||
| # until the distro removes them; scripts/verify-express-image-trust.sh asserts | ||
| # that none remain. | ||
| RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates tini && rm -rf /var/lib/apt/lists/* && \ | ||
| for f in $(find /usr/share/ca-certificates -name '*.crt'); do \ |
There was a problem hiding this comment.
Debian 12's ca-certificates (20250419~deb12u2) still ships eMudhra/emSign roots, so installing the package alone would swap one eMudhra trust path for another. This scrub is temporary and idempotent: once the distro drops the roots the loop and sed match nothing, and the verify script asserts the end state either way.
| # Validate outbound TLS against the system CA bundle instead of Node's embedded | ||
| # root store, so OS trust updates (e.g. revoked roots) take effect. | ||
| ENV NODE_OPTIONS=--use-openssl-ca |
There was a problem hiding this comment.
NODE_EXTRA_CA_CERTS can only add anchors to the trusted set — it cannot remove compromised roots. So verification moves off Node's embedded store entirely: in Node's NewRootCertStore() --use-openssl-ca selects X509_STORE_set_default_paths as a mutually exclusive alternative to the bundled roots, and SSL_CERT_FILE pins those paths to the scrubbed bundle. That also means future OS trust updates take effect without a Node bump.
| } | ||
| ' | ||
|
|
||
| # TLS verification must follow the configured system bundle. A server whose |
There was a problem hiding this comment.
The accept/reject pair is the real assertion: accept proves verification follows the configured bundle (dropping --use-openssl-ca breaks it, since the bundled roots then reject the probe CA), and reject proves the default bundle grants no trust by accident (removing the scrub or replacing the config with NODE_EXTRA_CA_CERTS alone breaks it). The embedded-store line is informational because that store is never consulted under the configured flags.
| GIT_HASH=${{ github.sha }} | ||
| SOCKET_SECURITY_MODE=${{ env.SOCKET_SECURITY_MODE }} | ||
|
|
||
| - name: Verify CA trust configuration |
There was a problem hiding this comment.
docker-build is the only gate that builds the real image from the Dockerfile, so the trust check lives here — it re-verifies every future base-image bump too, which is exactly how stale embedded trust data in a new base would get caught.
| `ca-certificates` package) instead of Node's embedded root store. The | ||
| container sets `NODE_OPTIONS=--use-openssl-ca` and | ||
| `SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt`. To trust an | ||
| additional CA (for example an internal CA), set `NODE_EXTRA_CA_CERTS` |
There was a problem hiding this comment.
Wording matters here and I verified it empirically against the built image: NODE_EXTRA_CA_CERTS extends the configured store (it does not replace it), while pointing SSL_CERT_FILE at another bundle replaces the default one.
Description
Rebuilds the BitGo Express image's TLS trust configuration so outbound
TLS can no longer validate against the eMudhra roots compromised in the
October 2026 CA incident.
Why:
bitgo/express:latest(17.6.0) validates TLS only againstNode's embedded root store and ships no system CA bundle, so
TLS-sensitive workloads behind Express accept eMudhra-rooted chains
(the WCN-3730 reproduction returned
ACCEPTED HTTP=200for a chainrooted at
emSign ECC Root CA - G3). Until this is released, thecurrent image should not be used for TLS-sensitive workloads.
What changed (4 files):
Dockerfile(runtime stage): installsca-certificatesso a usablesystem CA bundle exists, and strips the emSign/eMudhra anchors
Debian 12 (
ca-certificates20250419~deb12u2) still ships —installing the package alone would swap one eMudhra trust path for
another. Sets
NODE_OPTIONS=--use-openssl-caandSSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crtso outbound TLSvalidates against that scrubbed bundle instead of Node's embedded
roots. The Node base pins are untouched: master already carries
node:22.23.4 (merged fix(express): bump Dockerfile Node base image to 22.23.4 #10022), the ticket's corrected-trust-data
floor.
scripts/verify-express-image-trust.sh(new): asserts the bundleexists and carries no eMudhra/emSign trust, and that TLS
verification follows the configured store — a throwaway probe CA is
ACCEPTEDwhen bundled andREJECTED UNABLE_TO_VERIFY_LEAF_SIGNATUREwhen not. Both directions are asserted, so removing the scrub or
dropping
--use-openssl-cabreaks CI..github/workflows/ci.yml: runs the script indocker-build, theonly gate that builds the real image — it re-verifies every future
base bump too.
modules/express/README.md: documents the TLS trust model and howto add an extra CA (
NODE_EXTRA_CA_CERTSextends the trusted set;pointing
SSL_CERT_FILEat another bundle replaces it).Supersedes #10021 (closed): the same remediation minus its Node
24.15.0 base bump, which conflicts with the base-pin work in #10022
(merged) and #10023 (open, edits only the three
FROMpins).Issue Number
WCN-3730 — https://linear.app/bitgo/issue/WCN-3730/rebuild-bitgo-express-image-to-remove-compromised-emudhra-trust
Type of change
How Has This Been Tested?
Reduced-image smoke in this sandbox (the full
docker buildOOM-killsin the pre-existing
lerna run buildstep, unchanged by this diff; CIdocker-buildis the full-build gate and ran green for these hunks on#10021):
Dockerfilelines) onnode:22.23.4-bookworm-slim and ran
scripts/verify-express-image-trust.shagainst it — passed:system bundle certs: 145, 0 eMudhra/emSign in the bundle; probe CAACCEPTEDwhen in the configured bundle,REJECTED UNABLE_TO_VERIFY_LEAF_SIGNATUREwhen not.dv-g3b-emudhra.we-have-a-problem.comand
dv-g3b-aaa-emudhra.we-have-a-problem.comare bothREJECTEDin the rebuilt image. Note: both routes also reject on the stock
node:22.23.4 base now (
CERT_REVOKED), so they no longer reproducethe ticket's
ACCEPTEDwithout this change either — thediscriminating evidence for the trust policy is the probe pair above.
(
emSign Root CA - G1);--use-openssl-cakeeps embedded trust outof verification entirely (asserted by the probes).
bash -non the new script. No TS/JS source changed; thedockerfile-checkgate's managed COPY/LINK blocks are untouched.Outstanding after merge (cannot be done from a PR): run the release
workflow to publish the rebuilt image under a new immutable digest and
re-point
latest— ticket remediation item 4.Pre-PR ACI gate skipped: no
.github/ai-checks/pr-riskonmaster.Checklist:
scripts/verify-express-image-trust.sh, run in the CIdocker-buildjob)Ticket: WCN-3730