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
15 changes: 15 additions & 0 deletions COVERAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,21 @@ pattern, the digest match, the grep status check, `-I` or the symlink rule
each turns the suite red. Coverage: layer-lib and bt-layer unchanged at one
uncovered line each, the same lines as on 19.x.

## 2026-09-30: the machine-id reset

`layer_reset_machine_id` leaves every exported layer with an empty
`/etc/machine-id` and `/var/lib/dbus/machine-id` as a link to it, so systemd
gives each machine its own id at boot (docs/traps.md, "Every appliance built
from core has the same machine-id"). `bt-layer` runs it after
`layer_check_build`, before the rootfs copy and the tarball. `tests/layer`
covers a populated id and D-Bus copy, a tree already reset left untouched
(file and link), a missing id, a symlinked id, a D-Bus link elsewhere, no
D-Bus copy, no `/etc`, a directory in the way of either, the `core` tarball
and rootfs of a build that wrote an id, and a `bt-layer` run whose id cannot
be reset (refused, no tarball). Mutating each condition or the mode turns the
suite red. Coverage: layer-lib 383 of 384 and bt-layer 111 of 112, the same
one uncovered line each as on 19.x.

## 2026-09-26: the signing identity

`bin/generate-signature` no longer hardcodes TurnKey's release key. Its logic
Expand Down
44 changes: 44 additions & 0 deletions bin/layer-lib
Original file line number Diff line number Diff line change
Expand Up @@ -568,6 +568,50 @@ layer_audit_keys() {
return 0
}

# The machine identity a layer ships. Every image built from a layer holds
# the same bytes, so an id in a layer is an id every machine made from it
# shares: the same DHCP DUID and IAID, the same journal and networkd
# identity, the same MariaDB server_id (docs/traps.md, "Every appliance built
# from core has the same machine-id"). The build always leaves one, whatever
# the parent shipped: the bootstrap carries one, and systemd's postinst runs
# systemd-machine-id-setup on every configure, so a child that upgrades
# systemd writes a fresh one of its own.
#
# /etc/machine-id is left an empty file, as Debian's cloud images ship it,
# and systemd makes the id at each machine's boot. Not missing and not
# "uninitialized": either makes that boot a first boot to systemd, which
# presets every unit and undoes what the build enabled and masked
# (machine-id(5), "First Boot Semantics"). D-Bus's copy becomes the link to
# it that tmpfiles.d/dbus.conf makes when it is absent, because systemd reads
# that file when /etc/machine-id is empty and would bring the old id back.
LAYER_MACHINE_ID=etc/machine-id
LAYER_DBUS_MACHINE_ID=var/lib/dbus/machine-id

# A tree already in that state is not written, so a child layer does not
# copy its parent's empty file up into its own tarball.
layer_reset_machine_id() {
local rootfs=$1
local id=$rootfs/$LAYER_MACHINE_ID
local dbus=$rootfs/$LAYER_DBUS_MACHINE_ID
if [[ ! -d "$rootfs/etc" ]]; then
layer_error "no /etc in $rootfs to reset the machine-id in"
return 1
fi
if [[ -L "$id" || ! -f "$id" || -s "$id" ]]; then
if ! { rm -f "$id" && : > "$id" && chmod 0444 "$id"; } 2>/dev/null; then
layer_error "cannot leave an empty /$LAYER_MACHINE_ID in $rootfs"
return 1
fi
fi
if [[ -e "$dbus" || -L "$dbus" ]] && [[ "$(readlink "$dbus")" != "/$LAYER_MACHINE_ID" ]]; then
if ! { rm -f "$dbus" && ln -s "/$LAYER_MACHINE_ID" "$dbus"; } 2>/dev/null; then
layer_error "cannot link /$LAYER_DBUS_MACHINE_ID to /$LAYER_MACHINE_ID in $rootfs"
return 1
fi
fi
return 0
}

layer_check_build() {
local kind=$1
local build_dir=$2
Expand Down
2 changes: 2 additions & 0 deletions bt-layer
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,8 @@ fi

layer_check_build "$kind" "$build_dir" "$BT/conf/layer-inert-keys" \
|| fatal "nothing to export for $layer"
layer_reset_machine_id "$build_dir/root.patched" \
|| fatal "cannot reset the machine-id of $layer"

rootfs_copy=$layers_dir/$layer.rootfs
info "copying root.patched to $rootfs_copy"
Expand Down
80 changes: 78 additions & 2 deletions tests/layer
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,19 @@
# named, a quoted marker, a binary, a symlink out of the tree, an example
# unlisted, listed, its digest elsewhere, its path with another digest,
# grep failing, grep missing, the committed list well formed);
# layer_reset_machine_id (an id and a D-Bus copy, already reset and
# left untouched, missing, a symlink, a D-Bus link elsewhere, no D-Bus
# copy, no /etc, a directory in the way of either);
# layer_check_build (ok, no stamp, not mounted, no upper, a private key,
# no key list);
# layer_hash_size (ok, missing); layer_write_sha256; layer_umount_targets
# (none, mounted); layer_parent_resolve (ok, no rootfs, no manifest);
# layer_parse_args (help, name, parent, use-existing, no name, two
# names, unknown option, parent without value, self parent).
# bt-layer: --help; usage error; missing config; missing FAB_PATH; missing
# product; bad epoch; rootfs build with warning on BT_GPGKEY; RELEASE and
# product; bad epoch; rootfs build with warning on BT_GPGKEY, exported
# with an empty machine-id whatever the build left, refused when it
# cannot be reset; RELEASE and
# FAB_ARCH fallbacks; unmount guard; delta build with parent (overrides,
# altstrap created and removed, hash file with BT_GPGKEY); repeated
# export with -u is byte identical; -u without a build; unknown parent;
Expand Down Expand Up @@ -126,6 +131,10 @@ build/stamps/root.patched:
printf '#!/bin/sh\\n' > build/root.patched/bin/bash
chmod 755 build/root.patched/bin/bash
echo hostname > build/root.patched/etc/hostname
mkdir -p build/root.patched/var/lib/dbus
echo f0e97605ab594989b4d78f4a126b5b37 > build/root.patched/etc/machine-id
cp build/root.patched/etc/machine-id build/root.patched/var/lib/dbus/machine-id
if [ -f IDDIR ]; then rm build/root.patched/etc/machine-id && mkdir -p build/root.patched/etc/machine-id/x; fi
if [ -f LEAK ]; then mkdir -p build/root.patched/etc/ssh && cp LEAK build/root.patched/etc/ssh/ssh_host_rsa_key; fi
echo lib > build/.deck/root.build/upper/usr/lib/x.so
echo deb > build/.deck/root.build/upper/var/cache/apt/archives/x.deb
Expand Down Expand Up @@ -468,6 +477,61 @@ LAYER_GREP=$tmp/no-such-grep expect_fail keys-no-grep layer_audit_keys "$keys_ro
awk '!/^[[:space:]]*(#|$)/ && !($1 ~ /^[0-9a-f]{64}$/ && $2 ~ /^\// && NF == 2) { bad = 1; print "bad line: " $0 } END { exit bad }' \
"$BT/conf/layer-inert-keys" || fatal "conf/layer-inert-keys has a malformed line"

info "10d: layer_reset_machine_id"
# the machine-id every image carried on 2026-09-30, and the D-Bus copy of it
# dbus-daemon's postinst wrote as a file of its own
mid_root=$tmp/midfs
mkdir -p "$mid_root/etc" "$mid_root/var/lib/dbus"
echo f0e97605ab594989b4d78f4a126b5b37 > "$mid_root/etc/machine-id"
chmod 0444 "$mid_root/etc/machine-id"
cp "$mid_root/etc/machine-id" "$mid_root/var/lib/dbus/machine-id"
layer_reset_machine_id "$mid_root"
[[ -f "$mid_root/etc/machine-id" && ! -L "$mid_root/etc/machine-id" ]] || fatal "mid-reset: not a regular file"
[[ ! -s "$mid_root/etc/machine-id" ]] || fatal "mid-reset: the id is still there"
assert_eq mid-mode "$(stat -c %a "$mid_root/etc/machine-id")" 444
[[ -L "$mid_root/var/lib/dbus/machine-id" ]] || fatal "mid-dbus: not a symlink"
assert_eq mid-dbus-target "$(readlink "$mid_root/var/lib/dbus/machine-id")" /etc/machine-id
# a tree already reset is left as it is, so a child layer does not copy the
# parent's empty file up into its own tarball
touch -d @1234567890 "$mid_root/etc/machine-id"
chmod 0644 "$mid_root/etc/machine-id"
touch -h -d @1234567890 "$mid_root/var/lib/dbus/machine-id"
layer_reset_machine_id "$mid_root"
assert_eq mid-untouched "$(stat -c '%a %Y' "$mid_root/etc/machine-id")" "644 1234567890"
assert_eq mid-dbus-untouched "$(stat -c %Y "$mid_root/var/lib/dbus/machine-id")" 1234567890
# missing means a first boot to systemd, which presets every unit: an empty
# file is made
rm "$mid_root/etc/machine-id"
layer_reset_machine_id "$mid_root"
[[ -f "$mid_root/etc/machine-id" && ! -s "$mid_root/etc/machine-id" ]] || fatal "mid-missing: no empty file"
# a symlink, even to an empty file, is replaced by a file of the tree's own
rm "$mid_root/etc/machine-id"
: > "$tmp/elsewhere-id"
ln -s "$tmp/elsewhere-id" "$mid_root/etc/machine-id"
layer_reset_machine_id "$mid_root"
[[ -f "$mid_root/etc/machine-id" && ! -L "$mid_root/etc/machine-id" ]] || fatal "mid-symlink: still a link"
# a D-Bus link to anything but /etc/machine-id is replaced
ln -sfn /var/lib/dbus/other "$mid_root/var/lib/dbus/machine-id"
layer_reset_machine_id "$mid_root"
assert_eq mid-dbus-relink "$(readlink "$mid_root/var/lib/dbus/machine-id")" /etc/machine-id
# no D-Bus copy stays none: tmpfiles.d/dbus.conf makes the link at boot
rm "$mid_root/var/lib/dbus/machine-id"
layer_reset_machine_id "$mid_root"
[[ ! -e "$mid_root/var/lib/dbus/machine-id" && ! -L "$mid_root/var/lib/dbus/machine-id" ]] \
|| fatal "mid-no-dbus: a D-Bus copy was made"
expect_fail mid-no-etc layer_reset_machine_id "$tmp/no-such-root"
out=$(layer_reset_machine_id "$tmp/no-such-root" 2>&1) || true
[[ "$out" == *"no /etc"* ]] || fatal "mid-no-etc: unexpected message: $out"
rm "$mid_root/etc/machine-id"
mkdir "$mid_root/etc/machine-id"
expect_fail mid-directory layer_reset_machine_id "$mid_root"
rmdir "$mid_root/etc/machine-id"
mkdir -p "$mid_root/var/lib/dbus/machine-id/x"
expect_fail mid-dbus-directory layer_reset_machine_id "$mid_root"
out=$(layer_reset_machine_id "$mid_root" 2>&1) || true
[[ "$out" == *"cannot link /var/lib/dbus/machine-id"* ]] || fatal "mid-dbus-directory: unexpected message: $out"
rm -r "$mid_root/var/lib/dbus/machine-id"

info "11: layer_check_build"
build=$tmp/build
expect_fail check-nostamp layer_check_build rootfs "$build" "$keys_list"
Expand Down Expand Up @@ -606,7 +670,13 @@ assert_eq core-size "$(layer_manifest_get "$L/core.manifest" size)" "$(stat -c %
assert_eq core-sha256-file "$(cut -d ' ' -f 1 "$L/core.tar.zst.sha256")" \
"$(layer_manifest_get "$L/core.manifest" sha256)"
assert_eq core-members "$(zstd -dc "$L/core.tar.zst" | tar -t | sort | tr '\n' ' ')" \
"./ ./bin/ ./bin/bash ./etc/ ./etc/hostname "
"./ ./bin/ ./bin/bash ./etc/ ./etc/hostname ./etc/machine-id ./var/ ./var/lib/ ./var/lib/dbus/ ./var/lib/dbus/machine-id "
# the build wrote an id; the layer, and the rootfs its children start from,
# carry an empty file and a D-Bus link to it
assert_eq core-machine-id "$(zstd -dc "$L/core.tar.zst" | tar -xO ./etc/machine-id | wc -c)" 0
assert_eq core-dbus-link "$(zstd -dc "$L/core.tar.zst" | tar -tv ./var/lib/dbus/machine-id | awk '{print $NF}')" \
/etc/machine-id
[[ ! -s "$L/core.rootfs/etc/machine-id" ]] || fatal "core.rootfs: the machine-id is still there"
assert_eq core-mtime "$(zstd -dc "$L/core.tar.zst" | tar -tv --full-time ./etc/hostname | awk '{print $4}')" \
"$(date -u -d @1700000000 +%F)"

Expand Down Expand Up @@ -660,6 +730,12 @@ out=$("$sbt/bt-layer" leaky --parent core 2>&1) && fatal "leaky: a layer carryin
[[ "$out" == *"/etc/ssh/ssh_host_rsa_key"* ]] || fatal "leaky: the key is not named: $out"
[[ ! -e "$L/leaky.tar.zst" ]] || fatal "leaky: a tarball was written"
[[ ! -e "$L/leaky.manifest" ]] || fatal "leaky: a manifest was written"
# a machine-id that cannot be reset stops the export the same way
write_product "$tmp/products/idless" "turnkey.d" "turnkey.d"
touch "$tmp/products/idless/IDDIR"
out=$("$sbt/bt-layer" idless --parent core 2>&1) && fatal "idless: a layer whose machine-id cannot be reset was exported"
[[ "$out" == *"cannot reset the machine-id of idless"* ]] || fatal "idless: unexpected message: $out"
[[ ! -e "$L/idless.tar.zst" ]] || fatal "idless: a tarball was written"

info "22: bt-layer with units (a component supplied as a fab unit)"
# add_unit PRODUCT NAME VERSION: a unit fab would apply, pinned by a version
Expand Down
Loading