Skip to content

fix(express): rebuild image to drop eMudhra TLS trust - #10035

Draft
ralph-bitgo[bot] wants to merge 1 commit into
masterfrom
WCN-3730-express-image-ca-trust
Draft

ralph-bitgo[bot] wants to merge 1 commit into
masterfrom
WCN-3730-express-image-ca-trust

Conversation

@ralph-bitgo

@ralph-bitgo ralph-bitgo Bot commented Oct 22, 2026

Copy link
Copy Markdown

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 against
Node'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=200 for a chain
rooted at emSign ECC Root CA - G3). Until this is released, the
current image should not be used for TLS-sensitive workloads.

What changed (4 files):

  • Dockerfile (runtime stage): installs ca-certificates so a usable
    system CA bundle exists, and strips the emSign/eMudhra anchors
    Debian 12 (ca-certificates 20250419~deb12u2) still ships —
    installing the package alone would swap one eMudhra trust path for
    another. Sets NODE_OPTIONS=--use-openssl-ca and
    SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt so outbound TLS
    validates 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 bundle
    exists and carries no eMudhra/emSign trust, and that TLS
    verification follows the configured store — a throwaway probe CA is
    ACCEPTED when bundled and REJECTED UNABLE_TO_VERIFY_LEAF_SIGNATURE
    when not. Both directions are asserted, so removing the scrub or
    dropping --use-openssl-ca breaks CI.
  • .github/workflows/ci.yml: runs the script in docker-build, the
    only gate that builds the real image — it re-verifies every future
    base bump too.
  • modules/express/README.md: documents the TLS trust model and how
    to add an extra CA (NODE_EXTRA_CA_CERTS extends the trusted set;
    pointing SSL_CERT_FILE at 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 FROM pins).

Issue Number

WCN-3730 — https://linear.app/bitgo/issue/WCN-3730/rebuild-bitgo-express-image-to-remove-compromised-emudhra-trust

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires a documentation update

How Has This Been Tested?

Reduced-image smoke in this sandbox (the full docker build OOM-kills
in the pre-existing lerna run build step, unchanged by this diff; CI
docker-build is the full-build gate and ran green for these hunks on
#10021):

  • Built the runtime-stage recipe (exact Dockerfile lines) on
    node:22.23.4-bookworm-slim and ran
    scripts/verify-express-image-trust.sh against it — passed:
    system bundle certs: 145, 0 eMudhra/emSign in the bundle; probe CA
    ACCEPTED when in the configured bundle,
    REJECTED UNABLE_TO_VERIFY_LEAF_SIGNATURE when not.
  • Ticket reproduction routes dv-g3b-emudhra.we-have-a-problem.com
    and dv-g3b-aaa-emudhra.we-have-a-problem.com are both REJECTED
    in the rebuilt image. Note: both routes also reject on the stock
    node:22.23.4 base now (CERT_REVOKED), so they no longer reproduce
    the ticket's ACCEPTED without this change either — the
    discriminating evidence for the trust policy is the probe pair above.
  • Node 22.23.4's embedded store still lists one eMudhra anchor
    (emSign Root CA - G1); --use-openssl-ca keeps embedded trust out
    of verification entirely (asserted by the probes).
  • bash -n on the new script. No TS/JS source changed; the
    dockerfile-check gate'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-risk on master.

Checklist:

  • My code follows the style guidelines of this project
  • I have performed a self-review of my own code
  • My code compiles correctly for both Node and Browser environments (n/a — no TS/JS source changed)
  • I have commented my code, particularly in hard-to-understand areas
  • My commits follow Conventional Commits and I have properly described any BREAKING CHANGES
  • The ticket or github issue was included in the commit message as a reference
  • I have made corresponding changes to the documentation
  • I have added tests that prove my fix is effective (scripts/verify-express-image-trust.sh, run in the CI docker-build job)
  • New and existing unit tests pass locally with my changes (no unit-test-covered source changed; CI matrix pending on this branch)

Ticket: WCN-3730

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
@linear-code

linear-code Bot commented Oct 22, 2026

Copy link
Copy Markdown
Contributor

WCN-3730

Comment thread Dockerfile
Comment on lines +39 to +44
# 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 \

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread Dockerfile
Comment on lines +389 to +391
# 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

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread .github/workflows/ci.yml
GIT_HASH=${{ github.sha }}
SOCKET_SECURITY_MODE=${{ env.SOCKET_SECURITY_MODE }}

- name: Verify CA trust configuration

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread modules/express/README.md
`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`

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants