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' diff --git a/CHANGELOG.md b/CHANGELOG.md index b4c8aa2..d79801a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ ## 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`. +- 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 - Bumped the pinned reachability engine (the Coana CLI) from `15.12.10` to diff --git a/Dockerfile b/Dockerfile index 55abd0f..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" @@ -14,9 +19,27 @@ 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 + 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 \ @@ -85,8 +108,8 @@ RUN npm install socket -g && \ ENV PATH="/usr/local/go/bin:/usr/lib/go/bin:/root/.cargo/bin:${PATH}" ENV GOPATH="/go" -# 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