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
676 changes: 676 additions & 0 deletions .github/runtime-unfixed.json

Large diffs are not rendered by default.

30 changes: 23 additions & 7 deletions .github/workflows/security-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,28 @@ jobs:
set -euo pipefail
test "$(go env GOVERSION)" = go1.27.3
make validate test
python3 -B scripts/test-runtime-vulnerability-policy.py -v
python3 -B tests/test_runtime_security_review.py -v
make build smoke integration
- name: Retain the exact verified runtime
shell: bash
run: |
set -euo pipefail
mkdir -p dist/security-runtime
docker image inspect local/system-image-preloader:v0.3.1 > dist/security-runtime/images.json
docker save local/system-image-preloader:v0.3.1 | gzip -n > dist/security-runtime/images.tar.gz
docker image inspect local/system-image-preloader:v0.3.2 > dist/security-runtime/images.json
container=$(docker create --entrypoint /bin/true local/system-image-preloader:v0.3.2)
trap 'docker rm -f "$container" >/dev/null 2>&1 || true' EXIT
docker cp "$container:/licenses/third-party/security/." dist/security-runtime/
docker cp "$container:/usr/local/bin/docker" dist/security-runtime/docker-cli
docker run --rm --network none --entrypoint sha256sum \
local/system-image-preloader:v0.3.2 /usr/local/bin/docker \
> dist/security-runtime/docker-sha256.txt
python3 -B tests/docker_manifest_digest_proof.py \
> dist/security-runtime/docker-manifest-digest-proof.json
rm dist/security-runtime/docker-cli
docker rm "$container" >/dev/null
trap - EXIT
docker save local/system-image-preloader:v0.3.2 | gzip -n > dist/security-runtime/images.tar.gz
printf '%s\n' "$GITHUB_SHA" > dist/security-runtime/source-revision.txt
(cd dist/security-runtime && sha256sum images.json images.tar.gz source-revision.txt > SHA256SUMS)
- name: Scan the exact runtime and generate its SBOM
Expand All @@ -51,13 +65,15 @@ jobs:
set -euo pipefail
scanner=aquasec/trivy:0.74.0@sha256:62b1e65e8869bc4b4c6aa4fa2b21595256c7c2f6018a9d9ad61caf87187c1969
docker pull "$scanner"
for runtime in local/system-image-preloader:v0.3.1; do
for runtime in local/system-image-preloader:v0.3.2; do
name=${runtime##*/}; name=${name%%:*}
docker save "$runtime" --output "$RUNNER_TEMP/$name.tar"
docker run --rm -v "$RUNNER_TEMP/$name.tar:/image.tar:ro" -v "$PWD/dist/security-runtime:/evidence" "$scanner" image --input /image.tar --scanners vuln,secret --severity HIGH,CRITICAL --exit-code 1 --format json --output "/evidence/$name-security.json"
docker run --rm -v "$RUNNER_TEMP/$name.tar:/image.tar:ro" -v "$PWD/dist/security-runtime:/evidence" "$scanner" image --input /image.tar --scanners vuln,secret --list-all-pkgs --format json --output "/evidence/$name-security.json"
docker run --rm -v "$RUNNER_TEMP/$name.tar:/image.tar:ro" -v "$PWD/dist/security-runtime:/evidence" "$scanner" image --input /image.tar --format cyclonedx --output "/evidence/$name-sbom.cdx.json"
done
(cd dist/security-runtime && sha256sum -- *.json images.tar.gz source-revision.txt > SHA256SUMS)
python3 -B scripts/review-runtime-security.py
python3 -B scripts/enforce-runtime-vulnerabilities.py --phase runtime
(cd dist/security-runtime && sha256sum -- *.json *.txt images.tar.gz > SHA256SUMS)
- name: Upload verified runtime and checksums
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
Expand All @@ -78,7 +94,7 @@ jobs:
packages: write
env:
GH_TOKEN: ${{ github.token }}
RELEASE_TAG: v0.3.1
RELEASE_TAG: v0.3.2
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
Expand All @@ -97,7 +113,7 @@ jobs:
printf '%s' "$GH_TOKEN" | docker login ghcr.io -u "$GITHUB_ACTOR" --password-stdin
trap 'docker logout ghcr.io >/dev/null 2>&1 || true' EXIT
: > dist/security-runtime/registry-images.jsonl
local_image='local/system-image-preloader:v0.3.1'
local_image='local/system-image-preloader:v0.3.2'
remote_image="ghcr.io/pasturestack/system-image-preloader:$RELEASE_TAG"
if docker manifest inspect "$remote_image" >/dev/null 2>"$RUNNER_TEMP/manifest-error"; then
echo "Refusing to replace an existing immutable image tag: $remote_image" >&2; exit 1
Expand Down
47 changes: 20 additions & 27 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ ARG YQ_VERSION=v4.53.3
ARG YQ_GIT_COMMIT=1b9b4ac5187171d2e5e3129be0cfa827c7f9d53d
ARG GOMPLATE_VERSION=v5.1.0
ARG GOMPLATE_GIT_COMMIT=0f4c15bc8521939478426bf6ffca4718837b9cb4
ARG DOCKER_VERSION=29.4.2
ARG DOCKER_GIT_COMMIT=055a478ea9010a19d0d4674c0d0e87ade37a4223
ARG DOCKER_SOURCE_DATE_EPOCH=1776697064
ARG DOCKER_VERSION=29.9.2
ARG DOCKER_GIT_COMMIT=a82e528901260c8df6073311379eac65f92db204
ARG DOCKER_ARCHIVE_SHA256=9ac225999fd5e863f7694c728ef23d480616b1e5639328fa309befd4243d69eb
ARG DOCKER_BINARY_SHA256=6c71cfe71faff949a602946a4df4799b563232e79d0b8b6fd343d0ef081c9621
ARG UBUNTU_SNAPSHOT=20260825T000000Z

ADD --checksum=sha256:5b6326f1932cfd7be57130561dc78192f09ed9f58bfbe6606ed0c50510c3b6cd \
Expand Down Expand Up @@ -86,44 +87,36 @@ RUN set -eux; \
github.com/go-git/go-git/v5@v5.19.2 \
golang.org/x/crypto@v0.57.0 \
golang.org/x/net@v0.60.0 \
go.opentelemetry.io/otel/sdk@v1.45.0 \
google.golang.org/grpc@v1.83.2; \
go mod tidy; \
test "$(go list -m -f '{{.Version}}' golang.org/x/net)" = v0.60.0; \
test "$(go list -m -f '{{.Version}}' go.opentelemetry.io/otel/sdk)" = v1.45.0; \
mkdir -p /out/licenses/security; \
go list -deps -f '{{.ImportPath}}' ./cmd/gomplate > /out/licenses/security/gomplate-dependencies.txt; \
! grep -Eq '^golang.org/x/crypto/openpgp(/|$)' /out/licenses/security/gomplate-dependencies.txt; \
go build -trimpath -buildvcs=false \
-ldflags "-s -w -X github.com/hairyhenderson/gomplate/v5/version.Version=${GOMPLATE_VERSION} -X github.com/hairyhenderson/gomplate/v5/version.GitCommit=${GOMPLATE_GIT_COMMIT}" \
-o /out/gomplate ./cmd/gomplate; \
cp LICENSE /out/licenses/gomplate/LICENSE; \
/out/gomplate --version; \
go version -m /out/gomplate | grep -F 'golang.org/x/net'; \
go version -m /out/gomplate > /out/licenses/security/gomplate-buildinfo.txt; \
rm -rf /src/gomplate /tmp/go-build-cache /tmp/go-mod-cache

RUN set -eux; \
mkdir -p /out/licenses/docker-cli; \
mkdir -p /go/src/github.com/docker; \
git clone --branch "v${DOCKER_VERSION}" --depth 1 \
https://github.com/docker/cli /go/src/github.com/docker/cli; \
cd /go/src/github.com/docker/cli; \
test "$(git rev-parse HEAD)" = "${DOCKER_GIT_COMMIT}"; \
cp vendor.mod go.mod; \
cp vendor.sum go.sum; \
go get golang.org/x/net@v0.60.0 \
github.com/klauspost/compress@v1.18.7 \
github.com/moby/go-archive@v0.3.0 \
google.golang.org/grpc@v1.83.2; \
go mod vendor; \
test "$(go list -m -f '{{.Version}}' golang.org/x/net)" = v0.60.0; \
CGO_ENABLED=0 \
GOPATH=/go \
GO_STRIP=1 \
GITCOMMIT="${DOCKER_GIT_COMMIT}" \
SOURCE_DATE_EPOCH="${DOCKER_SOURCE_DATE_EPOCH}" \
TARGET=/out \
VERSION="${DOCKER_VERSION}" \
./scripts/build/binary; \
test -x /out/docker; \
cp LICENSE NOTICE /out/licenses/docker-cli/; \
git clone --branch "v${DOCKER_VERSION}" --depth 1 https://github.com/docker/cli /src/docker-cli; \
test "$(git -C /src/docker-cli rev-parse HEAD)" = "${DOCKER_GIT_COMMIT}"; \
cp /src/docker-cli/LICENSE /src/docker-cli/NOTICE /out/licenses/docker-cli/; \
curl -fsSL --retry 5 --retry-all-errors --connect-timeout 10 --max-time 300 \
-o /tmp/docker.tgz "https://download.docker.com/linux/static/stable/x86_64/docker-${DOCKER_VERSION}.tgz"; \
echo "${DOCKER_ARCHIVE_SHA256} /tmp/docker.tgz" | sha256sum -c -; \
tar -xOf /tmp/docker.tgz docker/docker > /out/docker; \
echo "${DOCKER_BINARY_SHA256} /out/docker" | sha256sum -c -; \
chmod +x /out/docker; \
/out/docker --version; \
rm -rf /go/src/github.com/docker/cli /tmp/go-build-cache /tmp/go-mod-cache
rm -rf /src/docker-cli /tmp/docker.tgz

FROM ${UBUNTU_IMAGE}

Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Created by PastureStack contributors for repeatable validation.

VERSION ?= v0.3.1
VERSION ?= v0.3.2
REVISION ?= $(shell git rev-parse HEAD)
IMAGE_REPOSITORY ?= local/system-image-preloader
IMAGE ?= $(IMAGE_REPOSITORY):$(VERSION)
Expand Down
25 changes: 20 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ This repository is a derivative maintenance project. It preserves the upstream G

## Reviewed release

The `v0.3.1` release provides:
The `v0.3.2` release provides:

- a self-contained public build based on a digest-pinned Ubuntu image;
- the `system-image-preloader` executable and image name;
- a checksum-verified Go toolchain download;
- source-pinned Docker CLI, `yq`, and `gomplate` builds;
- checksum-pinned official Docker CLI and source-pinned `yq`/`gomplate` builds;
- a finite, configurable image-pull retry policy;
- offline retry and URL-contract tests;
- offline `--help` and `--version` smoke paths; and
Expand Down Expand Up @@ -54,7 +54,8 @@ make smoke
```

The build needs internet access only for the pinned public Ubuntu base, Go
archive, and pinned Docker CLI, `yq`, and `gomplate` source trees.
archive, official Docker CLI archive and license source, and pinned `yq` and
`gomplate` source trees.

Run the isolated API-to-Docker integration test on a disposable Docker host:

Expand All @@ -72,7 +73,7 @@ restores the image-presence state. It never performs a broad image prune.
Use the immutable semantic tag:

```text
ghcr.io/pasturestack/system-image-preloader:v0.3.1
ghcr.io/pasturestack/system-image-preloader:v0.3.2
```

The deployment definition must mount `/var/run/docker.sock`, run one instance
Expand Down Expand Up @@ -118,6 +119,20 @@ This project is a background service with no user interface or runtime localizat

## October 2026 security maintenance

The source-built yq, gomplate, and Docker CLI helpers select `golang.org/x/net v0.60.0` for the October HTTP/2 fixes. Their scanned runtime module graph also pins official security fixes: x/text 0.42.0, go-git 5.19.2, grpc-go 1.83.2, klauspost/compress 1.18.7, and moby/go-archive 0.3.0. Upstream helper feature versions stay unchanged; CI exercises the rebuilt helpers and scans the actual final runtime.
Source-built yq and gomplate select `golang.org/x/net v0.60.0` for the October HTTP/2 fixes, with x/text 0.42.0, go-git 5.19.2 and grpc-go 1.83.2 where applicable. Their upstream feature versions stay unchanged. The official Docker CLI 29.9.2 is pinned by archive and executable SHA-256 and carries the vendor's patched Go 1.26.10 compiler; its exact source is commit `a82e528901260c8df6073311379eac65f92db204`. CI exercises these exact helpers and scans the actual final runtime.

Maintained Go builds select the patched Go 1.27.3 compiler without raising the existing Go language requirement. Ubuntu build/runtime stages apply the checksum-pinned `ca-certificates 20260816ubuntu0.26.04.1` security package after their frozen APT baseline; the final trust store and installed version are verified. Release artifacts must pass the repository's existing checks before publication.

The 0.3.2 rebuild pins gomplate's affected OpenTelemetry SDK to official 1.45.0
(CVE-2026-81870) and adopts the checksum-pinned official Docker CLI 29.9.2,
within the existing 29.x series. The daemon and gomplate 5.1.0 are unchanged.
Every scan severity is retained. Any available fix or unreviewed
finding blocks publication; Low/Medium vendor-unfixed entries require an exact
package/version/PURL and official branch-specific no-fix evidence. The only
OpenPGP non-applicability decision requires the exact built gomplate package
graph to exclude all deprecated OpenPGP packages; raw findings remain retained.
Docker's distribution finding is scoped to the exact official CLI bytes: its
existing manifest-digest validation was independently tested with valid and
forged schema2, OCI, and manifest-list responses. This is a CLI mitigation, not
a claim that the underlying distribution package is fixed. Missing Go module
metadata in the official binary does not imply that dependencies are absent.
2 changes: 1 addition & 1 deletion SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## Project status

PastureStack System Image Preloader is an independent compatibility maintenance project. Release `v0.3.0` has completed offline policy tests, container smoke tests, an isolated compatibility API-to-Docker cache lifecycle test, and a HIGH/CRITICAL container vulnerability gate.
PastureStack System Image Preloader is an independent compatibility maintenance project. Release candidates require offline policy tests, container smoke tests, an isolated compatibility API-to-Docker cache lifecycle test, and a full-severity container vulnerability gate. Every available fix, unreviewed Low/Medium finding, High/Critical/Unknown finding, or secret blocks publication. Exact official branch-specific no-fix evidence may admit Low/Medium findings; a narrowly scoped OpenPGP decision additionally requires proof that the affected packages are absent from the built gomplate dependency graph. Raw scans are always retained.

## Reporting a vulnerability

Expand Down
61 changes: 61 additions & 0 deletions scripts/enforce-runtime-vulnerabilities.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
"""Enforce release findings; only exact, reviewed unfixed low/medium entries may pass."""
import argparse
import json
from pathlib import Path

REPOSITORY = "PastureStack/system-image-preloader"
ROOT = Path(__file__).resolve().parent.parent
REPORTS = {"runtime": ROOT / "dist/security-runtime/system-image-preloader-security-effective.json"}
LEDGER = ROOT / ".github/runtime-unfixed.json"


def violations(report, ledger, repository):
if (not isinstance(report, dict) or report.get("SchemaVersion") != 2
or not isinstance(report.get("Results"), list) or not report["Results"]):
raise ValueError("missing or malformed Trivy scan results")
for result in report["Results"]:
if not isinstance(result, dict) or not isinstance(result.get("Target"), str):
raise ValueError("malformed Trivy result")
for key in ("Vulnerabilities", "Secrets"):
if key in result and result[key] is not None and not isinstance(result[key], list):
raise ValueError("malformed Trivy findings")
if ledger.get("repository") != repository:
raise ValueError("allowlist repository mismatch")
reviewed = {}
for entry in ledger.get("unfixed", []):
key = (entry["id"], entry["package"], entry["version"], entry.get("purl", ""))
if key in reviewed:
raise ValueError("duplicate reviewed finding")
if (entry.get("severity") not in ("LOW", "MEDIUM")
or entry.get("status") != "no_official_fix"
or not entry.get("officialEvidence", "").startswith("https://")
or not entry.get("reason") or not entry.get("checkedAt")):
raise ValueError("incomplete official no-fix evidence")
reviewed[key] = entry
errors = []
for result in report.get("Results") or []:
if result.get("Secrets"):
errors.append("secret findings remain")
for finding in result.get("Vulnerabilities") or []:
severity = finding.get("Severity", "UNKNOWN")
key = (finding.get("VulnerabilityID"), finding.get("PkgName"),
finding.get("InstalledVersion"),
(finding.get("PkgIdentifier") or {}).get("PURL", ""))
entry = reviewed.get(key)
fixed = str(finding.get("FixedVersion") or "").strip()
if (severity not in ("LOW", "MEDIUM") or fixed
or entry is None or entry["severity"] != severity):
errors.append("%s %s@%s severity=%s fixed=%s" % (
key[0], key[1], key[2], severity, fixed or "none"))
return errors


if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--phase", choices=tuple(REPORTS), required=True)
args = parser.parse_args()
failures = violations(json.loads(REPORTS[args.phase].read_text(encoding="utf-8")),
json.loads(LEDGER.read_text(encoding="utf-8")), REPOSITORY)
if failures:
raise SystemExit("Release vulnerability policy failed:\n" + "\n".join(failures))
print("RELEASE_VULNERABILITY_POLICY_OK")
2 changes: 1 addition & 1 deletion scripts/integration-smoke
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
set -Eeuo pipefail

PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
IMAGE="${IMAGE:-local/system-image-preloader:v0.3.1}"
IMAGE="${IMAGE:-local/system-image-preloader:v0.3.2}"
TEST_PULL_IMAGE="${TEST_PULL_IMAGE:-ghcr.io/pasturestack/container-cron:v0.6.0}"
PLATFORM_VERSION="${PLATFORM_VERSION:-v1.6.281}"
ACCESS_KEY="integration-access"
Expand Down
Loading
Loading