Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
d602fb0
A pin that is behind its upstream is held to a written review, and th…
DevomB Sep 19, 2026
b3e3fa3
The survey can read the sixteen hosts it could not, and four of those…
DevomB Sep 19, 2026
256c674
The pin reviews, and CI asks for them: 26 pins reviewed as fine, six …
DevomB Sep 19, 2026
9970703
The update channel's rules: which statement of what is current zone 0…
DevomB Sep 20, 2026
b17f278
The pin gate is sixty lines, not a hundred and fifty, and says the sa…
DevomB Sep 20, 2026
143b18f
kryptik-update checks a manifest and a pointer for the update channel…
DevomB Sep 20, 2026
bb1938f
What zone 0 keeps for the update channel, and what each of the three …
DevomB Sep 20, 2026
a2c38e5
The update channel's verbs on the broker, and `kryptik update` for th…
DevomB Sep 20, 2026
174995f
The net zone fetches for the update channel: a pipe with a Range head…
DevomB Sep 20, 2026
4001a01
Merge remote-tracking branch 'origin/main' into update-channel
DevomB Sep 20, 2026
603d35e
The update channel's design says what was built, and the roadmap says…
DevomB Sep 20, 2026
b529c30
The launch service's log line for an update verb names the verb and n…
DevomB Sep 20, 2026
43816d6
kryptik-update makes its own snapshot directory and never takes one f…
DevomB Sep 20, 2026
c82469d
Merge main into pin-currency-gate: the roadmap moved under it
DevomB Sep 20, 2026
80a241f
Merge remote-tracking branch 'origin/main' into update-channel
DevomB Sep 20, 2026
54aeda1
A pointer can be verified on a real image: the trust anchor enrols a …
DevomB Sep 20, 2026
da6113b
The release tool writes and signs the statement of what is current, a…
DevomB Sep 20, 2026
e0b2a5f
The update suite fetches a release over the network: the channel, end…
DevomB Sep 20, 2026
0e422ca
The update suite's network step, after review: a proven restart, a wa…
DevomB Sep 20, 2026
709d7db
Merge remote-tracking branch 'origin/main' into update-channel
DevomB Sep 20, 2026
bcea985
The broker's fuzz seeds gain the update channel's three verbs
DevomB Sep 20, 2026
b040194
Merge main into pin-currency-gate: the roadmap's pins entry moved und…
DevomB Sep 20, 2026
2176597
Three code scanning alerts fixed at the source: no shell in the EFI t…
DevomB Sep 20, 2026
9d54924
A password has its own way to the guest, so no path leads from one to…
DevomB Sep 20, 2026
a8ffcfb
The build checks that the device helper is executable, now that a too…
DevomB Sep 20, 2026
37203ee
util-linux 2.42.3 builds against glibc 2.40, and asks the kernel to b…
DevomB Sep 20, 2026
7dbd0a5
The suites' release host holds a requested name to its root before it…
DevomB Sep 20, 2026
6ac2a4f
Merge remote-tracking branch 'origin/main' into update-channel
DevomB Sep 20, 2026
c939eb0
Merge the util-linux-resolve-flag branch (#40): util-linux 2.42.3 bui…
DevomB Sep 20, 2026
cdb2471
Merge the code-scanning-round-two branch (#39): no shell in the EFI t…
DevomB Sep 20, 2026
fcffb04
Merge the pin-currency-gate branch (#26): a pin that is behind its up…
DevomB Sep 20, 2026
4d8f6e2
Merge the update-channel branch (#33): the net zone fetches, zone 0 b…
DevomB Sep 20, 2026
6fdccf8
The six installed-system suites share one copy of their helpers
DevomB Sep 20, 2026
1e17453
The state partition is LUKS2, asked for on the console at every boot
DevomB Sep 20, 2026
c281f2f
A kernel is judged before it gets its name on the ESP, and a read-bac…
DevomB Sep 20, 2026
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 .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,21 @@ jobs:
- name: Pinned series are supported upstream
run: ./tools/check-support-status.sh --strict

# Survey (network) then gate (no network). On a pull request the verdict
# is printed and does not fail the check: an upstream that released this
# morning is not the pull request's fault. The weekly run is the alarm.
- name: Pins behind upstream are reviewed
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
./tools/check-source-currency.sh --tsv > pin-survey.tsv
echo "survey: $(wc -l < pin-survey.tsv) sources"
if [ "${{ github.event_name }}" = "pull_request" ]; then
./tools/check-pin-reviews.sh --survey pin-survey.tsv || echo "pull request: informational"
else
./tools/check-pin-reviews.sh --survey pin-survey.tsv
fi

- name: Pinned kernel is longterm and not EOL
run: ./tools/check-kernel-eol.sh

Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/distro.yml
Original file line number Diff line number Diff line change
Expand Up @@ -316,8 +316,8 @@ jobs:
# Never the private halves of the keys: the acceptance job needs the
# certificate, its DER form and the variable stores, nothing that
# signs. An artifact of a public repository is public.
sudo tar --zstd -cf work-after-media.tar.zst -C work --exclude='images/kryptik-root.img' --exclude='images/esp-*.img' --exclude='keys/sb/kryptik-sb.key' --exclude='keys/release/kryptik-release' .
if sudo tar --zstd -tf work-after-media.tar.zst | grep -E 'kryptik-sb\.key$|keys/release/kryptik-release$'; then echo "a private key is in the artifact"; exit 1; fi
sudo tar --zstd -cf work-after-media.tar.zst -C work --exclude='images/kryptik-root.img' --exclude='images/esp-*.img' --exclude='keys/sb/kryptik-sb.key' --exclude='keys/release/kryptik-release' --exclude='keys/release/kryptik-latest' .
if sudo tar --zstd -tf work-after-media.tar.zst | grep -E 'kryptik-sb\.key$|keys/release/kryptik-(release|latest)$'; then echo "a private key is in the artifact"; exit 1; fi
sudo chown "$(id -u):$(id -g)" work-after-media.tar.zst
ls -la work-after-media.tar.zst; df -h /mnt | tail -1

Expand Down
31 changes: 29 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -99,11 +99,11 @@ CHROOT_ENV := KRYPTIK_ROOT="$(ROOT)" \

CHROOT_RUN := $(SUDO) env $(CHROOT_ENV) "$(CHROOTD)"

.PHONY: test help check check-kernel-eol sources lock verify verify-provenance \
.PHONY: test help check check-kernel-eol check-pins test-pin-reviews sources lock verify verify-provenance \
vm-disk vm-disk-boot vm-restart vm-measure cli-test update-tree-test identity-test serve-test \
test-harness test-hardening test-artifacts audit-artifacts test-boot-success \
audit-artifacts-strict manifest verify-manifest test-manifest \
test-s6-init smoke-userspace test-services test-netzone-time test-libc-unwind \
test-s6-init smoke-userspace test-services test-netzone-time test-update-verify test-update-fetch test-libc-unwind \
sign-image verify-image test-image-signing test-installer test-mkdisk-guards \
install-test \
image image-boot \
Expand Down Expand Up @@ -152,6 +152,9 @@ help:
@echo " make verify-provenance signed tags + publisher checksums for the rest"
@echo " make validate-kernel check kernel fragment against pinned source"
@echo " make check-kernel-eol fail if the pinned kernel is EOL or not LTS"
@echo " make check-pins survey every pin against its upstream (network), then"
@echo " fail on one that is behind without a current review in"
@echo " tools/pin-reviews.tsv. PINS_FLAGS=--no-held is what a release asks"
@echo " make validate-kernel-hardened check the linux-hardened fragment"
@echo " make check-kernel-hardening resolve the config against the pinned source as"
@echo " stage 05 does, refuse a dropped fragment line, then run"
Expand Down Expand Up @@ -185,6 +188,8 @@ help:
@echo " make smoke-userspace RUN the built userland in the chroot (needs root)"
@echo " make test-services validate the s6-rc service tree"
@echo " make test-netzone-time the net zone's time measurement, under every shell here"
@echo " make test-update-verify what kryptik-update believes: a payload, a manifest, a pointer"
@echo " make test-update-fetch the net zone's update fetcher, against a local server and broker"
@echo " make identity-test zone files, compositor colour table and zoneid audit agree"
@echo " make test-libc-unwind prove the target libc can unwind (needs root)"
@echo " make sign-image sign the disk image with a developer key"
Expand Down Expand Up @@ -247,6 +252,14 @@ validate-kernel:
check-kernel-eol:
@"$(TOOLS)"/check-kernel-eol.sh

# The survey asks the network and judges nothing; the gate reads the survey
# and tools/pin-reviews.tsv and never the network.
PINS_SURVEY ?= $(KRYPTIK_WORK)/pin-survey.tsv
check-pins:
@mkdir -p "$(dir $(PINS_SURVEY))"
@"$(TOOLS)"/check-source-currency.sh --tsv > "$(PINS_SURVEY)"
@"$(TOOLS)"/check-pin-reviews.sh --survey "$(PINS_SURVEY)" $(PINS_FLAGS)

validate-kernel-hardened:
@"$(TOOLS)"/validate-kernel-config.sh --hardened

Expand Down Expand Up @@ -561,6 +574,9 @@ test-hardening:
test-kernel-hardening:
@"$(TOOLS)"/test-check-kernel-hardening.sh

test-pin-reviews:
@"$(TOOLS)"/test-check-pin-reviews.sh

test-artifacts:
@"$(TOOLS)"/test-artifact-hardening.sh

Expand Down Expand Up @@ -600,6 +616,17 @@ test-services:
test-netzone-time:
@"$(TOOLS)"/test-netzone-time.sh

# What kryptik-update believes, with its own functions and a real ssh-keygen:
# a payload whose manifest is swapped under it, and the two checks the update
# channel runs on a manifest and on a statement of what is current.
test-update-verify:
@"$(TOOLS)"/test-update-manifest-snapshot.sh

# The net zone's half of the update channel: a faithful pipe, from the offsets
# zone 0 names, in pieces zone 0 takes, that stops when zone 0 says no.
test-update-fetch:
@"$(TOOLS)"/test-update-fetch.sh

# boot-success.sh's decision table (commit, refuse, fall back), driven on
# the host with stand-ins for the services, the ESP and the firmware.
test-boot-success:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
libmount: RESOLVE_NO_SYMLINKS is 0x04, and hook_idmap.c includes its definition

Carried by Kryptik against util-linux 2.42.3; not upstream at the time of
writing. Two defects in the restricted-mount hardening, both visible only
where the C library's <fcntl.h> does not bring <linux/openat2.h> in (glibc
before 2.43):

* libmount/src/hook_idmap.c uses RESOLVE_NO_SYMLINKS and includes nothing
that defines it, so it does not compile.
* include/fileutils.h defines a fallback for it, 0x02. In the kernel's ABI
0x02 is RESOLVE_NO_MAGICLINKS; RESOLVE_NO_SYMLINKS is 0x04. context.c and
hook_mount.c compile with the fallback, so a restricted mount asks the
kernel to block magic links and not symlinks.

fileutils.h now includes <linux/openat2.h> where configure found it, and its
fallback is the kernel's value; hook_idmap.c includes fileutils.h as the
other two do.

--- a/include/fileutils.h
+++ b/include/fileutils.h
@@ -65,8 +65,11 @@
extern int ul_openat_resolve(int dirfd, const char *path, int flags,
mode_t mode, unsigned long long resolve);

+#ifdef HAVE_LINUX_OPENAT2_H
+# include <linux/openat2.h>
+#endif
#ifndef RESOLVE_NO_SYMLINKS
-# define RESOLVE_NO_SYMLINKS 0x02
+# define RESOLVE_NO_SYMLINKS 0x04
#endif
#ifndef RESOLVE_BENEATH
# define RESOLVE_BENEATH 0x08
--- a/libmount/src/hook_idmap.c
+++ b/libmount/src/hook_idmap.c
@@ -26,6 +26,7 @@
#include "namespace.h"

#include "mountP.h"
+#include "fileutils.h"

#ifdef HAVE_LINUX_NSFS_H
# include <linux/nsfs.h>
27 changes: 27 additions & 0 deletions build/patches/util-linux-2.42.3/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# util-linux 2.42.3: the restricted-mount flag, where libc does not define it

Applied by `s_util_linux` in stage 04 through `apply_repo_patches`;
`SHA256SUMS` is verified before anything is applied.

util-linux 2.42 makes a restricted mount refuse symlinks in its paths by
passing `RESOLVE_NO_SYMLINKS` to `openat2()`. On a C library whose `<fcntl.h>`
does not bring `<linux/openat2.h>` in (glibc before 2.43; Kryptik is on 2.40)
that has two defects:

- `libmount/src/hook_idmap.c` uses the constant and includes nothing that
defines it. It does not compile: this is what stopped the build.
- `include/fileutils.h` defines a fallback of `0x02`. In the kernel's ABI
`0x02` is `RESOLVE_NO_MAGICLINKS`; `RESOLVE_NO_SYMLINKS` is `0x04`.
`context.c` compiled with the fallback, so a restricted mount asked the
kernel to block the wrong thing.

The patch makes `fileutils.h` include the kernel header where configure found
it, corrects the fallback, and has `hook_idmap.c` include `fileutils.h` as
`context.c` and `hook_mount.c` do. Checked on a glibc 2.39 host: the released
tarball fails at `hook_idmap.c:335`; patched, it builds, and `context.c`
preprocesses to `0x04` where it had `0x02`.

Kryptik's `mount` is not setuid, so its restricted mode is not reachable here;
the value is corrected because a carried patch should not leave a known-wrong
constant beside the line it fixes. Not upstream when this was written. Delete
this directory when a util-linux release carries the fix.
1 change: 1 addition & 0 deletions build/patches/util-linux-2.42.3/SHA256SUMS
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
fc9be07ae0bb2e7656d12e17cf64d21ce1982037516d777fe8b60348a096c8bb 0001-libmount-RESOLVE_NO_SYMLINKS-is-0x04-and-hook_idmap-includes-it.patch
9 changes: 7 additions & 2 deletions build/service-scripts/installer-run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,11 @@ fi
# `... | sed` followed by rc=$? reads sed's status, which is how a missing
# partitioner was once reported as rc=0.
logf=/run/kryptik-install.log
# The state passphrase goes in on standard input (printf is a builtin), the
# one place it is ever written down being the control disk.
sp="$(testctl_get state_passphrase)"
# shellcheck disable=SC2086 # preseed_args is deliberately word-split
/usr/sbin/kryptik-install --target "$target" --yes $preseed_args > "$logf" 2>&1
printf '%s\n' "$sp" | /usr/sbin/kryptik-install --target "$target" --yes $preseed_args > "$logf" 2>&1
rc=$?
sed 's/^/KRYPTIK_INSTALL: /' "$logf"
say "rc=${rc}"
Expand All @@ -93,12 +96,14 @@ if [ "$rc" -eq 0 ]; then
say "verify: could not mount the ESP read-only"
fi
st="$(blkid -t PARTLABEL=kryptik-state -o device 2>/dev/null | grep "^${target}" | head -1)"
if [ -n "$st" ] && mount -o ro "$st" /run/verify 2>/dev/null; then
if [ -n "$st" ] && printf '%s' "$sp" | cryptsetup open --readonly --type luks2 --key-file=- "$st" kryptik-verify-state 2>/dev/null \
&& mount -o ro /dev/mapper/kryptik-verify-state /run/verify 2>/dev/null; then
say "verify: state_marker=$([ -e /run/verify/.kryptik-state ] && echo yes || echo no)"
say "verify: install_json=$([ -r /run/verify/lib/kryptik/install.json ] && echo yes || echo no)"
say "verify: preseed=$([ -r /run/verify/lib/kryptik/firstboot.preseed ] && echo present || echo none)"
umount /run/verify
fi
cryptsetup close kryptik-verify-state 2>/dev/null || true
slot_a="$(blkid -t PARTLABEL=kryptik-a -o device 2>/dev/null | grep "^${target}" | head -1)"
if [ -n "$slot_a" ]; then
say "verify: slot_a_sha256=$(head -c "$(cat /etc/kryptik/root-image-bytes 2>/dev/null || echo 0)" "$slot_a" | sha256sum | cut -c1-64)"
Expand Down
41 changes: 36 additions & 5 deletions build/service-scripts/sysinit.sh
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
#!/bin/sh -e
# Idempotent on purpose: s6-rc may run this again after a runlevel change.

# The console is this script's while it runs, because it may ask for the state
# passphrase there and two readers on one terminal lose keystrokes:
# kryptik-console holds the getty back until this has finished, however it ends.
echo running > /run/kryptik-sysinit
trap 'echo finished > /run/kryptik-sysinit' EXIT

[ -r /etc/hostname ] && hostname "$(cat /etc/hostname)" || true

# The names under /etc the overlay's upper layer may carry: the account
Expand Down Expand Up @@ -38,6 +44,25 @@ prune_etc_upper() { # prune_etc_upper UPPER QUARANTINE
return 0
}

# Ask for the state passphrase on the console, three times at most. Echo goes
# off before the prompt is printed and stty never discards input, so an answer
# that arrives the moment the prompt appears is not lost. printf is a builtin:
# the passphrase reaches cryptsetup on a descriptor, never as an argument.
unlock_state() { # unlock_state DEVICE -> /dev/mapper/kryptik-state
try=1
while [ "$try" -le 3 ] && [ ! -b /dev/mapper/kryptik-state ]; do
stty -echo < /dev/console 2>/dev/null || true
printf 'sysinit: passphrase for the state partition (try %s of 3): ' "$try" > /dev/console
IFS= read -r pass < /dev/console || pass=""
stty echo < /dev/console 2>/dev/null || true
echo > /dev/console
printf '%s' "$pass" | cryptsetup open --type luks2 --key-file=- "$1" kryptik-state 2>/dev/null || true
try=$((try + 1))
done
pass=""
[ -b /dev/mapper/kryptik-state ]
}

# The kernel mounts devtmpfs itself (CONFIG_DEVTMPFS_MOUNT=y); these are the
# rest, each guarded because stage 2 init may already have done it.
mountpoint -q /proc || mount -t proc proc /proc -o nosuid,noexec,nodev
Expand Down Expand Up @@ -140,9 +165,15 @@ if ! mountpoint -q /var; then
state_dev="$(kryptik_part kryptik-state)"
if [ ! -b "$state_dev" ]; then
STATE=degraded; STATE_REASON="${state_dev} is not a block device"
elif mount -t ext4 -o nosuid,nodev,noatime "$state_dev" "$state_mnt" 2>/run/kryptik/state-mount.err; then
elif ! cryptsetup isLuks --type luks2 "$state_dev" 2>/dev/null; then
# Never mounted as found: a plain filesystem put in the encrypted
# one's place would otherwise be believed without a question.
STATE=degraded; STATE_REASON="${state_dev} carries no LUKS2 header"
elif ! unlock_state "$state_dev"; then
STATE=degraded; STATE_REASON="${state_dev} was not unlocked in three tries; reboot to try again"
elif mount -t ext4 -o nosuid,nodev,noatime /dev/mapper/kryptik-state "$state_mnt" 2>/run/kryptik/state-mount.err; then
STATE=persistent
echo "sysinit: state partition ${state_dev} mounted (disk ${root_disk})"
echo "sysinit: state partition ${state_dev} unlocked and mounted (disk ${root_disk})"
else
STATE=degraded; STATE_REASON="mount of ${state_dev} failed: $(tr '\n' ' ' < /run/kryptik/state-mount.err)"
fi
Expand Down Expand Up @@ -212,9 +243,9 @@ rmdir "$state_mnt" 2>/dev/null || true
# the release trust anchor (kryptik-update) /usr/share/kryptik/trust
# all of which sit on the verified root. What remains under /etc is what
# must be mutable: accounts and passwords, hostname, the local user's
# session hooks. Their protection is the state partition's, and that
# partition is not encrypted in this developer tier - a stated limitation,
# not tamper protection.
# session hooks. Their protection is the state partition's: encrypted, so an
# offline reader learns nothing, and not authenticated, so an offline writer
# can still damage it. That is why the list above stays.
if ! mountpoint -q /etc; then
mkdir -p /var/lib/kryptik/etc/upper /var/lib/kryptik/etc/work
if mount -t overlay overlay \
Expand Down
Loading
Loading