Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
bbf46c1
fix(security): trust corporate CA before Hermes final-stage fetches
cv Aug 4, 2026
3c0df7b
test(hermes): accept guarded npm remediation runs
cv Aug 4, 2026
7e40308
merge: refresh main for hermes CA ordering
cv Aug 4, 2026
75c9e34
test(security): validate Dockerfile remediation runs
cv Aug 4, 2026
270af86
Merge remote-tracking branch 'origin/main' into codex/hermes-final-st…
cv Aug 4, 2026
48b8cd9
fix(security): scope Hermes build CA trust
cv Aug 4, 2026
9dcaf72
Merge remote-tracking branch 'origin/main' into codex/hermes-final-st…
cv Aug 4, 2026
f44498c
fix(security): preserve Hermes package trust defaults
cv Aug 4, 2026
0c6994c
Merge remote-tracking branch 'origin/main' into codex/hermes-final-st…
cv Aug 4, 2026
d398aca
fix(security): reject hidden Dockerfile commands
cv Aug 4, 2026
3c776ce
fix(security): scan Dockerfile continuations safely
cv Aug 4, 2026
4c9e97c
test(hermes): exercise corporate CA image build
jyaunches Aug 4, 2026
ff64217
test(hermes): verify default CA trust in image
jyaunches Aug 4, 2026
fe93fdd
test(hermes): bound corporate CA image fixture
jyaunches Aug 4, 2026
591aaac
test(hermes): keep CA fixture test linear
jyaunches Aug 4, 2026
6b96ddb
fix(ci): select cross-signed CA trust path
jyaunches Aug 4, 2026
bad4683
fix(ci): eliminate CA fixture output race
jyaunches Aug 4, 2026
0c4fbd7
merge(main): refresh PR branch
cv Aug 4, 2026
47c2ff1
fix(ci): validate CA fixture file and build order
cv Aug 4, 2026
d00db4a
merge(main): refresh PR branch
cv Aug 4, 2026
91cbdf8
fix(ci): document CA output file identity check
cv Aug 4, 2026
6ef59fd
fix(security): validate opened CA output before writes
cv Aug 4, 2026
8bcb8f8
test(security): cover device CA outputs
cv Aug 4, 2026
abb8668
merge(main): refresh PR branch
cv Aug 4, 2026
a98b808
merge(main): refresh Hermes certificate ordering
cv Aug 4, 2026
2450e13
merge(main): refresh Hermes certificate ordering
cv Aug 5, 2026
ab2e28c
Merge branch 'main' into codex/hermes-final-stage-ca-ordering
cv Aug 5, 2026
7064312
merge(main): refresh Hermes CA fix
prekshivyas Aug 5, 2026
e2d6b04
ci(gates): refresh pull request gate state
prekshivyas Aug 5, 2026
33b741c
ci(gates): bind exact review receipts
prekshivyas Aug 5, 2026
a16c78f
merge(main): refresh Hermes CA fix
prekshivyas Aug 5, 2026
2b11b16
merge(main): refresh Hermes corporate CA fix
prekshivyas Aug 5, 2026
656951d
merge(main): integrate current main
cv Aug 5, 2026
58910e3
merge(main): integrate current main
cv Aug 5, 2026
fce1cba
merge(main): integrate current main
cv Aug 5, 2026
2fc6a3e
merge(main): integrate current main
cv Aug 5, 2026
24dd82d
merge(main): refresh PR 8228
cv Aug 5, 2026
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
42 changes: 42 additions & 0 deletions .github/workflows/sandbox-images-and-e2e.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,30 @@ jobs:
cache-from: type=gha,scope=hermes-production-${{ runner.os }}-${{ runner.arch }}
cache-to: type=gha,mode=max,scope=hermes-production-${{ runner.os }}-${{ runner.arch }}

