Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions .github/workflows/pr-image-check.yml
Original file line number Diff line number Diff line change
@@ -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
5 changes: 3 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -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
Expand Down
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,15 @@
"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",
"license": "ISC",
"engines": {
"node": ">=24"
},
"dependencies": {
"@opentelemetry/api": "^1.3.0",
"@opentelemetry/core": "1.19.0",
Expand Down
10 changes: 9 additions & 1 deletion readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -162,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
Expand Down
48 changes: 48 additions & 0 deletions scripts/check-tls.js
Original file line number Diff line number Diff line change
@@ -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 <image> 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();
Loading