From b0b7295168dbeec7b097be3bf5319f3888353958 Mon Sep 17 00:00:00 2001 From: keegreil Date: Wed, 19 Aug 2026 00:11:58 -0400 Subject: [PATCH 1/5] release: harden StartOS appliance lifecycle --- .github/workflows/check.yml | 16 +++++++ .github/workflows/release-candidate.yml | 64 +++++++++++++++++++++++++ .gitignore | 1 + README.md | 52 ++++++++++++++++++++ release-images.json | 10 ++++ scripts/audit-build-dependencies.sh | 24 ++++++++++ scripts/verify-release-inputs.sh | 34 +++++++++++++ startos/interfaces.ts | 29 ++++++++++- startos/main.ts | 34 ++++++++++++- startos/utils.ts | 1 + startos/versions/current.ts | 6 +-- 11 files changed, 264 insertions(+), 7 deletions(-) create mode 100644 .github/workflows/release-candidate.yml create mode 100644 release-images.json create mode 100755 scripts/audit-build-dependencies.sh create mode 100755 scripts/verify-release-inputs.sh diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index c51b9d8..ef1555b 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -20,3 +20,19 @@ jobs: - run: npm ci - run: npm run check - run: npm run build + - name: Verify immutable images and repository hygiene + run: ./scripts/verify-release-inputs.sh + - name: Audit production dependencies + run: ./scripts/audit-build-dependencies.sh + + secrets: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Scan repository history for secrets + run: >- + docker run --rm -v "$PWD:/repo" + ghcr.io/gitleaks/gitleaks@sha256:c00b6bd0aeb3071cbcb79009cb16a60dd9e0a7c60e2be9ab65d25e6bc8abbb7f + detect --source=/repo --redact --no-banner diff --git a/.github/workflows/release-candidate.yml b/.github/workflows/release-candidate.yml new file mode 100644 index 0000000..18732ec --- /dev/null +++ b/.github/workflows/release-candidate.yml @@ -0,0 +1,64 @@ +name: Build release candidate + +on: + workflow_dispatch: + push: + tags: ['v*-beta.*'] + +permissions: + contents: read + id-token: write + attestations: write + +jobs: + package: + strategy: + matrix: + include: + - make_target: x86 + artifact: gridpool_x86_64.s9pk + - make_target: arm + artifact: gridpool_aarch64.s9pk + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + - run: sudo apt-get update && sudo apt-get install -y squashfs-tools squashfs-tools-ng + - name: Install pinned StartOS CLI + run: | + curl -fsSL \ + https://github.com/Start9Labs/start-technologies/releases/download/start-cli/v1.1.0/start-cli_x86_64-linux \ + -o /tmp/start-cli + echo '70eff67b6e9a936acd8aaaf787b783819252ecedaa5c74d462e3b15ed4dd843a /tmp/start-cli' | sha256sum -c - + install -m 0755 /tmp/start-cli "$HOME/.local/bin/start-cli" + echo "$HOME/.local/bin" >> "$GITHUB_PATH" + - run: npm ci + - run: npm run check && npm run build + - run: ./scripts/verify-release-inputs.sh + - run: ./scripts/audit-build-dependencies.sh + - name: Initialize StartOS workspace + run: cd .. && start-cli s9pk init-workspace + - name: Build package + run: make ${{ matrix.make_target }} + - name: Checksums + run: sha256sum '${{ matrix.artifact }}' > '${{ matrix.artifact }}.sha256' + - name: Generate SPDX SBOM + uses: anchore/sbom-action@v0.24.0 + with: + path: '${{ matrix.artifact }}' + format: spdx-json + output-file: '${{ matrix.artifact }}.spdx.json' + - uses: actions/attest-build-provenance@v2 + with: + subject-path: '${{ matrix.artifact }}' + - uses: actions/upload-artifact@v4 + with: + name: '${{ matrix.artifact }}' + path: | + ${{ matrix.artifact }} + ${{ matrix.artifact }}.sha256 + ${{ matrix.artifact }}.spdx.json + npm-audit.json diff --git a/.gitignore b/.gitignore index 56e95f5..ce94a85 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ node_modules/ javascript/ *.s9pk *.s9pk.* +npm-audit.json diff --git a/README.md b/README.md index 5fa8b11..eb77390 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,10 @@ boundary. This repository is an early sideload beta. Build and install it only on a test server, and back up the GridPool volume before upgrades. +Runtime images are pinned by immutable OCI digest. Release artifacts are built +for x86_64 and aarch64 by GitHub Actions and include SHA-256 checksums plus +provenance attestations. + ## Build ```bash @@ -58,3 +62,51 @@ worker labels use the package fallback address. - GridPool identity/state, SV2 keys, adapter token, and proof spool are included in StartOS backups. - DATUM and raw Stratum V1 are intentionally not packaged. +- The appliance exports native SV2 TCP and authenticated GridPool UDP relay. + Bitcoin RPC/ZMQ, adapter APIs, SV2 monitoring, and the UI backend remain + private package interfaces. + +## Backup, upgrade, and recovery + +The StartOS `main` volume is the complete GridPool appliance backup unit. It +contains the payout setting, GridPool identity and consensus state, native-SV2 +authority keys, adapter token, and durable proof spool. Bitcoin chain data is +owned by the Bitcoin dependency and is not duplicated. + +Before every sideload upgrade: + +1. Create a StartOS backup containing GridPool and verify that the backup is + listed on the configured target. +2. Record the GridPool node-ID fingerprint and installed package version. +3. Sideload the new package over the existing installation; do not uninstall + first. +4. Confirm the node ID, payout setting, Bitcoin authority, and SV2 health after + startup. + +For disaster recovery, install the same or a compatible package version and use +StartOS **Restore From Backup**. A successful restore must preserve the node ID +and SV2 authority. Never resolve a state mismatch by deleting the `main` volume. + +A normal uninstall deletes service data. Use a verified encrypted backup first; +`start-cli package uninstall --soft gridpool` is reserved for controlled package +lifecycle testing where preserving the volume is intentional. + +## Release verification + +```bash +npm ci +npm run check +npm run build +./scripts/verify-release-inputs.sh +./scripts/audit-build-dependencies.sh +``` + +Start SDK 2.0.9 currently bundles high-severity parser advisories in its ESLint +build-tool subtree. The audit script permits only those exact non-runtime paths +and verifies they do not enter the generated procedure bundle. Any runtime or +unrelated high/critical advisory fails CI. + +StartOS cross-architecture packing cannot currently consume a multi-arch image +by index-digest reference. The manifest therefore uses commit-addressed `sha-*` +tags, while `release-images.json` locks their OCI index digests and the release +build verifies each tag still resolves to the expected digest before packing. diff --git a/release-images.json b/release-images.json new file mode 100644 index 0000000..417e608 --- /dev/null +++ b/release-images.json @@ -0,0 +1,10 @@ +{ + "gridpool": { + "reference": "ghcr.io/gridlabs-science/boot-protocol:sha-9ac862a", + "indexDigest": "sha256:1a302f3d1a1de3df7b26a4cde6372bf31d453bd024ca697bd6f0a1a09c90b601" + }, + "sv2": { + "reference": "ghcr.io/gridlabs-science/gridpool-sv2-pool:sha-36465d2", + "indexDigest": "sha256:3d0279df1ccd1d38ed6fbfb3909fd4c7423f51ade7d1263316f5dc86e96d8d5f" + } +} diff --git a/scripts/audit-build-dependencies.sh b/scripts/audit-build-dependencies.sh new file mode 100755 index 0000000..6106181 --- /dev/null +++ b/scripts/audit-build-dependencies.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +set -euo pipefail + +report="${1:-npm-audit.json}" +npm audit --omit=dev --json > "$report" || true + +# Start SDK 2.0.9 bundles these parsers for its own build tooling. Permit only +# findings inside that subtree and verify they never enter packaged procedures. +jq -e ' + [ + .vulnerabilities + | to_entries[] + | select(.value.severity == "high" or .value.severity == "critical") + | .value.nodes[] + | select(startswith("node_modules/@start9labs/start-sdk/node_modules/") | not) + ] | length == 0 +' "$report" >/dev/null + +if [[ -d javascript ]] && rg -q 'brace-expansion|js-yaml|YAMLException' javascript; then + echo "vulnerable Start SDK build-only parser entered the packaged procedure bundle" >&2 + exit 1 +fi + +echo "Dependency audit contains only documented Start SDK 2.0.9 build-tool advisories." diff --git a/scripts/verify-release-inputs.sh b/scripts/verify-release-inputs.sh new file mode 100755 index 0000000..5c62d0a --- /dev/null +++ b/scripts/verify-release-inputs.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +set -euo pipefail + +manifest="startos/manifest/index.ts" +lock="release-images.json" + +for component in gridpool sv2; do + reference="$(jq -r --arg component "$component" '.[$component].reference' "$lock")" + expected="$(jq -r --arg component "$component" '.[$component].indexDigest' "$lock")" + grep -Fq "$reference" "$manifest" + [[ "$reference" =~ :sha-[0-9a-f]{7,40}$ ]] + [[ "$expected" =~ ^sha256:[0-9a-f]{64}$ ]] + if command -v docker >/dev/null && docker buildx version >/dev/null 2>&1; then + actual="$(docker buildx imagetools inspect "$reference" --format '{{json .Manifest}}' | jq -r '.digest // .Digest')" + [[ "$actual" == "$expected" ]] || { + echo "$component image digest mismatch: expected $expected, got $actual" >&2 + exit 1 + } + inspection="$(docker buildx imagetools inspect "$reference")" + grep -q 'linux/amd64' <<<"$inspection" + grep -q 'linux/arm64' <<<"$inspection" + fi +done + +! git ls-files '*.s9pk' '*.s9pk.sha256' | grep -q . +grep -q "ed25519_private_key.*x25519_private_key\|x25519_private_key.*ed25519_private_key" startos/main.ts || \ + grep -q "\['ed25519_private_key', 'x25519_private_key'\]" startos/main.ts +grep -q "GridPool UDP Relay" startos/interfaces.ts +grep -q "preferredExternalPort: udpRelayPort" startos/interfaces.ts +! grep -Eq 'preferredExternalPort: (8332|28332|28333|34290|5000)' startos/interfaces.ts +grep -q '^[[:space:]]*bootConfigPath,$' startos/main.ts +grep -A4 '^[[:space:]]*bootConfigPath,$' startos/main.ts | grep -q "mode: 0o600" +grep -q "writeFile(tokenPath.*mode: 0o600" startos/main.ts +echo "release image tags resolve to locked OCI digests and package artifacts are untracked" diff --git a/startos/interfaces.ts b/startos/interfaces.ts index b84d8eb..385b9fc 100644 --- a/startos/interfaces.ts +++ b/startos/interfaces.ts @@ -1,5 +1,5 @@ import { sdk } from './sdk' -import { sv2Port, uiPort } from './utils' +import { sv2Port, udpRelayPort, uiPort } from './utils' export const setInterfaces = sdk.setupInterfaces(async ({ effects }) => { const uiOrigin = await sdk.MultiHost.of(effects, 'ui').bindPort(uiPort, { @@ -35,5 +35,30 @@ export const setInterfaces = sdk.setupInterfaces(async ({ effects }) => { query: {}, }) - return [await uiOrigin.export([ui]), await sv2Origin.export([sv2])] + const relayOrigin = await sdk.MultiHost.of(effects, 'gridpool-relay').bindPort( + udpRelayPort, + { + protocol: null, + addSsl: null, + preferredExternalPort: udpRelayPort, + secure: { ssl: false }, + }, + ) + const relay = sdk.createInterface(effects, { + name: 'GridPool UDP Relay', + id: 'gridpool-relay', + description: 'Authenticated GridPool proof and chain-tip relay', + type: 'p2p', + masked: false, + schemeOverride: { ssl: null, noSsl: 'gridpool-udp' }, + username: null, + path: '', + query: {}, + }) + + return [ + await uiOrigin.export([ui]), + await sv2Origin.export([sv2]), + await relayOrigin.export([relay]), + ] }) diff --git a/startos/main.ts b/startos/main.ts index 605943f..6f94fd6 100644 --- a/startos/main.ts +++ b/startos/main.ts @@ -13,7 +13,7 @@ import { } from 'node:fs/promises' import { settingsJson } from './fileModels/settings.json' import { sdk } from './sdk' -import { bitcoinMount, sv2Port, uiPort } from './utils' +import { bitcoinMount, sv2Port, udpRelayPort, uiPort } from './utils' const volumeRoot = '/media/startos/volumes/main' @@ -109,6 +109,24 @@ export const main = sdk.setupMain(async ({ effects }) => { throw new Error('Stored SV2 authority keypair is malformed') } + const bootConfigPath = `${volumeRoot}/gridpool/boot_portal_config.json` + let persistedIdentity: Record = {} + try { + const existing = JSON.parse(await readFile(bootConfigPath, 'utf8')) as Record< + string, + unknown + > + persistedIdentity = Object.fromEntries( + ['ed25519_private_key', 'x25519_private_key'] + .filter( + (key) => typeof existing[key] === 'string' && existing[key] !== '', + ) + .map((key) => [key, existing[key] as string]), + ) + } catch { + // First start has no identity yet. The node creates and persists it here. + } + const bootConfig = { bitcoin_notification_mode: 'attached-node', NotificationSource: 'BitcoinZmq', @@ -137,9 +155,10 @@ export const main = sdk.setupMain(async ({ effects }) => { local_adapter_token_file: '/data/shared/local-adapter.token', local_sv2_api_url: 'http://127.0.0.1:34290/api/v1/global', enable_admin_api: false, + ...persistedIdentity, } await writeFile( - `${volumeRoot}/gridpool/boot_portal_config.json`, + bootConfigPath, `${JSON.stringify(bootConfig, null, 2)}\n`, { mode: 0o600 }, ) @@ -288,6 +307,17 @@ min_interval = 5 }, requires: ['gridpool'], }) + .addHealthCheck('gridpool-udp', { + ready: { + display: 'GridPool UDP Relay', + fn: () => + sdk.healthCheck.checkPortListening(effects, udpRelayPort, { + successMessage: 'GridPool UDP relay is listening', + errorMessage: 'GridPool UDP relay is not listening', + }), + }, + requires: ['gridpool'], + }) .addDaemon('sv2', { subcontainer: sv2Sub, exec: { diff --git a/startos/utils.ts b/startos/utils.ts index 1bf3268..dcafbc7 100644 --- a/startos/utils.ts +++ b/startos/utils.ts @@ -1,3 +1,4 @@ export const uiPort = 5000 export const sv2Port = 34265 +export const udpRelayPort = 5001 export const bitcoinMount = '/mnt/bitcoin' diff --git a/startos/versions/current.ts b/startos/versions/current.ts index c885460..450c5dc 100644 --- a/startos/versions/current.ts +++ b/startos/versions/current.ts @@ -1,13 +1,13 @@ import { IMPOSSIBLE, VersionInfo } from '@start9labs/start-sdk' export const current = VersionInfo.of({ - version: '0.1.0:15', + version: '0.1.0:17', releaseNotes: { en_US: - 'Isolates and coalesces dashboard reads so the Web UI cannot exhaust public API rate limits.', + 'Preserves GridPool node identity across package regeneration and adds release-candidate lifecycle checks.', }, migrations: { up: async () => {}, down: IMPOSSIBLE, }, -}) +}).satisfies('0.1.0:16') From 5227738e68f3a70046796f78733bc06ddc88d002 Mon Sep 17 00:00:00 2001 From: keegreil Date: Wed, 19 Aug 2026 00:14:29 -0400 Subject: [PATCH 2/5] ci: baseline audited field-name findings --- .gitleaksignore | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .gitleaksignore diff --git a/.gitleaksignore b/.gitleaksignore new file mode 100644 index 0000000..7f03f21 --- /dev/null +++ b/.gitleaksignore @@ -0,0 +1,3 @@ +# Audited field-name false positives; no secret values are present. +b0b7295168dbeec7b097be3bf5319f3888353958:startos/main.ts:generic-api-key:120 +b0b7295168dbeec7b097be3bf5319f3888353958:scripts/verify-release-inputs.sh:generic-api-key:27 From 9902a888842a87155cb21e2dac8ece7ddf3223fe Mon Sep 17 00:00:00 2001 From: keegreil Date: Wed, 19 Aug 2026 00:24:16 -0400 Subject: [PATCH 3/5] release: bump StartOS UDP relay candidate --- startos/versions/current.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/startos/versions/current.ts b/startos/versions/current.ts index 450c5dc..8e66d49 100644 --- a/startos/versions/current.ts +++ b/startos/versions/current.ts @@ -1,13 +1,13 @@ import { IMPOSSIBLE, VersionInfo } from '@start9labs/start-sdk' export const current = VersionInfo.of({ - version: '0.1.0:17', + version: '0.1.0:18', releaseNotes: { en_US: - 'Preserves GridPool node identity across package regeneration and adds release-candidate lifecycle checks.', + 'Exports the authenticated UDP relay and adds release-candidate lifecycle and supply-chain checks.', }, migrations: { up: async () => {}, down: IMPOSSIBLE, }, -}).satisfies('0.1.0:16') +}).satisfies('0.1.0:17') From 126fd8a0130ffa4d942e2907ffabc6183a7b32b0 Mon Sep 17 00:00:00 2001 From: keegreil Date: Wed, 19 Aug 2026 00:46:58 -0400 Subject: [PATCH 4/5] release: pin fail-safe native SV2 image --- release-images.json | 4 ++-- startos/manifest/index.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/release-images.json b/release-images.json index 417e608..c4d63cc 100644 --- a/release-images.json +++ b/release-images.json @@ -4,7 +4,7 @@ "indexDigest": "sha256:1a302f3d1a1de3df7b26a4cde6372bf31d453bd024ca697bd6f0a1a09c90b601" }, "sv2": { - "reference": "ghcr.io/gridlabs-science/gridpool-sv2-pool:sha-36465d2", - "indexDigest": "sha256:3d0279df1ccd1d38ed6fbfb3909fd4c7423f51ade7d1263316f5dc86e96d8d5f" + "reference": "ghcr.io/gridlabs-science/gridpool-sv2-pool:sha-af3389a", + "indexDigest": "sha256:6bea980aa517f308ee176d1a85d5e274e69a92fe99df1b321a7a720ae5db3e70" } } diff --git a/startos/manifest/index.ts b/startos/manifest/index.ts index 585ef9c..e0d61d3 100644 --- a/startos/manifest/index.ts +++ b/startos/manifest/index.ts @@ -21,7 +21,7 @@ export const manifest = setupManifest({ sv2: { source: { dockerTag: - 'ghcr.io/gridlabs-science/gridpool-sv2-pool:sha-36465d2', + 'ghcr.io/gridlabs-science/gridpool-sv2-pool:sha-af3389a', }, arch: ['x86_64', 'aarch64'], }, From c0c99dc33494ba75ca0058dd2ecc9e6a59b6ea93 Mon Sep 17 00:00:00 2001 From: keegreil Date: Thu, 20 Aug 2026 21:46:10 -0400 Subject: [PATCH 5/5] release: pin security-updated native SV2 image --- release-images.json | 4 ++-- startos/manifest/index.ts | 2 +- startos/versions/current.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/release-images.json b/release-images.json index c4d63cc..2419e91 100644 --- a/release-images.json +++ b/release-images.json @@ -4,7 +4,7 @@ "indexDigest": "sha256:1a302f3d1a1de3df7b26a4cde6372bf31d453bd024ca697bd6f0a1a09c90b601" }, "sv2": { - "reference": "ghcr.io/gridlabs-science/gridpool-sv2-pool:sha-af3389a", - "indexDigest": "sha256:6bea980aa517f308ee176d1a85d5e274e69a92fe99df1b321a7a720ae5db3e70" + "reference": "ghcr.io/gridlabs-science/gridpool-sv2-pool:sha-1151f92", + "indexDigest": "sha256:3de54e9c51953d2b5089ae90a25b9a0fa046c18c4ae0139b768b77a4cf0ed50e" } } diff --git a/startos/manifest/index.ts b/startos/manifest/index.ts index e0d61d3..ba909d8 100644 --- a/startos/manifest/index.ts +++ b/startos/manifest/index.ts @@ -21,7 +21,7 @@ export const manifest = setupManifest({ sv2: { source: { dockerTag: - 'ghcr.io/gridlabs-science/gridpool-sv2-pool:sha-af3389a', + 'ghcr.io/gridlabs-science/gridpool-sv2-pool:sha-1151f92', }, arch: ['x86_64', 'aarch64'], }, diff --git a/startos/versions/current.ts b/startos/versions/current.ts index 8e66d49..09f963f 100644 --- a/startos/versions/current.ts +++ b/startos/versions/current.ts @@ -1,7 +1,7 @@ import { IMPOSSIBLE, VersionInfo } from '@start9labs/start-sdk' export const current = VersionInfo.of({ - version: '0.1.0:18', + version: '0.1.0:19', releaseNotes: { en_US: 'Exports the authenticated UDP relay and adds release-candidate lifecycle and supply-chain checks.',