diff --git a/agent-base/README.md b/agent-base/README.md index 6a65bc98f2..265231637c 100644 --- a/agent-base/README.md +++ b/agent-base/README.md @@ -1,6 +1,6 @@ # Node agent image builds -The compatibility base targets `ghcr.io/pasturestack/node-agent-base:v0.3.7`. +The compatibility base is `ghcr.io/pasturestack/node-agent-base:v0.3.7`. It builds Python 3.14.9 from the official SHA-256-verified archive, retains Docker CLI 29.9.2 (official Go 1.26.10 build), and uses the fixed Ubuntu 26.04 snapshot `20261023T000000Z`. Builder and runtime package inventories are compared; @@ -22,7 +22,7 @@ ensurepip wheel are updated, and a fresh venv runs the same streaming/TLS tests. This repack is not an official PyPA pip release. `release-inputs.env` pins the published mount-propagation 1.0.12 binary and -the existing `r` helper from the previous immutable base. The helper is +the `r` helper from its pinned immutable image input. The helper is checked by SHA-256 before entering the build context. No running host or existing image is patched in place. @@ -34,13 +34,12 @@ findings fail the gate; vendor-unfixed Medium/Low findings remain recorded. Publication then pushes that same image, refuses an existing version tag, and verifies the anonymous digest readback and original image identity. -After the workflow is reviewed and merged, publish the base with: +The node-agent wrapper is version `v1.2.33` and inherits this base through +its digest-pinned Dockerfile. Both `base` and `agent` components use the same +qualified runtime-package receipts, native OpenVEX guard, raw/effective scans, +runtime checks and publication gate. -```sh -gh workflow run publish-node-agent.yml --ref main -f component=base -f publish=true -``` - -The agent `v1.2.32` stage is enabled only after its Dockerfile pins the new -base digest. It runs the same image checks and publication gate with -`component=agent`. Image digests and proof of publication come from the -completed workflow evidence, not from this source description. +For a new version, update the reviewed source version and pins before +dispatching the protected workflow with `component=base` or `component=agent` +and `publish=true`. Existing numeric version tags are rejected. Completed +workflow evidence records each published digest and source revision. diff --git a/agent/Dockerfile b/agent/Dockerfile index 12053257a4..1e3c77c4b0 100644 --- a/agent/Dockerfile +++ b/agent/Dockerfile @@ -1,4 +1,4 @@ -FROM ghcr.io/pasturestack/node-agent-base:v0.3.6@sha256:1179c549a501551941c3435969ecbd5d6bacc1d9e327895c3f75b9019b461ec9 +FROM ghcr.io/pasturestack/node-agent-base:v0.3.7@sha256:b21453fae656ba81cb427e5f165fd07aff8daee2459d9c4902e200652e9c9632 ARG VCS_REF COPY control_plane_url.py register.py resolve_url.py run.sh / COPY loglevel /usr/bin/loglevel @@ -8,9 +8,9 @@ LABEL "io.rancher.container.system"="rancher-agent" \ org.opencontainers.image.source="https://github.com/PastureStack/server" \ org.opencontainers.image.description="PastureStack node-agent compatibility image." \ org.opencontainers.image.licenses="Apache-2.0" \ - org.opencontainers.image.version="v1.2.32" \ + org.opencontainers.image.version="v1.2.33" \ org.opencontainers.image.revision=$VCS_REF RUN test -s /usr/bin/share-mnt && \ chmod +x /run.sh /register.py /resolve_url.py /usr/bin/share-mnt /usr/bin/loglevel && \ chmod 0644 /control_plane_url.py -ENV RANCHER_AGENT_IMAGE=ghcr.io/pasturestack/node-agent:v1.2.32 +ENV RANCHER_AGENT_IMAGE=ghcr.io/pasturestack/node-agent:v1.2.33 diff --git a/scripts/build-agent-release.sh b/scripts/build-agent-release.sh index bf2ab62f3c..469402aad2 100644 --- a/scripts/build-agent-release.sh +++ b/scripts/build-agent-release.sh @@ -14,7 +14,7 @@ python3 scripts/test-agent-package-consistency.py case "${COMPONENT:-}" in base) repository=ghcr.io/pasturestack/node-agent-base; version=v0.3.7 ;; - agent) repository=ghcr.io/pasturestack/node-agent; version=v1.2.32 ;; + agent) repository=ghcr.io/pasturestack/node-agent; version=v1.2.33 ;; *) echo 'Unsupported agent component' >&2; exit 1 ;; esac [[ "${PUBLISH:-false}" = true || "${PUBLISH:-false}" = false ]] @@ -80,8 +80,8 @@ if [[ "$COMPONENT" = base ]]; then SHARE_MNT_BIN="$scratch/share-mnt" R_BIN="$scratch/r" IMAGE="$candidate" \ bash agent-base/build-image.sh |& tee "$evidence/build.log" else - grep -Eq '^FROM ghcr.io/pasturestack/node-agent-base:v0\.3\.6@sha256:[0-9a-f]{64}$' agent/Dockerfile - grep -Fq 'org.opencontainers.image.version="v1.2.32"' agent/Dockerfile + grep -Eq '^FROM ghcr.io/pasturestack/node-agent-base:v0\.3\.7@sha256:[0-9a-f]{64}$' agent/Dockerfile + grep -Fq 'org.opencontainers.image.version="v1.2.33"' agent/Dockerfile IMAGE="$candidate" bash agent/build-image.sh |& tee "$evidence/build.log" fi @@ -93,32 +93,23 @@ docker run --rm --network none --entrypoint cat "$candidate" \ /usr/share/pasturestack/python-builder-packages.tsv >"$evidence/builder-packages.tsv" docker run --rm --network none --entrypoint dpkg-query "$candidate" \ -W -f='${binary:Package}\t${Version}\n' >"$evidence/runtime-packages.tsv" -if [[ "$COMPONENT" = base ]]; then - for name in manifest qualification install; do - docker run --rm --network none --entrypoint cat "$candidate" \ - "/usr/share/pasturestack/security/ubuntu-backports/${name}.json" >"$evidence/backports-${name}.json" - done - jq -n --rawfile builder "$evidence/builder-packages.tsv" \ - --rawfile runtime "$evidence/runtime-packages.tsv" \ - --rawfile manifest "$evidence/backports-manifest.json" \ - --rawfile qualification "$evidence/backports-qualification.json" \ - --rawfile installation "$evidence/backports-install.json" \ - '{"builder-packages.tsv":$builder,"runtime-packages.tsv":$runtime, - "backports-manifest.json":$manifest,"backports-qualification.json":$qualification, - "backports-install.json":$installation}' \ - | python3 scripts/check-agent-package-consistency.py >"$evidence/package-comparison.json" - backports_container=$(docker create "$candidate") - docker cp "$backports_container:/usr/share/pasturestack/security/ubuntu-backports" "$evidence/ubuntu-backports" - docker rm -v "$backports_container" >/dev/null - backports_container= -else - # The wrapper remains pinned to the published 0.3.6 base until 0.3.7 passes - # publication. Its existing strict checks must continue to work meanwhile. - jq -n --rawfile builder "$evidence/builder-packages.tsv" \ - --rawfile runtime "$evidence/runtime-packages.tsv" \ - '{"builder-packages.tsv":$builder,"runtime-packages.tsv":$runtime}' \ - | python3 scripts/check-agent-package-consistency.py --legacy-wrapper >"$evidence/package-comparison.json" -fi +for name in manifest qualification install; do + docker run --rm --network none --entrypoint cat "$candidate" \ + "/usr/share/pasturestack/security/ubuntu-backports/${name}.json" >"$evidence/backports-${name}.json" +done +jq -n --rawfile builder "$evidence/builder-packages.tsv" \ + --rawfile runtime "$evidence/runtime-packages.tsv" \ + --rawfile manifest "$evidence/backports-manifest.json" \ + --rawfile qualification "$evidence/backports-qualification.json" \ + --rawfile installation "$evidence/backports-install.json" \ + '{"builder-packages.tsv":$builder,"runtime-packages.tsv":$runtime, + "backports-manifest.json":$manifest,"backports-qualification.json":$qualification, + "backports-install.json":$installation}' \ + | python3 scripts/check-agent-package-consistency.py >"$evidence/package-comparison.json" +backports_container=$(docker create "$candidate") +docker cp "$backports_container:/usr/share/pasturestack/security/ubuntu-backports" "$evidence/ubuntu-backports" +docker rm -v "$backports_container" >/dev/null +backports_container= docker run --rm --network none --entrypoint /bin/bash "$candidate" -ec \ 'ldd "$(python3 -c "import _ssl; print(_ssl.__file__)")"; curl --version' \ >"$evidence/python-ssl-linkage.txt" @@ -154,8 +145,6 @@ docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \ docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \ -v "$scratch/trivy-cache:/root/.cache/trivy" -v "$evidence:/evidence" "$trivy" image --scanners vuln,secret --list-all-pkgs --format json \ --output /evidence/image-security.json "$candidate" -pip_scan_input="$evidence/image-security.json" -if [[ "$COMPONENT" = base ]]; then backports_image=$(sed -n 's/^FROM \(ghcr.io\/pasturestack\/ubuntu-security-backports:v0\.1\.0@sha256:[0-9a-f]\{64\}\) AS ubuntu_security_backports$/\1/p' agent-base/Dockerfile) [[ "$backports_image" =~ ^ghcr.io/pasturestack/ubuntu-security-backports:v0\.1\.0@sha256:[0-9a-f]{64}$ ]] docker pull --platform linux/amd64 "$backports_image" @@ -176,7 +165,6 @@ docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \ --vex /evidence/ubuntu-backports/openvex.json --format json \ --output /evidence/image-security-os-effective.json "$candidate" pip_scan_input="$evidence/image-security-os-effective.json" -fi python3 tools/pip-security-overlay/filter-pip-vendor-findings.py \ --raw "$pip_scan_input" --proof "$evidence/pip-vendor-slice.json" \ --sbom "$evidence/image.cdx.json" \ diff --git a/scripts/check-agent-package-consistency.py b/scripts/check-agent-package-consistency.py index 5005566c8e..58632bab4c 100644 --- a/scripts/check-agent-package-consistency.py +++ b/scripts/check-agent-package-consistency.py @@ -19,20 +19,6 @@ def inventory(text): return result -def verify_legacy_wrapper(data): - """Preserve the published 0.3.6 wrapper contract until its new base exists.""" - builder = inventory(data['builder-packages.tsv']) - runtime = inventory(data['runtime-packages.tsv']) - common = builder.keys() & runtime.keys() - require(all(builder[name] == runtime[name] for name in common), 'Legacy wrapper builder/runtime drift') - expected = {'libssl3t64': '3.5.5-1ubuntu3.7', 'curl': '8.18.0-1ubuntu2.8', - 'libc6': '2.43-2ubuntu2.4', 'ca-certificates': '20260816ubuntu0.26.04.1'} - require(all(runtime.get(name) == version for name, version in expected.items()), - 'Unexpected published-wrapper security package') - return {'builder_packages': len(builder), 'runtime_packages': len(runtime), - 'common_packages_same_versions': len(common), 'expected_security_packages': True} - - def verify(data): builder = inventory(data['builder-packages.tsv']) runtime = inventory(data['runtime-packages.tsv']) @@ -83,7 +69,7 @@ def verify(data): if __name__ == '__main__': - require(sys.argv[1:] in ([], ['--legacy-wrapper']), 'Unsupported arguments') + require(not sys.argv[1:], 'Unsupported arguments') data = json.load(sys.stdin) - report = verify_legacy_wrapper(data) if sys.argv[1:] else verify(data) + report = verify(data) print(json.dumps(report, indent=2)) diff --git a/scripts/test-agent-package-consistency.py b/scripts/test-agent-package-consistency.py index 21144e96fe..d2da643018 100644 --- a/scripts/test-agent-package-consistency.py +++ b/scripts/test-agent-package-consistency.py @@ -5,6 +5,9 @@ import json from pathlib import Path import unittest +import subprocess +import sys +import tempfile spec = importlib.util.spec_from_file_location('package_consistency', Path(__file__).with_name('check-agent-package-consistency.py')) module = importlib.util.module_from_spec(spec) @@ -72,18 +75,19 @@ def test_changed_qualification_is_rejected(self): self.data['backports-qualification.json'] = '{}' with self.assertRaisesRegex(ValueError, 'checksum'): self.verify() - def test_published_wrapper_keeps_its_original_strict_contract(self): - self.runtime = dict(self.builder) - self.verify_files_only() - self.assertTrue(module.verify_legacy_wrapper(self.data)['expected_security_packages']) - self.runtime['libc6'] = self.versions['libc6'] - self.verify_files_only() - with self.assertRaisesRegex(ValueError, 'drift'): - module.verify_legacy_wrapper(self.data) - - def verify_files_only(self): - for filename, data in [('builder-packages.tsv', self.builder), ('runtime-packages.tsv', self.runtime)]: - self.data[filename] = ''.join(name + '\t' + version + '\n' for name, version in data.items()) + def test_cli_preserves_stdin_and_rejects_argument_paths(self): + self.verify() + payload = json.dumps(self.data).encode('utf-8') + script = str(Path(__file__).with_name('check-agent-package-consistency.py')) + result = subprocess.run([sys.executable, '-B', script], input=payload, capture_output=True) + self.assertEqual(result.returncode, 0, result.stderr.decode()) + self.assertEqual(json.loads(result.stdout)['backport_manifest_sha256'], + hashlib.sha256(self.data['backports-manifest.json'].encode('utf-8')).hexdigest()) + with tempfile.TemporaryDirectory(prefix='agent-stdin-unit-') as directory: + path = Path(directory) / 'valid-inventory.json'; path.write_bytes(payload) + rejected = subprocess.run([sys.executable, '-B', script, str(path)], input=payload, capture_output=True) + self.assertNotEqual(rejected.returncode, 0) + self.assertIn(b'Unsupported arguments', rejected.stderr) def test_inventory_text_preserves_bytes_and_rejects_malformed_rows(self): result = self.verify()