Skip to content

fix(security): rebuild HyperDX on Node 22.23.5 (digest-pinned) - #7

Merged
gascity-infra-agent[bot] merged 1 commit into
gascity-ssofrom
gascity/node-22.23.5
Nov 12, 2026
Merged

gascity-infra-agent[bot] merged 1 commit into
gascity-ssofrom
gascity/node-22.23.5

Conversation

@gascity-infra-agent

Copy link
Copy Markdown

Not HOLD. HyperDX runs in internal-services, not identity-ha, and it is not a box-shared image. Merging into gascity-sso only runs gascity-image.yml. That workflow verifies, builds, runs the symbol guard and pushes ghcr.io/gascity/hyperdx:2.42.0-gascity-<sha12> plus :gascity-sso. Nothing deploys until the separate repin PR in gascity/internal-services is merged.

Prod status. Prod HyperDX still runs sha256:17098cbf… (Node 22.23.4). sha256:d0bf80e5… (2.42.0-gascity-59a28cbdd09c, the #6 merge) is built and waiting for a repin; it is the Brotli KEV item. The image built from this PR would replace d0bf80e5 for the repin: it is the same app on Node 22.23.5.

Advisories fixed

These come from the Node.js security release of 2026-11-11, moving Node 22.23.4 to 22.23.5:

Our own triage found these not reachable as configured: HyperDX speaks plain HTTP/1.1 behind the gate and has no TLS server or HTTP/2. This is a defense-in-depth rebuild. Alpine moves from 3.24.3 to 3.24.4.

Change

docker/hyperdx/Dockerfile: NODE_VERSION=22.23.5 and NODE_DIGEST=sha256:1314fb30ec37592d5845e5a160d522c0e680972aa534549940b8d73bde88481a. Both Node stages (node_base, prod) pick up the change through the existing FROM node:${NODE_VERSION}-alpine3.24@${NODE_DIGEST}. Nothing else changes.

These files are deliberately not touched: .nvmrc (22.23.1) and packages/{app,api}/Dockerfile (node:22.16.0-alpine). They are upstream-owned and identical to upstream. Neither the fork CI nor the image build uses them, and changing them would widen the fork's upstream footprint.

Verified (on this exact tree, from a pristine git archive)

Check Result
Base digest crane digest node:22.23.5-alpine3.24 = node:22.23.5-alpine = 22.23-alpine3.24 = 22-alpine3.24 = sha256:1314fb30…481a. It is an OCI image index for linux/amd64, arm/v6, arm/v7, arm64/v8 and s390x, plus attestations. Source: nodejs/docker-node@1274f3ee:22/alpine3.24, config NODE_VERSION=22.23.5
gascity-pr steps in node:22.23.5-alpine3.24@1314fb30 yarn install --immutable OK; api lint 0 errors (301 warnings, unchanged); tsc --noEmit clean; jest fork suites 4/4, 82/82; API build OK; source symbol guard 10/10
docker buildx build --no-cache --pull --target prod with the CI build contexts and CODE_VERSION OK, amd64
Runtime in the built image node --version v22.23.5, llhttp 9.4.4, sqlite 3.54.1 (d0bf80e5 has v22.23.4 / 9.4.3 / 3.53.4), OpenSSL 3.5.9, Alpine 3.24.4
CVE-2026-123877 behavior marker new https.Agent().getName({host:'h',port:443,ca:'X'}) returns h:443::s1:X:…, the new type/length-encoded TLS policy key. The d0bf80e5 image returns h:443::X:…
Brotli KEV marker (CVE-2026-111497) zlib.constants.BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH = -17 (still present)
Image symbol guard (exact gascity-image.yml list) 10/10
gascity/smoke harness 107/107, 0 fail. Default auth 31 plus seed/verify-seed; production posture (SMOKE_TELEMETRY=off) auth 31, telemetry 4 and verify-seed in a second container; trace plus OTEL_LOG_LEVEL=debug auth 31; failclosed 3; rotation 7. 0 restarts, no OOM, 0 uncaught/TLS/fatal/deprecation lines, 0 fake-secret hits in stdout. The baseline d0bf80e5 gives 107/107 with identical outcome lines
Trivy 0.70 (image) 224 → 224, with nothing added or removed. The only OS finding, zlib 1.3.2-r1 ALPINE CVE-2026-4738 (MEDIUM), is in both images (Alpine has no fix yet)
osv-scanner 2.7 (image) 68 → 68, with nothing added or removed

Neither scanner catalogs the Node runtime, so the Node CVEs never appear in their output, old or new. The evidence that they are fixed is node --version, the llhttp and sqlite versions, the https.Agent marker, and the official v22.23.5 changelog, which lists all 15 CVEs above.

Rollout

  1. Review, then merge into gascity-sso. The gascity-image run publishes ghcr.io/gascity/hyperdx:2.42.0-gascity-<sha12>; take the digest from the run's "published" output.
  2. Confirm docker run --rm --entrypoint node <img@digest> --version prints v22.23.5. Optionally rerun the smoke harness against the digest.
  3. Repin in gascity/internal-services flux/apps/hyperdx/hyperdx.yaml. That is a separate PR and deploy step for Julian, and it is not opened here. This image replaces the pending d0bf80e5 repin. If the d0bf80e5 repin lands first, this one follows it.
  4. After deploy: /api/health shows the new CODE_VERSION, with 0 restarts; rerun the §6.3 count-only checks.

Rollback

Revert this commit on gascity-sso, or simply don't repin. Prod keeps whatever digest internal-services pins: 17098cbf… today, or d0bf80e5… once that repin lands. Both are the same app on Node 22.23.4.

🤖 Generated with Claude Code

Node security release 2026-11-11 (22.23.5): CVE-2026-123864, -123866,
-123863, -123865, -123872, -123874, -123828, -123867, -123868, -123871,
-123875, -123877, -123829, -123862, -123873; llhttp 9.4.4, sqlite 3.54.1.

Both Node stages move to node:22.23.5-alpine3.24 pinned to the multi-arch
OCI index sha256:1314fb30ec37592d5845e5a160d522c0e680972aa534549940b8d73bde88481a
(same index as node:22.23.5-alpine).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

@gascity-infra-agent gascity-infra-agent Bot left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Independent max-effort review. Verdict: CLEAN, with no HIGH or MED findings. Merging with a merge commit.

Scope.

  • The only file changed is docker/hyperdx/Dockerfile (+6/−6): NODE_VERSION=22.23.5, NODE_DIGEST=sha256:1314fb30…481a, and the comment.
  • Both Node stages pick the pin up through the existing FROM node:${NODE_VERSION}-alpine3.24@${NODE_DIGEST}: node_base at L50 and prod at L112. common-utils-builder and builder derive from node_base.
  • Nothing overrides either ARG. gascity-image.yml passes only CODE_VERSION.
  • Head f4b257ae sits directly on the current gascity-sso tip, 59a28cbd.

Base digest sha256:1314fb30ec37592d5845e5a160d522c0e680972aa534549940b8d73bde88481a

  • Tags: crane digest returns it for node:22.23.5-alpine3.24, 22.23.5-alpine, 22.23-alpine3.24 and 22-alpine3.24.
  • Independent sources agree: the registry Docker-Content-Digest matches, the SHA-256 recomputed over the raw index bytes matches, and so do the Docker Hub tag API and docker-library repo-info.
  • Index contents: an OCI image index covering linux/amd64, arm/v6, arm/v7, arm64/v8 and s390x, plus attestation manifests.
  • amd64 config: NODE_VERSION=22.23.5, created 2026-11-11.
  • Provenance:
    • SLSA provenance says docker-library built it from nodejs/docker-node@1274f3ee:22/alpine3.24.
    • official-images library/node maps the tag to the same GitCommit and Directory.
    • That Dockerfile installs the GPG-verified nodejs.org v22.23.5 tarball.

Advisories.

  • The description's 15 CVEs (6 High, 6 Medium, 3 Low) match the official v22.23.5 changelog's notable changes exactly.
  • The nodejs.org dist index marks 22.23.5 as a security release. OpenSSL (3.5.9) and the module ABI (127) are unchanged, so native modules are unaffected.
  • The CVE-2026-123877 change checks out in source: lib/https.js at v22.23.5 encodes TLS option keys as <type><len>:<value>, so ca:'X' becomes s1:X.

Build. I ran an independent docker buildx build --no-cache --pull --target prod from a pristine git archive of f4b257ae, with the CI build contexts, in a fresh isolated builder that shares no cache. It succeeded:

  • yarn install, the API build and the Next build (26/26 static pages) are all OK.
  • The image's first 4 layers are byte-identical (by diff ID) to the pinned amd64 base.
  • Its config has NODE_VERSION=22.23.5, and it has 16 layers, like the earlier published images.

CI.

  • The required auth quality gate (Actions, app 15368) is green on f4b257ae: immutable install, lint 0 errors (301 warnings), tsc, jest 4 suites 82/82, API build, and the production-symbol guard 10/10.
  • CodeQL is green.

Runtime evidence is from the author's local run. I re-analyzed the raw artifacts rather than taking the summary.

  • node --version is v22.23.5, with llhttp 9.4.4 and sqlite 3.54.1.
  • The image symbol guard passes 10/10.
  • Smoke is 107/107. Its PASS/FAIL/SUMMARY lines are identical to the d0bf80e5 baseline run, which was also 107/107.
  • The Trivy finding sets are identical (224 rows), and so are the osv sets (68 rows).
  • Alpine 3.24.3 → 3.24.4 changes only alpine-release, apk-tools and libapk (3.0.8 → 3.0.9).

Merging only builds and publishes.

  • On a push to gascity-sso, only gascity-image.yml runs. Its steps are verify, build, the symbol guard, and push :2.42.0-gascity-<sha12> and :gascity-sso. It has no deploy step.
  • The repo has no webhooks and no environments.
  • The only consumer of ghcr.io/gascity/hyperdx pins it by digest and has no image automation, so nothing deploys until the separate repin.
  • This PR is not under the release freeze: it is not identity-ha, a box-shared image, or the S1-min tree.

Findings (none blocking)

  1. INFO: CI runtime drift.
    • gascity-pr.yml L36–38 and gascity-image.yml L44–46 use setup-node node-version: 22 without check-latest. As a result, this PR's required gate ran on the runner's cached v22.23.4, and the image's runtime is never exercised before merge.
    • The author's in-container verify on 22.23.5 and the independent build cover that gap here.
    • Optional follow-up: pin the verify jobs to the Dockerfile's NODE_VERSION.
  2. LOW (process, carried over from #5): bots can't see the pin. The ARG-based pin (Dockerfile L14–15, L50, L112) is invisible to Dependabot and Renovate, so each Node security release remains a manual bump.
  3. INFO: the smoke A/B counter differences are timing, not a regression.
    • Posture B shows "CH proxy captured 6" (baseline 1) and 117 stdout lines (baseline 84). Both come from one 5-query evaluation (hdx-alert-* query ids) of the alert seeded in posture A, which landed inside B's capture window.
    • Posture C differs only in trace volume and the OTLP batch count.
    • T13 still passed against every captured request.
    • The harness could ignore hdx-alert-* captures to make these counters deterministic.
  4. INFO: the 22.23.5 changes don't touch HyperDX's runtime paths.
    • llhttp 9.4.4 (a private release) only tightens header-value validation; the fast path used to admit 0x0E–0x1F.
    • The TLS, http2, sqlite and permission-model changes are not on HyperDX's path: there is no TLS server, no HTTP/2, no node:sqlite, and no first-party use of Agent keys.
  5. INFO (description): the d0bf80e5 "Brotli KEV item" label.
    • The description calls d0bf80e5 "the Brotli KEV item". Prod 17098cbf already runs Node 22.23.4, which has the CVE-2026-111497 fix.
    • The KEV-aliased item in #6 is andybalholm/brotli (GO-2026-6888) in the migrate tool. Only the all-in-one targets ship that tool, not prod.
    • No change is needed here.

Not re-run independently, because of a host disk-pressure pause:

  • the smoke harness;
  • the Trivy and osv scans;
  • running node --version against my own build.

For these I re-analyzed the author's raw logs and scan JSON instead. After merging I'll check the published image through registry metadata.

🤖 Generated with Claude Code

@gascity-infra-agent
gascity-infra-agent Bot merged commit 7e28e8e into gascity-sso Nov 12, 2026
6 checks passed
@gascity-infra-agent

Copy link
Copy Markdown
Author

Merged and published.

  • Merge: 7e28e8ea85dd3bbcfcecddbbc27d4b0a489737dc. Its parents are 59a28cbd and f4b257ae, and its tree is identical to the reviewed head.
  • gascity-image run 43310687615: success.
    • The verify job passed.
    • The build used FROM node:22.23.5-alpine3.24@sha256:1314fb30….
    • The image symbol guard passed 10/10 before the push.
    • CodeQL on the merge commit is also green.
  • Published image: ghcr.io/gascity/hyperdx@sha256:4d67b7b32213a03a7cd63e5fb7f471160558d7fdde1aa6f34843dae4f50f02a6. 2.42.0-gascity-7e28e8ea85dd and gascity-sso both resolve to it, and the run log and crane digest agree.
  • Image metadata (checked from the registry, not by pulling):
    • a docker v2 amd64 manifest with 16 layers;
    • its first 4 layers, both compressed digests and diff IDs, are exactly the pinned base's amd64 layers;
    • config NODE_VERSION=22.23.5 and CODE_VERSION=2.42.0-gascity-7e28e8ea85dd;
    • user, entrypoint and healthcheck are unchanged.
  • Not deployed. Prod keeps its current digest until the separate internal-services repin, which is a deploy step and is not opened here.

🤖 Generated with Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant