Skip to content

Publish Port Preflight Runtime Patch #15

Publish Port Preflight Runtime Patch

Publish Port Preflight Runtime Patch #15

name: Publish Port Preflight Runtime Patch
on:
workflow_dispatch:
permissions:
contents: read
id-token: write
attestations: write
packages: write
artifact-metadata: write
concurrency:
group: server-port-preflight-runtime-patch
cancel-in-progress: false
jobs:
publish:
if: github.repository == 'PastureStack/server' && github.ref == 'refs/heads/main'
runs-on: ubuntu-24.04
timeout-minutes: 60
env:
RELEASE_TAG: v1.6.355
SOURCE_SHA: ${{ github.sha }}
TARGET_IMAGE: ghcr.io/pasturestack/server:v1.6.355
steps:
- name: Check out immutable Server source
shell: bash
run: |
set -Eeuo pipefail
git clone --filter=blob:none \
"https://github.com/${GITHUB_REPOSITORY}.git" source
git -C source checkout --detach "$SOURCE_SHA"
test "$(git -C source rev-parse HEAD)" = "$SOURCE_SHA"
test -z "$(git -C source status --short)"
- name: Run Server source gates
shell: bash
working-directory: source
run: |
set -Eeuo pipefail
bash scripts/check-server-source-gates.sh
- name: Build and compare two clean candidates
id: build
shell: bash
working-directory: source
run: |
set -Eeuo pipefail
candidate_a="pasturestack-validation/server:${RELEASE_TAG#v}-a-${GITHUB_RUN_ID}"
candidate_b="pasturestack-validation/server:${RELEASE_TAG#v}-b-${GITHUB_RUN_ID}"
PASTURESTACK_BUILD_NO_CACHE=1 IMAGE="$candidate_a" \
bash server/build-port-preflight-runtime-patch-image.sh |
tee "$RUNNER_TEMP/candidate-a.log"
PASTURESTACK_BUILD_NO_CACHE=1 IMAGE="$candidate_b" \
bash server/build-port-preflight-runtime-patch-image.sh |
tee "$RUNNER_TEMP/candidate-b.log"
image_a="$(docker image inspect "$candidate_a" --format '{{.Id}}')"
image_b="$(docker image inspect "$candidate_b" --format '{{.Id}}')"
runtime_payload_sha256() {
docker run --rm --entrypoint bash "$1" -lc '
set -euo pipefail
web_root=$(readlink -f /usr/share/cattle/war)
{
find "${web_root}" -type f -print0 |
sort -z | xargs -0 sha256sum
find "${web_root}" -printf "%y|%m|%U|%G|%p|%l\n" |
LC_ALL=C sort
sha256sum \
/usr/share/cattle/cattle.jar \
/usr/share/cattle/artifacts/node-agent-0.13.22.tar.gz \
/usr/share/cattle/artifacts/node-agent-0.13.22-windows-amd64.zip \
/usr/share/cattle/env_vars \
/usr/bin/authentication-service.real \
/usr/bin/catalog-service.real \
/usr/bin/govc \
/usr/bin/websocket-proxy.real \
/usr/bin/pasturestack-console-broker
} | sha256sum | cut -d " " -f 1
'
}
payload_a="$(runtime_payload_sha256 "$candidate_a")"
payload_b="$(runtime_payload_sha256 "$candidate_b")"
config_a="$(docker image inspect "$candidate_a" \
--format '{{json .Config}}')"
config_b="$(docker image inspect "$candidate_b" \
--format '{{json .Config}}')"
config_sha256_a="$(printf '%s' "$config_a" | sha256sum | cut -d ' ' -f 1)"
config_sha256_b="$(printf '%s' "$config_b" | sha256sum | cut -d ' ' -f 1)"
printf 'SERVER_PORT_PREFLIGHT_PATCH_COMPARISON payload_a=%s payload_b=%s config_a=%s config_b=%s\n' \
"$payload_a" "$payload_b" "$config_sha256_a" "$config_sha256_b"
test "$payload_a" = "$payload_b"
test "$config_a" = "$config_b"
[[ "$payload_a" =~ ^[0-9a-f]{64}$ ]]
docker tag "$candidate_a" "$TARGET_IMAGE"
test "$(docker image inspect "$TARGET_IMAGE" \
--format '{{index .Config.Labels "org.opencontainers.image.version"}}')" = \
"$RELEASE_TAG"
test "$(docker image inspect "$TARGET_IMAGE" \
--format '{{index .Config.Labels "org.opencontainers.image.revision"}}')" = \
"$SOURCE_SHA"
printf 'SERVER_PORT_PREFLIGHT_PATCH_REPRODUCIBLE source=%s release=%s payload_sha256=%s candidate_a=%s candidate_b=%s\n' \
"$SOURCE_SHA" "$RELEASE_TAG" "$payload_a" "$image_a" "$image_b" |
tee "$RUNNER_TEMP/reproducibility.txt"
- name: Verify scanner and enforce image security gates
shell: bash
env:
GH_TOKEN: ${{ github.token }}
run: |
set -Eeuo pipefail
trivy_version=0.73.0
trivy_sha256=2edd39da482bb4e9831962487b68f68e3928ec3137794757f54d00383d79547b
trivy_source_sha=40c73e5d6166dcc0346a1ab4e94499d1572854e4
trivy_root="${RUNNER_TEMP}/trivy-${trivy_version}"
trivy_asset="${trivy_root}/trivy_${trivy_version}_Linux-64bit.tar.gz"
mkdir -p "$trivy_root"
curl --fail --show-error --silent --location \
"https://github.com/aquasecurity/trivy/releases/download/v${trivy_version}/trivy_${trivy_version}_Linux-64bit.tar.gz" \
--output "$trivy_asset"
printf '%s %s\n' "$trivy_sha256" "$trivy_asset" | sha256sum --check
trivy_attestation_status=verified
attestation_error="${trivy_root}/attestation-error.txt"
if ! gh attestation verify "$trivy_asset" \
--repo aquasecurity/trivy \
--signer-workflow aquasecurity/trivy/.github/workflows/reusable-release.yaml \
--source-ref "refs/tags/v${trivy_version}" \
--source-digest "$trivy_source_sha" \
--signer-digest "$trivy_source_sha" \
--deny-self-hosted-runners \
2>"$attestation_error"; then
if grep -Fq 'organization has an IP allow list enabled' \
"$attestation_error"; then
trivy_attestation_status=unavailable-source-ip-allow-list
printf '::warning::Trivy provenance API is unavailable because the source organization rejects this hosted-runner IP. Exact asset SHA-256 verification remains mandatory.\n'
else
cat "$attestation_error" >&2
exit 1
fi
fi
tar -xzf "$trivy_asset" -C "$trivy_root"
test "$("${trivy_root}/trivy" --version | awk '/^Version:/ {print $2}')" = \
"$trivy_version"
"${trivy_root}/trivy" image \
--cache-dir "${RUNNER_TEMP}/trivy-cache" \
--scanners vuln,secret \
--format json \
--output "${RUNNER_TEMP}/server-security-scan.json" \
"$TARGET_IMAGE"
jq -e '.SchemaVersion == 2 and (.Results | type == "array")' \
"${RUNNER_TEMP}/server-security-scan.json" >/dev/null
jq -r '
.Results[]?
| .Target as $target
| .Vulnerabilities[]?
| select(.Severity == "CRITICAL" or .Severity == "HIGH")
| [.Severity, .VulnerabilityID, .PkgName, .InstalledVersion,
(.FixedVersion // ""), ($target // "")]
| @tsv
' "${RUNNER_TEMP}/server-security-scan.json" |
LC_ALL=C sort -u >"${RUNNER_TEMP}/server-critical-high.tsv"
jq -r '
.Results[]?
| .Target as $target
| .Secrets[]?
| [.RuleID, .Category, .Severity, (.Title // ""), ($target // "")]
| @tsv
' "${RUNNER_TEMP}/server-security-scan.json" |
LC_ALL=C sort -u >"${RUNNER_TEMP}/server-secrets.tsv"
test ! -s "${RUNNER_TEMP}/server-critical-high.tsv"
test ! -s "${RUNNER_TEMP}/server-secrets.tsv"
"${trivy_root}/trivy" image \
--cache-dir "${RUNNER_TEMP}/trivy-cache" \
--format cyclonedx \
--output "${RUNNER_TEMP}/server.cdx.json" \
"$TARGET_IMAGE"
jq -e '
.bomFormat == "CycloneDX"
and ((.specVersion | type) == "string")
and ((.components | type) == "array")
and ((.components | length) > 0)
' "${RUNNER_TEMP}/server.cdx.json" >/dev/null
printf '%s\n' "$trivy_attestation_status" \
>"${RUNNER_TEMP}/trivy-attestation-verification.txt"
"${trivy_root}/trivy" --version >"${RUNNER_TEMP}/trivy-version.txt"
sha256sum \
"${RUNNER_TEMP}/server-security-scan.json" \
"${RUNNER_TEMP}/server-critical-high.tsv" \
"${RUNNER_TEMP}/server-secrets.tsv" \
"${RUNNER_TEMP}/server.cdx.json" \
"${RUNNER_TEMP}/trivy-attestation-verification.txt" \
"${RUNNER_TEMP}/trivy-version.txt" \
>"${RUNNER_TEMP}/server-security-evidence.sha256"
- name: Publish public semantic-version image
id: publish
shell: bash
working-directory: source
env:
REGISTRY_TOKEN: ${{ github.token }}
run: |
set -Eeuo pipefail
if docker manifest inspect "$TARGET_IMAGE" >/dev/null 2>&1; then
echo "Container tag already exists: $TARGET_IMAGE" >&2
exit 1
fi
printf '%s' "$REGISTRY_TOKEN" |
docker login ghcr.io --username "$GITHUB_ACTOR" --password-stdin >/dev/null
docker push "$TARGET_IMAGE" | tee "$RUNNER_TEMP/push.log"
docker logout ghcr.io >/dev/null
digest="$(docker buildx imagetools inspect "$TARGET_IMAGE" \
--format '{{.Manifest.Digest}}')"
[[ "$digest" =~ ^sha256:[0-9a-f]{64}$ ]]
docker buildx imagetools inspect "$TARGET_IMAGE" \
>"$RUNNER_TEMP/image-inspect.txt"
printf 'SERVER_PORT_PREFLIGHT_PATCH_PUBLISHED source=%s release=%s reference=%s digest=%s\n' \
"$SOURCE_SHA" "$RELEASE_TAG" "$TARGET_IMAGE" "$digest" |
tee "$RUNNER_TEMP/published.txt"
printf 'digest=%s\n' "$digest" >>"$GITHUB_OUTPUT"
- name: Attest published image provenance
uses: actions/attest@508db95dd578ae2727ebd6217d5ba78e4fbda05d # v4.2.1
with:
subject-name: ghcr.io/pasturestack/server
subject-digest: ${{ steps.publish.outputs.digest }}
push-to-registry: true
- name: Attest published image SBOM
uses: actions/attest@508db95dd578ae2727ebd6217d5ba78e4fbda05d # v4.2.1
with:
subject-name: ghcr.io/pasturestack/server
subject-digest: ${{ steps.publish.outputs.digest }}
sbom-path: ${{ runner.temp }}/server.cdx.json
push-to-registry: true
- name: Retain publication evidence
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: server-${{ env.RELEASE_TAG }}-publication-evidence
path: |
${{ runner.temp }}/candidate-a.log
${{ runner.temp }}/candidate-b.log
${{ runner.temp }}/reproducibility.txt
${{ runner.temp }}/push.log
${{ runner.temp }}/image-inspect.txt
${{ runner.temp }}/published.txt
${{ runner.temp }}/server-security-scan.json
${{ runner.temp }}/server-critical-high.tsv
${{ runner.temp }}/server-secrets.tsv
${{ runner.temp }}/server.cdx.json
${{ runner.temp }}/server-security-evidence.sha256
${{ runner.temp }}/trivy-attestation-verification.txt
${{ runner.temp }}/trivy-version.txt
if-no-files-found: error
retention-days: 30
compression-level: 9