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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Security

- guest OS: disable automatic GPT partition discovery and host-supplied system
credentials on the measured kernel command line. Remove production systemd
generators that act on host-controlled, unmeasured input; pin the allowed
generator set and assert the hardening configuration during builds. Includes
the unreleased 0.6.2 fixes for GHSA-c79w-3x5w-gmm5.
- guest-agent: gate every external API route by scopes granted through signed
Ed25519 JWTs or `[cvm.guest_agent_api].anonymous_scopes`. No scopes are
anonymous by default; app SDKs keep using the internal Unix socket. The
Expand Down
15 changes: 15 additions & 0 deletions os/image/kernel-cmdline.sh
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,20 @@
# Intel's confidential-computing guest hardening names early PCI code as
# something to disable, so it stays off.

# `systemd.gpt_auto=0` disables systemd-gpt-auto-generator, which reads the root
# disk's GPT and mounts or activates partitions found there. That table is
# host-controlled and unmeasured, so without this a partition the host appends
# (swap, /var, /home, ...) is acted on inside the guest. The rootfs is located
# by PARTLABEL and verified by dm-verity in the initramfs, so nothing we rely on
# depends on the generator. Every systemd since v252 parses the flag the same
# way, unlike `systemd.image_policy=` which older versions ignore and v254/v255
# mishandle.
#
# `systemd.import_credentials=no` stops PID1 importing system credentials from
# SMBIOS and qemu fw_cfg, both host-controlled and unmeasured. Many units
# consume such credentials (sysusers, tmpfiles, udev rules, networkd, ...), so
# this closes that injection path at the source instead of masking each consumer.

# Emit the guest kernel command line.
# $1 dm-verity root hash
# $2 rootfs data size in bytes
Expand All @@ -39,5 +53,6 @@ dstack_kernel_cmdline() {
echo "console=ttyS0 init=/init panic=1 net.ifnames=0 biosdevname=0" \
"mce=off oops=panic pci=noearly random.trust_cpu=y" \
"random.trust_bootloader=n tsc=reliable no-kvmclock" \
"systemd.gpt_auto=0 systemd.import_credentials=no" \
"dstack.rootfs_hash=$root_hash dstack.rootfs_size=$data_size"
}
7 changes: 7 additions & 0 deletions os/mkosi/components/kernel/kernel.config
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,13 @@ CONFIG_SUSPEND=n
CONFIG_PM=n
CONFIG_PM_SLEEP=n

# SMBIOS tables and qemu fw_cfg blobs are host-controlled and unmeasured; these
# expose them to userspace, where systemd imports them as system credentials.
# pstore restores records the firmware hands back from a previous boot.
CONFIG_DMI_SYSFS=n
CONFIG_FW_CFG_SYSFS=n
CONFIG_PSTORE=n

