diff --git a/.github/workflows/release-verification.yml b/.github/workflows/release-verification.yml index 11fc1ef..f71f356 100644 --- a/.github/workflows/release-verification.yml +++ b/.github/workflows/release-verification.yml @@ -1,8 +1,9 @@ name: Release Verification on: - release: - types: [published] + workflow_run: + workflows: ["Release pipeline"] + types: [completed] workflow_dispatch: inputs: tag: @@ -14,6 +15,10 @@ permissions: jobs: verify: + if: >- + github.event_name == 'workflow_dispatch' || + (github.event.workflow_run.conclusion == 'success' && + startsWith(github.event.workflow_run.head_branch, 'v')) runs-on: ubuntu-latest steps: - name: Checkout tests @@ -24,17 +29,20 @@ jobs: tests/contracts/monitor-snapshot-v1.txt sparse-checkout-cone-mode: false - - name: Setup cosign - uses: sigstore/cosign-installer@v3.5.0 - - name: Determine Tag id: tag + env: + MANUAL_TAG: ${{ inputs.tag }} + RELEASE_TAG: ${{ github.event.workflow_run.head_branch }} run: | if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then - echo "tag=${{ github.event.inputs.tag }}" >> $GITHUB_OUTPUT + tag="$MANUAL_TAG" else - echo "tag=${{ github.event.release.tag_name }}" >> $GITHUB_OUTPUT + tag="$RELEASE_TAG" fi + [[ "$tag" =~ ^v[0-9]+\.[0-9]+\.[0-9]+([.-][0-9A-Za-z.-]+)?$ ]] \ + || { echo "Invalid release tag: $tag" >&2; exit 1; } + echo "tag=$tag" >> "$GITHUB_OUTPUT" - name: Verify Assets List env: diff --git a/deploy/ochenstarik-server-monitor-manager.sh b/deploy/ochenstarik-server-monitor-manager.sh index 3a1f899..4e25e61 100755 --- a/deploy/ochenstarik-server-monitor-manager.sh +++ b/deploy/ochenstarik-server-monitor-manager.sh @@ -26,6 +26,10 @@ readonly HUB_MESH_ADDRESS="10.77.0.1/24" # Trust anchors — see docs/release-policy.md for the full signing and identity contract. readonly COSIGN_ISSUER="https://token.actions.githubusercontent.com" readonly COSIGN_IDENTITY_REGEXP="^https://github.com/ochenstarik-ui/server-monitor-manager/\.github/workflows/linux-release\.yml@refs/tags/v.*$" +readonly COSIGN_VERSION="v3.1.3" +readonly COSIGN_SHA256_AMD64="4629c757b7618056f8ddd7e2625ae9fdd94c0372a65049520bc7d9df9efc7f71" +readonly COSIGN_SHA256_ARM64="c5d324e091826b0d7a78eb16fef316450b4eb9aaec045611c08ba06f5e73220a" +readonly COSIGN_INSTALL_PATH="/usr/local/bin/cosign" TEMP_DIR="" MESH_PEER_CODE="" @@ -120,6 +124,57 @@ require_command() { command -v "$1" >/dev/null 2>&1 || fail "Required command is missing: $1" } +ensure_cosign() { + local existing architecture expected_sha256 download_url download_path actual_sha256 + if existing="$(command -v cosign 2>/dev/null)"; then + "$existing" version >/dev/null 2>&1 \ + || fail "Existing cosign cannot run: $existing" + return 0 + fi + + require_root + require_command curl + require_command sha256sum + require_command mktemp + require_command install + + case "$(uname -m)" in + x86_64) + architecture="amd64" + expected_sha256="$COSIGN_SHA256_AMD64" + ;; + aarch64|arm64) + architecture="arm64" + expected_sha256="$COSIGN_SHA256_ARM64" + ;; + *) fail "Cannot provision cosign $COSIGN_VERSION for unsupported architecture: $(uname -m)" ;; + esac + + download_url="https://github.com/sigstore/cosign/releases/download/${COSIGN_VERSION}/cosign-linux-${architecture}" + TEMP_DIR="$(mktemp -d -t smm-cosign.XXXXXXXX)" + chmod 700 "$TEMP_DIR" + download_path="$TEMP_DIR/cosign-linux-${architecture}" + + log "Downloading cosign $COSIGN_VERSION for $architecture." + if ! curl --fail --silent --show-error --location --retry 3 \ + --output "$download_path" "$download_url"; then + fail "Could not download cosign $COSIGN_VERSION. Expected SHA-256: $expected_sha256. Install path: $COSIGN_INSTALL_PATH. Download URL: $download_url. Install this exact binary manually and verify its checksum." + fi + + actual_sha256="$(sha256sum "$download_path" | awk '{ print $1 }')" + if [[ "${actual_sha256,,}" != "${expected_sha256,,}" ]]; then + fail "cosign $COSIGN_VERSION checksum mismatch. Expected SHA-256: $expected_sha256. Actual SHA-256: $actual_sha256. Install path: $COSIGN_INSTALL_PATH." + fi + + install -m 0755 "$download_path" "$COSIGN_INSTALL_PATH" \ + || fail "Could not install cosign $COSIGN_VERSION at $COSIGN_INSTALL_PATH after verifying SHA-256 $expected_sha256." + "$COSIGN_INSTALL_PATH" version >/dev/null 2>&1 \ + || fail "Installed cosign $COSIGN_VERSION cannot run at $COSIGN_INSTALL_PATH. Expected SHA-256: $expected_sha256." + rm -rf -- "$TEMP_DIR" + TEMP_DIR="" + log "Installed cosign $COSIGN_VERSION at $COSIGN_INSTALL_PATH." +} + validate_platform() { [[ -r /etc/os-release ]] || fail "/etc/os-release is missing." # shellcheck disable=SC1091 @@ -295,7 +350,7 @@ verify_manifest() { log "WARNING: Signature verification skipped due to SMM_ALLOW_UNSIGNED=1." return 0 fi - require_command cosign + ensure_cosign [[ -f "$manifest" ]] || fail "Manifest not found: $manifest" [[ -f "$signature" ]] || fail "Signature not found: $signature" log "Verifying manifest signature..." @@ -1483,6 +1538,7 @@ preflight() { for command_name in openssl sha256sum tar systemctl getent useradd groupadd; do require_command "$command_name" done + ensure_cosign log "Supported platform: $(. /etc/os-release; printf '%s %s' "$ID" "$VERSION_ID"), $(uname -m)" } diff --git a/deploy/smm-setup.sh b/deploy/smm-setup.sh index 2abad49..ca5cd09 100644 --- a/deploy/smm-setup.sh +++ b/deploy/smm-setup.sh @@ -3,7 +3,7 @@ set -Eeuo pipefail IFS=$'\n\t' readonly PROGRAM_NAME="smm-setup" -readonly DEFAULT_RELEASE_TAG="v0.1.0-alpha.14" +readonly DEFAULT_RELEASE_TAG="v0.1.0-alpha.15" readonly DEFAULT_REPOSITORY="ochenstarik-ui/server-monitor-manager" readonly INNER_ASSET="ochenstarik-server-monitor-manager.sh" @@ -27,7 +27,7 @@ force pass-through. Common bootstrap commands: backup-create | backup-restore | version Environment overrides: - SMM_TAG Release tag (default: v0.1.0-alpha.14) + SMM_TAG Release tag (default: v0.1.0-alpha.15) SMM_REPOSITORY GitHub repository (default: ochenstarik-ui/server-monitor-manager) SMM_CACHE_DIR Verified-download cache directory USAGE diff --git a/docs/linux-bootstrap.md b/docs/linux-bootstrap.md index a68b711..55f8ee7 100644 --- a/docs/linux-bootstrap.md +++ b/docs/linux-bootstrap.md @@ -6,11 +6,11 @@ Server Monitor Manager устанавливает Control (Hub) и Agent (Node) ## Быстрая установка -Скачайте и проверьте convenience installer из `v0.1.0-alpha.14`: +Скачайте и проверьте convenience installer из `v0.1.0-alpha.15`: ```bash -curl -fsSLO https://github.com/ochenstarik-ui/server-monitor-manager/releases/download/v0.1.0-alpha.14/smm-setup.sh -curl -fsSLO https://github.com/ochenstarik-ui/server-monitor-manager/releases/download/v0.1.0-alpha.14/smm-setup.sh.sha256 +curl -fsSLO https://github.com/ochenstarik-ui/server-monitor-manager/releases/download/v0.1.0-alpha.15/smm-setup.sh +curl -fsSLO https://github.com/ochenstarik-ui/server-monitor-manager/releases/download/v0.1.0-alpha.15/smm-setup.sh.sha256 sha256sum -c smm-setup.sh.sha256 chmod 700 smm-setup.sh ``` @@ -42,6 +42,15 @@ sudo ./smm-setup.sh install-node Сертификат нужен потому, что manifest подписывается keyless-режимом cosign: подпись проверяется эфемерным сертификатом, привязанным к workflow выпуска, а не постоянным ключом. +Bootstrap сам обеспечивает `cosign`, если пригодный бинарь ещё не найден в `PATH`. Поставка закреплена на `cosign v3.1.3`: установщик публично скачивает официальный `cosign-linux-amd64` или `cosign-linux-arm64`, проверяет SHA-256 до первого запуска и устанавливает проверенный файл как `/usr/local/bin/cosign` с режимом `0755`. Уже установленный `cosign` не заменяется, но должен успешно выполнять `cosign version`. + +Закреплённые контрольные суммы: + +- `cosign-linux-amd64`: `4629c757b7618056f8ddd7e2625ae9fdd94c0372a65049520bc7d9df9efc7f71`; +- `cosign-linux-arm64`: `c5d324e091826b0d7a78eb16fef316450b4eb9aaec045611c08ba06f5e73220a`. + +Смена версии или любой из сумм является осознанным обновлением поставки и должна проходить через новый релиз. Если GitHub недоступен, установка останавливается с сообщением, содержащим версию, ожидаемую сумму, URL и путь `/usr/local/bin/cosign`, чтобы оператор мог вручную поставить ровно этот бинарь и проверить его до запуска. + Bootstrap проверяет подпись manifest, затем SHA-256 архива по manifest, и принимает в архиве только каталоги `agent`, `control`, `deploy` и `bootstrap`. ## 1. Ручная установка главного сервера (Hub) diff --git a/docs/release-policy.md b/docs/release-policy.md index fdf9c0d..b86264a 100644 --- a/docs/release-policy.md +++ b/docs/release-policy.md @@ -15,6 +15,7 @@ Known release history: - `v0.1.0-alpha.11` exists as a tag, but its Release pipeline failed before a GitHub Release was published. The version number is burned and must not be moved, deleted, recreated, or reused. - `v0.1.0-alpha.12` was published with a keyless manifest signature but without the Fulcio signing certificate, so consumers cannot verify that signature. Its Windows `SHA256SUMS` asset also used CRLF and was not consumable by GNU `sha256sum -c`. The immutable release remains published as historical evidence; neither defect is repaired in place. - `v0.1.0-alpha.13` corrected the checksum portability defect, but it was also published with a keyless manifest signature and without the Fulcio signing certificate required by production consumers. The immutable release remains published as historical evidence; the producer/consumer certificate contract is corrected under a higher version. -- `v0.1.0-alpha.14` is the first release whose contract requires the manifest, keyless signature, and Fulcio certificate to be published and verified together. Its tag may be created only from `main` after the release pull request, required CI, and a branch `workflow_dispatch` dry run of the Release pipeline are green. +- `v0.1.0-alpha.14` is the first release with the complete manifest, keyless signature, and Fulcio certificate set, so its published assets can be verified. A clean host cannot install it because the release does not provision cosign. Preserve it for verification and historical evidence; do not use it for installation. +- `v0.1.0-alpha.15` is the first release that provisions a pinned, checksum-verified cosign binary and can therefore be installed on a clean supported host without manual cosign setup. Every release candidate must pass a branch `workflow_dispatch` run of the Release pipeline before its immutable version tag is created. The release owner has sole write ownership of version sources, `deploy/**`, `tests/bootstrap/**`, release workflows, the root README release status, and translated README release statuses. Other contributors request changes to those paths in their report; they do not edit or bump them directly. One pull request covers one release topic and may merge only after required CI is green. diff --git a/tests/bootstrap/test-cosign-provisioning.sh b/tests/bootstrap/test-cosign-provisioning.sh new file mode 100644 index 0000000..b3655fe --- /dev/null +++ b/tests/bootstrap/test-cosign-provisioning.sh @@ -0,0 +1,109 @@ +#!/usr/bin/env bash +set -Eeuo pipefail +IFS=$'\n\t' + +root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)" +bootstrap="$root/deploy/ochenstarik-server-monitor-manager.sh" + +extract_bootstrap_function() { + local name="$1" + awk -v signature="$name() {" ' + $0 == signature { emitting = 1 } + emitting { print } + emitting && $0 == "}" { exit } + ' "$bootstrap" +} + +ensure_cosign_definition="$(extract_bootstrap_function ensure_cosign)" +[[ -n "$ensure_cosign_definition" ]] +source <(printf '%s\n' "$ensure_cosign_definition") + +work="$(mktemp -d -t smm-cosign-contract.XXXXXXXX)" +trap 'rm -rf -- "$work"' EXIT +mkdir -p "$work/bin" "$work/installed" + +cat >"$work/cosign-fixture" <<'FIXTURE' +#!/usr/bin/env bash +[[ "${1:-}" == "version" ]] +printf '%s\n' 'cosign fixture' +FIXTURE +chmod +x "$work/cosign-fixture" +fixture_sha256="$(sha256sum "$work/cosign-fixture" | awk '{ print $1 }')" + +cat >"$work/bin/curl" <>'$work/urls' +[[ "\${SMM_TEST_COSIGN_DOWNLOAD_FAIL:-0}" != "1" ]] || exit 22 +cp '$work/cosign-fixture' "\$output" +EOF_CURL +chmod +x "$work/bin/curl" + +cat >"$work/bin/uname" <<'EOF_UNAME' +#!/usr/bin/env bash +printf '%s\n' "${SMM_TEST_ARCH:-x86_64}" +EOF_UNAME +chmod +x "$work/bin/uname" + +run_ensure_cosign() { + local architecture="$1" amd64_sha="$2" arm64_sha="$3" install_path="$4" + SMM_TEST_ARCH="$architecture" \ + PATH="$work/installed:$work/bin:/usr/bin:/bin" \ + COSIGN_VERSION="v3.1.3" \ + COSIGN_SHA256_AMD64="$amd64_sha" \ + COSIGN_SHA256_ARM64="$arm64_sha" \ + COSIGN_INSTALL_PATH="$install_path" \ + TEMP_DIR="" \ + ensure_cosign +} + +require_root() { :; } +require_command() { command -v "$1" >/dev/null 2>&1 || fail "missing $1"; } +log() { printf '%s\n' "$*"; } +fail() { printf '%s\n' "$*" >&2; exit 1; } + +install_path="$work/installed/cosign" +run_ensure_cosign x86_64 "$fixture_sha256" "$fixture_sha256" "$install_path" +[[ -x "$install_path" ]] +[[ "$(stat -c '%a' "$install_path")" == "755" ]] +"$install_path" version >/dev/null +grep -Fq '/v3.1.3/cosign-linux-amd64' "$work/urls" + +rm -f -- "$install_path" +if (run_ensure_cosign x86_64 "$(printf '0%.0s' {1..64})" "$fixture_sha256" "$install_path") \ + >"$work/mismatch.out" 2>&1; then + printf '%s\n' 'tampered cosign checksum was accepted' >&2 + exit 1 +fi +grep -Fq 'cosign v3.1.3 checksum mismatch' "$work/mismatch.out" +grep -Fq 'Expected SHA-256: 0000000000000000000000000000000000000000000000000000000000000000' "$work/mismatch.out" +grep -Fq "Install path: $install_path" "$work/mismatch.out" +[[ ! -e "$install_path" ]] + +if (SMM_TEST_COSIGN_DOWNLOAD_FAIL=1 \ + run_ensure_cosign aarch64 "$fixture_sha256" "$fixture_sha256" "$install_path") \ + >"$work/download.out" 2>&1; then + printf '%s\n' 'cosign download failure was accepted' >&2 + exit 1 +fi +grep -Fq 'Could not download cosign v3.1.3' "$work/download.out" +grep -Fq "Expected SHA-256: $fixture_sha256" "$work/download.out" +grep -Fq "Install path: $install_path" "$work/download.out" +grep -Fq '/v3.1.3/cosign-linux-arm64' "$work/download.out" + +cp "$work/cosign-fixture" "$install_path" +chmod 0755 "$install_path" +: >"$work/urls" +run_ensure_cosign x86_64 "$(printf '0%.0s' {1..64})" "$(printf '0%.0s' {1..64})" "$install_path" +[[ ! -s "$work/urls" ]] + +printf '%s\n' 'COSIGN_PROVISIONING=PASS' diff --git a/tests/bootstrap/test-release-contract.sh b/tests/bootstrap/test-release-contract.sh index 3912b57..37c24de 100644 --- a/tests/bootstrap/test-release-contract.sh +++ b/tests/bootstrap/test-release-contract.sh @@ -4,7 +4,9 @@ IFS=$'\n\t' root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)" setup="$root/deploy/smm-setup.sh" +bootstrap="$root/deploy/ochenstarik-server-monitor-manager.sh" workflow="$root/.github/workflows/linux-release.yml" +verification_workflow="$root/.github/workflows/release-verification.yml" windows_workflow="$root/.github/workflows/windows-release.yml" policy="$root/docs/release-policy.md" installer_contract="$root/docs/installer-contract.md" @@ -16,7 +18,7 @@ v1_fixture="$root/tests/fixtures/alpha8-v1-release" exit 1 } bash -n "$setup" -grep -Fq 'readonly DEFAULT_RELEASE_TAG="v0.1.0-alpha.14"' "$setup" +grep -Fq 'readonly DEFAULT_RELEASE_TAG="v0.1.0-alpha.15"' "$setup" grep -Fq 'install-hub PUBLIC_HOST [HTTPS_PORT] [WG_PORT]' "$setup" grep -Fxq ' install-node' "$setup" if grep -Fq 'validate_control_url' "$setup" || grep -Fq '${CONTROL_URL%/}/control' "$setup"; then @@ -57,7 +59,21 @@ grep -Fq -- '--output-certificate server-monitor-manager-manifest.pem' "$workflo grep -Fq 'server-monitor-manager-manifest.pem' "$workflow" grep -Fq 'v0.1.0-alpha.13' "$policy" grep -Fq 'v0.1.0-alpha.14' "$policy" -grep -Fq 'first release whose contract requires the manifest, keyless signature, and Fulcio certificate' "$policy" +grep -Fq 'v0.1.0-alpha.15' "$policy" +grep -Fq 'readonly COSIGN_VERSION="v3.1.3"' "$bootstrap" +grep -Fq 'readonly COSIGN_SHA256_AMD64="4629c757b7618056f8ddd7e2625ae9fdd94c0372a65049520bc7d9df9efc7f71"' "$bootstrap" +grep -Fq 'readonly COSIGN_SHA256_ARM64="c5d324e091826b0d7a78eb16fef316450b4eb9aaec045611c08ba06f5e73220a"' "$bootstrap" +grep -Fq 'readonly COSIGN_INSTALL_PATH="/usr/local/bin/cosign"' "$bootstrap" +grep -Fq 'ensure_cosign' "$bootstrap" +grep -Fq 'workflow_run:' "$verification_workflow" +grep -Fq 'workflows: ["Release pipeline"]' "$verification_workflow" +grep -Fq "github.event.workflow_run.conclusion == 'success'" "$verification_workflow" +grep -Fq "startsWith(github.event.workflow_run.head_branch, 'v')" "$verification_workflow" +grep -Fq 'workflow_dispatch:' "$verification_workflow" +if grep -Fq 'sigstore/cosign-installer' "$verification_workflow"; then + printf '%s\n' 'Release Verification must test installer-provisioned cosign' >&2 + exit 1 +fi grep -Fq 'Published release tags and their assets are immutable.' "$installer_contract" grep -Fq 'publish a new, higher version tag' "$installer_contract" @@ -167,8 +183,8 @@ chmod +x "$work/bin/uname" HOME="$work/home" PATH="$work/bin:$PATH" bash "$setup" version >"$work/output" grep -Fq 'INNER_ARGS=version ' "$work/output" -grep -Fq '/releases/download/v0.1.0-alpha.14/ochenstarik-server-monitor-manager.sh' "$work/urls" -grep -Fq '/releases/download/v0.1.0-alpha.14/ochenstarik-server-monitor-manager.sh.sha256' "$work/urls" +grep -Fq '/releases/download/v0.1.0-alpha.15/ochenstarik-server-monitor-manager.sh' "$work/urls" +grep -Fq '/releases/download/v0.1.0-alpha.15/ochenstarik-server-monitor-manager.sh.sha256' "$work/urls" if HOME="$work/home" PATH="$work/bin:$PATH" bash "$setup" install-hub >"$work/invalid.out" 2>&1; then printf '%s\n' 'install-hub accepted a missing PUBLIC_HOST' >&2 @@ -217,4 +233,6 @@ for required in \ done done +bash "$root/tests/bootstrap/test-cosign-provisioning.sh" + printf '%s\n' 'RELEASE_CONTRACT=PASS' diff --git a/tests/release-verification/run-positive-installation.sh b/tests/release-verification/run-positive-installation.sh index 30b8bbc..7bc2072 100644 --- a/tests/release-verification/run-positive-installation.sh +++ b/tests/release-verification/run-positive-installation.sh @@ -22,6 +22,11 @@ METRICS_SCRIPT="/usr/local/libexec/ochenstarik-smm-metrics" echo "Running positive installation test for $TAG..." +if command -v cosign >/dev/null 2>&1; then + echo "FAIL: cosign is already present before the clean-host installation test" >&2 + exit 1 +fi + download() { local name="$1" curl --fail --silent --show-error --location --retry 3 \ @@ -49,14 +54,19 @@ download server-monitor-manager-manifest.json download server-monitor-manager-manifest.sig download server-monitor-manager-manifest.pem +sudo bash smm-setup.sh --tag "$TAG" install-hub 127.0.0.1 "$CONTROL_PORT" 51820 +[[ "$(command -v cosign)" == "/usr/local/bin/cosign" ]] \ + || { echo "FAIL: installer did not provision /usr/local/bin/cosign" >&2; exit 1; } +cosign version >/dev/null + +# Exercise the direct bootstrap paths after the short install-hub path has +# provisioned cosign on the otherwise clean runner. sudo bash smm-setup.sh --tag "$TAG" preflight sudo bash smm-setup.sh --tag "$TAG" verify-manifest \ server-monitor-manager-manifest.json \ server-monitor-manager-manifest.sig \ server-monitor-manager-manifest.pem sudo bash smm-setup.sh --tag "$TAG" verify-release "$ARCHIVE" -sudo bash smm-setup.sh --tag "$TAG" install-control "$ARCHIVE" 127.0.0.1 "$CONTROL_PORT" -sudo bash smm-setup.sh --tag "$TAG" mesh-init 127.0.0.1 51820 echo "Checking Control healthz..." for _ in {1..30}; do @@ -73,9 +83,19 @@ sudo curl --fail --silent --show-error \ echo "Enrolling a node..." NODE_CODE="$(sudo bash smm-setup.sh --tag "$TAG" node-code test-node)" +# The file did not exist before this test and was created by install-hub above. +# Remove only that test-provisioned copy so install-node is also exercised from +# a host without cosign. +sudo rm -f -- /usr/local/bin/cosign +if command -v cosign >/dev/null 2>&1; then + echo "FAIL: cosign is still present before the clean-host install-node test" >&2 + exit 1 +fi SMM_ENROLL_CODE="$NODE_CODE" SMM_ACCEPT_CA_FINGERPRINT=1 \ sudo --preserve-env=SMM_ENROLL_CODE,SMM_ACCEPT_CA_FINGERPRINT \ - bash smm-setup.sh --tag "$TAG" install-node "$ARCHIVE" + bash smm-setup.sh --tag "$TAG" install-node +[[ "$(command -v cosign)" == "/usr/local/bin/cosign" ]] \ + || { echo "FAIL: install-node did not provision /usr/local/bin/cosign" >&2; exit 1; } sudo systemctl is-active --quiet ochenstarik-smm-agent.service sudo systemctl is-active --quiet ochenstarik-smm-control.service