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
11 changes: 3 additions & 8 deletions build/recipes/services.sh
Original file line number Diff line number Diff line change
@@ -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/
Expand Down Expand Up @@ -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"
Expand Down
35 changes: 10 additions & 25 deletions build/service-scripts/boot-success.sh
Original file line number Diff line number Diff line change
@@ -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}"
Expand All @@ -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 ------------------------------------------
Expand Down Expand Up @@ -94,12 +87,8 @@ commit_slot() { # commit_slot <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"
Expand All @@ -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"
Expand Down Expand Up @@ -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)"
Expand All @@ -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"
Expand Down
11 changes: 4 additions & 7 deletions build/service-scripts/installer-run.sh
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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)"
Expand All @@ -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.
Expand All @@ -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)"
Expand Down
55 changes: 17 additions & 38 deletions build/service-scripts/sysinit.sh
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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=""
Expand Down Expand Up @@ -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: ******************************************************************" \
Expand All @@ -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
Expand All @@ -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 \
Expand All @@ -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
Expand All @@ -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
Expand Down
19 changes: 8 additions & 11 deletions build/service-scripts/testctl.sh
Original file line number Diff line number Diff line change
@@ -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 \
Expand Down
7 changes: 3 additions & 4 deletions build/service-scripts/time-floor.sh
Original file line number Diff line number Diff line change
@@ -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
Loading