# The production build intentionally omits NEMOCLAW_CORPORATE_CA_B64. A
# successful final stage therefore proves its registry remediations and
# Hermes agent-install phase complete with the base image's default trust.
- name: Verify Hermes default-trust final image
shell: bash
run: |
set -euo pipefail
docker run --rm \
--network none \
--read-only \
--cap-drop ALL \
--security-opt no-new-privileges \
--pids-limit 64 \
--memory 256m \
--entrypoint /bin/sh \
nemoclaw-hermes-production -eu -c '
test "$NODE_EXTRA_CA_CERTS" = /usr/local/share/nemoclaw/corporate-ca.pem
test ! -e /usr/local/share/nemoclaw/corporate-ca.pem
test ! -L /usr/local/share/nemoclaw/corporate-ca.pem
test -x /usr/local/bin/hermes
node -e "const tls = require(\"node:tls\"); if (tls.rootCertificates.length === 0) process.exit(1); tls.createSecureContext()"
/opt/hermes/.venv/bin/python -I -c "import ssl; assert ssl.create_default_context().get_ca_certs()"
'

- name: Scan completed Hermes image for node-tar
id: node-tar-scan
shell: bash
Expand Down Expand Up @@ -416,14 +440,32 @@ jobs:
shell: bash
run: |
set -euo pipefail
# curl and Python replace their default roots with this build argument.
compact_ca_bundle="$(mktemp)"
trap 'rm -f "$compact_ca_bundle"' EXIT
node --experimental-strip-types scripts/checks/select-ci-endpoint-ca-roots.mts \
--output "$compact_ca_bundle"
corporate_ca_b64="$(base64 -w 0 "$compact_ca_bundle")"
corporate_ca_sha256="$(sha256sum "$compact_ca_bundle" | cut -d ' ' -f 1)"
messaging_plan_b64="$(node --experimental-strip-types scripts/check-messaging-plan-image-boundary.mts plan hermes)"
build_args=(
-f agents/hermes/Dockerfile
--build-arg "BASE_IMAGE=${HERMES_BASE_IMAGE}"
--build-arg "NEMOCLAW_CORPORATE_CA_B64=${corporate_ca_b64}"
--build-arg "NEMOCLAW_MESSAGING_PLAN_B64=${messaging_plan_b64}"
)
scripts/check-production-build-args.sh "${build_args[@]}"
docker build "${build_args[@]}" -t nemoclaw-hermes-plan-boundary .
installed_ca_sha256="$(
docker run --rm --network none --entrypoint sha256sum \
nemoclaw-hermes-plan-boundary \
/usr/local/share/nemoclaw/corporate-ca.pem |
cut -d ' ' -f 1
)"
test "$installed_ca_sha256" = "$corporate_ca_sha256"
docker run --rm --network none --entrypoint openssl \
nemoclaw-hermes-plan-boundary crl2pkcs7 -nocrl \
-certfile /usr/local/share/nemoclaw/corporate-ca.pem -out /dev/null
node --experimental-strip-types scripts/check-messaging-plan-image-boundary.mts verify \
nemoclaw-hermes-plan-boundary hermes

Expand Down
98 changes: 62 additions & 36 deletions agents/hermes/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -150,24 +150,70 @@ COPY scripts/checks/node-tar-image-scan.mts /scripts/checks/node-tar-image-scan.
# hadolint ignore=DL3006
FROM ${BASE_IMAGE}

# Base64-encoded host corporate-proxy CA bundle (#6210). Empty by default. When
# onboard detects an operator-supplied corporate CA on the host it bakes it
# here; the RUN below decodes it to a root-owned file that the entrypoint
# appends to the OpenShell trust bundle at runtime. The CA is a public
# certificate, not a secret, so baking it into an image layer is acceptable.
ARG NEMOCLAW_CORPORATE_CA_B64

