From 59346db7a43956eb64cef3a2502a697af862d371 Mon Sep 17 00:00:00 2001 From: coder9io <342252087+coder9io@users.noreply.github.com> Date: Wed, 21 Oct 2026 00:38:34 +0000 Subject: [PATCH 1/3] Move Docker image off end-of-life Node 18 to Node 24 LTS Node 18 is EOL and will never get the eMudhra SPKI blocklist (nodejs/node d9a3f3d). Moving to the active LTS line means the next 24.x release carrying the backport reaches this image on a plain rebuild. engines.node documents the supported runtime. Part of #8: no Node release contains the fix yet; v24.22.0 still accepts the compromised chain. --- Dockerfile | 5 +++-- package.json | 3 +++ readme.md | 3 ++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index ee0c26d..8a845e8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,6 @@ -# Use the official Node.js 18 image as a parent image -FROM node:18 +# Use the official Node.js 24 (active LTS) image as a parent image. +# Node 18 is end-of-life and will not receive TLS trust-store fixes. +FROM node:24 # Set the working directory in the container WORKDIR /usr/src/app diff --git a/package.json b/package.json index 8a11cc6..2fe6b0b 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,9 @@ "keywords": [], "author": "anik3tra0", "license": "ISC", + "engines": { + "node": ">=24" + }, "dependencies": { "@opentelemetry/api": "^1.3.0", "@opentelemetry/core": "1.19.0", diff --git a/readme.md b/readme.md index a4ef66d..59bab73 100644 --- a/readme.md +++ b/readme.md @@ -16,7 +16,8 @@ You can checkout these links to know ## Prerequisites -You need a vmagent running that the underlying OTEL Exporter can write to. +- Node.js 24 (the Docker image uses `node:24`). +- A vmagent running that the underlying OTEL Exporter can write to. ## Features From da143f0ea06c9e30ee8462db66fce1991b61cc39 Mon Sep 17 00:00:00 2001 From: coder9io <342252087+coder9io@users.noreply.github.com> Date: Wed, 21 Oct 2026 03:10:38 +0000 Subject: [PATCH 2/3] Add TLS check for the eMudhra intermediate blocklist Issue #8 requires proof that the image's Node rejects the compromised eMudhra chain and still reaches a trusted endpoint. The check accepts only CERT_REVOKED, the code the upstream SPKI blocklist reports, so a DNS failure or unrelated chain error cannot pass as the fix. It fails on Node 24.22.0, the latest release, which still accepts the compromised chain. --- package.json | 3 ++- readme.md | 7 +++++++ scripts/check-tls.js | 48 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 scripts/check-tls.js diff --git a/package.json b/package.json index 2fe6b0b..1835f4e 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,8 @@ "description": "", "main": "index.js", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "test": "echo \"Error: no test specified\" && exit 1", + "check:tls": "node scripts/check-tls.js" }, "keywords": [], "author": "anik3tra0", diff --git a/readme.md b/readme.md index 59bab73..8d0a9f4 100644 --- a/readme.md +++ b/readme.md @@ -163,6 +163,13 @@ isolated across different environments. docker run -p 3000:3000 your-app-name ``` +3. **Check the image's TLS trust policy**: + ```bash + docker run --rm your-app-name npm run check:tls + ``` + This passes only when the image's Node rejects the compromised eMudhra intermediates with `CERT_REVOKED` and still + connects to a normal HTTPS endpoint. Pull requests run it against a fresh build of the image. + ### Using Docker Compose The application can also be run using Docker Compose, which simplifies the process of configuring and launching the diff --git a/scripts/check-tls.js b/scripts/check-tls.js new file mode 100644 index 0000000..ebad6b8 --- /dev/null +++ b/scripts/check-tls.js @@ -0,0 +1,48 @@ +// Verifies the Node runtime's TLS trust policy for issue #8: a chain through a +// compromised eMudhra intermediate must be rejected as revoked, while a normal trusted +// endpoint must still connect. Exits non-zero if either expectation fails. +// Run inside the built image: `docker run --rm node scripts/check-tls.js`. +const https = require('https'); + +const BLOCKED_URL = 'https://dv-g3b-emudhra.we-have-a-problem.com/'; +const TRUSTED_URL = 'https://nodejs.org/'; +const TIMEOUT_MS = 15000; + +function probe(url) { + return new Promise((resolve) => { + const req = https.get(url, { timeout: TIMEOUT_MS }, (res) => { + res.resume(); + resolve({ ok: true, status: res.statusCode }); + }); + req.on('timeout', () => req.destroy(new Error(`timed out after ${TIMEOUT_MS}ms`))); + req.on('error', (err) => resolve({ ok: false, code: err.code, message: err.message })); + }); +} + +// The upstream SPKI blocklist (nodejs/node@d9a3f3d) reports CERT_REVOKED. +// Accept only that code: DNS failures, timeouts or an unrelated chain error +// would otherwise pass without proving the blocklist is present. +const BLOCKLIST_ERROR_CODE = 'CERT_REVOKED'; + +async function main() { + console.log(`process.version=${process.version}`); + console.log(`openssl=${process.versions.openssl}`); + + const blocked = await probe(BLOCKED_URL); + console.log(`${BLOCKED_URL} -> ${JSON.stringify(blocked)}`); + const trusted = await probe(TRUSTED_URL); + console.log(`${TRUSTED_URL} -> ${JSON.stringify(trusted)}`); + + const failures = []; + if (blocked.code !== BLOCKLIST_ERROR_CODE) { + failures.push(`expected ${BLOCKLIST_ERROR_CODE} from ${BLOCKED_URL}`); + } + if (!trusted.ok) { + failures.push(`expected ${TRUSTED_URL} to connect`); + } + for (const failure of failures) console.error(`FAIL: ${failure}`); + if (failures.length) process.exit(1); + console.log('PASS: compromised chain rejected, trusted endpoint accepted'); +} + +main(); From 2c5731fbc991a2bb2991f77e8fe492db3d0f9459 Mon Sep 17 00:00:00 2001 From: coder9io <342252087+coder9io@users.noreply.github.com> Date: Wed, 21 Oct 2026 03:11:13 +0000 Subject: [PATCH 3/3] Validate the docker image on pull requests Base-image changes were only built at release time, so a runtime bump could ship unverified. Build the image from a clean cache on every PR, record the Node and OpenSSL versions, smoke-run the app and run the TLS check. The job uses no secrets and pushes nothing. Kept in its own commit because it touches .github/workflows. --- .github/workflows/pr-image-check.yml | 45 ++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 .github/workflows/pr-image-check.yml diff --git a/.github/workflows/pr-image-check.yml b/.github/workflows/pr-image-check.yml new file mode 100644 index 0000000..34d9e84 --- /dev/null +++ b/.github/workflows/pr-image-check.yml @@ -0,0 +1,45 @@ +name: Validate docker image + +# Builds the release image from a clean cache on every pull request and checks +# the Node runtime it ships, so a base-image change is proven before release +# (issue #8). Uses no secrets and pushes nothing. + +on: + pull_request: + +permissions: + contents: read + +jobs: + validate: + name: Build, smoke and TLS-check the image + runs-on: ubuntu-latest + + steps: + - name: Check out the repo + uses: actions/checkout@v4 + + - name: Build image without cached layers + run: docker build --pull --no-cache -t nodejs-otel:pr . + + - name: Record runtime versions + run: | + docker image inspect nodejs-otel:pr --format '{{.Id}}' + docker run --rm nodejs-otel:pr node -p 'process.version + " openssl " + process.versions.openssl' + + - name: Smoke-run the app against a dead OTLP endpoint + run: | + docker run -d --name smoke \ + -e WORKFLOW_ID_COUNT=2 -e CUSTOMER_ID_COUNT=2 \ + -e OTLP_ENDPOINT=http://127.0.0.1:9/v1/metrics \ + -e SERVICE_NAME=smoke -e OTLP_METER_NAME=smoke -e ENVIRONMENT=ci \ + -e OTLP_METRIC_EXPORTER_FREQUENCY=1000 \ + nodejs-otel:pr + sleep 10 + docker logs smoke > smoke.log 2>&1 + cat smoke.log + test "$(docker inspect -f '{{.State.Running}}' smoke)" = true + grep -q 'items to be sent' smoke.log + + - name: Check TLS trust policy + run: docker run --rm nodejs-otel:pr npm run --silent check:tls