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
19 changes: 18 additions & 1 deletion agent-base/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# syntax=docker/dockerfile:1.7
FROM ghcr.io/pasturestack/ubuntu-security-backports:v0.1.0@sha256:2742a3d429e4e14bc587f6dedc51ded4246cf90bbaedcd86c67d0885e32a6f2a AS ubuntu_security_backports
FROM ubuntu:26.04@sha256:2260313b31c8c011cd2eebe728008efac1b3982be73eb71348ea2648d2c0e09b AS python-builder

RUN rm -f /usr/bin/pebble
Expand Down Expand Up @@ -69,7 +70,7 @@ ARG VCS_REF
LABEL org.opencontainers.image.source="https://github.com/PastureStack/server" \
org.opencontainers.image.description="PastureStack node-agent compatibility base image." \
org.opencontainers.image.licenses="Apache-2.0" \
org.opencontainers.image.version="v0.3.6" \
org.opencontainers.image.version="v0.3.7" \
org.opencontainers.image.revision="$VCS_REF"

RUN rm -f /usr/bin/pebble
Expand Down Expand Up @@ -147,4 +148,20 @@ RUN rm -f /etc/apt/sources.list /etc/apt/sources.list.d/*.list /etc/apt/sources.
rm /var/run && \
mkdir /var/run

# Only the final runtime consumes these qualified packages. Keep the Python
# compiler stage and the existing coreutils provider unchanged.
RUN --mount=from=ubuntu_security_backports,source=/out,target=/opt/pasturestack/ubuntu-security-backports,ro \
set -eux; \
data=/opt/pasturestack/ubuntu-security-backports; \
evidence=/usr/share/pasturestack/security/ubuntu-backports; \
mkdir -p "$evidence"; \
"$data/install-existing-backports" --install-in-docker-build > "$evidence/install.json"; \
"$data/install-existing-backports" --verify-installed > "$evidence/verify.json"; \
for name in manifest.json qualification.json source-inputs.lock.json openvex.json; do \
install -m 0644 "$data/$name" "$evidence/$name"; \
done; \
printf '%s\n' 'ghcr.io/pasturestack/ubuntu-security-backports:v0.1.0@sha256:2742a3d429e4e14bc587f6dedc51ded4246cf90bbaedcd86c67d0885e32a6f2a' > "$evidence/bundle-image.txt"; \
cd "$evidence"; \
sha256sum manifest.json qualification.json openvex.json source-inputs.lock.json install.json verify.json bundle-image.txt > SHA256SUMS

ENTRYPOINT ["/pasturestack-entrypoint.sh"]
12 changes: 10 additions & 2 deletions agent-base/README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
# Node agent image builds

The compatibility base targets `ghcr.io/pasturestack/node-agent-base:v0.3.6`.
The compatibility base targets `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;
the runtime verifies the patched OpenSSL, curl, libc and CA package versions.
only exact qualified security-backport versions may differ in the final runtime.
The Python compiler stage keeps its original libc. The runtime verifies the
patched OpenSSL, curl, libc and CA package versions.

The digest-pinned, build-only Ubuntu backport bundle updates only already
installed packages, preserving the existing coreutils provider and package set.
Its source/native-test qualification, package manifest and installation receipt
are retained under `/usr/share/pasturestack/security/ubuntu-backports/`.
The bundle and installer are read-only build mounts, not runtime dependencies.

The shared `tools/pip-security-overlay` helper builds the clearly identified
downstream wheel `pip 26.2.2+pasturestack.1` from pinned official inputs using
Expand Down
2 changes: 1 addition & 1 deletion agent-base/build-image.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ set -euo pipefail

cd "$(dirname "$0")"

IMAGE=${IMAGE:-ghcr.io/pasturestack/node-agent-base:v0.3.6}
IMAGE=${IMAGE:-ghcr.io/pasturestack/node-agent-base:v0.3.7}

SHARE_MNT_BIN="${SHARE_MNT_BIN:-}"
if [ -z "$SHARE_MNT_BIN" ] || [ ! -f "$SHARE_MNT_BIN" ]; then
Expand Down
82 changes: 60 additions & 22 deletions scripts/build-agent-release.sh
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,23 @@ test "$source_sha" = "$GITHUB_SHA"
evidence=${EVIDENCE_DIR:?EVIDENCE_DIR is required}
mkdir -p "$evidence"
printf '%s\n' "$source_sha" >"$evidence/source-revision.txt"
python3 scripts/test-agent-package-consistency.py

case "${COMPONENT:-}" in
base) repository=ghcr.io/pasturestack/node-agent-base; version=v0.3.6 ;;
base) repository=ghcr.io/pasturestack/node-agent-base; version=v0.3.7 ;;
agent) repository=ghcr.io/pasturestack/node-agent; version=v1.2.32 ;;
*) echo 'Unsupported agent component' >&2; exit 1 ;;
esac
[[ "${PUBLISH:-false}" = true || "${PUBLISH:-false}" = false ]]
target="$repository:$version"
candidate="local/pasturestack-node-agent-${COMPONENT}:${GITHUB_RUN_ID}"
helper_container="pasturestack-agent-helper-${GITHUB_RUN_ID}"
backports_container=
bundle_container=
scratch=$(mktemp -d)
cleanup() {
if [[ -n "$backports_container" ]]; then docker rm -v "$backports_container" >/dev/null 2>&1 || true; fi
if [[ -n "$bundle_container" ]]; then docker rm -v "$bundle_container" >/dev/null 2>&1 || true; fi
docker rm -v "$helper_container" >/dev/null 2>&1 || true
docker logout ghcr.io >/dev/null 2>&1 || true
rm -rf "$scratch"
Expand Down Expand Up @@ -76,23 +81,32 @@ 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"
python3 - "$evidence" <<'PY'
from pathlib import Path
import json, sys
root = Path(sys.argv[1])
def packages(name):
return dict(line.split('\t', 1) for line in (root / name).read_text().splitlines())
builder, runtime = packages('builder-packages.tsv'), packages('runtime-packages.tsv')
common = builder.keys() & runtime.keys()
assert all(builder[name] == runtime[name] for name in common), 'builder/runtime package drift'
for package, version in {'libssl3t64:amd64': '3.5.5-1ubuntu3.7', 'curl': '8.18.0-1ubuntu2.8',
'libc6:amd64': '2.43-2ubuntu2.4', 'ca-certificates': '20260816ubuntu0.26.04.1'}.items():
assert runtime.get(package) == version, (package, runtime.get(package), version)
report = {'builder_packages': len(builder), 'runtime_packages': len(runtime),
'common_packages_same_versions': len(common), 'expected_security_packages': True}
(root / 'package-comparison.json').write_text(json.dumps(report, indent=2) + '\n')
print(json.dumps(report))
PY
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
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 @@ -121,14 +135,38 @@ docker run --rm --entrypoint python3 "$candidate" -c \

trivy=ghcr.io/aquasecurity/trivy:0.74.0@sha256:62b1e65e8869bc4b4c6aa4fa2b21595256c7c2f6018a9d9ad61caf87187c1969
docker pull "$trivy"
mkdir -p "$scratch/trivy-cache"
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
-v "$evidence:/evidence" "$trivy" image --format cyclonedx \
-v "$scratch/trivy-cache:/root/.cache/trivy" -v "$evidence:/evidence" "$trivy" image --format cyclonedx \
--output /evidence/image.cdx.json "$candidate"
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
-v "$evidence:/evidence" "$trivy" image --scanners vuln,secret --list-all-pkgs --format json \
-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"
bundle_container=$(docker create --entrypoint /unused "$backports_image")
docker cp "$bundle_container:/out/verify-consumer-evidence.py" "$scratch/verify-consumer-evidence.py"
docker cp "$bundle_container:/out/SHA256SUMS" "$scratch/backports-SHA256SUMS"
docker rm "$bundle_container" >/dev/null
bundle_container=
python3 "$scratch/verify-consumer-evidence.py" --evidence-dir "$evidence/ubuntu-backports" \
--sbom "$evidence/image.cdx.json" --expected-bundle-digest "${backports_image##*@}" \
--bundle-checksums "$scratch/backports-SHA256SUMS" \
>"$evidence/ubuntu-backports-consumer-proof.json"
# Preserve the original full scan. Native Trivy applies only the qualified,
# exact-package OpenVEX, followed by the existing pip-vendor interpretation.
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 --skip-db-update --skip-vex-repo-update \
--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 "$evidence/image-security.json" --proof "$evidence/pip-vendor-slice.json" \
--raw "$pip_scan_input" --proof "$evidence/pip-vendor-slice.json" \
--output "$evidence/image-security-effective.json" --vex "$evidence/image.vex.json" \
--subject "urn:sha256:${local_id#sha256:}"
python3 - "$evidence" <<'PY'
Expand Down Expand Up @@ -164,7 +202,7 @@ if [[ "$PUBLISH" = true ]]; then
docker pull "$repository@$digest"
test "$(docker image inspect "$repository@$digest" --format '{{.Id}}')" = "$local_id"
python3 tools/pip-security-overlay/filter-pip-vendor-findings.py \
--raw "$evidence/image-security.json" --proof "$evidence/pip-vendor-slice.json" \
--raw "$pip_scan_input" --proof "$evidence/pip-vendor-slice.json" \
--output "$evidence/image-security-effective.json" --vex "$evidence/image.vex.json" \
--subject "https://ghcr.io/v2/${repository#ghcr.io/}/manifests/$digest"
printf 'repository=%s\nversion=%s\nsource=%s\ndigest=%s\nimage_id=%s\n' \
Expand Down
89 changes: 89 additions & 0 deletions scripts/check-agent-package-consistency.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
#!/usr/bin/env python3
"""Compare the real Python builder/runtime inventories and qualified deltas."""
import hashlib
import json
import sys


def require(value, message):
if not value:
raise ValueError(message)


def inventory(text):
require(isinstance(text, str), 'Package inventory must be text')
rows = [line.split('\t') for line in text.splitlines()]
require(all(len(row) == 2 for row in rows), 'Malformed package inventory')
result = {name.split(':', 1)[0]: version for name, version in rows}
require(len(result) == len(rows), 'Duplicate/multiarch package inventory')
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'])
manifest_bytes = data['backports-manifest.json'].encode('utf-8')
manifest = json.loads(manifest_bytes)
qualification_bytes = data['backports-qualification.json'].encode('utf-8')
qualification = json.loads(qualification_bytes)
receipt = json.loads(data['backports-install.json'])
require(manifest['schemaVersion'] == 1 and manifest['bundleVersion'] == '0.1.0'
and manifest['platform'] == {'distribution': 'ubuntu', 'release': '26.04', 'architecture': 'amd64'},
'Unexpected backport manifest identity')
require(manifest['qualification']['sha256'] == hashlib.sha256(qualification_bytes).hexdigest(),
'Backport qualification checksum mismatch')
fixtures = qualification['consumerFixtures']
require(qualification['status'] == 'qualified' and len(fixtures) == 2
and {item['provider'] for item in fixtures} == {'gnu', 'uutils'}
and all(item['status'] == 'PASS' for item in fixtures),
'Backport consumer qualification is incomplete')
allowed = {'libc6', 'libc-bin', 'libc-gconv-modules-extra', 'libpcre2-8-0', 'libpcre2-posix3',
'rust-coreutils', 'jq', 'libjq1', 'p11-kit', 'p11-kit-modules', 'libp11-kit0', 'libxrender1'}
packages = manifest['packages']
qualified = {p['name']: p['version'] for p in packages}
require(len(packages) == len(qualified) == 12 and set(qualified) == allowed,
'Unexpected qualified package catalog')
require(receipt['schemaVersion'] == 1 and receipt['bundleVersion'] == '0.1.0'
and receipt['status'] == 'installed-and-verified'
and receipt['providerPreserved'] is True and receipt['configurationPreserved'] is True,
'Runtime installation did not preserve its provider/configuration')
selected = set(receipt['packages'])
require(len(selected) == len(receipt['packages']) and selected == allowed & runtime.keys(),
'Installation receipt does not match the existing runtime package selection')
for name in selected:
require(runtime[name] == qualified[name], 'Unqualified runtime version: ' + name)
common = builder.keys() & runtime.keys()
changed = {name for name in common if builder[name] != runtime[name]}
require(changed <= selected, 'Unrelated builder/runtime package drift: ' + ', '.join(sorted(changed - selected)))
# Keep the original compiler inputs. Only final-runtime library versions
# may differ, and only by the exact qualified manifest above.
require(builder.get('libc6') == '2.43-2ubuntu2.4', 'Unexpected Python-builder libc version')
expected = {'libssl3t64': '3.5.5-1ubuntu3.7', 'curl': '8.18.0-1ubuntu2.8',
'ca-certificates': '20260816ubuntu0.26.04.1'}
for name, version in expected.items():
require(runtime.get(name) == version, 'Unexpected unchanged security package: ' + name)
return {'builder_packages': len(builder), 'runtime_packages': len(runtime),
'common_packages_same_versions': len(common - changed),
'qualified_runtime_deltas': sorted(changed), 'expected_security_packages': True,
'backport_manifest_sha256': hashlib.sha256(manifest_bytes).hexdigest()}


if __name__ == '__main__':
require(sys.argv[1:] in ([], ['--legacy-wrapper']), 'Unsupported arguments')
data = json.load(sys.stdin)
report = verify_legacy_wrapper(data) if sys.argv[1:] else verify(data)
print(json.dumps(report, indent=2))
Loading