Skip to content

Commit ff84ba8

Browse files
authored
Stop trusting the compromised eMudhra intermediate CAs (#415)
* 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. * 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. * 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.
1 parent e6093a2 commit ff84ba8

3 files changed

Lines changed: 113 additions & 6 deletions

File tree

‎.github/workflows/dependency-review.yml‎

Lines changed: 78 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ jobs:
8181
echo "python_deps_changed=$(has_file '^(pyproject\.toml|uv\.lock)$')"
8282
echo "fixture_npm_changed=$(has_file '^tests/e2e/fixtures/simple-npm/')"
8383
echo "fixture_pypi_changed=$(has_file '^tests/e2e/fixtures/simple-pypi/')"
84-
echo "dockerfile_changed=$(has_file '^Dockerfile$')"
84+
echo "dockerfile_changed=$(has_file '^(Dockerfile|scripts/docker-entrypoint\.sh)$')"
8585
echo "workflow_or_action_changed=$(has_file '^\.github/workflows/|^\.github/actions/|^\.github/dependabot\.yml$')"
8686
} >> "$GITHUB_OUTPUT"
8787
@@ -562,7 +562,18 @@ jobs:
562562
dockerfile-smoke:
563563
needs: inspect
564564
if: needs.inspect.outputs.dockerfile_changed == 'true'
565-
runs-on: ubuntu-latest
565+
# Build every architecture the release publishes, each on a native runner,
566+
# so an arm64-only break shows up here rather than in release.yml.
567+
strategy:
568+
fail-fast: false
569+
matrix:
570+
include:
571+
- arch: amd64
572+
runner: ubuntu-24.04
573+
- arch: arm64
574+
runner: ubuntu-24.04-arm
575+
name: dockerfile-smoke (${{ matrix.arch }})
576+
runs-on: ${{ matrix.runner }}
566577
timeout-minutes: 20
567578
steps:
568579
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
@@ -576,6 +587,71 @@ jobs:
576587
--build-arg USE_LOCAL_INSTALL=true
577588
-t socket-python-cli:dependabot-smoke .
578589
590+
- name: Check the tools in the image
591+
run: |
592+
docker run --rm --entrypoint sh socket-python-cli:dependabot-smoke -c '
593+
set -eu
594+
echo "node $(node -v), npm $(npm -v), yarn $(yarn -v)"
595+
socket --version
596+
socketcli --version
597+
'
598+
599+
# The eMudhra test hosts serve certificates from the compromised
600+
# intermediates. Require the specific rejection, not just any failure, so
601+
# a DNS or network error cannot pass as "rejected".
602+
- name: Check TLS clients reject the compromised eMudhra intermediates
603+
run: |
604+
docker run --rm --entrypoint sh socket-python-cli:dependabot-smoke -c '
605+
set -u
606+
status=0
607+
for host in dv-g3b-emudhra.we-have-a-problem.com dv-g3b-aaa-emudhra.we-have-a-problem.com; do
608+
code=0
609+
curl -sS -m 30 -o /dev/null "https://$host/" 2>/dev/null || code=$?
610+
if [ "$code" = 60 ]; then
611+
echo "curl rejects $host"
612+
else
613+
echo "::error::curl on $host exited $code, expected 60 (certificate verification failed)"
614+
status=1
615+
fi
616+
result=$(node -e "
617+
const req = require(\"https\").get(process.argv[1], { timeout: 30000 }, (res) => {
618+
console.log(\"ACCEPTED\"); process.exit(0);
619+
});
620+
req.on(\"timeout\", () => { console.log(\"TIMEOUT\"); process.exit(0); });
621+
req.on(\"error\", (e) => { console.log(e.code || e.message); process.exit(0); });
622+
" "https://$host/")
623+
case "$result" in
624+
CERT_REVOKED|UNABLE_TO_GET_ISSUER_CERT_LOCALLY|UNABLE_TO_VERIFY_LEAF_SIGNATURE) echo "node rejects $host ($result)" ;;
625+
*) echo "::error::node on $host: $result"; status=1 ;;
626+
esac
627+
result=$(python -c "
628+
import sys, requests
629+
try:
630+
requests.get(sys.argv[1], timeout=30)
631+
print(\"ACCEPTED\")
632+
except requests.exceptions.SSLError:
633+
print(\"SSLError\")
634+
except Exception as e:
635+
print(type(e).__name__)
636+
" "https://$host/")
637+
if [ "$result" = SSLError ]; then
638+
echo "python rejects $host"
639+
else
640+
echo "::error::python requests on $host: $result"
641+
status=1
642+
fi
643+
done
644+
for url in https://api.socket.dev/v0/ https://registry.npmjs.org/ https://pypi.org/simple/; do
645+
if curl -sS -m 30 -o /dev/null "$url"; then
646+
echo "curl connects to $url"
647+
else
648+
echo "::error::curl cannot connect to $url"
649+
status=1
650+
fi
651+
done
652+
exit $status
653+
'
654+
579655
workflow-notice:
580656
needs: inspect
581657
if: needs.inspect.outputs.workflow_or_action_changed == 'true'

‎CHANGELOG.md‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,14 @@
22

33
## 2.11.2
44

5+
### Fixed: stop trusting the compromised eMudhra intermediate CAs
6+
7+
- The CLI and every tool in the Docker image now reject certificates issued
8+
from the compromised eMudhra intermediate CAs.
9+
- Bumped `certifi` to `2026.10.19` and the image's bundled `uv` to `0.13.4`.
10+
- The image now takes Node.js from the official `node:24-alpine` image (`24.22.1`)
11+
instead of Alpine's `nodejs` package, which hasn't shipped Node's fix.
12+
513
### Changed: bump pinned Coana CLI to 15.12.11
614

715
- Bumped the pinned reachability engine (the Coana CLI) from `15.12.10` to

‎Dockerfile‎

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
# Node.js from the official Alpine image instead of Alpine's nodejs package, which
2+
# lags Node's security releases. Match the major version to what reachability needs
3+
# and the Alpine release to the python:3-alpine base below.
4+
FROM node:24-alpine3.24 AS node
5+
16
FROM python:3-alpine
27
LABEL org.opencontainers.image.authors="socket.dev"
38

@@ -14,9 +19,27 @@ ARG PIP_EXTRA_INDEX_URL=https://pypi.org/simple
1419
ARG USE_LOCAL_INSTALL=false
1520

1621
# Install base packages first
22+
# ca-certificates must be 20261020-r0 or newer, which drops the roots the
23+
# compromised eMudhra intermediates chain to. The base image ships an older
24+
# one, and `apk add` does not upgrade installed packages, so upgrade it here.
1725
RUN apk update && apk add --no-cache \
18-
git nodejs npm yarn curl wget \
19-
ruby ruby-dev build-base strace
26+
git curl wget \
27+
ruby ruby-dev build-base strace && \
28+
apk upgrade --no-cache ca-certificates && \
29+
[ "$(apk version -t "$(apk list -I ca-certificates | awk '{print $1}' | sed 's/^ca-certificates-//')" 20261020-r0)" != "<" ]
30+
31+
# npm, npx and yarn are symlinks in the node image. COPY would dereference them
32+
# into flat files and break their relative requires, so recreate the links.
33+
COPY --from=node /usr/local/bin/node /usr/local/bin/node
34+
COPY --from=node /usr/local/lib/node_modules /usr/local/lib/node_modules
35+
COPY --from=node /opt /opt
36+
# Node rejects the compromised eMudhra intermediates from 24.22.1.
37+
RUN ln -s ../lib/node_modules/npm/bin/npm-cli.js /usr/local/bin/npm && \
38+
ln -s ../lib/node_modules/npm/bin/npx-cli.js /usr/local/bin/npx && \
39+
ln -s ../lib/node_modules/corepack/dist/corepack.js /usr/local/bin/corepack && \
40+
ln -s /opt/yarn-v*/bin/yarn /usr/local/bin/yarn && \
41+
ln -s /opt/yarn-v*/bin/yarnpkg /usr/local/bin/yarnpkg && \
42+
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)'
2043

2144
# Install Go with version control
2245
RUN if [ "$GO_VERSION" = "system" ]; then \
@@ -85,8 +108,8 @@ RUN npm install socket -g && \
85108
ENV PATH="/usr/local/go/bin:/usr/lib/go/bin:/root/.cargo/bin:${PATH}"
86109
ENV GOPATH="/go"
87110

88-
# Install uv
89-
COPY --from=ghcr.io/astral-sh/uv:0.10.4 /uv /usr/local/bin/uv
111+
# Install uv (0.13.4+ ships the patched webpki-roots)
112+
COPY --from=ghcr.io/astral-sh/uv:0.13.4 /uv /usr/local/bin/uv
90113

91114
# Install pyenv
92115
# pyenv lets us build/install arbitrary Python versions on demand. We install

0 commit comments

Comments
 (0)