# Yocto dstack fragments and minimal guest-only hardware policy.
CONFIG_9P_FS_POSIX_ACL=y
CONFIG_SERIAL_8250_PCI=y
Expand Down
27 changes: 27 additions & 0 deletions os/mkosi/mkosi.profiles/prod/mkosi.conf
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,33 @@ RemoveFiles=
/usr/lib/systemd/system/systemd-logind.service
/usr/lib/systemd/system/multi-user.target.wants/systemd-logind.service
/usr/lib/systemd/logind.conf.d
# Generators that act on host-controlled, unmeasured input or are
# simply unreachable in this image. systemd.gpt_auto=0 on the measured
# command line already disables gpt-auto; removing these binaries is
# defence in depth and mirrors meta-dstack's systemd_%.bbappend, whose
# deletion of gpt-auto the mkosi rewrite had dropped.
#
# - gpt-auto: mounts/activates partitions from the host-controlled GPT.
# - hibernate-resume: reads resume= and the EFI HibernateLocation; the
# kernel drops CONFIG_PM so hibernation cannot happen anyway.
# - bless-boot / system-update: boot-assessment and offline-update
# handshakes this image never performs.
# - sysv / rc-local: every shipped SysV script has a native unit, and
# there is no /etc/rc.local.
# - ssh: binds sshd to AF_VSOCK, but prod ships no sshd.
#
# parity.json pins the generators prod may keep.
/usr/lib/systemd/system-generators/systemd-gpt-auto-generator
/usr/lib/systemd/system-generators/systemd-hibernate-resume-generator
/usr/lib/systemd/systemd-hibernate-resume
/usr/lib/systemd/system/systemd-hibernate-resume.service
/usr/lib/systemd/system/systemd-hibernate-clear.service
/usr/lib/systemd/system/sysinit.target.wants/systemd-hibernate-clear.service
/usr/lib/systemd/system-generators/systemd-bless-boot-generator
/usr/lib/systemd/system-generators/systemd-system-update-generator
/usr/lib/systemd/system-generators/systemd-sysv-generator
/usr/lib/systemd/system-generators/systemd-rc-local-generator
/usr/lib/systemd/system-generators/systemd-ssh-generator
# Suspend/hibernate units: the kernel drops CONFIG_PM entirely, so
# these can only produce failed jobs.
/usr/lib/systemd/system/suspend.target
Expand Down
8 changes: 7 additions & 1 deletion os/mkosi/parity.json
Original file line number Diff line number Diff line change
Expand Up @@ -174,12 +174,18 @@
"usr/bin/login",
"usr/bin/loginctl",
"usr/sbin/agetty",
"usr/lib/systemd/system-generators/systemd-getty-generator",
"usr/lib/systemd/system/getty@.service",
"usr/lib/systemd/system/serial-getty@.service",
"usr/lib/systemd/system/debug-shell.service",
"usr/lib/systemd/system/systemd-logind.service"
],
"prod_allowed_generators": [
"systemd-debug-generator",
"systemd-fstab-generator",
"systemd-run-generator",
"systemd-tpm2-generator",
"zfs-mount-generator"
],
"required_ldconfig_entries": [
"libnvidia-ml.so.1",
"libnvidia-container.so.1",
Expand Down
10 changes: 10 additions & 0 deletions os/mkosi/tests/acceptance.sh
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,16 @@ if grep -q 'random.trust_bootloader' "$D/scripts/make-release-artifacts.sh"; the
echo 'kernel command line must not be restated outside kernel-cmdline.sh' >&2
exit 1
fi
# These keep the guest from acting on the host-controlled GPT and on host-supplied
# system credentials; they only protect anything while they stay measured.
# shellcheck source=../../image/kernel-cmdline.sh
cmdline=$(. "$D/../image/kernel-cmdline.sh" && dstack_kernel_cmdline hash size)
for flag in systemd.gpt_auto=0 systemd.import_credentials=no; do
if [[ " $cmdline " != *" $flag "* ]]; then
echo "kernel command line lacks $flag" >&2
exit 1
fi
done
grep -q 'Archiving UKI image' "$D/../image/assemble.sh"
grep -Fq "FLAVORS=\${FLAVORS:-prod}" "$D/build.sh"
# A cache hit must never be able to answer the question repro-check asks.
Expand Down
17 changes: 17 additions & 0 deletions os/mkosi/tests/check-parity.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,23 @@ def resolves_in_rootfs(root: str, path: str) -> bool:
for path in spec.get("prod_forbidden_paths", []):
if os.path.lexists(os.path.join(rootfs, path)):
missing.append(f"prod contains forbidden path:/{path}")
# Generators run as root before any unit and act on whatever they find, so
# prod pins the exact set: a package upgrade that ships a new one fails the
# build instead of silently widening what the host can influence.
generators = {
os.path.basename(path)
for pattern in (
"usr/lib/systemd/system-generators/*",
"usr/local/lib/systemd/system-generators/*",
"etc/systemd/system-generators/*",
)
for path in glob.glob(os.path.join(rootfs, pattern))
}
allowed = set(spec["prod_allowed_generators"])
for name in sorted(generators - allowed):
missing.append(f"prod contains unexpected generator: {name}")
for name in sorted(allowed - generators):
missing.append(f"prod is missing allowed generator: {name}")
for path, expected in spec.get("required_symlink_resolutions", {}).items():
# Which netfilter frontend the image programs is decided by a symlink, and
# on this backend that symlink comes from the distribution's default rather
Expand Down
Loading