Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
d0c34af
fix(security): update sandbox dependency fixes
senthilr-nv Jul 26, 2026
7467b51
fix(security): probe the installed Vim binary
senthilr-nv Jul 26, 2026
2fbe3b4
fix(security): correct Perl component probes
senthilr-nv Jul 26, 2026
c0a461b
merge(main): refresh security remediation
senthilr-nv Jul 26, 2026
3fc0d68
test(security): cover both Vim package architectures
senthilr-nv Jul 26, 2026
bbc8abc
fix(security): allow contained npm bin links
senthilr-nv Jul 26, 2026
3db39af
docs(security): clarify npm symlink boundary
senthilr-nv Jul 26, 2026
a7164c5
merge(main): refresh security remediation
senthilr-nv Jul 26, 2026
54a7d6f
fix(security): cover all managed base images
senthilr-nv Jul 26, 2026
f1f2c63
test(security): execute Hermes apt fixture
senthilr-nv Jul 26, 2026
20d41f2
merge(main): refresh security remediation
senthilr-nv Jul 26, 2026
f3794f7
Merge branch 'main' into codex/nspect-followup-remediation
senthilr-nv Jul 26, 2026
55f910c
fix(images): close remediation review gaps
senthilr-nv Jul 26, 2026
b25073b
test(images): keep cleanup fixture linear
senthilr-nv Jul 26, 2026
be7a982
fix(images): verify final package inventory
senthilr-nv Jul 26, 2026
691d490
fix(onboard): trust final inventory guard
senthilr-nv Jul 26, 2026
e1db05f
Merge remote-tracking branch 'upstream/main' into codex/nspect-follow…
senthilr-nv Jul 26, 2026
48d2559
test(images): cover base inventory fixture
senthilr-nv Jul 26, 2026
28e0428
test(images): stay within fixture budget
senthilr-nv Jul 26, 2026
632d490
Merge remote-tracking branch 'upstream/main' into codex/nspect-follow…
senthilr-nv Jul 26, 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
1 change: 1 addition & 0 deletions .github/workflows/base-image.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ on:
- "scripts/lib/openclaw-npm-remediation.mts"
- "scripts/lib/reviewed-npm-audit.mts"
- "scripts/checks/node-tar-image-scan.mts"
- "scripts/patch-bundled-npm-brace-expansion.mts"
- "scripts/patch-bundled-npm-tar.mts"
- "scripts/upgrade-bundled-npm.mts"
- "scripts/lib/sandbox-rlimits.sh"
Expand Down
38 changes: 38 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ COPY ci/npm-audit-exceptions.json /scripts/npm-audit-exceptions.json
COPY scripts/lib/reviewed-npm-archive.mts /scripts/lib/reviewed-npm-archive.mts
COPY scripts/lib/reviewed-npm-audit.mts /scripts/lib/reviewed-npm-audit.mts
COPY scripts/lib/openclaw-npm-remediation.mts /scripts/lib/openclaw-npm-remediation.mts
COPY scripts/patch-bundled-npm-brace-expansion.mts /scripts/patch-bundled-npm-brace-expansion.mts
COPY scripts/patch-bundled-npm-tar.mts /scripts/patch-bundled-npm-tar.mts

FROM scratch AS openclaw-plugin-payload
Expand Down Expand Up @@ -147,6 +148,11 @@ RUN --mount=type=bind,from=openclaw-dependency-payload,source=/,target=/run/nemo
RUN 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.
# hadolint ignore=DL3059
RUN node --experimental-strip-types /scripts/patch-bundled-npm-brace-expansion.mts \
--npm-root /usr/local/lib/node_modules/npm