# Decode the host corporate-proxy CA (#6210) to a root-owned, read-only file
# when onboard baked one in. No-op when NEMOCLAW_CORPORATE_CA_B64 is empty. The
# ARG is expanded by the shell (not interpolated into source), and its value is
# base64 sanitized host-side, so this is not an injection vector. Must run as
# root, before the USER sandbox drop below.
# hadolint ignore=DL3059,DL4006
RUN if [ -n "${NEMOCLAW_CORPORATE_CA_B64}" ]; then \
command -v base64 >/dev/null 2>&1 || { echo "[nemoclaw] base64 is required to decode NEMOCLAW_CORPORATE_CA_B64 but is not installed in the build image" >&2; exit 1; }; \
command -v openssl >/dev/null 2>&1 || { echo "[nemoclaw] openssl is required to validate NEMOCLAW_CORPORATE_CA_B64 but is not installed in the build image (#6210)" >&2; exit 1; }; \
mkdir -p /usr/local/share/nemoclaw \
&& { printf '%s' "${NEMOCLAW_CORPORATE_CA_B64}" | base64 --decode > /tmp/nemoclaw-corporate-ca.decoded 2>/dev/null \
|| { echo "[nemoclaw] NEMOCLAW_CORPORATE_CA_B64 is not valid base64; expected a single-line base64-encoded PEM (#6210)" >&2; exit 1; }; } \
&& awk '/-----BEGIN CERTIFICATE-----/{f=1} f{print} /-----END CERTIFICATE-----/{f=0}' /tmp/nemoclaw-corporate-ca.decoded > /usr/local/share/nemoclaw/corporate-ca.pem \
&& rm -f /tmp/nemoclaw-corporate-ca.decoded \
&& { grep -qF -- "-----BEGIN CERTIFICATE-----" /usr/local/share/nemoclaw/corporate-ca.pem || { echo "[nemoclaw] NEMOCLAW_CORPORATE_CA_B64 did not decode to a bundle of valid X.509 certificates (#6210)" >&2; exit 1; }; } \
&& { openssl crl2pkcs7 -nocrl -certfile /usr/local/share/nemoclaw/corporate-ca.pem >/dev/null 2>&1 || { echo "[nemoclaw] NEMOCLAW_CORPORATE_CA_B64 did not decode to a bundle of valid X.509 certificates (#6210)" >&2; exit 1; }; } \
&& chown root:root /usr/local/share/nemoclaw/corporate-ca.pem \
&& chmod 0444 /usr/local/share/nemoclaw/corporate-ca.pem \
&& echo "[nemoclaw] baked host corporate-proxy CA into image trust (#6210)"; \
fi

# Use the decoded CA for Node.js package operations in this final stage. Node.js
# ignores the path when no CA was baked. At runtime, nemoclaw-start replaces it
# with the merged OpenShell and corporate bundle.
ENV NODE_EXTRA_CA_CERTS=/usr/local/share/nemoclaw/corporate-ca.pem

# Cross-stage root copies are accepted by Docker's legacy builder and create
# one final-image layer while preserving metadata on existing parent paths.
COPY --from=hermes-npm-patch-payload / /

# The final Hermes image owns the shipped dependency boundary independently of
# base freshness. Reassert the idempotent npm-private node-tar fix here.
RUN node --experimental-strip-types /scripts/patch-bundled-npm-tar.mts \
--npm-root /usr/local/lib/node_modules/npm
# base freshness. Reassert the idempotent npm-private node-tar fix here. When
# onboarding supplied a corporate CA, use it for the registry-backed download.
RUN if [ -f /usr/local/share/nemoclaw/corporate-ca.pem ]; then \
export CURL_CA_BUNDLE=/usr/local/share/nemoclaw/corporate-ca.pem; \
fi; \
node --experimental-strip-types /scripts/patch-bundled-npm-tar.mts \
--npm-root /usr/local/lib/node_modules/npm

# Reassert the npm-private brace-expansion fix for the exact final filesystem.
# When onboarding supplied a corporate CA, use it for the registry-backed
# download.
# hadolint ignore=DL3059
RUN node --experimental-strip-types /scripts/patch-bundled-npm-brace-expansion.mts \
--npm-root /usr/local/lib/node_modules/npm
RUN if [ -f /usr/local/share/nemoclaw/corporate-ca.pem ]; then \
export CURL_CA_BUNDLE=/usr/local/share/nemoclaw/corporate-ca.pem; \
fi; \
node --experimental-strip-types /scripts/patch-bundled-npm-brace-expansion.mts \
--npm-root /usr/local/lib/node_modules/npm

