Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions .github/actions/setup-nix/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: setup-nix
description: >
Install Nix (Determinate) and, if the SourceOS GCS binary cache is configured,
add it as a substituter. Replaces the sunset magic-nix-cache-action. Graceful:
with no cache vars set it just installs Nix (falls back to cache.nixos.org).
inputs:
cache-url:
description: "Public HTTPS base of the GCS Nix cache, e.g. https://storage.googleapis.com/<bucket>/nix-cache"
required: false
default: ""
cache-pubkey:
description: "Trusted public key for the cache (form name:base64). Pair with the push secret."
required: false
default: ""
runs:
using: composite
steps:
- name: Compose nix extra-conf (cache substituter, if configured)
id: conf
shell: bash
run: |
if [ -n "${{ inputs.cache-url }}" ] && [ -n "${{ inputs.cache-pubkey }}" ]; then
{
echo 'extra-conf<<NIXEOF'
echo "extra-substituters = ${{ inputs.cache-url }}"
echo "extra-trusted-public-keys = ${{ inputs.cache-pubkey }}"
echo 'NIXEOF'
} >> "$GITHUB_OUTPUT"
echo "[setup-nix] GCS cache substituter enabled"
else
echo 'extra-conf=' >> "$GITHUB_OUTPUT"
echo "[setup-nix] no cache vars — using cache.nixos.org only"
fi
- uses: DeterminateSystems/nix-installer-action@v14
with:
extra-conf: ${{ steps.conf.outputs.extra-conf }}
15 changes: 13 additions & 2 deletions .github/workflows/build-custom.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,10 @@ jobs:
permissions: { contents: read, id-token: write }
steps:
- uses: actions/checkout@v4
- uses: DeterminateSystems/nix-installer-action@v14
- uses: DeterminateSystems/magic-nix-cache-action@v8
- uses: ./.github/actions/setup-nix
with:
cache-url: ${{ vars.NIX_CACHE_URL }}
cache-pubkey: ${{ vars.NIX_CACHE_PUBKEY }}
- name: Free disk space
run: sudo rm -rf /usr/share/dotnet /opt/ghc /usr/local/lib/android || true; df -h

Expand All @@ -56,11 +58,20 @@ jobs:

- name: Build custom image
id: build
env:
NIX_CACHE_BUCKET: ${{ vars.NIX_CACHE_BUCKET }}
NIX_CACHE_SECRET_KEY: ${{ secrets.NIX_CACHE_SECRET_KEY }}
# Self-managed signing key (minisign, no third-party authority). If
# the secret is unset the build still produces unsigned provenance.
SOURCEOS_SIGN_SECRET_KEY: ${{ secrets.SOURCEOS_SIGN_SECRET_KEY }}
SOURCEOS_SIGN_PUBKEY: ${{ vars.SOURCEOS_SIGN_PUBKEY }}
run: |
PREFIX="gs://${GCS_BUCKET}/user-builds/${{ github.event.inputs.uid }}/${{ github.event.inputs.build_id }}"
printf '%s' '${{ github.event.inputs.spec }}' > /tmp/spec.json
SPEC_FILE=/tmp/spec.json OUT=out GCS_PREFIX="$PREFIX" \
TARGET="${{ github.event.inputs.target || 'iso' }}" \
BUILD_ID="${{ github.event.inputs.build_id }}" BUILD_UID="${{ github.event.inputs.uid }}" \
VERSION="${{ vars.SOURCEOS_VERSION || '26.11' }}" SOURCE_REV="${GITHUB_SHA}" \
bash scripts/build-custom-image.sh
echo "artifact=$(cat out/artifact-url.txt)" >> "$GITHUB_OUTPUT"

Expand Down
12 changes: 8 additions & 4 deletions .github/workflows/image-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,10 @@ jobs:
| sudo tee /etc/udev/rules.d/99-kvm4all.rules >/dev/null
sudo udevadm control --reload-rules && sudo udevadm trigger --name-match=kvm || true
ls -l /dev/kvm || echo "no /dev/kvm (test will use slower TCG)"
- uses: DeterminateSystems/nix-installer-action@v14
- uses: DeterminateSystems/magic-nix-cache-action@v8
- uses: ./.github/actions/setup-nix
with:
cache-url: ${{ vars.NIX_CACHE_URL }}
cache-pubkey: ${{ vars.NIX_CACHE_PUBKEY }}
- name: Free disk space
run: sudo rm -rf /usr/share/dotnet /opt/ghc /usr/local/lib/android || true; df -h
- name: Run boot test
Expand Down Expand Up @@ -88,8 +90,10 @@ jobs:
timeout-minutes: 90
steps:
- uses: actions/checkout@v4
- uses: DeterminateSystems/nix-installer-action@v14
- uses: DeterminateSystems/magic-nix-cache-action@v8
- uses: ./.github/actions/setup-nix
with:
cache-url: ${{ vars.NIX_CACHE_URL }}
cache-pubkey: ${{ vars.NIX_CACHE_PUBKEY }}
- name: Enable KVM
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' \
Expand Down
6 changes: 4 additions & 2 deletions .github/workflows/nix-build-images.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,10 @@ jobs:
steps:
- uses: actions/checkout@v4

