Publish Port Preflight Runtime Patch #15
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |