Skip to content

Commit f5d4fba

Browse files
SecAI-Hubclaude
andcommitted
M49: Signed-first install path — eliminate unverified bootstrap
Production installs now use secai-bootstrap.sh which configures the container signing policy (policy.json + registries.d + cosign public key) BEFORE the first rpm-ostree rebase, so the signed transport is used from day one. The unverified recovery path is moved to a separate doc. CI now publishes the image digest for pinned installs, and a first-boot setup wizard guides users through integrity verification, vault setup, and health checks. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 1e5d7e1 commit f5d4fba

13 files changed

Lines changed: 1026 additions & 94 deletions

File tree

.github/workflows/build.yml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,3 +63,26 @@ jobs:
6363
"$IMAGE_REF"
6464
env:
6565
COSIGN_PRIVATE_KEY: ${{ secrets.SIGNING_SECRET }}
66+
67+
# Publish the image digest so users can pin installs to an exact build.
68+
# The digest appears in the workflow summary and as an artifact.
69+
- name: Extract and publish image digest
70+
if: github.event_name != 'pull_request'
71+
id: digest
72+
run: |
73+
DIGEST=$(skopeo inspect "docker://${IMAGE_REF}:latest" 2>/dev/null | jq -r '.Digest' || echo "")
74+
if [ -z "$DIGEST" ] || [ "$DIGEST" = "null" ]; then
75+
echo "WARNING: Could not extract image digest"
76+
echo "digest=unknown" >> "$GITHUB_OUTPUT"
77+
else
78+
echo "digest=${DIGEST}" >> "$GITHUB_OUTPUT"
79+
echo "${DIGEST}" > IMAGE_DIGEST
80+
echo "## Image Digest" >> "$GITHUB_STEP_SUMMARY"
81+
echo "" >> "$GITHUB_STEP_SUMMARY"
82+
echo "Pinned install reference:" >> "$GITHUB_STEP_SUMMARY"
83+
echo '```' >> "$GITHUB_STEP_SUMMARY"
84+
echo "sudo bash secai-bootstrap.sh --digest ${DIGEST}" >> "$GITHUB_STEP_SUMMARY"
85+
echo '```' >> "$GITHUB_STEP_SUMMARY"
86+
echo "" >> "$GITHUB_STEP_SUMMARY"
87+
echo "Full image ref: \`${IMAGE_REF}@${DIGEST}\`" >> "$GITHUB_STEP_SUMMARY"
88+
fi

.github/workflows/release.yml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,23 @@ jobs:
141141
env:
142142
COSIGN_PRIVATE_KEY: ${{ secrets.SIGNING_SECRET }}
143143