- uses: DeterminateSystems/nix-installer-action@v14
- uses: DeterminateSystems/magic-nix-cache-action@v8
- uses: ./.github/actions/setup-nix
with:
cache-url: ${{ vars.NIX_CACHE_URL }}
cache-pubkey: ${{ vars.NIX_CACHE_PUBKEY }}

- name: Free disk space
run: |
Expand Down
7 changes: 4 additions & 3 deletions .github/workflows/nix-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,10 @@ jobs:
steps:
- uses: actions/checkout@v4

- uses: DeterminateSystems/nix-installer-action@v14

- uses: DeterminateSystems/magic-nix-cache-action@v8
- uses: ./.github/actions/setup-nix
with:
cache-url: ${{ vars.NIX_CACHE_URL }}
cache-pubkey: ${{ vars.NIX_CACHE_PUBKEY }}

- name: nix flake check
run: nix flake check --system x86_64-linux --no-build 2>&1 | tee /tmp/flake-check.log || true
Expand Down
22 changes: 16 additions & 6 deletions .github/workflows/release-images.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,10 @@ jobs:
permissions: { contents: read, id-token: write }
steps:
- uses: actions/checkout@v4
- uses: DeterminateSystems/nix-installer-action@v14
- uses: DeterminateSystems/magic-nix-cache-action@v8
- uses: ./.github/actions/setup-nix
with:
cache-url: ${{ vars.NIX_CACHE_URL }}
cache-pubkey: ${{ vars.NIX_CACHE_PUBKEY }}
- name: Free disk space
run: sudo rm -rf /usr/share/dotnet /opt/ghc /usr/local/lib/android; df -h
- name: Build ISO
Expand All @@ -71,8 +73,10 @@ jobs:
permissions: { contents: read, id-token: write }
steps:
- uses: actions/checkout@v4
- uses: DeterminateSystems/nix-installer-action@v14
- uses: DeterminateSystems/magic-nix-cache-action@v8
- uses: ./.github/actions/setup-nix
with:
cache-url: ${{ vars.NIX_CACHE_URL }}
cache-pubkey: ${{ vars.NIX_CACHE_PUBKEY }}
- name: Free disk space
run: sudo rm -rf /usr/share/dotnet /opt/ghc /usr/local/lib/android || true; df -h
- name: Build ISO
Expand All @@ -97,7 +101,10 @@ jobs:
permissions: { contents: read }
steps:
- uses: actions/checkout@v4
- uses: DeterminateSystems/nix-installer-action@v14
- uses: ./.github/actions/setup-nix
with:
cache-url: ${{ vars.NIX_CACHE_URL }}
cache-pubkey: ${{ vars.NIX_CACHE_PUBKEY }}
- name: Build m1n1 boot bundle
run: |
bash scripts/build-m1n1-bundle.sh out "${{ github.event.inputs.tag || github.ref_name }}"
Expand All @@ -117,7 +124,10 @@ jobs:
permissions: { contents: read }
steps:
- uses: actions/checkout@v4
- uses: DeterminateSystems/nix-installer-action@v14
- uses: ./.github/actions/setup-nix
with:
cache-url: ${{ vars.NIX_CACHE_URL }}
cache-pubkey: ${{ vars.NIX_CACHE_PUBKEY }}
- name: Build SourceOS Asahi OS package
run: |
sudo -E env "PATH=$PATH" bash scripts/build-asahi-package.sh out "${{ github.event.inputs.tag || github.ref_name }}" || \
Expand Down
46 changes: 46 additions & 0 deletions docs/NIX_CACHE_SETUP.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# SourceOS Nix binary cache — one-time setup

Replaces the sunset `magic-nix-cache` (which threw `418 / ResourceExhausted`
throttling on large builds) with a persistent, GCS-backed Nix binary cache.
Builds **pull** warm over public HTTPS and **push** their closures so later
builds — especially custom image builds — don't rebuild from scratch.