# OpenClaw 2026.7.1 loads some generated source through jiti. Disable its
# filesystem transform cache so source fragments that mention provider marker
# names do not persist under /tmp/jiti inside the sandbox.
Expand Down Expand Up @@ -1651,6 +1657,7 @@ RUN check_metadata() { \
exit 1; \
fi; \
} \
&& check_metadata /scripts/patch-bundled-npm-brace-expansion.mts 'root:root:755' \
&& check_metadata /scripts/patch-bundled-npm-tar.mts 'root:root:755' \
&& check_metadata /opt/nemoclaw/openclaw.plugin.json 'root:root:644' \
&& check_metadata /usr/local/lib/nemoclaw/patch-openclaw-tool-catalog.mts 'root:root:755' \
Expand Down Expand Up @@ -1727,6 +1734,37 @@ HEALTHCHECK --interval=30s --timeout=5s --start-period=45s --retries=3 \
python3 -c 'import pathlib, sys; proc = pathlib.Path(sys.argv[1]); expected = sys.argv[2].encode("ascii"); port = sys.argv[3].encode(); parse = lambda data: (lambda fields: (fields[0], fields[19]))(data.rsplit(b") ", 1)[1].split()); before = parse((proc / "stat").read_bytes()); raw = (proc / "cmdline").read_bytes(); after = parse((proc / "stat").read_bytes()); trimmed = raw.rstrip(b"\0"); padding = len(raw) - len(trimmed); title = padding >= 1 and trimmed in (b"openclaw", b"openclaw-gateway"); argv = raw[:-1].split(b"\0") if padding == 1 else []; interpreters = (b"node", b"nodejs", b"/usr/local/bin/node", b"/usr/local/bin/nodejs", b"/usr/bin/node", b"/usr/bin/nodejs"); launchers = (b"/usr/local/bin/openclaw", b"/usr/local/lib/node_modules/openclaw/openclaw.mjs"); index = 1 if argv and argv[0] in interpreters else 0; command = index < len(argv) and argv[index] in launchers and argv[index + 1:] in ([b"gateway", b"run", b"--port", port], [b"gateway", b"run", b"--port=" + port]); identity = before[1] == expected == after[1] and before[0] != b"Z" and after[0] != b"Z"; raise SystemExit(not (identity and (title or command)))' "/proc/$gwpid" "$gwstart" "$port" 2>/dev/null || exit 1; \
[ -s /tmp/gateway.log ]

# Verify the immutable security package inventory in the completed image.
# hadolint ignore=DL4006
RUN set -eu; \
security_inventory=/usr/local/share/nemoclaw/security-packages.txt; \
arch="$(dpkg --print-architecture)"; \
test -f "$security_inventory"; \
test ! -L "$security_inventory"; \
test "$(stat -c '%u:%g:%a' "$security_inventory")" = "0:0:444"; \
printf '%s\n' \
"architecture=$arch" \
"libexpat1=2.8.2-1" \
"libonig5=6.9.9-1+b1" \
"libjq1=1.8.2-1" \
"jq=1.8.2-1" \
"vim-common=2:9.2.0782-1" \
"vim-tiny=2:9.2.0782-1" \
| cmp -s - "$security_inventory"; \
test "$(dpkg-query -W -f='${Version}' libexpat1)" = "2.8.2-1"; \
test "$(dpkg-query -W -f='${Version}' libonig5)" = "6.9.9-1+b1"; \
test "$(dpkg-query -W -f='${Version}' libjq1)" = "1.8.2-1"; \
test "$(dpkg-query -W -f='${Version}' jq)" = "1.8.2-1"; \
test "$(dpkg-query -W -f='${Version}' vim-common)" = "2:9.2.0782-1"; \
test "$(dpkg-query -W -f='${Version}' vim-tiny)" = "2:9.2.0782-1"; \
ldd /usr/bin/jq | grep -Eq 'libonig[.]so[.]5'; \
test "$(jq --version)" = "jq-1.8.2"; \
printf '%s\n' '{"sandbox":"healthy"}' | jq -e '.sandbox == "healthy"' >/dev/null; \
python3 -c "import pyexpat; assert pyexpat.EXPAT_VERSION == 'expat_2.8.2', pyexpat.EXPAT_VERSION"; \
vim.tiny --version | head -n 1 | grep -Eq '^VIM - Vi IMproved 9[.]2 '; \
test -z "$(dpkg --audit)"
# End completed-image security package verification.

