diff --git a/build/recipes/services.sh b/build/recipes/services.sh index af86e01b..71f85edf 100644 --- a/build/recipes/services.sh +++ b/build/recipes/services.sh @@ -1,15 +1,11 @@ #!/usr/bin/env bash -# services: a stage 04 recipe, sourced by build/stages/04-base-system.sh, -# which runs it in the order its list gives. -# The s6-rc database compiled from build/services, the scripts the services run -# (build/service-scripts) and the sysctl fragments, all on the verified root. +# The s6-rc database, the service scripts and the sysctl fragments, all on the verified root. s_services() { local src="${KRYPTIK_ROOT}/build/services" [[ -d "$src" ]] || { echo "no service source tree at ${src}"; return 1; } - # The scripts live outside the s6-rc source tree: s6-rc-compile reads every - # directory there as a service. + # Outside the s6-rc source tree, where s6-rc-compile reads every directory as a service. local scripts="${KRYPTIK_ROOT}/build/service-scripts" install -d -m 0755 /usr/libexec/kryptik install -m 0755 "$scripts"/*.sh /usr/libexec/kryptik/ @@ -47,8 +43,7 @@ s_services() { echo "--- keyboard layouts ---" awk '!/^#/ && NF { printf "%s ", $1 } END { print "" }' "$table" - # s6-rc-compile will not overwrite: build beside and swap, as a half-written - # database does not boot. + # s6-rc-compile will not overwrite; build beside and swap, as a half-written one does not boot. local dbdir=/usr/lib/kryptik/s6-rc local tmpdb="$dbdir/compiled.new" rm -rf "$tmpdb" diff --git a/build/service-scripts/boot-success.sh b/build/service-scripts/boot-success.sh index 95c30d7c..dfa1f8ce 100755 --- a/build/service-scripts/boot-success.sh +++ b/build/service-scripts/boot-success.sh @@ -1,17 +1,9 @@ #!/bin/sh -# A/B boot-success tracking (docs/design/boot-and-updates.md): decides, late in -# boot, whether the slot that booted is one to keep. -# /var/lib/kryptik/boot/trial holds the slot kryptik-update armed, then armed=0 -# (before BootNext was set) or armed=1; last-result holds the last boot's -# outcome, which also stops the updater re-arming a failed payload. -# A trial slot that passes health() is committed (its kernel becomes -# BOOTX64.EFI). One that fails reboots, and with BootNext spent that lands on -# the committed slot. A committed slot is only reported on, never rebooted. -# A slot that runs with no trial on record and is not the committed one was -# booted from outside: it is reported as uncommitted and rebooted from, once. -# The paths are overridable for tools/tests/boot-success.sh only. +# A/B boot success (docs/design/boot-and-updates.md): commit a healthy trial, reboot a failed one. +# A committed slot is only reported on, never rebooted; a slot booted from outside is rebooted from once. set -u say() { echo "boot-success: $*"; } +# The overrides are for tools/tests/boot-success.sh only. RUN="${KRYPTIK_RUN:-/run/kryptik}" B="${KRYPTIK_BOOT_STATE:-/var/lib/kryptik/boot}" SVC="${KRYPTIK_SERVICE_DIR:-/run/service}" @@ -37,11 +29,12 @@ if [ -z "$slot" ]; then exit 0 fi +# trial: the slot kryptik-update armed, then armed=0 (before BootNext was set) or armed=1. trial=""; armed="" if [ -r "$B/trial" ]; then trial="$(sed -n '1p' "$B/trial")" armed="$(sed -n 's/^armed=//p' "$B/trial" | head -1)" - [ -n "$armed" ] || armed=1 # a record from before the armed= line: assume it was + [ -n "$armed" ] || armed=1 # an older record has no armed= line: assume armed fi # --- the essential-readiness check ------------------------------------------ @@ -94,12 +87,8 @@ commit_slot() { # commit_slot : make BOOTX64.EFI this slot's kernel return "$rc" } -# However a trial ends, remove BootNext and both slots' entries, so the -# firmware boots the disk's own entry (BOOTX64.EFI, the committed slot). A -# firmware re-adds that entry at the end of BootOrder when devices change, so a -# leftover entry for the other slot would win every cold boot. The committed -# slot then gets its own entry back: it boots what BOOTX64.EFI boots, and is a -# second way to it should that one file be lost. +# However a trial ends: a stale slot entry would outrank BOOTX64.EFI at every cold boot, +# and the committed slot's own entry is a second way to it should that file be lost. forget_entries() { # forget_entries COMMITTED-SLOT if ! kryptik-efiboot forget >/dev/null 2>&1; then say "the firmware's Kryptik entries could not be removed; its own boot order may not name the committed slot" @@ -110,8 +99,7 @@ forget_entries() { # forget_entries COMMITTED-SLOT return 0 } -# On a degraded state the trial record is unreadable; the ESP still names the -# committed slot, and any other slot is on trial. +# The committed slot as the ESP names it, read whenever no trial is on record. esp_committed() { e="$(kryptik_part kryptik-esp 2>/dev/null)" && [ -n "$e" ] || return 0 mkdir -p "$ESP_MNT" @@ -160,8 +148,7 @@ if [ -n "$trial" ]; then [ ! -f "$B/trial" ] || mv -f "$B/trial" "$B/trial.failed" sync if ! forget_entries "$(other_slot "$slot")" && [ -n "$unrecorded" ]; then - # With no record of this trial, only removing its entries - # stops the next boot from repeating it. + # No trial record: only removing its entries stops the next boot repeating it. say "not rebooting: with its entries still there the firmware could boot this trial again" elif [ "${KRYPTIK_NO_REBOOT:-0}" = 1 ]; then say "not rebooting (KRYPTIK_NO_REBOOT=1)" @@ -174,14 +161,12 @@ if [ -n "$trial" ]; then fi else if [ "$armed" = 1 ]; then - # BootNext is spent and the old slot is running: the trial did not - # come up. The updater will not re-arm this payload without --retry. + # trial.failed stops kryptik-update re-arming this payload without --retry. say "trial slot $trial did NOT boot; running slot $slot again" result "trial-failed $trial" mv -f "$B/trial" "$B/trial.failed" forget_entries "$slot" else - # The updater stopped before setting BootNext: nothing was tried. say "the arming of slot $trial was interrupted before BootNext was set; nothing was tried" result "arming-interrupted $trial" rm -f "$B/trial" diff --git a/build/service-scripts/installer-run.sh b/build/service-scripts/installer-run.sh index 518b6238..b8f9b253 100755 --- a/build/service-scripts/installer-run.sh +++ b/build/service-scripts/installer-run.sh @@ -1,6 +1,5 @@ #!/bin/sh -# Unattended install (or recovery) for the VM tests: only on an install medium -# whose test control disk asks for it (testctl.sh). Users run kryptik-install. +# Unattended install or recovery for the VM tests, when a test control disk asks (testctl.sh). set -u . /usr/libexec/kryptik/testctl.sh . /usr/libexec/kryptik/esp-records.sh @@ -67,8 +66,7 @@ if [ ! -x /usr/sbin/kryptik-install ]; then exit 0 fi -# An account for the test driver: the installer writes it to the new state -# partition, for kryptik-firstboot to consume once. +# The test driver's account, left on the new state partition for kryptik-firstboot to consume. preseed_args="" pu="$(testctl_get preseed_user)"; ph="$(testctl_get preseed_password_hash)" rh="$(testctl_get preseed_root_hash)" @@ -77,7 +75,7 @@ if [ -n "$pu" ] && [ -n "$ph" ]; then printf 'user=%s\npassword_hash=%s\nroot_password_hash=%s\n' "$pu" "$ph" "$rh" > /run/kryptik/firstboot.preseed preseed_args="--preseed /run/kryptik/firstboot.preseed" fi -# Replacing an old Kryptik disk is asked for by name, here as by a user. +# Replacing an old Kryptik disk takes the same explicit flag a user gives. replace_arg="" [ "$(testctl_get install_replace)" = "1" ] && replace_arg="--replace-kryptik" # install_slot_mib=MIB: a slot size, as a user gives one. @@ -100,8 +98,7 @@ sed 's/^/KRYPTIK_INSTALL: /' "$logf" say "rc=${rc}" if [ "$rc" -eq 0 ]; then - # Check the disk independently of the installer's report, finding each - # partition by label as the boot chain will. + # Check the disk apart from the installer's report, finding partitions by label as boot does. say "verify: table=$(sfdisk -l "$target" 2>/dev/null | grep -c "^${target}")" for lbl in kryptik-esp kryptik-a kryptik-b kryptik-state; do dev="$(blkid -t PARTLABEL="$lbl" -o device 2>/dev/null | grep "^${target}" | head -1)" diff --git a/build/service-scripts/sysinit.sh b/build/service-scripts/sysinit.sh index bb6fa6a7..94dbdbc3 100755 --- a/build/service-scripts/sysinit.sh +++ b/build/service-scripts/sysinit.sh @@ -1,16 +1,13 @@ #!/bin/sh -e -# Early boot: filesystems, the state partition, the /etc overlay, sysctls. -# Idempotent, since s6-rc may run it again after a runlevel change. +# Early boot: filesystems, state, the /etc overlay, sysctls; idempotent, as s6-rc may rerun it. -# kryptik-console holds the getty back until this finishes: it may ask for the -# state passphrase, and two readers on one terminal lose keystrokes. +# kryptik-console holds its getty until this ends, so the passphrase prompt gets every keystroke. echo running > /run/kryptik-sysinit trap 'echo finished > /run/kryptik-sysinit' EXIT [ -r /etc/hostname ] && hostname "$(cat /etc/hostname)" || true -# The only names the /etc overlay's upper layer may carry: accounts (with the -# shadow tools' backups and lock), identity, clock and zone 0's resolver. +# The only names the /etc upper layer may carry: accounts, identity, clock, zone 0's resolver. ETC_MUTABLE="passwd shadow group gshadow subuid subgid passwd- shadow- group- gshadow- subuid- subgid- .pwd.lock hostname machine-id localtime adjtime resolv.conf" prune_etc_upper() { # prune_etc_upper UPPER QUARANTINE up="$1"; q="$2"; moved=0 @@ -25,9 +22,9 @@ prune_etc_upper() { # prune_etc_upper UPPER QUARANTINE name="${e##*/}" keep=0 for k in $ETC_MUTABLE; do [ "$name" = "$k" ] && keep=1; done - # Accounts must be regular files, not FIFOs or links into mutable - # state. localtime may point only into the verified zoneinfo tree. + # A kept name must be a regular file, not a FIFO or a link into mutable state. if [ "$keep" = 1 ] && [ -f "$e" ] && [ ! -L "$e" ]; then continue; fi + # localtime may link only into the verified zoneinfo tree. if [ "$name" = localtime ] && [ -L "$e" ] && [ -f "$e" ]; then case "$(realpath -e -- "$e")" in /usr/share/zoneinfo/*) continue ;; esac fi @@ -41,13 +38,12 @@ prune_etc_upper() { # prune_etc_upper UPPER QUARANTINE return 0 } -# Up to three passphrase prompts, on every console (ask.sh). printf is a -# builtin: the passphrase never appears as an argument. . /usr/libexec/kryptik/ask.sh unlock_state() { # unlock_state DEVICE -> /dev/mapper/kryptik-state try=1 while [ "$try" -le 3 ] && [ ! -b /dev/mapper/kryptik-state ]; do pass=$(ask -s 0 "sysinit: passphrase for the state partition (try $try of 3): ") || pass="" + # printf is a builtin: the passphrase never appears as an argument. printf '%s' "$pass" | cryptsetup open --type luks2 --key-file=- "$1" kryptik-state 2>/dev/null || true try=$((try + 1)) done @@ -70,12 +66,10 @@ keyboard_at_boot() { return 0 } -# The kernel mounts devtmpfs (CONFIG_DEVTMPFS_MOUNT=y); stage 2 init may -# already have mounted the rest. +# devtmpfs is the kernel's (CONFIG_DEVTMPFS_MOUNT=y); stage 2 init may have mounted the rest. mountpoint -q /proc || mount -t proc proc /proc -o nosuid,noexec,nodev mountpoint -q /sys || mount -t sysfs sysfs /sys -o nosuid,noexec,nodev -# Neither securityfs nor cgroup2 may abort this `sh -e` script: s6-rc would -# then start no services at all. +# Neither securityfs nor cgroup2 may abort this `sh -e` script, or s6-rc starts no services. if ! mountpoint -q /sys/kernel/security 2>/dev/null; then if mount -t securityfs securityfs /sys/kernel/security \ -o nosuid,noexec,nodev 2>/dev/null; then @@ -101,8 +95,7 @@ fi mkdir -p /dev/pts /dev/shm mountpoint -q /dev/pts || mount -t devpts devpts /dev/pts -o gid=5,mode=620,nosuid,noexec mountpoint -q /dev/shm || mount -t tmpfs tmpfs /dev/shm -o nosuid,nodev -# efivarfs: the A/B trial (boot-success, kryptik-update) uses Boot#### and -# BootNext. boot-success reports a non-UEFI boot. +# efivarfs, for the A/B trial's Boot#### and BootNext (boot-success, kryptik-update). if [ -d /sys/firmware/efi/efivars ] && ! mountpoint -q /sys/firmware/efi/efivars; then mount -t efivarfs efivarfs /sys/firmware/efi/efivars -o nosuid,noexec,nodev 2>/dev/null \ || echo "sysinit: efivarfs did not mount" >&2 @@ -119,14 +112,8 @@ done # A medium keeps the kernel's layout; a second run of this script asks nothing. [ -n "$media" ] || mountpoint -q /var || keyboard_at_boot -# --- persistent state -------------------------------------------------------- -# The root is read-only; what changes lives on the kryptik-state partition of -# the root's own disk (devices.sh), seeded once from the image's /var. -# persistent the state partition is mounted at /var -# tmpfs install medium: nothing persists -# degraded installed, but the state partition cannot be used: /var is a -# tmpfs for repair, and first boot, the session, the update -# commit and the updater refuse (/run/kryptik/state-degraded) +# --- persistent state, on the root disk's kryptik-state partition ----------- +# STATE: persistent (mounted on /var), tmpfs (a medium) or degraded (unusable; /var is a tmpfs). . /usr/libexec/kryptik/devices.sh state_mnt=/run/kryptik/state STATE=""; STATE_REASON=""; state_dev=""; root_disk="" @@ -167,6 +154,7 @@ if ! mountpoint -q /var; then mount -t tmpfs -o nosuid,nodev,mode=0755 tmpfs "$state_mnt" fi if [ "$STATE" = degraded ]; then + # First boot, the session, the update commit and the updater refuse while this exists. printf '%s\n' "$STATE_REASON" > /run/kryptik/state-degraded tell "" \ "sysinit: ******************************************************************" \ @@ -191,9 +179,7 @@ if ! mountpoint -q /var; then chmod 0755 "$state_mnt/home" date -Iseconds > "$state_mnt/.kryptik-state" 2>/dev/null || : > "$state_mnt/.kryptik-state" fi - # State is not authenticated, and root honours files under /etc unasked - # (ld.so.preload, nsswitch.conf, udev rules, login configuration), so the - # upper layer is pruned to ETC_MUTABLE before the overlay is mounted. + # State is unauthenticated, and root obeys /etc unasked (ld.so.preload, nsswitch.conf, udev). prune_etc_upper "$state_mnt/lib/kryptik/etc/upper" "$state_mnt/lib/kryptik/etc/quarantine" || { echo "sysinit: refusing to boot with an unsafe /etc upper layer; recover from the install medium" >&2 exit 1 @@ -204,13 +190,7 @@ else fi rmdir "$state_mnt" 2>/dev/null || true -# /etc as an overlay: the verified root's /etc under the machine's changes on -# state. The upper layer is not authenticated, so nothing deciding privilege or -# trust is read from /etc; those come from the verified root: -# init, services /usr/lib/s6-linux-init, /usr/lib/kryptik/s6-rc -# sysctls /usr/lib/kryptik/sysctl.d -# zones /usr/lib/kryptik/zones -# release anchor /usr/share/kryptik/trust +# Nothing deciding privilege or trust is read from /etc: its upper layer is unauthenticated. if ! mountpoint -q /etc; then mkdir -p /var/lib/kryptik/etc/upper /var/lib/kryptik/etc/work if mount -t overlay overlay \ @@ -230,9 +210,8 @@ mountpoint -q /tmp || mount -t tmpfs -o nosuid,nodev,mode=1777 tmpfs /tmp mkdir -p /run/kryptik /run/lock /var/log/kryptik /var/lib/kryptik/boot chmod 0700 /run/kryptik chmod 0755 /run/lock /var/log/kryptik -# Transfer consent (kryptikd consent.rs): broker questions, answers from the -# desktop session (group kryptik). Not under the 0700 /run/kryptik; no zone has -# a path here. Setgid so the session's answers belong to the group. +# Consent questions (consent.rs) for the session's group kryptik, setgid so its answers stay +# the group's; no zone has a path here. mkdir -p /run/kryptik-consent chown root:kryptik /run/kryptik-consent 2>/dev/null || true chmod 2770 /run/kryptik-consent @@ -242,7 +221,7 @@ printf 'slot=%s\nmedia=%s\nstate=%s\nstate_dev=%s\nroot_disk=%s\n' \ "$slot" "$media" "$STATE" "$state_dev" "$root_disk" > /run/kryptik/boot-identity echo "sysinit: booted slot='${slot}' media='${media}' state=${STATE}${state_dev:+ (${state_dev})}" -# Kernel tunables, from the verified root only. Failures are reported. +# Kernel tunables, from the verified root only. if [ -d /usr/lib/kryptik/sysctl.d ]; then for f in /usr/lib/kryptik/sysctl.d/*.conf; do [ -r "$f" ] || continue diff --git a/build/service-scripts/testctl.sh b/build/service-scripts/testctl.sh index f7bd03e2..df26f3f5 100755 --- a/build/service-scripts/testctl.sh +++ b/build/service-scripts/testctl.sh @@ -1,22 +1,19 @@ #!/bin/sh -# Test control for install media, sourced by boot-time services. A disk -# labelled kryptik-testctl carries a key=value file, read only when booted from -# an install medium (kryptik.media=), so such a disk cannot reinstall or shut -# down an installed system, and only when the kryptik-testctl key the medium's -# anchor lists signed it, so no one else's disk can arm an install either. +# Test control, sourced by boot services: key=value from a disk labelled kryptik-testctl. +# Read only on an install medium, so the disk cannot reinstall or shut down an installed +# system, and only when the anchor's kryptik-testctl key signed it, so no other disk can arm one. # testctl_load 0, with TESTCTL_FILE set, when a control file was read # testctl_get KEY the value, or empty -# Keys: install_target=/dev/vdb smoke_poweroff=1 preseed_user=NAME -# preseed_password_hash=HASH preseed_root_hash=HASH install_wait=SECONDS -# recover_disk=/dev/vda recover_slot=a|b recover_mode=restore|commit|header|status +# Keys: install_target=/dev/vdb install_replace=1 install_slot_mib=MIB install_keyboard=NAME +# install_wait=SECONDS state_passphrase=TEXT preseed_user=NAME preseed_password_hash=HASH +# preseed_root_hash=HASH smoke_poweroff=1 recover_disk=/dev/vda recover_slot=a|b +# recover_mode=restore|commit|header|status TESTCTL_MNT=/run/kryptik/testctl TESTCTL_FILE="" TESTCTL_ANCHOR="${TESTCTL_ANCHOR:-/usr/share/kryptik/trust/release-signers}" -# The file and a signature over it by the kryptik-testctl key the anchor -# lists, in that key's own namespace: the holder of that key alone can arm an -# install on a machine that boots this medium. +# FILE.sig must verify with the anchor's kryptik-testctl key, in that key's own namespace. testctl_signed() { # testctl_signed FILE [ -r "$1" ] && [ -r "$1.sig" ] || return 1 ssh-keygen -Y verify -f "$TESTCTL_ANCHOR" -I kryptik-testctl -n kryptik-testctl \ diff --git a/build/service-scripts/time-floor.sh b/build/service-scripts/time-floor.sh index 69a304ca..539f6daa 100755 --- a/build/service-scripts/time-floor.sh +++ b/build/service-scripts/time-floor.sh @@ -1,12 +1,11 @@ #!/bin/sh -# Clock floor (docs/design/time.md): a clock earlier than the image's build -# date, or the newest release committed to, is raised to it before the -# network is asked. -# Always exits 0, so the net zone that depends on this oneshot still starts. +# Clock floor (docs/design/time.md): a clock behind the build date, or the newest release +# committed to, is raised to it before the network is asked. log=/var/log/kryptik/time.log mkdir -p /var/log/kryptik out="$(/usr/bin/kryptikd time floor 2>&1)"; rc=$? echo "=== time floor $(date -Iseconds 2>/dev/null) rc=${rc} === ${out}" >> "$log" 2>/dev/null # On the console too, so a serial log shows it. echo "time-floor: ${out}" +# Always 0: the net zone depends on this oneshot. exit 0