Until the steps below are done, the pipelines simply install Nix and fall back
to `cache.nixos.org` (a strict reliability improvement over the throttling
action). The cache turns on once the vars/secret are present — nothing breaks
in the meantime.

## How it works
- **Pull**: `.github/actions/setup-nix` adds `https://storage.googleapis.com/<bucket>/nix-cache`
as an extra substituter (public-read prefix) + the trusted public key.
- **Push**: `scripts/nix-cache-push.sh` copies built store paths to a signed
`file://` cache and `gsutil rsync`s them to `gs://<bucket>/nix-cache/`
(using the workflow's existing WIF GCP auth). `build-custom-image.sh` calls it
after every build, so both the GitHub and GCP build lanes seed the cache.

## One-time setup
1. **Generate a cache key pair** (ed25519, Nix's own format):
```sh
nix-store --generate-binary-cache-key sourceos-nix-cache-1 cache-secret.key cache-public.key
cat cache-public.key # e.g. sourceos-nix-cache-1:AbC123...=
```
2. **Make the cache prefix public-read** (pull is anonymous HTTPS):
```sh
gsutil iam ch allUsers:objectViewer gs://sourceos-artifacts-socioprophet
# (or scope a bucket policy to the nix-cache/ prefix)
```
3. **Set repo Actions variables** (Settings → Secrets and variables → Actions → Variables):
- `NIX_CACHE_URL` = `https://storage.googleapis.com/sourceos-artifacts-socioprophet/nix-cache`
- `NIX_CACHE_PUBKEY` = the `sourceos-nix-cache-1:…` line from step 1
- `NIX_CACHE_BUCKET` = `sourceos-artifacts-socioprophet`
4. **Set the push secret** (Settings → Secrets → Actions → Secrets):
- `NIX_CACHE_SECRET_KEY` = contents of `cache-secret.key`
5. Optional but recommended — **seed the editions** once so the first user
builds are fast: run `build-custom` for desktop/server/edge (the closures get
pushed automatically).

## Notes
- Push is best-effort: missing secret/bucket → no-op, never fails a build.
- The GCP build-VM lane (`gcp-build-custom-startup.sh`) inherits the same env
via instance metadata if you pass `NIX_CACHE_*` there too (follow-up).
- Pull is anonymous, so PRs from forks still benefit from a warm cache.
68 changes: 68 additions & 0 deletions docs/SIGNING_SETUP.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# SourceOS image signing + provenance — one-time setup

Every built image gets a **self-sovereign** signature + provenance bundle. There
is **no third-party signing authority** — no Apple Developer ID, no Google/Play
app signing, no notarization service. The key is an ed25519 keypair *you*
generate with [`minisign`](https://jedisct1.github.io/minisign/); anyone can
verify a download with the public key alone.

## What each build emits (beside the artifact)

| File | Purpose | Referenced by |
|------|---------|---------------|
| `<artifact>.minisig` | ed25519 detached signature | `OSImage.provenance.signatureRef` |
| `<artifact>.intoto.json` | in-toto Statement (the attestation) | `OSImage.provenance.statementRef` (`urn:srcos:attestation:…`) |
| `<artifact>.slsa.json` | SLSA v1 provenance predicate | `OSImage.provenance.slsaPredicateRef` (`urn:srcos:slsa:…`) |
| `<artifact>.sbom.json` | Nix-closure SBOM (ISO only) | `OSImage.provenance.sbomRef` |
| `<artifact>.osimage.json` | `OSImage` doc, sourceos-spec v2 (ISO only) | — |

Graceful: with **no** signing key the build still emits provenance (just
unsigned — `signatureRef` is omitted) and never fails.

## One-time setup

1. **Generate an unencrypted (CI-friendly) keypair.** `-W` writes a key with no
password so CI can use it non-interactively:
```sh
minisign -G -W -p sourceos-pub.key -s sourceos-sec.key
# or, with nix and no minisign installed:
nix run nixpkgs#minisign -- -G -W -p sourceos-pub.key -s sourceos-sec.key
```
Keep `sourceos-sec.key` secret. **Never commit it** (it belongs with the
SOPS material under `/etc/sourceos/`, not in git).

2. **Set the signing secret** (repo → Settings → Secrets and variables →
Actions → Secrets):
- `SOURCEOS_SIGN_SECRET_KEY` = contents of `sourceos-sec.key`

3. **Publish the public key** so users can verify:
- Set Actions variable `SOURCEOS_SIGN_PUBKEY` = the last line of
`sourceos-pub.key` (the `RWQ…` key line).
- Also host `sourceos-pub.key` on the download site so anyone can check a
download without trusting us at fetch time.

4. **GCP build lane** (paid tier): the backend passes the same key to the build
VM as instance metadata `sourceos-sign-secret-key` (plus
`sourceos-version`, and the cache metadata). Unset → unsigned, same as CI.

## Verifying a download

```sh
# pubkey can be the RWQ… line or a key file; or set $SOURCEOS_SIGN_PUBKEY
scripts/verify-image.sh sourceos-desktop-x86_64.iso "RWQ...the-public-key-line..."
```
Exits 0 only if the signature verifies; it also cross-checks the in-toto
subject digest against the file when `<artifact>.intoto.json` is present.

Raw minisign equivalent:
```sh
minisign -V -P "RWQ...pubkey..." -m sourceos-desktop-x86_64.iso # needs the .minisig beside it
```

## Notes
- Rotating the key: generate a new pair, update the secret + the published
pubkey; old downloads stay verifiable with the old pubkey.
- The SLSA predicate currently marks `reproducible: false` and does not attest
the full build environment/materials — it records the source repo + commit,
build parameters, and the artifact digest. Tightening to a hermetic,
reproducible attestation is a later step.
23 changes: 23 additions & 0 deletions scripts/build-custom-image.sh
Original file line number Diff line number Diff line change
Expand Up @@ -132,11 +132,13 @@ EOF
log "composed build: target=$TARGET edition=$EDITION arch=$ARCH host=$HOSTNAME pkgs=[$(jq -rc '.packages // []' <<<"$SPEC")]"

# ── Build ────────────────────────────────────────────────────────────────────
CACHE_PATHS=() # store paths to push to the Nix binary cache (warms later builds)
if [[ "$TARGET" == "iso" ]]; then
log "nix build install-iso (long step)..."
nix build --no-link --print-out-paths \
"$WORK/build#packages.${ARCH}-linux.image" --print-build-logs > "$WORK/outpath" || die "nix build failed"
RESULT="$(cat "$WORK/outpath")"
CACHE_PATHS+=("$RESULT")
# NB: the store path itself ends in `.iso` but is a DIRECTORY (iso/ inside);
# match files only, first hit, no pipe (avoids matching the dir + SIGPIPE).
ISO="$(find -L "$RESULT" -type f -name '*.iso' -print -quit)"
Expand All @@ -149,6 +151,7 @@ elif [[ "$TARGET" == "netboot" ]]; then
log "nix build netboot kernel + initramfs (long step)..."
base="$WORK/build#nixosConfigurations.netboot.config.system.build"
KDIR="$(nix build --no-link --print-out-paths "$base.kernel" --print-build-logs)" || die "kernel build failed"
CACHE_PATHS+=("$KDIR")
KERNEL="$(find -L "$KDIR" -type f \( -name 'bzImage' -o -name 'Image' \) -print -quit)"
[[ -n "$KERNEL" ]] || die "no kernel found in $KDIR"
RAMDISK_DIR="$(nix build --no-link --print-out-paths "$base.netbootRamdisk")" || die "ramdisk build failed"
Expand All @@ -171,6 +174,15 @@ else
fi
log "built artifacts in $OUT:"; ls -lh "$OUT"

# ── Sign + provenance (self-managed minisign key; graceful no-op without it) ───
# Emits <artifact>.minisig + in-toto statement + SLSA predicate (+ SBOM/OSImage
# for the ISO). No third-party signing authority is involved.
_selfdir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
OUT="$OUT" TARGET="$TARGET" EDITION="$EDITION" ARCH="$ARCH" NAME="${NAME:-}" \
HOSTNAME_="$HOSTNAME" BUILD_ID="${BUILD_ID:-}" BUILD_UID="${UID:-}" \
STORE_PATHS="${CACHE_PATHS[*]:-}" GCS_PREFIX="$GCS_PREFIX" \
bash "$_selfdir/sign-and-provenance.sh" || true

# ── Upload ───────────────────────────────────────────────────────────────────
if [[ -n "$GCS_PREFIX" ]]; then
log "uploading to $GCS_PREFIX/ ..."
Expand All @@ -183,5 +195,16 @@ if [[ -n "$GCS_PREFIX" ]]; then
echo "$GCS_PREFIX/netboot-manifest.json" > "$OUT/artifact-url.txt"
log "netboot base URL: $GCS_PREFIX/"
fi
# Provenance sidecars (best-effort; only those that were produced).
shopt -s nullglob
prov_files=( "$OUT"/*.minisig "$OUT"/*.intoto.json "$OUT"/*.slsa.json "$OUT"/*.sbom.json "$OUT"/*.osimage.json )
shopt -u nullglob
[[ "${#prov_files[@]}" -gt 0 ]] && gsutil cp "${prov_files[@]}" "$GCS_PREFIX/" && log "uploaded ${#prov_files[@]} provenance sidecar(s)"
fi

# ── Warm the Nix binary cache (best-effort; no-op without NIX_CACHE_* env) ─────
if [[ "${#CACHE_PATHS[@]}" -gt 0 ]]; then
_selfdir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
bash "$_selfdir/nix-cache-push.sh" "${CACHE_PATHS[@]}" || true
fi
log "done."
2 changes: 2 additions & 0 deletions scripts/deploy-stage2.sh
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ if [[ $DRY_RUN -eq 0 ]]; then
print(vm['sshLocalPort'] if vm else '')")
[[ -n "${LIMA_PORT}" ]] || die "Could not determine lima SSH port"

# shellcheck disable=SC2034 # diagnostic capture, kept intentionally
LIMA_KEY="$(limactl shell "${LIMA_VM}" -- \
bash -c 'cat /proc/1/environ 2>/dev/null | tr \\0 \\n | grep SSH_AUTH' 2>/dev/null || true)"

Expand Down Expand Up @@ -136,6 +137,7 @@ diskutil info "${USB_DEV}" >/dev/null 2>&1 || die "Device ${USB_DEV} not found"

DISK_NAME=$(diskutil info "${USB_DEV}" | grep "Device / Media Name" | awk -F': ' '{print $2}' | xargs)
DISK_SIZE=$(diskutil info "${USB_DEV}" | grep "Disk Size" | awk -F': ' '{print $2}' | awk '{print $1,$2}')
# shellcheck disable=SC2034 # informational probe, kept intentionally
IS_INTERNAL=$(diskutil info "${USB_DEV}" | grep "Solid State" | head -1)

echo
Expand Down
8 changes: 8 additions & 0 deletions scripts/gcp-build-custom-startup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ SPEC="$(md sourceos-spec)"
UID_="$(md sourceos-uid)"
BUILD_ID="$(md sourceos-build-id)"
PREFIX="$(md sourceos-gcs-prefix)"
# Optional: self-managed signing + cache (graceful if unset).
SIGN_KEY="$(md sourceos-sign-secret-key || true)"
CACHE_KEY="$(md sourceos-nix-cache-secret-key || true)"
CACHE_BUCKET="$(md sourceos-nix-cache-bucket || true)"
OS_VERSION="$(md sourceos-version || true)"; OS_VERSION="${OS_VERSION:-26.11}"

status() { printf '%s' "$1" | gsutil cp - "$PREFIX/status.json" || true; }
teardown() { gcloud --quiet compute instances delete "$NAME" --zone="$ZONE" || true; }
Expand All @@ -48,6 +53,9 @@ cd /root/source-os
printf '%s' "$SPEC" > /tmp/spec.json

if SPEC_FILE=/tmp/spec.json OUT=/root/out GCS_PREFIX="$PREFIX" \
BUILD_ID="$BUILD_ID" BUILD_UID="$UID_" VERSION="$OS_VERSION" \
SOURCEOS_SIGN_SECRET_KEY="$SIGN_KEY" \
NIX_CACHE_SECRET_KEY="$CACHE_KEY" NIX_CACHE_BUCKET="$CACHE_BUCKET" \
bash scripts/build-custom-image.sh; then
ART="$(cat /root/out/artifact-url.txt 2>/dev/null || true)"
status "$(printf '{"status":"complete","lane":"gcp","artifact":"%s"}' "$ART")"
Expand Down
1 change: 1 addition & 0 deletions scripts/install-on-device.sh
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ fi

if [[ "${FORMAT}" == "yes" ]]; then
# Detect whether EFI partition needs formatting (0-byte size = no filesystem).
# shellcheck disable=SC2034 # probe kept for diagnostics; format decision uses EFI_FS below
EFI_SIZE=$(lsblk -bno SIZE "${EFI_DEV}" 2>/dev/null || blockdev --getsize64 "${EFI_DEV}" 2>/dev/null || echo "0")
EFI_FS=$(blkid -s TYPE -o value "${EFI_DEV}" 2>/dev/null || true)
FORMAT_EFI="no"
Expand Down
Loading
Loading