# Reassert the npm-private ip-address fix for the exact final filesystem.
# Reassert the npm-private ip-address fix for the exact final filesystem. When
# onboarding supplied a corporate CA, use it for the registry-backed download.
# hadolint ignore=DL3059
RUN node --experimental-strip-types /scripts/lib/patch-bundled-npm-ip-address.mts \
--npm-root /usr/local/lib/node_modules/npm
RUN if [ -f /usr/local/share/nemoclaw/corporate-ca.pem ]; then \
export CURL_CA_BUNDLE=/usr/local/share/nemoclaw/corporate-ca.pem; \
fi; \
node --experimental-strip-types /scripts/lib/patch-bundled-npm-ip-address.mts \
--npm-root /usr/local/lib/node_modules/npm

# Keep the final image contract explicit even when the published base image
# changes independently of this Dockerfile.
Expand Down Expand Up @@ -605,12 +651,6 @@ ARG NEMOCLAW_HERMES_TOOL_GATEWAY_BROKER=0
ARG NEMOCLAW_HERMES_TOOL_GATEWAY_PRESETS_B64=W10=
ARG NEMOCLAW_BUILD_ID=default
ARG NEMOCLAW_DARWIN_VM_COMPAT=0
# Base64-encoded host corporate-proxy CA bundle (#6210). Empty by default. When
# onboard detects an operator-supplied corporate CA on the host it bakes it
# here; the RUN below decodes it to a root-owned file that the entrypoint
# appends to the OpenShell trust bundle at runtime. The CA is a public
# certificate, not a secret, so baking it into an image layer is acceptable.
ARG NEMOCLAW_CORPORATE_CA_B64
# Total model context window (input + output tokens). Empty by default so
# Hermes auto-detects from the endpoint's /v1/models max_model_len; onboard
# rewrites this ARG (via dockerfile-patch) when it probes a runtime value or
Expand Down Expand Up @@ -660,34 +700,20 @@ RUN node --experimental-strip-types /src/lib/messaging/applier/build/messaging-b
# Apply messaging agent-install hooks as root so Hermes Python packages can update
# /opt/hermes/.venv before the runtime drops to the sandbox user.
WORKDIR /opt/hermes
# Clear inherited Python and uv trust overrides before package installation.
# When the decoded corporate CA exists, use it only for this RUN instruction.
# hadolint ignore=DL3059
RUN node --experimental-strip-types /src/lib/messaging/applier/build/messaging-build-applier.mts --agent hermes --phase agent-install \
RUN unset SSL_CERT_FILE REQUESTS_CA_BUNDLE; \
if [ -f /usr/local/share/nemoclaw/corporate-ca.pem ]; then \
export SSL_CERT_FILE=/usr/local/share/nemoclaw/corporate-ca.pem; \
export REQUESTS_CA_BUNDLE=/usr/local/share/nemoclaw/corporate-ca.pem; \
fi; \
node --experimental-strip-types /src/lib/messaging/applier/build/messaging-build-applier.mts --agent hermes --phase agent-install \
&& if [ "$NEMOCLAW_MANAGED_IMAGE_CAPABILITY_UNION" = "1" ]; then \
node --experimental-strip-types /src/lib/messaging/applier/build/messaging-build-applier.mts \
--agent hermes --phase managed-image-capability-union; \
fi

