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
19 changes: 19 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,25 @@ jobs:
scripts/build-bundle.sh --arch "${{ matrix.arch }}" --version ci \
--variant generic-boot ${{ matrix.flag }} --out dist

# The published record must be the one in the tree, and it must match the
# kernel and initrd the tree carries. pins.env names the kernel by digest.
- name: Check the boot-asset digests
run: |
set -eu
r="$(ls dist/*.boot-assets.sha256)"
d="$(mktemp -d)"
tar -xzf "$(ls dist/*.tar.gz)" -C "$d"
g="$(echo "$d"/*/share/guest)"
cmp "$r" "$g/SHA256SUMS"
(cd "$g" && sha256sum -c SHA256SUMS)
awk '{print $2}' "$g/SHA256SUMS" | LC_ALL=C sort | tr '\n' ' ' > "$d/names"
case "$(cat "$d/names")" in
"bzImage container-initrd "|"Image container-initrd ") ;;
*) echo "SHA256SUMS lists $(cat "$d/names")rather than the kernel and the initrd"; exit 1 ;;
esac
grep -Eq '^KERNEL_SOURCE=.+@sha256:[0-9a-f]{64}$' dist/*.pins.env \
|| { echo "pins.env does not name the kernel by digest"; exit 1; }

# install.sh retargets a user install by rewriting the layout the bundle
# was generated with, which it reads from here.
- name: Check the bundle records its layout
Expand Down
7 changes: 3 additions & 4 deletions DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,9 @@ now done and is what `install.sh` and `scripts/build-bundle.sh` produce.
protocol must match the urunc shim, so hull-assets' prebuilt initrd (hull's
agent) is not usable; the build assembles a brig initrd from urunit +
`urunit-agent` (from the same urunc commit as the shim) + busybox + urunc's
`container-init`. The kernel is still fetched, not built: on amd64 from
the bunny Cloud-Hypervisor kernel image
(`harbor.nbfc.io/nubificus/bunny/linux-kernel-cloud-hypervisor`), on arm64 from
`ghcr.io/nofireai/hull-assets`.
`container-init`. The kernel is still fetched, not built: from
`ghcr.io/nofireai/hull-assets` on both arches, by digest, once its cosign
signature checks out.
Because urunc and the initrd ship in one tarball built together, their agent
commit matches by construction.
- **The installer's own job** is the host wiring the tarball cannot carry: create
Expand Down
29 changes: 23 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,14 @@ verify it on a machine that has cosign, copy it across, then:
# INSTALL_BRIG_BUNDLE=./brig-standalone-v0.1.0-linux-amd64.tar.gz sh install.sh
```

Copy the release's `checksums.txt`, `checksums.txt.sig` and `checksums.txt.pem`
across too, into the same directory as the tarball. `install.sh` keeps them
beside the kernel and initrd, which is what lets brig check those two files
against the release before a boot. Without them, brig warns before every run
that it cannot check the kernel, and refuses under `BRIG_VERIFY=require`. That
check asks Sigstore online, so a host with no network at all still gets the
warning.

### Verifying

A downloaded tarball is checked against the release's `checksums.txt`, which the
Expand Down Expand Up @@ -105,7 +113,8 @@ $ sha256sum -c checksums.txt --ignore-missing
libexec/cni/ CNI plugins
etc/ urunc.toml, containerd.toml, nerdctl.toml, brig-env.sh,
cni/net.d/, systemd/, certs.d/
share/guest/ Image or bzImage, container-initrd, bundle.json
share/guest/ Image or bzImage, container-initrd, bundle.json, SHA256SUMS,
and the release's checksums.txt, .sig and .pem
share/completions/ bash, zsh, fish completions
pins.env every bundled version, the one manifest
.install-stamp what this install created, read by the uninstaller
Expand Down Expand Up @@ -223,7 +232,7 @@ Everything is driven by environment variables and a couple of flags.
| `INSTALL_BRIG_POOL_SIZE` | `100G` | thin pool data size, sparse |
| `INSTALL_BRIG_POOL_PREALLOC` | `false` | `true` to `fallocate` the backing files |
| `INSTALL_BRIG_SKIP_START` | `false` | lay the tree down without starting it |
| `INSTALL_BRIG_SKIP_SIGCHECK` | `false` | install a remote tarball unverified |
| `INSTALL_BRIG_SKIP_SIGCHECK` | `false` | install a remote tarball unverified, keeping no signed record of the kernel and initrd (brig then warns before every run) |
| `INSTALL_BRIG_FORCE` | `false` | take over an `/var/lib/brig/data` we did not create |
| `INSTALL_BRIG_DEBUG` | `false` | `set -x` |

Expand Down Expand Up @@ -274,8 +283,7 @@ the three that do not:
| urunc, containerd-shim-urunc-v2 | built from `urunc-dev/urunc` at commit `74dd0cc` (branch `feat/unchanged_containers-exec-fixes`) | CGO-static, built in a Go container |
| urunit | built from `NOFireAI/urunit` at commit `71bfdee` (branch `urunit_agent`) | C-static; goes into the initrd |
| container-initrd | built from the above | assembled for brig, not fetched |
| guest kernel (amd64) | `harbor.nbfc.io/nubificus/bunny/linux-kernel-cloud-hypervisor` | fetched; extracted from the bunny image's `/.boot/kernel` |
| guest kernel (arm64) | `ghcr.io/nofireai/hull-assets` | fetched; the same kernel hull and brig use |
| guest kernel | `ghcr.io/nofireai/hull-assets` | fetched by digest once its cosign signature checks out; the kernel hull and brig boot |
| monitors, virtiofsd | `urunc-dev/monitors-build` | fetched |
| containerd, runc, nerdctl, CNI | upstream releases | fetched, upstream checksums |
| cosign | `sigstore/cosign` | fetched, pinned by sha256 |
Expand Down Expand Up @@ -326,8 +334,8 @@ carries hull's agent. So `build-bundle.sh` builds a brig initrd with urunc's own
`packaging/container-initrd` tooling, from `urunit`, a `urunit-agent` built from
the same urunc commit as the shim, a static `busybox`, and urunc's
`container-init`. The urunc binary and the initrd's agent ship in one tarball, so
they always match. Only the kernel is fetched rather than built — it is generic:
on amd64 from the bunny Cloud-Hypervisor kernel image, on arm64 from hull-assets.
they always match. Only the kernel is fetched rather than built. It is generic,
and comes from hull-assets on both arches, checked against its signature.

Two properties of the initrd are checked when the build packs it and again when
the runtime boots it, because each fails in a way that does not name itself:
Expand All @@ -341,6 +349,15 @@ through `BRIG_BOOT_ASSETS`, so brig uses the packed assets rather than fetching
anything on first run. The annotations that carry them are
`com.urunc.unikernel.bootKernel` and `com.urunc.unikernel.bootInitrd`.

`share/guest/SHA256SUMS` holds the sha256 of the kernel and of the initrd. Each
release publishes the same bytes as `<bundle>.boot-assets.sha256`, which the
release's signed `checksums.txt` covers. `install.sh` keeps that `checksums.txt`,
with `checksums.txt.sig` and `checksums.txt.pem`, in `share/guest`, so brig can
check the files it is about to boot against a record that the release signed.
It keeps them only when `checksums.txt` lists this `SHA256SUMS`. A local tarball
keeps them when they were copied across beside it. A remote install under
`INSTALL_BRIG_SKIP_SIGCHECK` keeps none.

## What it touches outside the base directories

Six things, all recorded in `.install-stamp` and all undone by the uninstaller.
Expand Down
46 changes: 19 additions & 27 deletions docs/variants.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,33 +65,25 @@ unpacks what the build produced.
The two files a generic boot names are the guest kernel and the initrd, and they
come from different places.

**The kernel** is fetched per architecture, and the two arches draw from
different sources:
**The kernel** is fetched from hull-assets on both arches, the OCI artifact hull
and brig already boot from, one tag per platform, pulled with the bundled `oras`:

- **amd64** takes the kernel from the bunny-built Cloud-Hypervisor kernel image,
a plain OCI image that carries the kernel at `/.boot/kernel`:

```
harbor.nbfc.io/nubificus/bunny/linux-kernel-cloud-hypervisor:latest
```

The image is scratch-style (no shell), so `build-bundle.sh` copies the kernel
out of a throwaway container with `docker create` + `docker cp` rather than
`oras`. The default is overridable with `KERNEL_IMAGE_AMD64`; set it empty to
fall back to the hull-assets path below.

- **arm64** takes the kernel from hull-assets, the OCI artifact hull and brig
already use, one tag per platform, pulled with the bundled `oras`:

```
ghcr.io/nofireai/hull-assets:<version>-linux-<arch> immutable
ghcr.io/nofireai/hull-assets:linux-<arch> moving
```
```
ghcr.io/nofireai/hull-assets:<version>-linux-<arch> immutable
ghcr.io/nofireai/hull-assets:linux-<arch> moving
```

The artifact's layers are the files themselves, each named by its
`org.opencontainers.image.title`.
The build resolves the `<version>` tag once. The bundled `cosign` checks the
signature on the digest it names, against the identity brig checks: the
`build-assets.yml` workflow in `NOFireAI/hull-assets`. The build then pulls that
digest. The artifact's layers are the files themselves, each named by its
`org.opencontainers.image.title`, so the kernel's sha256 is its layer digest in
the signed manifest.

Either way the kernel is generic and not brig-specific.
The kernel is generic and not brig-specific. On arm64 it is the same file hull
boots on macOS. `pins.env` records it as `KERNEL_SOURCE`, by the digest the build
pulled. `share/guest/SHA256SUMS` holds the sha256 of the kernel and the initrd,
and each release publishes it as `<bundle>.boot-assets.sha256`: see the README.

**The initrd is not taken from hull-assets.** brig execs into a guest through an
in-guest agent, `urunit-agent`, whose wire protocol (`pkg/agentproto`) is a
Expand Down Expand Up @@ -176,9 +168,9 @@ refuses a stock binary, rather than printing a table of zeroes.
assets optional? A separate build keeps a stock tarball small.
2. Where do the Option-C patches live once they are pushed? A branch is enough to
build from; upstream is better.
3. Settled: the guest kernel is fetched — on amd64 from the bunny
Cloud-Hypervisor kernel image, on arm64 from `hull-assets` by the same tags
hull uses; the runtime is built from `urunc-dev/urunc` at a pinned commit
3. Settled: the guest kernel is fetched from `hull-assets`, by the same tags hull
uses, and checked against its signature; the runtime is built from
`urunc-dev/urunc` at a pinned commit
(`74dd0cc`, on `feat/unchanged_containers-exec-fixes`); and the initrd is
built for brig from `NOFireAI/urunit` at a pinned commit (`71bfdee`, on
`urunit_agent`) plus urunc's own `packaging/container-initrd`, so its agent
Expand Down
63 changes: 60 additions & 3 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,9 @@ nothing.
(default: ask; non-interactive falls back to yes)
INSTALL_BRIG_VERBOSE true for a line per stage (default: false, quiet)
INSTALL_BRIG_SKIP_START true to lay the tree down without starting it
INSTALL_BRIG_SKIP_SIGCHECK true to install a remote tarball unverified
INSTALL_BRIG_SKIP_SIGCHECK true to install a remote tarball unverified. It
keeps no signed record of the kernel and initrd,
so brig warns before every run
INSTALL_BRIG_REQUIRE_SIGCHECK true to refuse unless the cosign signature
over checksums.txt verifies
INSTALL_BRIG_FORCE true to take over a /var/lib/brig/data we did not create
Expand Down Expand Up @@ -554,6 +556,14 @@ fetch_tarball() {
[ -f "$BUNDLE" ] || fatal "tarball '$BUNDLE' not found"
say "installing from the local tarball $BUNDLE"
cp "$BUNDLE" "$TARBALL"
# The release's checksums.txt, signature and certificate, when
# they were copied across beside the tarball, are the record
# keep_release_record keeps. The tarball is still taken as given.
for f in checksums.txt checksums.txt.sig checksums.txt.pem; do
if [ -f "$(dirname "$BUNDLE")/$f" ]; then
cp "$(dirname "$BUNDLE")/$f" "$TMP_DIR/$f"
fi
done
;;
esac
}
Expand All @@ -572,9 +582,15 @@ verify_tarball() {
|| fatal "no checksums.txt next to the tarball, cannot verify it.
Set INSTALL_BRIG_SKIP_SIGCHECK=true to install it unverified anyway."

if command -v cosign >/dev/null 2>&1 \
&& fetch_quiet "$base/checksums.txt.pem" "$TMP_DIR/checksums.txt.pem" \
# The signature is fetched whether or not cosign is here to check it:
# keep_release_record puts it beside the boot assets, where brig checks it
# with the bundle's own cosign before a boot.
sig_fetched=false
if fetch_quiet "$base/checksums.txt.pem" "$TMP_DIR/checksums.txt.pem" \
&& fetch_quiet "$base/checksums.txt.sig" "$TMP_DIR/checksums.txt.sig"; then
sig_fetched=true
fi
if command -v cosign >/dev/null 2>&1 && [ "$sig_fetched" = "true" ]; then
# A signature that fails is not a signature that is absent. The first
# says the bytes or the identity are wrong and is fatal; only the second
# degrades to the hash.
Expand Down Expand Up @@ -640,6 +656,7 @@ unpack_tarball() {
done
[ -f "$root/pins.env" ] && cp "$root/pins.env" "$PREFIX/pins.env"
chmod 0755 "$PREFIX"
keep_release_record

mkdir -p "$DATA_DIR/containerd" "$DATA_DIR/nerdctl" "$DATA_DIR/log" "$RUN_DIR"
retarget_tree
Expand All @@ -648,6 +665,46 @@ unpack_tarball() {
# switches at runtime without rewriting anything.
}

# Keep the release's checksums.txt, and its signature and certificate, beside
# the boot assets. checksums.txt lists <bundle>.boot-assets.sha256, which is
# share/guest/SHA256SUMS, so brig can check the kernel and initrd against a
# record the release signed before every boot.
#
# Without the three files brig cannot check that record: it warns before every
# run, and refuses under BRIG_VERIFY=require. A local tarball with nothing
# beside it, a remote one under INSTALL_BRIG_SKIP_SIGCHECK, and a bundle built
# before the record (no SHA256SUMS) keep none, as asked. A release that gave
# checksums.txt and not its signature, or whose checksums.txt does not list
# this SHA256SUMS, keeps none and says so: brig would refuse that record before
# every boot.
keep_release_record() {
guest="$PREFIX/share/guest"
[ -f "$guest/SHA256SUMS" ] || return 0
if [ ! -s "$TMP_DIR/checksums.txt" ]; then
say "no checksums.txt from a release, so brig cannot check the boot assets' record"
return 0
fi
for f in checksums.txt.sig checksums.txt.pem; do
if [ ! -s "$TMP_DIR/$f" ]; then
warn "the release gave no $f, so no record of the kernel and initrd is kept.
brig warns before every run that it cannot check them, and refuses under BRIG_VERIFY=require."
return 0
fi
done
sums="$(sha256sum "$guest/SHA256SUMS" | awk '{print $1}')"
if ! awk -v h="$sums" '$1 == h && $2 ~ /[.]boot-assets[.]sha256$/ { found = 1 } END { exit !found }' \
"$TMP_DIR/checksums.txt"; then
warn "the release's checksums.txt does not list this bundle's SHA256SUMS, so no record of
the kernel and initrd is kept. brig warns before every run that it cannot check them."
return 0
fi
for f in checksums.txt checksums.txt.sig checksums.txt.pem; do
cp "$TMP_DIR/$f" "$guest/$f" || fatal "could not keep $f in $guest"
chmod 0644 "$guest/$f"
done
say "kept the release's signed checksums.txt in $guest"
}

# Move the tree's idea of where it lives. The generated wrappers, configs and
# units carry the build layout as literal paths; no binary does. The grep at the
# end is what keeps that true: a tenth file added upstream fails the install
Expand Down
Loading
Loading