# Entrypoint runs as root to start the gateway as the gateway user,
# then drops to sandbox for agent commands. See nemoclaw-start.sh.
ENTRYPOINT ["/usr/local/bin/nemoclaw-start"]
Expand Down
65 changes: 58 additions & 7 deletions Dockerfile.base
Original file line number Diff line number Diff line change
Expand Up @@ -168,11 +168,12 @@ ENV AWS_EC2_METADATA_DISABLED=true

ENV DEBIAN_FRONTEND=noninteractive

# Trixie has not published fixes for CVE-2026-45186 (libexpat) or
# CVE-2026-49839 (jq). Install the fixed Debian packages from an immutable
# snapshot, with architecture-specific hashes, until Trixie carries them.
# Trixie has not published fixes for the reviewed libexpat, jq, or Vim
# advisories. Install fixed Debian packages from an immutable snapshot, with
# architecture-specific hashes, until Trixie carries them.
# The checksum and runtime probes put the validating command last in each pipe.
# hadolint ignore=DL4006
# The runtime Vim probe verifies the package that this layer installs.
# hadolint ignore=DL3001,DL4006
RUN apt-get update && apt-get install -y --no-install-recommends \
python3=3.13.5-1 \
python3-pip=25.1.1+dfsg-1 \
Expand All @@ -189,31 +190,39 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
e2fsprogs=1.47.2-3+b11 \
"dos2unix=7.5.2-1*" \
libonig5=6.9.9-1+b1 \
vim-tiny=2:9.1.1230-2 \
openssh-sftp-server=1:10.0p1-7+deb13u4 \
tmux=3.5a-3 \
&& arch="$(dpkg --print-architecture)" \
&& case "$arch" in \
amd64) \
libexpat_sha256="37d24b40a745107941f823d1f22c38f197f01981f7f0783777fe0026af016463"; \
libonig_sha256="3abee130696244050500bcc7870e3b4cb82ddd87149ece3fd55010c3d4e1d18c"; \
libjq_sha256="9a5bf964cef39ed8f0f162e20d856e31961d28a57772b5313989b42a8be7e941"; \
jq_sha256="b973a5d304f666845e8ccefab492e3850d4bc2e7aa2a1e7450862095125f2cc0" \
jq_sha256="b973a5d304f666845e8ccefab492e3850d4bc2e7aa2a1e7450862095125f2cc0"; \
vim_tiny_sha256="0e6e231d6d2430a92cf76f8a78506090418fa37758c33b31ed50dfbfc76e22ed" \
;; \
arm64) \
libexpat_sha256="df928e3a8e4da79408d4b18e8cd80a03dffa90130d0698e50041aab5e14f9397"; \
libonig_sha256="137e708575c0622d347815d19cb471a107546b16e9602805ee27afad7bba107f"; \
libjq_sha256="eae4a828df2eb53d728f88109d9f9549e0983a90b573cf0c7fa1e4bbc7533a7e"; \
jq_sha256="c25086443abd04d1457cbb322a0837f9ba986f82b28f44670467c8dc9be1f696" \
jq_sha256="c25086443abd04d1457cbb322a0837f9ba986f82b28f44670467c8dc9be1f696"; \
vim_tiny_sha256="be30f7e9de0b872bec0128ccd890452c0e0e29d99017d16c0f3aa74164f6700d" \
;; \
*) echo "Unsupported architecture for Debian security packages: $arch" >&2; exit 1 ;; \
esac \
&& perl_version_before="$(dpkg-query -W -f='${Version}' perl)" \
&& security_deb_dir="/tmp/nemoclaw-debian-security" \
&& snapshot_url="https://snapshot.debian.org/archive/debian/20260724T000000Z/pool/main" \
&& vim_common_sha256="6b063038246492c4a20e0a212c896dde4d5aa9f59d6fb43ff33d10080bc53a39" \
&& mkdir -p "$security_deb_dir" \
&& curl --proto '=https' --tlsv1.2 -fsSL \
--retry 5 --retry-all-errors --retry-delay 2 --connect-timeout 15 --max-time 120 \
-o "$security_deb_dir/libexpat1.deb" \
"$snapshot_url/e/expat/libexpat1_2.8.2-1_${arch}.deb" \
&& curl --proto '=https' --tlsv1.2 -fsSL \
--retry 5 --retry-all-errors --retry-delay 2 --connect-timeout 15 --max-time 120 \
-o "$security_deb_dir/libonig5.deb" \
"$snapshot_url/libo/libonig/libonig5_6.9.9-1+b1_${arch}.deb" \
&& curl --proto '=https' --tlsv1.2 -fsSL \
--retry 5 --retry-all-errors --retry-delay 2 --connect-timeout 15 --max-time 120 \
-o "$security_deb_dir/libjq1.deb" \
Expand All @@ -222,22 +231,53 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
--retry 5 --retry-all-errors --retry-delay 2 --connect-timeout 15 --max-time 120 \
-o "$security_deb_dir/jq.deb" \
"$snapshot_url/j/jq/jq_1.8.2-1_${arch}.deb" \
&& curl --proto '=https' --tlsv1.2 -fsSL \
--retry 5 --retry-all-errors --retry-delay 2 --connect-timeout 15 --max-time 120 \
-o "$security_deb_dir/vim-common.deb" \
"$snapshot_url/v/vim/vim-common_9.2.0782-1_all.deb" \
&& curl --proto '=https' --tlsv1.2 -fsSL \
--retry 5 --retry-all-errors --retry-delay 2 --connect-timeout 15 --max-time 120 \
-o "$security_deb_dir/vim-tiny.deb" \
"$snapshot_url/v/vim/vim-tiny_9.2.0782-1_${arch}.deb" \
&& printf '%s %s\n' \
"$libexpat_sha256" "$security_deb_dir/libexpat1.deb" \
"$libonig_sha256" "$security_deb_dir/libonig5.deb" \
"$libjq_sha256" "$security_deb_dir/libjq1.deb" \
"$jq_sha256" "$security_deb_dir/jq.deb" \
"$vim_common_sha256" "$security_deb_dir/vim-common.deb" \
"$vim_tiny_sha256" "$security_deb_dir/vim-tiny.deb" \
| sha256sum -c - \
&& dpkg -i \
"$security_deb_dir/libexpat1.deb" \
"$security_deb_dir/libonig5.deb" \
"$security_deb_dir/libjq1.deb" \
"$security_deb_dir/jq.deb" \
"$security_deb_dir/vim-common.deb" \
"$security_deb_dir/vim-tiny.deb" \
&& test "$(dpkg-query -W -f='${Version}' libexpat1)" = "2.8.2-1" \
&& test "$(dpkg-query -W -f='${Version}' libonig5)" = "6.9.9-1+b1" \
&& test "$(dpkg-query -W -f='${Version}' libjq1)" = "1.8.2-1" \
&& test "$(dpkg-query -W -f='${Version}' jq)" = "1.8.2-1" \
&& test "$(dpkg-query -W -f='${Version}' vim-common)" = "2:9.2.0782-1" \
&& test "$(dpkg-query -W -f='${Version}' vim-tiny)" = "2:9.2.0782-1" \
&& test "$(dpkg-query -W -f='${Version}' perl)" = "$perl_version_before" \
&& test "$(jq --version)" = "jq-1.8.2" \
&& ldd /usr/bin/jq | grep -Eq 'libonig[.]so[.]5' \
&& printf '%s\n' '{"sandbox":"healthy"}' | jq -e '.sandbox == "healthy"' >/dev/null \
&& python3 -c "import pyexpat; assert pyexpat.EXPAT_VERSION == 'expat_2.8.2', pyexpat.EXPAT_VERSION" \
&& vim.tiny --version | head -n 1 | grep -Eq '^VIM - Vi IMproved 9[.]2 ' \
&& install -d -o root -g root -m 0755 /usr/local/share/nemoclaw \
&& printf '%s\n' \
"architecture=$arch" \
"libexpat1=2.8.2-1" \
"libonig5=6.9.9-1+b1" \
"libjq1=1.8.2-1" \
"jq=1.8.2-1" \
"vim-common=2:9.2.0782-1" \
"vim-tiny=2:9.2.0782-1" \
> /usr/local/share/nemoclaw/security-packages.txt \
&& chown root:root /usr/local/share/nemoclaw/security-packages.txt \
&& chmod 0444 /usr/local/share/nemoclaw/security-packages.txt \
&& rm -rf "$security_deb_dir" \
&& rm -rf /var/lib/apt/lists/* \
&& ln -s /usr/bin/python3 /usr/local/bin/python
Expand All @@ -251,6 +291,10 @@ RUN apt-get update \
&& test "$(perl -e 'print $^V')" = "v5.44.0" \
&& test "$(perl -MSocket -e 'print Socket->VERSION')" = "2.041" \
&& test "$(perl -MStorable -e 'print Storable->VERSION')" = "3.41" \
&& test "$(perl -MHTTP::Tiny -e 'print HTTP::Tiny->VERSION')" = "0.096" \
&& test "$(perl -MIO::Compress::Base -e 'print IO::Compress::Base->VERSION')" = "2.223" \
&& test "$(perl -MIO::Uncompress::Unzip -e 'print IO::Uncompress::Unzip->VERSION')" = "2.223" \
&& test "$(perl -MFile::GlobMapper -e 'print File::GlobMapper->VERSION')" = "1.001" \
&& perl -MSocket=pack_ip_mreq_source -e \
'eval { pack_ip_mreq_source("\0" x 4, "\0" x 3) }; die "short source accepted" unless $@ =~ /Bad arg length/' \
&& perl -e \
Expand Down Expand Up @@ -411,6 +455,7 @@ COPY ci/npm-audit-exceptions.json /scripts/npm-audit-exceptions.json
COPY scripts/lib/reviewed-npm-archive.mts /scripts/lib/reviewed-npm-archive.mts
COPY scripts/lib/reviewed-npm-audit.mts /scripts/lib/reviewed-npm-audit.mts
COPY scripts/lib/openclaw-npm-remediation.mts /scripts/lib/openclaw-npm-remediation.mts
COPY scripts/patch-bundled-npm-brace-expansion.mts /scripts/patch-bundled-npm-brace-expansion.mts
COPY scripts/patch-bundled-npm-tar.mts /scripts/patch-bundled-npm-tar.mts
COPY scripts/upgrade-bundled-npm.mts /scripts/upgrade-bundled-npm.mts

Expand All @@ -426,6 +471,12 @@ RUN node --experimental-strip-types /scripts/patch-bundled-npm-tar.mts \
RUN node --experimental-strip-types /scripts/upgrade-bundled-npm.mts \
--npm-root /usr/local/lib/node_modules/npm

# npm 11.18.0 contains brace-expansion 5.0.7. Replace only that private
# package from the reviewed 5.0.8 archive after the complete npm upgrade.
# hadolint ignore=DL3059
RUN node --experimental-strip-types /scripts/patch-bundled-npm-brace-expansion.mts \
--npm-root /usr/local/lib/node_modules/npm

# Keep OpenClaw's jiti-generated source cache out of /tmp so provider marker
# names do not persist in runtime snapshots or leak-scan inputs.
ENV JITI_FS_CACHE=false
Expand Down
40 changes: 39 additions & 1 deletion agents/hermes/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ ARG BASE_IMAGE=ghcr.io/nvidia/nemoclaw/hermes-sandbox-base@sha256:fa05221f5c7bca
FROM scratch AS hermes-npm-patch-payload

COPY scripts/lib/reviewed-npm-archive.mts /scripts/lib/reviewed-npm-archive.mts
COPY scripts/patch-bundled-npm-brace-expansion.mts /scripts/patch-bundled-npm-brace-expansion.mts
COPY scripts/patch-bundled-npm-tar.mts /scripts/patch-bundled-npm-tar.mts

FROM scratch AS hermes-agent-payload
Expand Down Expand Up @@ -72,6 +73,11 @@ RUN --mount=type=bind,from=hermes-npm-patch-payload,source=/,target=/run/nemocla
RUN 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.
# hadolint ignore=DL3059
RUN node --experimental-strip-types /scripts/patch-bundled-npm-brace-expansion.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.
RUN set -eu; \
Expand Down Expand Up @@ -182,7 +188,7 @@ RUN find /opt/nemoclaw-hermes-config -type d -exec chmod 755 {} + \
&& find /opt/nemoclaw-hermes-config -type f -exec chmod 444 {} + \
&& chmod 444 /src/lib/tool-disclosure.ts \
&& chmod 444 /scripts/lib/reviewed-npm-archive.mts /scripts/lib/openclaw-npm-remediation.mts \
/scripts/patch-bundled-npm-tar.mts \
/scripts/patch-bundled-npm-brace-expansion.mts /scripts/patch-bundled-npm-tar.mts \
&& chmod -R a+rX /src/lib/messaging

RUN --mount=type=bind,from=hermes-runtime-payload,source=/,target=/run/nemoclaw-payload \
Expand Down Expand Up @@ -753,6 +759,7 @@ RUN check_metadata() { \
&& check_absent /root/.cache/electron \
&& check_absent /root/.cache/node-gyp \
&& check_absent /sandbox/.cache \
&& check_metadata /scripts/patch-bundled-npm-brace-expansion.mts 'root:root 444' \
&& check_metadata /scripts/patch-bundled-npm-tar.mts 'root:root 444' \
&& check_metadata /opt/nemoclaw-hermes-config/generate-config.ts 'root:root 444' \
&& check_metadata /usr/local/lib/nemoclaw/validate-hermes-env-secret-boundary.py 'root:root 755' \
Expand All @@ -766,6 +773,37 @@ RUN check_metadata() { \
> /usr/local/share/nemoclaw/node-tar-inventory.json \
&& chmod 0444 /usr/local/share/nemoclaw/node-tar-inventory.json

# Verify the immutable security package inventory in the completed image.
# hadolint ignore=DL4006
RUN set -eu; \
security_inventory=/usr/local/share/nemoclaw/security-packages.txt; \
arch="$(dpkg --print-architecture)"; \
test -f "$security_inventory"; \
test ! -L "$security_inventory"; \
test "$(stat -c '%u:%g:%a' "$security_inventory")" = "0:0:444"; \
printf '%s\n' \
"architecture=$arch" \
"libexpat1=2.8.2-1" \
"libonig5=6.9.9-1+b1" \
"libjq1=1.8.2-1" \
"jq=1.8.2-1" \
"vim-common=2:9.2.0782-1" \
"vim-tiny=2:9.2.0782-1" \
| cmp -s - "$security_inventory"; \
test "$(dpkg-query -W -f='${Version}' libexpat1)" = "2.8.2-1"; \
test "$(dpkg-query -W -f='${Version}' libonig5)" = "6.9.9-1+b1"; \
test "$(dpkg-query -W -f='${Version}' libjq1)" = "1.8.2-1"; \
test "$(dpkg-query -W -f='${Version}' jq)" = "1.8.2-1"; \
test "$(dpkg-query -W -f='${Version}' vim-common)" = "2:9.2.0782-1"; \
test "$(dpkg-query -W -f='${Version}' vim-tiny)" = "2:9.2.0782-1"; \
ldd /usr/bin/jq | grep -Eq 'libonig[.]so[.]5'; \
test "$(jq --version)" = "jq-1.8.2"; \
printf '%s\n' '{"sandbox":"healthy"}' | jq -e '.sandbox == "healthy"' >/dev/null; \
python3 -c "import pyexpat; assert pyexpat.EXPAT_VERSION == 'expat_2.8.2', pyexpat.EXPAT_VERSION"; \
vim.tiny --version | head -n 1 | grep -Eq '^VIM - Vi IMproved 9[.]2 '; \
test -z "$(dpkg --audit)"
# End completed-image security package verification.

# start.sh handles privilege separation: runs as root initially, then drops
# to 'gateway' user via gosu for the agent process. See start.sh.
ENTRYPOINT ["/usr/local/bin/nemoclaw-start"]
Expand Down
Loading
Loading