Skip to content
Merged
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
21 changes: 10 additions & 11 deletions agent-base/README.md
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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.

Expand All @@ -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.
6 changes: 3 additions & 3 deletions agent/Dockerfile
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
52 changes: 20 additions & 32 deletions scripts/build-agent-release.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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 ]]
Expand Down Expand Up @@ -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

Expand All @@ -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"
Expand Down Expand Up @@ -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"
Expand All @@ -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" \
Expand Down
18 changes: 2 additions & 16 deletions scripts/check-agent-package-consistency.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'])
Expand Down Expand Up @@ -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))
28 changes: 16 additions & 12 deletions scripts/test-agent-package-consistency.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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()
Expand Down