diff --git a/.arg.template b/.arg.template index 03811934..aa556a19 100644 --- a/.arg.template +++ b/.arg.template @@ -54,9 +54,33 @@ FORCE_INTERACTIVE_INSTALL=false # UKI only: when UKI_BRING_YOUR_OWN_KEYS=true, controls whether the Palette # systemd-extension certificate (SPECTRO_EXTENSION_CERT_IMAGE) is merged into -# the UEFI db during +uki-genkey. Leave true unless you don't want any external certificate int the db trust store. In that case, you have to +# the UEFI db during +uki-genkey. Leave true unless you don't want any external certificate int the db trust store. In that case, you have to # ENROLL_SPECTRO_EXTENSION_CERT=true +# Signing k8s systemd extensions with your own key (BYOK Secure Boot users): +# 1. Set ENROLL_SPECTRO_EXTENSION_CERT=false. Your custom db cert is +# already enrolled into UEFI db as part of the OS db, so systemd-sysext +# will validate against it directly — no need to also merge Spectro's +# extension cert into db. +# 2. Sign the k8s sysext extensions with the paired secure-boot/private-keys/db.key +# by running +build-signed-extensions. The target COPIes db.key / +# db.pem the same way +uki-byok already does, then invokes +# palette-sysext + auroraboot inside WITH DOCKER — never place the +# private key in .arg: +# ./earthly.sh +build-signed-extensions +# Without --push: output is a docker-save tar per image under +# ./build/signed-extensions/; `docker load` + push it to whichever +# registry the fleet consumes. +# With --push (./earthly.sh --push +build-signed-extensions): the +# target passes --push to palette-sysext and skips the tar step; +# earthly.sh auto-forwards ~/.docker/config.json as the +# DOCKER_AUTH_CONFIG secret, so `docker login` beforehand on the +# host is sufficient. +# NOTE: this target intentionally only SAVE ARTIFACT AS LOCAL (no +# SAVE IMAGE --push). If you ever add one, restructure first — +# otherwise the COPIed db.key would end up in the pushed image layers. +# PALETTE_SYSEXT_IMAGE=us-docker.pkg.dev/palette-images/edge/kubernetes/extensions/palette-sysext:v1.0.1 + # Bundle Kubernetes binaries and the agent-provider binaries into the provider # image (UKI and non-UKI). # false (default): on systemd >= 255. On older systemd, they ARE baked in. diff --git a/Earthfile b/Earthfile index 355a207a..fe76cafd 100644 --- a/Earthfile +++ b/Earthfile @@ -161,6 +161,10 @@ ARG ENROLL_SPECTRO_EXTENSION_CERT=true # OCI image (scratch) with palette-sysext-cert.pem; merged into UEFI db during +uki-genkey ARG SPECTRO_EXTENSION_CERT_IMAGE=us-east1-docker.pkg.dev/spectro-images/dev/arun/sysext/palette-sysext-cert:latest +# palette-sysext OCI image containing the CLI binary + built-in extensions/ tree. +# Consumed by +palette-sysext-bin and +build-signed-extensions. +ARG PALETTE_SYSEXT_IMAGE=us-docker.pkg.dev/palette-images/edge/kubernetes/extensions/palette-sysext:v1.0.1 + # Bundle the Kubernetes binaries and the agent-provider binaries into the # provider image (both UKI and non-UKI). # @@ -362,6 +366,24 @@ trust-boot-unpack: COPY --platform=linux/${ARCH} +build-provider-trustedboot-image/ /image RUN FILE="file:/$(find /image -type f -name "*.tar" | head -n 1)" && \ luet util unpack $FILE /trusted-boot + + # kairos-agent < v2.26.0 shim (kairos-io/kairos#4345, kairos-agent#1106): + # rewrite type-2 "uki " to type-1 "efi " and add loader.conf + # default. Drop when source-image agent >= v2.26.0. Skip on Hadron (no + # existing clusters to support upgrade from). + IF [ "$OS_DISTRIBUTION" != "hadron" ] + RUN set -e; \ + entries=/trusted-boot/loader/entries; \ + loader=/trusted-boot/loader/loader.conf; \ + test -d "$entries" && test -f "$loader"; \ + find "$entries" -maxdepth 1 -name '*.conf' -exec sed -i 's|^uki |efi |' {} +; \ + if ! grep -q '^default ' "$loader"; then \ + { printf 'default norole.conf\n'; cat "$loader"; } > "$loader.new" && mv "$loader.new" "$loader"; \ + fi; \ + grep -q '^efi /EFI/kairos/norole.efi$' "$entries/norole.conf"; \ + grep -q '^default norole.conf$' "$loader" + END + SAVE ARTIFACT /trusted-boot/* stylus-image-pack: @@ -410,7 +432,7 @@ install-k8s: SAVE ARTIFACT --keep-ts /output/ . build-uki-iso: - FROM --platform=linux/${ARCH} $OSBUILDER_IMAGE + FROM --platform=linux/${ARCH} $AURORABOOT_IMAGE ENV ISO_NAME=${ISO_NAME} COPY overlay/files-iso/ /overlay/ COPY --if-exists +validate-user-data/user-data /overlay/config.yaml @@ -441,22 +463,39 @@ build-uki-iso: WORKDIR /build COPY --platform=linux/${ARCH} --keep-own +iso-image-rootfs/rootfs /build/image + RUN mkdir -p /iso IF [ "$ARCH" = "arm64" ] - RUN CMD="/entrypoint.sh --name $ISO_NAME build-iso --date=false --overlay-iso /overlay dir:/build/image --output /iso/ --arch $ARCH" && \ - if [ "$DEBUG" = "true" ]; then CMD="$CMD --debug"; else CMD="$CMD"; fi && \ - $CMD + # No UKI ISO on arm64 upstream; fall through to a plain installer ISO. + RUN CMD="auroraboot" && \ + if [ "$DEBUG" = "true" ]; then CMD="$CMD --debug"; fi && \ + $CMD build-iso --overlay-iso /overlay --arch arm64 \ + --output /iso --override-name "$ISO_NAME" \ + dir:/build/image ELSE IF [ "$ARCH" = "amd64" ] COPY secure-boot/enrollment/ secure-boot/private-keys/ secure-boot/public-keys/ /keys RUN ls -liah /keys - RUN mkdir /iso + # enki -k /keys unbundled: --public-keys (PK/KEK/db .auth), + # --sb-key/--sb-cert (UKI signing), --tpm-pcr-private-key (PCR policy). IF [ "$AUTO_ENROLL_SECUREBOOT_KEYS" = "true" ] - RUN enki --config-dir /config build-uki dir:/build/image --extend-cmdline "$CMDLINE" --overlay-iso /overlay --secure-boot-enroll force -t iso -d /iso -k /keys --boot-branding "$BRANDING" + LET SECURE_BOOT_ENROLL=force ELSE - RUN enki --config-dir /config build-uki dir:/build/image --extend-cmdline "$CMDLINE" --overlay-iso /overlay -t iso -d /iso -k /keys --boot-branding "$BRANDING" + LET SECURE_BOOT_ENROLL=if-safe END + RUN CMD="auroraboot" && \ + if [ "$DEBUG" = "true" ]; then CMD="$CMD --debug"; fi && \ + $CMD build-uki -t iso -d /iso \ + --extend-cmdline "$CMDLINE" \ + --overlay-iso /overlay \ + --boot-branding "$BRANDING" \ + --public-keys /keys \ + --sb-key /keys/db.key \ + --sb-cert /keys/db.pem \ + --tpm-pcr-private-key /keys/tpm2-pcr-private.pem \ + --secure-boot-enroll "$SECURE_BOOT_ENROLL" \ + --name "$ISO_NAME" \ + dir:/build/image END WORKDIR /iso - RUN mv /iso/*.iso $ISO_NAME.iso SAVE ARTIFACT /iso/* iso: @@ -532,34 +571,20 @@ build-iso: fi # AuroraBoot uses Go arch names for both amd64 and arm64 (osbuilder used - # "x86_64" for amd64). --output/--override-name are inert for "dir:" - # sources: the ISO always lands at /tmp/auroraboot/kairos--- - # core--generic-v.iso, so we leave --output default and - # hoist the produced ISO into /iso/ ourselves. - # - # The Hadron-specific WITH DOCKER path is unnecessary now that all builds - # are FROM $AURORABOOT_IMAGE -- AuroraBoot names the grub stage - # grubx64.efi (was: EFI/BOOT/grub.efi under enki), and AuroraBoot v0.26.2 - # fixes the UEFI-only boot path via GPT-hybrid + gcdx64.efi.signed for all - # distros, not just Hadron. - # Positional source MUST come last. AuroraBoot uses urfave/cli v2 which - # follows Go stdlib flag semantics: flag parsing stops at the first - # positional argument. If dir:/build/image comes first, --overlay-iso - # and --arch are silently discarded as extra positional args. That is - # what caused the Palette-branded /boot/grub2/grub.cfg (and user-data, - # content bundles, cluster config) to silently disappear from produced - # ISOs before this fix. Empirically verified against v0.26.2. + IF [ "$ARCH" = "arm64" ] RUN CMD="auroraboot" && \ if [ "$DEBUG" = "true" ]; then CMD="$CMD --debug"; fi && \ - $CMD build-iso --overlay-iso /overlay --arch arm64 dir:/build/image + $CMD build-iso --overlay-iso /overlay --arch arm64 \ + --output /iso --override-name "$ISO_NAME" \ + dir:/build/image ELSE IF [ "$ARCH" = "amd64" ] RUN CMD="auroraboot" && \ if [ "$DEBUG" = "true" ]; then CMD="$CMD --debug"; fi && \ - $CMD build-iso --overlay-iso /overlay --arch amd64 dir:/build/image + $CMD build-iso --overlay-iso /overlay --arch amd64 \ + --output /iso --override-name "$ISO_NAME" \ + dir:/build/image END - RUN mkdir -p /iso && \ - mv /tmp/auroraboot/*.iso "/iso/$ISO_NAME.iso" WORKDIR /iso RUN sha256sum "$ISO_NAME.iso" > "$ISO_NAME.iso.sha256" SAVE ARTIFACT --keep-ts /iso/* @@ -634,6 +659,193 @@ spectro-extension-cert-esl: /cert/spectro-cert.pem /cert/spectro-db.esl SAVE ARTIFACT /cert/spectro-db.esl spectro-db.esl +# Extract the palette-sysext CLI + built-in extensions/ tree from the OCI +# distribution image so downstream targets can consume them directly. +palette-sysext-bin: + ARG ARCH=amd64 + FROM --platform=linux/${ARCH} $PALETTE_SYSEXT_IMAGE + SAVE ARTIFACT /bin/palette-sysext palette-sysext + SAVE ARTIFACT /share/palette-sysext/extensions extensions + +# Build and sign a Palette systemd extension using the CanvOS UKI BYOK +# db key/cert pair, so the same certificate that signs the boot chain also +# signs the k8s sysext extensions. Runs docker-in-docker via WITH DOCKER +# because palette-sysext delegates to `docker buildx` (repack) and +# `docker run` (auroraboot) internally. +# +# Inputs from .arg: +# K8S_DISTRIBUTION distro to sign (kubeadm[-fips], k3s, rke2, canonical) +# K8S_VERSION bare semver; k3s/rke2 get their flavor suffix appended +# ARCH target arch(es); comma-separated for multi-arch +# IMAGE_REGISTRY when set, tags the OCI image as +# $IMAGE_REGISTRY/palette-sysext-extensions: +# FIPS_ENABLED when true, adds --fips (in addition to whatever +# kubeadm-fips already implies) +# +# Signing material is COPIed from secure-boot/private-keys/db.key + +# secure-boot/public-keys/db.pem — same files, same mechanism +uki-byok +# uses to sign the UKI itself. The key sits in this target's build-cache +# layer but is never pushed anywhere (the target only SAVE ARTIFACT AS +# LOCAL). See the SECURITY note in the target body before adding push. +# Just run: +# +# ./earthly.sh +build-signed-extensions +# +# The signed OCI image is loaded into the WITH DOCKER daemon, saved as a tar +# via `docker save`, and exposed as a local artifact under +# ./build/signed-extensions/. Push is intentionally out of scope for this +# target — `docker load` + `docker push` the tar into whatever registry the +# fleet consumes, using whichever credentials that registry needs. +build-signed-extensions: + FROM --allow-privileged earthly/dind:alpine-3.19-docker-25.0.5-r0 + RUN apk add --no-cache bash jq + + ARG --required K8S_DISTRIBUTION + ARG --required K8S_VERSION + ARG ARCH=amd64 + # IMAGE_REGISTRY and FIPS_ENABLED are Earthfile-scope globals (lines 13 + # and 51) — no need to re-declare. IMAGE_REGISTRY drives palette-sysext + # --repo (OCI tag host/path); FIPS_ENABLED forwards --fips on top of + # whatever kubeadm-fips already implies. + # + # + # Optional override for the package source image used by palette-sysext. + # Useful when the default source (e.g. us-docker.pkg.dev/palette-images/…) + # has been mirrored to a private registry. + ARG SOURCE_IMAGE + # DRY_RUN=true prints the exact docker + auroraboot commands without + # executing them — matches palette-sysext's own --dry-run flag. + ARG DRY_RUN=false + # FORCE=true rebuilds even when palette-sysext detects a matching + # fingerprint in the target registry. Defaults to true because the + # whole point of this target is to sign with the caller's own key — + # the registry image was signed by someone else and must be rebuilt. + ARG FORCE=true + + # palette-sysext ships as a statically-linked linux/amd64 binary and + # runs on the Earthly host (DinD image is amd64); the target output arch + # is passed through via --arch (may be a comma-separated multi-arch list). + COPY (+palette-sysext-bin/palette-sysext --ARCH=amd64) /usr/local/bin/palette-sysext + COPY (+palette-sysext-bin/extensions --ARCH=amd64) /extensions + RUN chmod +x /usr/local/bin/palette-sysext + + # Distro → extension mapping. Also normalizes K8S_VERSION to match how + # palette-sysext's extension.yaml lists it per distro: + # kubeadm / canonical: bare semver (e.g. 1.34.5) + # k3s: 1.34.5- (e.g. 1.34.5-k3s1) + # rke2: 1.34.5- (e.g. 1.34.5-rke2r1) + # If K8S_VERSION already carries the suffix (users may set the full + # form), leave it alone. + # kubeadm-fips selects the kubeadm extension with --fips; + # nodeadm has no palette-sysext extension and is rejected explicitly. + RUN set -eu; \ + VER="$K8S_VERSION"; \ + case "$K8S_DISTRIBUTION" in \ + kubeadm) echo "k8s/kubeadm" > /extid; : > /fips ;; \ + kubeadm-fips) echo "k8s/kubeadm" > /extid; echo --fips > /fips ;; \ + k3s) \ + echo "k8s/k3s" > /extid; : > /fips; \ + case "$VER" in *-k3s*) ;; *) VER="${VER}-${K3S_FLAVOR_TAG}" ;; esac ;; \ + rke2) \ + echo "k8s/rke2" > /extid; : > /fips; \ + case "$VER" in *-rke2*) ;; *) VER="${VER}-${RKE2_FLAVOR_TAG}" ;; esac ;; \ + canonical) echo "k8s/canonical" > /extid; : > /fips ;; \ + nodeadm) \ + echo "ERROR: K8S_DISTRIBUTION=nodeadm has no palette-sysext extension." >&2; \ + exit 1 ;; \ + *) \ + echo "ERROR: unsupported K8S_DISTRIBUTION=$K8S_DISTRIBUTION" >&2; \ + exit 1 ;; \ + esac; \ + echo "$VER" > /extver; \ + echo "resolved: extension=$(cat /extid) version=$(cat /extver) fips=$(cat /fips)" + + RUN mkdir -p /output + + # Same COPY pattern +uki-byok uses to consume the BYOK db key/cert. + # The COPY runs as root inside BuildKit, so 0600 root-owned files + # (as produced by +uki-genkey) are readable regardless of the host + # user's perms. The key lands in this target's build-cache layer; + # this target only does SAVE ARTIFACT AS LOCAL, so nothing is pushed. + # SECURITY: if you add SAVE IMAGE --push to this target later, the + # key WILL end up in a layer of the pushed image (visible via + # `docker history`). Split into a separate stage that doesn't COPY + # the key before enabling push. + COPY secure-boot/private-keys/db.key /keys/db.key + COPY secure-boot/public-keys/db.pem /keys/db.pem + RUN chmod 0600 /keys/db.key /keys/db.pem + + WITH DOCKER --pull $AURORABOOT_IMAGE + RUN --secret DOCKER_AUTH_CONFIG \ + set -eu; \ + EXT_ID="$(cat /extid)"; \ + EXT_VER="$(cat /extver)"; \ + FIPS_ARG="$(cat /fips)"; \ + # FIPS_ENABLED=true also selects --fips even if K8S_DISTRIBUTION + # is the non-FIPS form (matches how the rest of CanvOS flips + # between FIPS and non-FIPS off this single knob). + if [ "$FIPS_ENABLED" = "true" ] && [ -z "$FIPS_ARG" ]; then \ + FIPS_ARG="--fips"; \ + fi; \ + REPO_ARG=""; \ + if [ -n "${IMAGE_REGISTRY:-}" ]; then \ + REPO_ARG="--repo=$IMAGE_REGISTRY/palette-sysext-extensions"; \ + fi; \ + SRC_ARG=""; [ -n "${SOURCE_IMAGE:-}" ] && SRC_ARG="--source-image=$SOURCE_IMAGE"; \ + DRY_RUN_ARG=""; [ "$DRY_RUN" = "true" ] && DRY_RUN_ARG="--dry-run"; \ + FORCE_ARG=""; [ "$FORCE" = "true" ] && FORCE_ARG="--force"; \ + PUSH_ARG=""; \ + if [ "${EARTHLY_PUSH:-false}" = "true" ]; then \ + PUSH_ARG="--push"; \ + # palette-sysext calls `docker push` inside WITH DOCKER's + # fresh dockerd — it has no auth by default. earthly.sh + # forwards ~/.docker/config.json as the DOCKER_AUTH_CONFIG + # secret when +build-signed-extensions is invoked; we + # materialize it as /root/.docker/config.json so the CLI + # picks it up. + if [ -z "${DOCKER_AUTH_CONFIG:-}" ]; then \ + echo "ERROR: --push requires DOCKER_AUTH_CONFIG secret." >&2; \ + echo " Run './earthly.sh --push +build-signed-extensions' which" >&2; \ + echo " auto-forwards ~/.docker/config.json, or make sure that" >&2; \ + echo " file exists and is readable by the invoking user." >&2; \ + exit 1; \ + fi; \ + mkdir -p /root/.docker; \ + printf '%s' "$DOCKER_AUTH_CONFIG" > /root/.docker/config.json; \ + chmod 0600 /root/.docker/config.json; \ + fi; \ + echo "==> palette-sysext build --extension=$EXT_ID --version=$EXT_VER --arch=$ARCH $FIPS_ARG $REPO_ARG $FORCE_ARG $PUSH_ARG $DRY_RUN_ARG"; \ + palette-sysext doctor || true; \ + palette-sysext build \ + --extension="$EXT_ID" \ + --version="$EXT_VER" \ + --arch="$ARCH" \ + --extensions-dir=/extensions \ + --private-key=/keys/db.key \ + --certificate=/keys/db.pem \ + --report-file=/output \ + $FIPS_ARG $REPO_ARG $SRC_ARG $FORCE_ARG $PUSH_ARG $DRY_RUN_ARG; \ + # Clear registry auth from the target FS before any subsequent + # SAVE ARTIFACT. Belt-and-braces — /root/.docker/config.json is + # inside a WITH DOCKER container that gets torn down anyway. + rm -f /root/.docker/config.json 2>/dev/null || true; \ + # When pushing, palette-sysext has already put the images in + # the registry; skip the local docker-save tar step. + if [ "$DRY_RUN" != "true" ] && [ "${EARTHLY_PUSH:-false}" != "true" ]; then \ + for report in /output/*.json; do \ + [ -f "$report" ] || continue; \ + for tag in $(jq -r '.archs[]? | select(.built == true) | .image // empty' "$report"); do \ + [ -n "$tag" ] || continue; \ + safe="$(echo "$tag" | tr '/:' '__')"; \ + echo "==> docker save $tag -> /output/${safe}.tar"; \ + docker save "$tag" -o "/output/${safe}.tar"; \ + done; \ + done; \ + fi + END + + SAVE ARTIFACT /output AS LOCAL ./build/signed-extensions/ + # Self-contained merge of the Spectro extension cert into the UEFI db enrollment # material. Gated on ENROLL_SPECTRO_EXTENSION_CERT: when true, it fetches the ESL, # appends it to db.esl and (re)generates db.auth/db.der so the db is fully ready; @@ -899,10 +1111,19 @@ provider-image-rootfs: SAVE ARTIFACT --keep-own /. rootfs build-provider-trustedboot-image: - FROM --platform=linux/${ARCH} $OSBUILDER_IMAGE + FROM --platform=linux/${ARCH} $AURORABOOT_IMAGE COPY --platform=linux/${ARCH} --keep-own +provider-image-rootfs/rootfs /build/image COPY secure-boot/enrollment/ secure-boot/private-keys/ secure-boot/public-keys/ /keys - RUN /entrypoint.sh build-uki dir:/build/image -t container -d /output -k /keys --boot-branding "Palette eXtended Kubernetes Edge" + RUN mkdir -p /output + RUN CMD="auroraboot" && \ + if [ "$DEBUG" = "true" ]; then CMD="$CMD --debug"; fi && \ + $CMD build-uki -t container -d /output \ + --boot-branding "$BRANDING" \ + --public-keys /keys \ + --sb-key /keys/db.key \ + --sb-cert /keys/db.pem \ + --tpm-pcr-private-key /keys/tpm2-pcr-private.pem \ + dir:/build/image SAVE ARTIFACT /output/* AS LOCAL ./trusted-boot/ stylus-image: diff --git a/README.md b/README.md index 8c83d428..d3100293 100644 --- a/README.md +++ b/README.md @@ -247,6 +247,7 @@ cp .arg.template .arg | AUTO_ENROLL_SECUREBOOT_KEYS | Auto enroll SecureBoot keys when device boots up and is in setup mode of secure boot | boolean | `true` | | ENROLL_SPECTRO_EXTENSION_CERT | Merge the Palette systemd extension certificate into the generated UEFI db, allowing signed Palette extensions to be trusted and activated under Secure Boot. Disable this option when only customer certificates are permitted; in that case, customers must sign the extensions with their own certificates. | boolean | `true` | | SPECTRO_EXTENSION_CERT_IMAGE | OCI image containing `palette-sysext-cert.pem`, used when enrolling the Palette systemd-extension certificate. | String | See `.arg.template` | +| PALETTE_SYSEXT_IMAGE | OCI image providing the `palette-sysext` CLI and built-in `extensions/` tree, consumed by `+build-signed-extensions`. | String | See `.arg.template` | | EDGE_CUSTOM_CONFIG | Path to edge custom configuration file | string | `.edge-custom-config.yaml` | | MAAS_IMAGE_NAME | Custom name for the final MAAS image (without .raw.gz extension). Only used when building MAAS images. | string | `kairos-ubuntu-maas` | | IS_MAAS | Build MAAS-compatible disk images. Set to `true` when building for MAAS deployment. | boolean | `false` | @@ -273,6 +274,99 @@ To build just the installer image ./earthly.sh +iso --ARCH=amd64 ``` +### Signing k8s Systemd Extensions with Custom Keys + +CanvOS integrates the `spectrocloud/k8s-extensions` `palette-sysext` tool so +you can build and sign the k8s systemd extensions (`kubeadm`, `k3s`, `rke2`, +`canonical`) with your own certificate — the same one CanvOS enrols into UEFI +db for UKI Secure Boot. + +At boot, Kairos' immucore extracts PK/KEK/DB certs from the UEFI firmware to +`/run/verity.d/`, and `systemd-sysext` validates every extension against +those certs. Signing extensions with the same key CanvOS puts in db closes +the trust chain. + +**Prerequisites** + +1. Custom UKI keys under `secure-boot/private-keys/db.key` + + `secure-boot/public-keys/db.pem` (produced by `./earthly.sh +uki-genkey` + or provided per `sb-private-ca/howto.md`). Because your db cert is + already enrolled into UEFI db as part of the OS db, set + `ENROLL_SPECTRO_EXTENSION_CERT=false` in `.arg` — no need to also merge + Spectro's default extension cert. +2. `K8S_DISTRIBUTION` and `K8S_VERSION` in `.arg` match a version listed in + the extension's `extension.yaml`. `kubeadm-fips` selects the `kubeadm` + extension with `--fips`; `nodeadm` has no sysext form and is rejected. +3. `IMAGE_REGISTRY` in `.arg` — used as the OCI tag prefix + (`$IMAGE_REGISTRY/palette-sysext-extensions`) so the produced tar can be + `docker load`ed and pushed directly to your registry. +4. `FIPS_ENABLED=true` in `.arg` — forwards as `--fips` to palette-sysext + (redundant with `K8S_DISTRIBUTION=kubeadm-fips`; either is sufficient). + +**Build (local tar under `./build/signed-extensions/`):** + +```shell +./earthly.sh +build-signed-extensions +``` + +**Build and push to `$IMAGE_REGISTRY`:** + +```shell +./earthly.sh --push +build-signed-extensions +``` + +When Earthly's `--push` flag is passed, the target forwards `--push` to +palette-sysext and skips the local `docker save` step. `earthly.sh` also +auto-forwards `~/.docker/config.json` as the `DOCKER_AUTH_CONFIG` secret so +palette-sysext's nested `docker push` inside `WITH DOCKER` can authenticate +— run `docker login` on the host before invoking, once, for whichever +registry `IMAGE_REGISTRY` points at. + +The target COPIes `secure-boot/private-keys/db.key` + +`secure-boot/public-keys/db.pem` into the build container — the same +mechanism `+uki-byok` uses for the same files. The COPY runs as root +inside BuildKit, so 0600 root-owned files (as produced by +`+uki-genkey`) are readable regardless of your host user. + +**Security note.** This target only writes a local artifact +(`SAVE ARTIFACT … AS LOCAL`); it never pushes an image, so the key stays +in your local Earthly cache. If you later add `SAVE IMAGE --push` to this +target, the key **will** be visible in the pushed image via +`docker history` — split into a separate publish stage that does not COPY +the key first. + +Output: the signed OCI image is loaded into the docker-in-docker daemon, +saved as a tar via `docker save`, and copied out to +`./build/signed-extensions/`. `docker load` + `docker push` the tar into +whatever registry the fleet consumes. + +Target flags: + +| Flag | Purpose | +| --------------------- | ---------------------------------------------------------------------------------- | +| `--DRY_RUN=true` | Print the exact docker / auroraboot commands; do not execute. | +| `--FORCE=true` | (Default) Force rebuild even if the target registry already has a matching-fingerprint image. Required for BYOK signing because the registry image is signed by Spectro's key, not yours. Set `--FORCE=false` if you deliberately want palette-sysext's fingerprint-skip behavior. | +| `--SOURCE_IMAGE=…` | Override the package source image (useful for private mirrors). | +| `--ARCH=amd64,arm64` | Multi-arch build (comma-separated); produces one image per arch plus a stitched manifest on `--PUSH`. | + +**k8s version format.** CanvOS's `K8S_VERSION` in `.arg` uses the bare +semver form (`1.34.5`). The target automatically appends the flavor suffix +palette-sysext expects for k3s (`-`, default `k3s1`) and +rke2 (`-`, default `rke2r1`); kubeadm and canonical use +the bare form. If you set the full form in `.arg` (e.g. `1.34.5-k3s1`) it +passes through unchanged. + +The runtime pipeline inside the target is +`docker buildx → auroraboot → systemd-repart --make-ddi=sysext`, all inside +Earthly's `WITH DOCKER --allow-privileged` block. + +**Note on runtime consumption.** This target signs and packages the +extension; it does not change how nodes discover them. Routing nodes to a +different registry (or replacing the on-node `extensions-index.yaml`) is a +stylus / kairos-agent concern outside CanvOS. `--SOURCE_IMAGE` lets you +build against a mirrored *source* image, but the *output* image tag and how +nodes locate it are separate concerns not addressed by this target. + ### Building AWS Cloud Images CanvOS can build Amazon Machine Images (AMIs) for AWS EC2 deployment. The build process converts the raw disk image into an AMI that can be launched directly in AWS. diff --git a/earthly.sh b/earthly.sh index 9ae7bd92..605b5bec 100755 --- a/earthly.sh +++ b/earthly.sh @@ -169,6 +169,18 @@ if [ "${UBUNTU_PRO_ATTACH:-false}" = "true" ]; then EARTHLY_SECRET_ARGS=(--secret UBUNTU_PRO_KEY) fi +# When +build-signed-extensions is being built with --push, forward the +# caller's docker registry credentials as a secret so palette-sysext's +# nested `docker push` inside WITH DOCKER can authenticate. Same secret +# forwarding pattern as UBUNTU_PRO_KEY: value flows via -e + --secret, +# never onto any command line or into the build cache. +if [[ "$*" == *"+build-signed-extensions"* ]] && [ -r "$HOME/.docker/config.json" ]; then + DOCKER_AUTH_CONFIG="$(cat "$HOME/.docker/config.json")" + export DOCKER_AUTH_CONFIG + DOCKER_SECRET_ENV+=(-e DOCKER_AUTH_CONFIG) + EARTHLY_SECRET_ARGS+=(--secret DOCKER_AUTH_CONFIG) +fi + # Workaround to support deprecated field PROXY_CERT_PATH if [ -n "$PROXY_CERT_PATH" ]; then echo "PROXY_CERT_PATH is deprecated. Please place your certificates in the certs directory."