fix: provision cosign for verified installs

This commit is contained in:
ochenstarik-ui 2026-08-15 23:29:49 +07:00
parent 0405b0b4c6
commit 6bd58abe2f
8 changed files with 242 additions and 21 deletions

View file

@ -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:

View file

@ -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)"
}

View file

@ -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

View file

@ -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)

View file

@ -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.

View file

@ -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" <<EOF_CURL
#!/usr/bin/env bash
set -Eeuo pipefail
output=""
url=""
while [[ \$# -gt 0 ]]; do
case "\$1" in
--output) output="\$2"; shift 2 ;;
--*) shift ;;
*) url="\$1"; shift ;;
esac
done
printf '%s\n' "\$url" >>'$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'

View file

@ -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'

View file

@ -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