# Decode the host corporate-proxy CA (#6210) to a root-owned, read-only file
# when onboard baked one in. No-op when NEMOCLAW_CORPORATE_CA_B64 is empty. The
# ARG is expanded by the shell (not interpolated into source), and its value is
# base64 sanitized host-side, so this is not an injection vector. Must run as
# root, before the USER sandbox drop below.
# hadolint ignore=DL3059,DL4006
RUN if [ -n "${NEMOCLAW_CORPORATE_CA_B64}" ]; then \
command -v base64 >/dev/null 2>&1 || { echo "[nemoclaw] base64 is required to decode NEMOCLAW_CORPORATE_CA_B64 but is not installed in the build image" >&2; exit 1; }; \
command -v openssl >/dev/null 2>&1 || { echo "[nemoclaw] openssl is required to validate NEMOCLAW_CORPORATE_CA_B64 but is not installed in the build image (#6210)" >&2; exit 1; }; \
mkdir -p /usr/local/share/nemoclaw \
&& { printf '%s' "${NEMOCLAW_CORPORATE_CA_B64}" | base64 --decode > /tmp/nemoclaw-corporate-ca.decoded 2>/dev/null \
|| { echo "[nemoclaw] NEMOCLAW_CORPORATE_CA_B64 is not valid base64; expected a single-line base64-encoded PEM (#6210)" >&2; exit 1; }; } \
&& awk '/-----BEGIN CERTIFICATE-----/{f=1} f{print} /-----END CERTIFICATE-----/{f=0}' /tmp/nemoclaw-corporate-ca.decoded > /usr/local/share/nemoclaw/corporate-ca.pem \
&& rm -f /tmp/nemoclaw-corporate-ca.decoded \
&& { grep -qF -- "-----BEGIN CERTIFICATE-----" /usr/local/share/nemoclaw/corporate-ca.pem || { echo "[nemoclaw] NEMOCLAW_CORPORATE_CA_B64 did not decode to a bundle of valid X.509 certificates (#6210)" >&2; exit 1; }; } \
&& { openssl crl2pkcs7 -nocrl -certfile /usr/local/share/nemoclaw/corporate-ca.pem >/dev/null 2>&1 || { echo "[nemoclaw] NEMOCLAW_CORPORATE_CA_B64 did not decode to a bundle of valid X.509 certificates (#6210)" >&2; exit 1; }; } \
&& chown root:root /usr/local/share/nemoclaw/corporate-ca.pem \
&& chmod 0444 /usr/local/share/nemoclaw/corporate-ca.pem \
&& echo "[nemoclaw] baked host corporate-proxy CA into image trust (#6210)"; \
fi

WORKDIR /sandbox
USER sandbox

Expand Down
5 changes: 5 additions & 0 deletions ci/source-shape-test-budget.json
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,11 @@
"test": "trusts the corporate CA before the DCode discovery runtime npm install",
"category": "security"
},
{
"file": "test/corporate-ca-build-tls-anchor.test.ts",
"test": "uses the corporate CA conditionally for all Hermes registry remediations",
"category": "security"
},
{
"file": "test/dcode-base-image-workflow.test.ts",
"test": "accepts every discovered publisher and rejects supply-chain mutations",
Expand Down
7 changes: 6 additions & 1 deletion docs/security/configure-corporate-ca-trust.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,12 @@ It sets `NODE_EXTRA_CA_CERTS` before build-time Node.js dependency verification,

<AgentOnly variant="hermes">

The Hermes Dockerfile decodes the bundle after its managed build-time dependency steps, so the corporate CA does not apply to those earlier operations.
The Hermes discovery-runtime installer applies the corporate CA before its npm operations.
The final Hermes image stage decodes the CA immediately after `FROM ${BASE_IMAGE}` and sets `NODE_EXTRA_CA_CERTS` before later npm operations.
The registry-backed npm remediations set `CURL_CA_BUNDLE` before each download only when the decoded certificate file exists.
The Hermes package installer clears inherited `SSL_CERT_FILE` and `REQUESTS_CA_BUNDLE` values before its build-time `uv pip install` commands.
When the decoded CA exists, it sets both variables to that file for those commands.
If the file does not exist, uv and Python use their default trust configuration.

</AgentOnly>

Expand Down
Loading
Loading