From 02ddcc29f93b46b17631b30eeadb81bbcc7bb1dd Mon Sep 17 00:00:00 2001 From: lelia <2418071+lelia@users.noreply.github.com> Date: Tue, 20 Oct 2026 22:12:35 -0400 Subject: [PATCH 1/3] Stop trusting the compromised eMudhra intermediate CAs Require Alpine ca-certificates 20261020-r0+ in the image, bump certifi to 2026.10.19, point Node at the system store with NODE_OPTIONS=--use-openssl-ca until Node ships its fix, and bump the bundled uv to 0.13.4. --- CHANGELOG.md | 6 ++++++ Dockerfile | 15 ++++++++++++--- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b4c8aa2..440587c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ ## 2.11.2 +### Fixed: stop trusting the compromised eMudhra intermediate CAs + +- The CLI and every tool in the Docker image now reject certificates issued + from the compromised eMudhra intermediate CAs. +- Bumped `certifi` to `2026.10.19` and the image's bundled `uv` to `0.13.4`. + ### Changed: bump pinned Coana CLI to 15.12.11 - Bumped the pinned reachability engine (the Coana CLI) from `15.12.10` to diff --git a/Dockerfile b/Dockerfile index 55abd0f..bc9654e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,9 +14,14 @@ ARG PIP_EXTRA_INDEX_URL=https://pypi.org/simple ARG USE_LOCAL_INSTALL=false # Install base packages first +# ca-certificates must be 20261020-r0 or newer, which drops the roots the +# compromised eMudhra intermediates chain to. The base image ships an older +# one, and `apk add` does not upgrade installed packages, so upgrade it here. RUN apk update && apk add --no-cache \ git nodejs npm yarn curl wget \ - ruby ruby-dev build-base strace + ruby ruby-dev build-base strace && \ + apk upgrade --no-cache ca-certificates && \ + [ "$(apk version -t "$(apk list -I ca-certificates | awk '{print $1}' | sed 's/^ca-certificates-//')" 20261020-r0)" != "<" ] # Install Go with version control RUN if [ "$GO_VERSION" = "system" ]; then \ @@ -84,9 +89,13 @@ RUN npm install socket -g && \ # Set environment paths ENV PATH="/usr/local/go/bin:/usr/lib/go/bin:/root/.cargo/bin:${PATH}" ENV GOPATH="/go" +# Node only rejects the compromised eMudhra intermediates from 22.23.4 / 24.22.1, +# and Alpine's nodejs package is older. Use the system store, which ca-certificates +# above already fixed. Drop this once Alpine's nodejs includes Node's fix. +ENV NODE_OPTIONS="--use-openssl-ca" -# Install uv -COPY --from=ghcr.io/astral-sh/uv:0.10.4 /uv /usr/local/bin/uv +# Install uv (0.13.4+ ships the patched webpki-roots) +COPY --from=ghcr.io/astral-sh/uv:0.13.4 /uv /usr/local/bin/uv # Install pyenv # pyenv lets us build/install arbitrary Python versions on demand. We install From cbdcd0ed16dcff559634c6daa0fb43e534fd04bf Mon Sep 17 00:00:00 2001 From: lelia <2418071+lelia@users.noreply.github.com> Date: Wed, 21 Oct 2026 16:10:32 -0400 Subject: [PATCH 2/3] Install Node from the official node image Alpine's nodejs package is still 24.18.1, which trusts the compromised eMudhra intermediates. node:24-alpine3.24 ships Node 24.22.1, which rejects them, so copy Node, npm, corepack and yarn from it and fail the build below 24.22.1. That makes the NODE_OPTIONS=--use-openssl-ca workaround and its entrypoint handling unnecessary, so drop both. --- CHANGELOG.md | 2 ++ Dockerfile | 24 +++++++++++++++++++----- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 440587c..d79801a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ - The CLI and every tool in the Docker image now reject certificates issued from the compromised eMudhra intermediate CAs. - Bumped `certifi` to `2026.10.19` and the image's bundled `uv` to `0.13.4`. +- The image now takes Node.js from the official `node:24-alpine` image (`24.22.1`) + instead of Alpine's `nodejs` package, which hasn't shipped Node's fix. ### Changed: bump pinned Coana CLI to 15.12.11 diff --git a/Dockerfile b/Dockerfile index bc9654e..15a6ada 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,3 +1,8 @@ +# Node.js from the official Alpine image instead of Alpine's nodejs package, which +# lags Node's security releases. Match the major version to what reachability needs +# and the Alpine release to the python:3-alpine base below. +FROM node:24-alpine3.24 AS node + FROM python:3-alpine LABEL org.opencontainers.image.authors="socket.dev" @@ -18,11 +23,24 @@ ARG USE_LOCAL_INSTALL=false # compromised eMudhra intermediates chain to. The base image ships an older # one, and `apk add` does not upgrade installed packages, so upgrade it here. RUN apk update && apk add --no-cache \ - git nodejs npm yarn curl wget \ + git curl wget \ ruby ruby-dev build-base strace && \ apk upgrade --no-cache ca-certificates && \ [ "$(apk version -t "$(apk list -I ca-certificates | awk '{print $1}' | sed 's/^ca-certificates-//')" 20261020-r0)" != "<" ] +# npm, npx and yarn are symlinks in the node image. COPY would dereference them +# into flat files and break their relative requires, so recreate the links. +COPY --from=node /usr/local/bin/node /usr/local/bin/node +COPY --from=node /usr/local/lib/node_modules /usr/local/lib/node_modules +COPY --from=node /opt /opt +# Node rejects the compromised eMudhra intermediates from 24.22.1. +RUN ln -s ../lib/node_modules/npm/bin/npm-cli.js /usr/local/bin/npm && \ + ln -s ../lib/node_modules/npm/bin/npx-cli.js /usr/local/bin/npx && \ + ln -s ../lib/node_modules/corepack/dist/corepack.js /usr/local/bin/corepack && \ + ln -s /opt/yarn-v*/bin/yarn /usr/local/bin/yarn && \ + ln -s /opt/yarn-v*/bin/yarnpkg /usr/local/bin/yarnpkg && \ + node -e 'const [a,b,c]=process.versions.node.split(".").map(Number); process.exit(a>24||(a===24&&(b>22||(b===22&&c>=1)))?0:1)' + # Install Go with version control RUN if [ "$GO_VERSION" = "system" ]; then \ apk add --no-cache go && \ @@ -89,10 +107,6 @@ RUN npm install socket -g && \ # Set environment paths ENV PATH="/usr/local/go/bin:/usr/lib/go/bin:/root/.cargo/bin:${PATH}" ENV GOPATH="/go" -# Node only rejects the compromised eMudhra intermediates from 22.23.4 / 24.22.1, -# and Alpine's nodejs package is older. Use the system store, which ca-certificates -# above already fixed. Drop this once Alpine's nodejs includes Node's fix. -ENV NODE_OPTIONS="--use-openssl-ca" # Install uv (0.13.4+ ships the patched webpki-roots) COPY --from=ghcr.io/astral-sh/uv:0.13.4 /uv /usr/local/bin/uv From f73c5871388589395a871123bcfda6a11896a34a Mon Sep 17 00:00:00 2001 From: lelia <2418071+lelia@users.noreply.github.com> Date: Wed, 21 Oct 2026 16:22:37 -0400 Subject: [PATCH 3/3] Smoke-test the Docker image on arm64 and check its TLS clients dockerfile-smoke only built the image on amd64 and ran nothing in it, so an arm64 break or a regression in the image's CA trust would first show up in release.yml. Build on native amd64 and arm64 runners, check the bundled tools run, and require curl, Node and Python requests to reject the eMudhra test hosts while still reaching api.socket.dev, npm and PyPI. Also run it when the entrypoint script changes. --- .github/workflows/dependency-review.yml | 80 ++++++++++++++++++++++++- 1 file changed, 78 insertions(+), 2 deletions(-) diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index afe59af..df2ac35 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -81,7 +81,7 @@ jobs: echo "python_deps_changed=$(has_file '^(pyproject\.toml|uv\.lock)$')" echo "fixture_npm_changed=$(has_file '^tests/e2e/fixtures/simple-npm/')" echo "fixture_pypi_changed=$(has_file '^tests/e2e/fixtures/simple-pypi/')" - echo "dockerfile_changed=$(has_file '^Dockerfile$')" + echo "dockerfile_changed=$(has_file '^(Dockerfile|scripts/docker-entrypoint\.sh)$')" echo "workflow_or_action_changed=$(has_file '^\.github/workflows/|^\.github/actions/|^\.github/dependabot\.yml$')" } >> "$GITHUB_OUTPUT" @@ -562,7 +562,18 @@ jobs: dockerfile-smoke: needs: inspect if: needs.inspect.outputs.dockerfile_changed == 'true' - runs-on: ubuntu-latest + # Build every architecture the release publishes, each on a native runner, + # so an arm64-only break shows up here rather than in release.yml. + strategy: + fail-fast: false + matrix: + include: + - arch: amd64 + runner: ubuntu-24.04 + - arch: arm64 + runner: ubuntu-24.04-arm + name: dockerfile-smoke (${{ matrix.arch }}) + runs-on: ${{ matrix.runner }} timeout-minutes: 20 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -576,6 +587,71 @@ jobs: --build-arg USE_LOCAL_INSTALL=true -t socket-python-cli:dependabot-smoke . + - name: Check the tools in the image + run: | + docker run --rm --entrypoint sh socket-python-cli:dependabot-smoke -c ' + set -eu + echo "node $(node -v), npm $(npm -v), yarn $(yarn -v)" + socket --version + socketcli --version + ' + + # The eMudhra test hosts serve certificates from the compromised + # intermediates. Require the specific rejection, not just any failure, so + # a DNS or network error cannot pass as "rejected". + - name: Check TLS clients reject the compromised eMudhra intermediates + run: | + docker run --rm --entrypoint sh socket-python-cli:dependabot-smoke -c ' + set -u + status=0 + for host in dv-g3b-emudhra.we-have-a-problem.com dv-g3b-aaa-emudhra.we-have-a-problem.com; do + code=0 + curl -sS -m 30 -o /dev/null "https://$host/" 2>/dev/null || code=$? + if [ "$code" = 60 ]; then + echo "curl rejects $host" + else + echo "::error::curl on $host exited $code, expected 60 (certificate verification failed)" + status=1 + fi + result=$(node -e " + const req = require(\"https\").get(process.argv[1], { timeout: 30000 }, (res) => { + console.log(\"ACCEPTED\"); process.exit(0); + }); + req.on(\"timeout\", () => { console.log(\"TIMEOUT\"); process.exit(0); }); + req.on(\"error\", (e) => { console.log(e.code || e.message); process.exit(0); }); + " "https://$host/") + case "$result" in + CERT_REVOKED|UNABLE_TO_GET_ISSUER_CERT_LOCALLY|UNABLE_TO_VERIFY_LEAF_SIGNATURE) echo "node rejects $host ($result)" ;; + *) echo "::error::node on $host: $result"; status=1 ;; + esac + result=$(python -c " + import sys, requests + try: + requests.get(sys.argv[1], timeout=30) + print(\"ACCEPTED\") + except requests.exceptions.SSLError: + print(\"SSLError\") + except Exception as e: + print(type(e).__name__) + " "https://$host/") + if [ "$result" = SSLError ]; then + echo "python rejects $host" + else + echo "::error::python requests on $host: $result" + status=1 + fi + done + for url in https://api.socket.dev/v0/ https://registry.npmjs.org/ https://pypi.org/simple/; do + if curl -sS -m 30 -o /dev/null "$url"; then + echo "curl connects to $url" + else + echo "::error::curl cannot connect to $url" + status=1 + fi + done + exit $status + ' + workflow-notice: needs: inspect if: needs.inspect.outputs.workflow_or_action_changed == 'true'