144+
- name: Record release image digest
145+
run: |
146+
IMAGE_REF="ghcr.io/${{ github.repository }}"
147+
TAG="${{ github.ref_name }}"
148+
DIGEST=$(skopeo inspect "docker://${IMAGE_REF}:${TAG}" 2>/dev/null | jq -r '.Digest' || echo "")
149+
if [ -n "$DIGEST" ] && [ "$DIGEST" != "null" ]; then
150+
echo "${DIGEST}" > dist/IMAGE_DIGEST
151+
echo "${IMAGE_REF}@${DIGEST}" > dist/IMAGE_REF_PINNED
152+
echo "## Install with digest pinning" >> "$GITHUB_STEP_SUMMARY"
153+
echo '```bash' >> "$GITHUB_STEP_SUMMARY"
154+
echo "sudo bash secai-bootstrap.sh --digest ${DIGEST}" >> "$GITHUB_STEP_SUMMARY"
155+
echo '```' >> "$GITHUB_STEP_SUMMARY"
156+
else
157+
echo "WARNING: Could not extract image digest for tag ${TAG}"
158+
echo "unknown" > dist/IMAGE_DIGEST
159+
fi
160+
144161
- name: Create GitHub Release
145162
if: ${{ !inputs.dry_run }}
146163
uses: softprops/action-gh-release@da05d552573ad5aba039eaac05058a918a7bf631 # v2.2.2
@@ -150,5 +167,7 @@ jobs:
150167
dist/*-sbom.cdx.json
151168
dist/SHA256SUMS
152169
dist/SHA256SUMS.sig
170+
dist/IMAGE_DIGEST
171+
dist/IMAGE_REF_PINNED
153172
generate_release_notes: true
154173
fail_on_unmatched_files: false

README.md

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -49,24 +49,26 @@ Built on [uBlue](https://universal-blue.org/) (Fedora Atomic / Silverblue). All
4949
### Install (Fedora Atomic)
5050

5151
```bash
52-
# 1. Verify image signature BEFORE installing (mandatory)
53-
cosign verify --key cosign.pub ghcr.io/sec_ai/secai_os:latest
52+
# 1. Download and review the signed bootstrap script
53+
curl -sSfL https://raw.githubusercontent.com/SecAI-Hub/SecAI_OS/main/files/scripts/secai-bootstrap.sh \
54+
-o /tmp/secai-bootstrap.sh
55+
less /tmp/secai-bootstrap.sh
5456

55-
# 2. Rebase to the signed image
56-
sudo rpm-ostree rebase ostree-image-signed:docker://ghcr.io/sec_ai/secai_os:latest
57-
sudo systemctl reboot
57+
# 2. Run the bootstrap (configures signing policy + verified rebase)
58+
# Use --digest from the latest release for production installs
59+
sudo bash /tmp/secai-bootstrap.sh --digest sha256:RELEASE_DIGEST
5860

59-
# 3. Set up encrypted vault
60-
sudo /usr/libexec/secure-ai/setup-vault.sh /dev/sdX
61+
# 3. Reboot and run the setup wizard
62+
sudo systemctl reboot
63+
sudo /usr/libexec/secure-ai/secai-setup-wizard.sh
6164
```
6265

63-
> **First install on a fresh Fedora Silverblue?** The signed transport requires that
64-
> the signing policy is already configured. On a fresh install, see
65-
> [docs/install/bare-metal.md](docs/install/bare-metal.md) for the one-time bootstrap
66-
> procedure (uses cosign verification + a single unverified pull, then locks to signed
67-
> transport permanently).
66+
The bootstrap script verifies the image signature and configures the signing policy
67+
**before** the rebase, so the first pull uses the signed transport — no unverified
68+
pull is ever performed. See the [latest release](https://github.com/SecAI-Hub/SecAI_OS/releases/latest)
69+
for the digest, or omit `--digest` for evaluation.
6870

69-
See [docs/install/](docs/install/) for detailed guides: [bare metal](docs/install/bare-metal.md) | [virtual machine](docs/install/vm.md) | [development](docs/install/dev.md)
71+
See [docs/install/](docs/install/) for detailed guides: [bare metal](docs/install/bare-metal.md) | [virtual machine](docs/install/vm.md) | [development](docs/install/dev.md) | [recovery](docs/install/recovery-bootstrap.md)
7072

7173
### Get Your First Model
7274

@@ -156,7 +158,7 @@ Every model passes through the same fully automatic pipeline:
156158
| **Updates** | Cosign-verified rpm-ostree, staged workflow, greenboot auto-rollback |
157159
| **Supply Chain** | Per-service CycloneDX SBOMs, SLSA3 provenance attestation, cosign-signed checksums |
158160

159-
See [docs/threat-model.md](docs/threat-model.md) for threat classes, residual risks, and security invariants. See [docs/security-status.md](docs/security-status.md) for implementation status of all 48 milestones.
161+
See [docs/threat-model.md](docs/threat-model.md) for threat classes, residual risks, and security invariants. See [docs/security-status.md](docs/security-status.md) for implementation status of all 49 milestones.
160162

161163
### Verify Image Signatures
162164

@@ -239,7 +241,7 @@ All CI jobs are defined in [`.github/workflows/ci.yml`](.github/workflows/ci.yml
239241
| [Threat Model](docs/threat-model.md) | Threat classes, invariants, residual risks |
240242
| [API Reference](docs/api.md) | HTTP API for all services |
241243
| [Policy Schema](docs/policy-schema.md) | Full policy.yaml schema reference |
242-
| [Security Status](docs/security-status.md) | Implementation status of all 48 milestones |
244+
| [Security Status](docs/security-status.md) | Implementation status of all 49 milestones |
243245
| [Test Matrix](docs/test-matrix.md) | Test coverage: 1,141 tests across Go and Python (see [test-counts.json](docs/test-counts.json)) |
244246
| [Compatibility Matrix](docs/compatibility-matrix.md) | GPU, VM, and hardware support |
245247
| [Security Test Matrix](docs/security-test-matrix.md) | Security feature test coverage |
@@ -376,7 +378,7 @@ See [docs/test-matrix.md](docs/test-matrix.md) for full breakdown.
376378
## Roadmap
377379

378380
<details>
379-
<summary>All 47 project milestones (click to expand)</summary>
381+
<summary>All 49 project milestones (click to expand)</summary>
380382

381383
- [x] **Milestone 0** -- Threat model, dataflow, invariants, policy files
382384
- [x] **Milestone 1** -- Bootable OS, encrypted vault, GPU drivers
@@ -427,6 +429,7 @@ See [docs/test-matrix.md](docs/test-matrix.md) for full breakdown.
427429
- [x] **Milestone 46** -- Operational maturity: bootstrap trust gap fix (cosign verify before rebase), CI runs on all changes (removed paths-ignore for .md), Python quality gates (ruff + bandit + split test suites), docs-validation CI job, production-readiness checklist, SLOs, release channel policy, support lifecycle, sample verification output
428430
- [x] **Milestone 47** -- CI enforcement hardening: enforced vulnerability scanning (govulncheck + pip-audit + bandit fail on HIGH/HIGH) with waiver mechanism, mypy type checking for security-sensitive services, pinned reproducible Python CI dependencies, Go 1.23→1.25 (12 stdlib CVE fixes), verification-first bootstrap docs
429431
- [x] **Milestone 48** -- Production hardening: build script fail-closed (fatal errors for 12 required services + binary verification gate), incident store fsync (crash-safe persistence), GPU backend metadata recording, llama-server watchdog (Type=notify + WatchdogSec=30), model catalog externalization (YAML with fallback), circuit breaker for inter-service HTTP calls, post-upgrade model verification in Greenboot, cosign key rotation documentation (full lifecycle)
432+
- [x] **Milestone 49** -- Signed-first install path: bootstrap script configures signing policy before first rebase (eliminates unverified transport), digest-pinned install flow (CI publishes digests in build summary + release assets), first-boot setup wizard (interactive integrity verification + vault + TPM2 + health check), recovery/dev path separated into dedicated doc
430433

431434
</details>
432435

docs/install/bare-metal.md

Lines changed: 44 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -67,117 +67,85 @@ Replace `/dev/sdX` or `/dev/rdiskN` with your actual USB device. Double-check th
6767

6868
After booting into the fresh Fedora Silverblue installation, open a terminal.
6969

70-
### 4a. Verify image signature (mandatory)
70+
### Production Install (Recommended)
7171

72-
Before installing the image, verify its authenticity using cosign.
73-
**Do not skip this step — it is the cryptographic attestation that the
74-
image you are about to install was built by the SecAI project.**
72+
The bootstrap script configures the container signing policy **before** pulling the image, so the very first rebase uses the signed transport. No unverified pull is ever performed.
7573

7674
```bash
77-
# Install cosign (if not already present)
78-
sudo dnf install -y cosign
75+
# 1. Download the bootstrap script
76+
curl -sSfL https://raw.githubusercontent.com/SecAI-Hub/SecAI_OS/main/files/scripts/secai-bootstrap.sh \
77+
-o /tmp/secai-bootstrap.sh
7978

80-
# Fetch the project's public key
81-
curl -sSfL https://raw.githubusercontent.com/SecAI-Hub/SecAI_OS/main/cosign.pub -o /tmp/cosign.pub
79+
# 2. Review the script before running (ALWAYS review downloaded scripts)
80+
less /tmp/secai-bootstrap.sh
8281

83-
# Verify the image signature — STOP if this fails
84-
cosign verify --key /tmp/cosign.pub ghcr.io/sec_ai/secai_os:latest
82+
# 3. Run the bootstrap (use the digest from the latest release for production)
83+
sudo bash /tmp/secai-bootstrap.sh --digest sha256:RELEASE_DIGEST
8584
```
8685

87-
You must see `The following checks were performed on each of these signatures: ...`
88-
with a successful verification result. **Do not proceed if verification fails.**
86+
> **Where do I find the digest?** Check the
87+
> [latest release](https://github.com/SecAI-Hub/SecAI_OS/releases/latest)
88+
> for the `IMAGE_DIGEST` asset, or the build workflow summary.
89+
> For evaluation, you can omit `--digest` to use `:latest`.
8990
90-
### 4b. First-time bootstrap (fresh Fedora Silverblue only)
91+
The script will:
9192

92-
> **Why an unverified pull?** A fresh Fedora Silverblue installation does
93-
> not yet have the SecAI signing policy in its local ostree store. The very
94-
> first rebase therefore uses `ostree-unverified-registry:` as a one-time
95-
> bootstrapping step. This is safe because you verified the image signature
96-
> out-of-band in step 4a above. After this single unverified pull, all
97-
> future updates use the signed transport and are verified automatically
98-
> by rpm-ostree.
99-
>
100-
> **Risk acknowledgment:** If you skipped step 4a, this unverified pull
101-
> has no integrity guarantee. Go back and run the cosign verification first.
93+
1. Install cosign (if needed) and fetch the SecAI public signing key
94+
2. Verify the key's SHA256 fingerprint against a hardcoded value
95+
3. Configure the signing policy on your system (`policy.json` + `registries.d`)
96+
4. Verify the image signature using cosign
97+
5. Rebase using the **signed** transport (`ostree-image-signed:docker://`)
98+
6. Prompt you to reboot
10299

103-
```bash
104-
# One-time unverified pull (safe ONLY because you verified the signature in 4a)
105-
sudo rpm-ostree rebase ostree-unverified-registry:ghcr.io/sec_ai/secai_os:latest
106-
sudo systemctl reboot
107-
```
108-
109-
### 4c. Lock to signed transport (mandatory)
110-
111-
Immediately after the first reboot, switch to the signed image transport.
112-
**This step is not optional** — it ensures all future updates are
113-
cryptographically verified by rpm-ostree before they are applied.
100+
After the script completes:
114101

115102
```bash
116-
# Lock to signed transport — all future updates verified automatically
117-
sudo rpm-ostree rebase ostree-image-signed:docker://ghcr.io/sec_ai/secai_os:latest
118103
sudo systemctl reboot
119104
```
120105

121-
After this reboot, the system is running SecAI OS with full signature
122-
verification enabled. All subsequent `rpm-ostree upgrade` commands will
123-
reject unsigned or tampered images.
124-
125-
### Returning users / existing SecAI OS installs
106+
### Returning Users / Existing SecAI OS Installs
126107

127108
If you are upgrading an existing SecAI OS installation (already on the
128109
signed transport), simply run:
129110

130111
```bash
131-
cosign verify --key /path/to/cosign.pub ghcr.io/sec_ai/secai_os:latest
132112
sudo rpm-ostree upgrade
133113
sudo systemctl reboot
134114
```
135115

136-
---
137-
138-
## Step 5: Set Up the Encrypted Vault
116+
All upgrades are automatically verified against the cosign signing key
117+
baked into the image.
139118

140-
On first boot after rebasing, the firstboot script runs automatically. It will:
119+
### Recovery / Development Install
141120

142-
1. Create the encrypted vault partition at `/var/lib/secure-ai/vault` (if not already present).
143-
2. Initialize the registry manifest.
144-
3. Set up systemd service dependencies.
145-
4. Configure nftables firewall rules.
146-
5. Run Greenboot health checks.
147-
148-
You will be prompted to set a vault passphrase. This passphrase encrypts the LUKS volume that stores your models and configuration. Store it securely -- there is no recovery mechanism.
121+
> **WARNING**: The recovery path uses an unverified container transport.
122+
> Use it **only** when the signing policy is broken or for development/CI.
123+
> See [Recovery Bootstrap](recovery-bootstrap.md) for instructions.
149124
150125
---
151126

152-
## Step 6: First Boot Verification
127+
## Step 5: First-Boot Setup Wizard
153128

154-
After firstboot completes, run the automated health check:
129+
After rebooting into SecAI OS, run the interactive setup wizard:
155130

156131
```bash
157-
# Comprehensive health check (validates all services, endpoints, security posture)
158-
sudo /usr/libexec/secure-ai/first-boot-check.sh
132+
sudo /usr/libexec/secure-ai/secai-setup-wizard.sh
159133
```
160134

161-
This validates all core services are running, health endpoints respond, attestation
162-
state is verified, no open incidents exist, and no services are exposed on public
163-
interfaces. See [docs/production-operations.md](../production-operations.md) for details.
164-
165-
You can also verify manually:
166-
167-
```bash
168-
# Check that all services are running
169-
systemctl status secure-ai-registry
170-
systemctl status secure-ai-tool-firewall
171-
systemctl status secure-ai-ui
135+
The wizard walks you through:
172136

173-
# Check firewall rules
174-
sudo nft list ruleset
137+
1. **System identity** — OS version, deployment origin, Secure Boot + TPM2 status
138+
2. **Image integrity** — Cosign signature verification of the running image
139+
3. **Transport check** — Confirms you are on signed transport (offers to switch if not)
140+
4. **Vault setup** — Creates the encrypted LUKS volume for models and secrets
141+
5. **TPM2 sealing** (optional) — Seals the vault key to TPM2 PCRs for auto-unlock on trusted boots
142+
6. **Health check** — Validates all services are running and endpoints are reachable
143+
7. **Summary** — Security posture card and next steps
175144

176-
# Check vault status
177-
curl http://localhost:8480/api/vault/status
145+
You can also run the health check independently at any time:
178146

179-
# Open the UI
180-
xdg-open http://localhost:8480
147+
```bash
148+
sudo /usr/libexec/secure-ai/first-boot-check.sh
181149
```
182150

183151
---
@@ -220,3 +188,5 @@ nvidia-smi
220188
```bash
221189
sudo cryptsetup status secure-ai-vault
222190
```
191+
192+
**Bootstrap script fails:** See [Recovery Bootstrap](recovery-bootstrap.md) for the manual fallback procedure.

0 commit comments

Comments
 (0)