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

RUN rm -f /usr/bin/pebble
Expand Down Expand Up @@ -70,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.7" \
org.opencontainers.image.version="v0.3.8" \
org.opencontainers.image.revision="$VCS_REF"

RUN rm -f /usr/bin/pebble
Expand Down Expand Up @@ -160,7 +160,7 @@ RUN --mount=from=ubuntu_security_backports,source=/out,target=/opt/pasturestack/
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"; \
printf '%s\n' 'ghcr.io/pasturestack/ubuntu-security-backports:v0.1.1@sha256:45d6b507b63a18459ae5cf76afd4d9731f8ef43b9c40e4c3dbb01899e1752b19' > "$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

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.7}
IMAGE=${IMAGE:-ghcr.io/pasturestack/node-agent-base:v0.3.8}

SHARE_MNT_BIN="${SHARE_MNT_BIN:-}"
if [ -z "$SHARE_MNT_BIN" ] || [ ! -f "$SHARE_MNT_BIN" ]; then
Expand Down
19 changes: 16 additions & 3 deletions scripts/build-agent-release.sh
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ 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.7 ;;
base) repository=ghcr.io/pasturestack/node-agent-base; version=v0.3.8 ;;
agent) repository=ghcr.io/pasturestack/node-agent; version=v1.2.33 ;;
*) echo 'Unsupported agent component' >&2; exit 1 ;;
esac
Expand Down Expand Up @@ -113,6 +113,19 @@ 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"
docker run --rm --network none --entrypoint python3 \
-e PYTHONDONTWRITEBYTECODE=1 -v "$PWD:/source:ro" -w /source "$candidate" \
scripts/check-agent-brotli-runtime.py >"$evidence/brotli-runtime.json"
entry_args=()
if [[ "$COMPONENT" = agent ]]; then entry_args=(--); fi
docker run --rm --network none "$candidate" "${entry_args[@]}" /bin/bash -ec \
'test "$(python3 --version)" = "Python 3.14.9"; curl --version; printf "AGENT_ENTRYPOINT_EXEC_OK\n"' \
>"$evidence/entrypoint-smoke.txt"
if [[ "$COMPONENT" = agent ]]; then
docker run --rm --network none --entrypoint bash \
-v "$PWD:/source:ro" -w /source "$candidate" \
scripts/check-agent-run-downloads.sh >"$evidence/bootstrap-download-smoke.txt"
fi
docker run --rm --network none --entrypoint /bin/bash "$candidate" -ec \
'test "$(dpkg-query -W ca-certificates | cut -f2)" = 20260816ubuntu0.26.04.1; docker --version | grep -Fx "Docker version 29.9.2, build a82e528"; printf "6c71cfe71faff949a602946a4df4799b563232e79d0b8b6fd343d0ef081c9621 /usr/bin/docker\n" | sha256sum -c -; test -s /usr/bin/share-mnt; test -x /usr/bin/r' \
|& tee "$evidence/runtime-tools.txt"
Expand Down Expand Up @@ -145,8 +158,8 @@ 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"
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}$ ]]
backports_image=$(sed -n 's/^FROM \(ghcr.io\/pasturestack\/ubuntu-security-backports:v0\.1\.1@sha256:[0-9a-f]\{64\}\) AS ubuntu_security_backports$/\1/p' agent-base/Dockerfile)
[[ "$backports_image" =~ ^ghcr.io/pasturestack/ubuntu-security-backports:v0\.1\.1@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"
Expand Down
6 changes: 3 additions & 3 deletions scripts/check-agent-base-downloads.sh
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ if ! bash -n agent-base/build-image.sh; then
fail "AGENT_BASE_BUILD_SCRIPT_SYNTAX_INVALID"
fi

require_marker agent-base/Dockerfile 'org.opencontainers.image.version="v0.3.7"' AGENT_BASE_VERSION_NOT_ROLLED_FORWARD
require_marker agent-base/Dockerfile 'org.opencontainers.image.version="v0.3.8"' AGENT_BASE_VERSION_NOT_ROLLED_FORWARD
require_marker agent-base/Dockerfile 'COPY ./share-mnt /usr/bin/share-mnt' AGENT_BASE_SHARE_MNT_NOT_CONTEXT_BACKED
require_marker agent-base/Dockerfile 'COPY ./r /usr/bin/r' AGENT_BASE_NETWORK_HELPER_NOT_CONTEXT_BACKED
require_marker agent-base/Dockerfile 'COPY ./update-platform-ssl /usr/bin/update-platform-ssl' AGENT_BASE_SSL_SCRIPT_NOT_CONTEXT_BACKED
Expand All @@ -52,7 +52,7 @@ require_marker agent-base/Dockerfile 'echo "${DOCKER_SHA256} $docker_tgz" | sha
require_marker agent-base/Dockerfile 'tar xzf "$docker_tgz" -C /usr/bin --strip-components=1 docker/docker' AGENT_BASE_DOCKER_EXTRACT_NOT_FILE_BACKED
require_marker agent-base/Dockerfile 'echo "${DOCKER_BINARY_SHA256} /usr/bin/docker" | sha256sum -c -' AGENT_BASE_DOCKER_BINARY_SHA_NOT_CHECKED
require_marker agent-base/Dockerfile 'rm -f "$docker_tgz"' AGENT_BASE_DOCKER_TMP_NOT_REMOVED
require_marker agent-base/build-image.sh 'IMAGE=${IMAGE:-ghcr.io/pasturestack/node-agent-base:v0.3.7}' AGENT_BASE_BUILD_DEFAULT_IMAGE_NOT_UPDATED
require_marker agent-base/build-image.sh 'IMAGE=${IMAGE:-ghcr.io/pasturestack/node-agent-base:v0.3.8}' AGENT_BASE_BUILD_DEFAULT_IMAGE_NOT_UPDATED
require_marker agent-base/build-image.sh 'SHARE_MNT_BIN="${SHARE_MNT_BIN:-}"' AGENT_BASE_BUILD_SHARE_MNT_INPUT_MISSING
require_marker agent-base/build-image.sh 'Set SHARE_MNT_BIN to the reviewed mount-propagation binary path before building $IMAGE' AGENT_BASE_BUILD_SHARE_MNT_FAIL_CLOSED_MISSING
require_marker agent-base/build-image.sh 'cp "$SHARE_MNT_BIN" "$tmpdir/share-mnt"' AGENT_BASE_BUILD_SHARE_MNT_CONTEXT_COPY_MISSING
Expand Down Expand Up @@ -95,4 +95,4 @@ if [ "$failure_count" -ne 0 ]; then
exit 1
fi

printf 'AGENT_BASE_DOWNLOADS_OK base=v0.3.7 agent=v1.2.33 base_share_mnt_nonempty=1 tracked_loglevel=1 docker_cli=29.9.2 python_sha=1 docker_sha=1 remote_legacy_helpers=0\n'
printf 'AGENT_BASE_DOWNLOADS_OK base=v0.3.8 agent=v1.2.33 base_share_mnt_nonempty=1 tracked_loglevel=1 docker_cli=29.9.2 python_sha=1 docker_sha=1 remote_legacy_helpers=0\n'
37 changes: 37 additions & 0 deletions scripts/check-agent-brotli-runtime.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
#!/usr/bin/env python3
"""Check the installed qualified Brotli library with ordinary valid inputs."""
import ctypes
import json
from pathlib import Path
import subprocess


manifest = json.loads(Path('/usr/share/pasturestack/security/ubuntu-backports/manifest.json').read_text())
expected = [package['version'] for package in manifest['packages'] if package['name'] == 'libbrotli1']
assert len(expected) == 1, 'Qualified Brotli package is absent or ambiguous'
actual = subprocess.check_output(['dpkg-query', '-W', '-f=${Version}', 'libbrotli1'], text=True)
assert actual == expected[0], 'Installed Brotli does not match the qualified package'
encoder = ctypes.CDLL('libbrotlienc.so.1')
decoder = ctypes.CDLL('libbrotlidec.so.1')
size = ctypes.c_size_t
encoder.BrotliEncoderMaxCompressedSize.argtypes = [size]
encoder.BrotliEncoderMaxCompressedSize.restype = size
encoder.BrotliEncoderCompress.argtypes = [ctypes.c_int, ctypes.c_int, ctypes.c_int,
size, ctypes.c_void_p, ctypes.POINTER(size), ctypes.c_void_p]
encoder.BrotliEncoderCompress.restype = ctypes.c_int
decoder.BrotliDecoderDecompress.argtypes = [size, ctypes.c_void_p, ctypes.POINTER(size), ctypes.c_void_p]
decoder.BrotliDecoderDecompress.restype = ctypes.c_int
for content in (b'', b'PastureStack ordinary Brotli runtime control\n' * 32, bytes(range(256))):
encoded_size = size(encoder.BrotliEncoderMaxCompressedSize(len(content)))
encoded = ctypes.create_string_buffer(max(1, encoded_size.value))
assert encoder.BrotliEncoderCompress(5, 22, 0, len(content), content,
ctypes.byref(encoded_size), encoded) == 1
decoded_size = size(max(1, len(content)))
decoded = ctypes.create_string_buffer(decoded_size.value)
assert decoder.BrotliDecoderDecompress(encoded_size.value, encoded,
ctypes.byref(decoded_size), decoded) == 1
assert decoded.raw[:decoded_size.value] == content
print(json.dumps({'status': 'PASS', 'package': 'libbrotli1', 'version': actual,
'bundleVersion': manifest['bundleVersion'], 'ordinaryRoundTrips': 3,
'publicSonameControls': ['libbrotlienc.so.1', 'libbrotlidec.so.1'],
'securityTriggerExecuted': False}, sort_keys=True))
9 changes: 5 additions & 4 deletions scripts/check-agent-package-consistency.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ def verify(data):
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'
require(manifest['schemaVersion'] == 1 and manifest['bundleVersion'] == '0.1.1'
and manifest['platform'] == {'distribution': 'ubuntu', 'release': '26.04', 'architecture': 'amd64'},
'Unexpected backport manifest identity')
require(manifest['qualification']['sha256'] == hashlib.sha256(qualification_bytes).hexdigest(),
Expand All @@ -38,12 +38,13 @@ def verify(data):
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'}
'rust-coreutils', 'jq', 'libjq1', 'p11-kit', 'p11-kit-modules', 'libp11-kit0', 'libxrender1',
'libbrotli1'}
packages = manifest['packages']
qualified = {p['name']: p['version'] for p in packages}
require(len(packages) == len(qualified) == 12 and set(qualified) == allowed,
require(len(packages) == len(qualified) == 13 and set(qualified) == allowed,
'Unexpected qualified package catalog')
require(receipt['schemaVersion'] == 1 and receipt['bundleVersion'] == '0.1.0'
require(receipt['schemaVersion'] == 1 and receipt['bundleVersion'] == '0.1.1'
and receipt['status'] == 'installed-and-verified'
and receipt['providerPreserved'] is True and receipt['configurationPreserved'] is True,
'Runtime installation did not preserve its provider/configuration')
Expand Down
19 changes: 13 additions & 6 deletions scripts/test-agent-package-consistency.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,20 +23,23 @@ def setUp(self):
'rust-coreutils': '0.10.0-1ubuntu2~26.04.1+pasturestack.1', 'jq': '1.8.2-0pasturestack1~26.04.1',
'libjq1': '1.8.2-0pasturestack1~26.04.1', 'p11-kit': '0.26.2-2+pasturestack.1',
'p11-kit-modules': '0.26.2-2+pasturestack.1', 'libp11-kit0': '0.26.2-2+pasturestack.1',
'libxrender1': '1:0.9.12-1build1+pasturestack.1'}
'libxrender1': '1:0.9.12-1build1+pasturestack.1',
'libbrotli1': '1.2.0-3build1+pasturestack.1'}
self.builder = {'libc6': '2.43-2ubuntu2.4', 'libpcre2-8-0': '10.46-1build1',
'libssl3t64': '3.5.5-1ubuntu3.7', 'curl': '8.18.0-1ubuntu2.8',
'ca-certificates': '20260816ubuntu0.26.04.1', 'zlib1g': '1.3.1'}
'ca-certificates': '20260816ubuntu0.26.04.1', 'zlib1g': '1.3.1',
'libbrotli1': '1.2.0-3build1'}
self.runtime = dict(self.builder, libc6=self.versions['libc6'])
self.runtime['libpcre2-8-0'] = self.versions['libpcre2-8-0']
self.receipt = {'schemaVersion': 1, 'bundleVersion': '0.1.0', 'status': 'installed-and-verified',
self.runtime['libbrotli1'] = self.versions['libbrotli1']
self.receipt = {'schemaVersion': 1, 'bundleVersion': '0.1.1', 'status': 'installed-and-verified',
'providerPreserved': True, 'configurationPreserved': True,
'packages': ['libc6', 'libpcre2-8-0']}
'packages': ['libc6', 'libpcre2-8-0', 'libbrotli1']}
qualification = {'status': 'qualified', 'consumerFixtures': [
{'provider': 'gnu', 'status': 'PASS'}, {'provider': 'uutils', 'status': 'PASS'}]}
content = (json.dumps(qualification) + '\n').encode()
self.data['backports-qualification.json'] = content.decode('utf-8')
self.manifest = {'schemaVersion': 1, 'bundleVersion': '0.1.0',
self.manifest = {'schemaVersion': 1, 'bundleVersion': '0.1.1',
'platform': {'distribution': 'ubuntu', 'release': '26.04', 'architecture': 'amd64'},
'qualification': {'sha256': hashlib.sha256(content).hexdigest()},
'packages': [{'name': name, 'version': version} for name, version in self.versions.items()]}
Expand All @@ -49,7 +52,7 @@ def verify(self):
return module.verify(self.data)

def test_only_qualified_final_runtime_differences_pass(self):
self.assertEqual(self.verify()['qualified_runtime_deltas'], ['libc6', 'libpcre2-8-0'])
self.assertEqual(self.verify()['qualified_runtime_deltas'], ['libbrotli1', 'libc6', 'libpcre2-8-0'])
self.assertEqual(self.builder['libc6'], '2.43-2ubuntu2.4')

def test_unrelated_drift_is_rejected(self):
Expand All @@ -60,6 +63,10 @@ def test_nonqualified_library_version_is_rejected(self):
self.runtime['libc6'] = '2.44'
with self.assertRaisesRegex(ValueError, 'Unqualified'): self.verify()

def test_unpatched_brotli_is_rejected(self):
self.runtime['libbrotli1'] = self.builder['libbrotli1']
with self.assertRaisesRegex(ValueError, 'Unqualified runtime version: libbrotli1'): self.verify()

def test_builder_changes_are_not_a_workaround(self):
self.builder['libc6'] = self.versions['libc6']
with self.assertRaisesRegex(ValueError, 'Python-builder'): self.verify()
Expand Down