diff --git a/man/systemd-vmspawn.xml b/man/systemd-vmspawn.xml
index d75993846f..20be0c099b 100644
--- a/man/systemd-vmspawn.xml
+++ b/man/systemd-vmspawn.xml
@@ -385,6 +385,30 @@
+
+
+
+ Caveat: This feature is experimental, and is likely to be changed (or removed in
+ its current form) in a future version of systemd.
+
+ Configures whether to run the guest as a confidential VM. Takes one of
+ no or sev-snp. Defaults to no.
+
+ sev-snp enables AMD SEV-SNP. This requires KVM on an x86_64 host with
+ SNP-capable hardware and firmware. must point to a raw SNP-built
+ OVMF .fd image; the standard pflash + NVRAM split is not supported under
+ SNP, so the firmware is loaded via QEMU's and Secure Boot is
+ unavailable. SMBIOS credentials passed via or
+ are rejected because they are outside the SNP launch
+ measurement. Direct kernel boot via is required so that the
+ kernel, initrd and command line are hashed into the launch measurement
+ (kernel-hashes=on); booting the kernel off the disk image via the
+ firmware would leave it outside the measurement. A vTPM, if attached via
+ , must be treated as untrusted by the guest.
+
+
+
+
diff --git a/src/vmspawn/vmspawn-settings.c b/src/vmspawn/vmspawn-settings.c
index 502189e7be..d05c4e1a9e 100644
--- a/src/vmspawn/vmspawn-settings.c
+++ b/src/vmspawn/vmspawn-settings.c
@@ -36,3 +36,10 @@ static const char *const firmware_table[_FIRMWARE_MAX] = {
};
DEFINE_STRING_TABLE_LOOKUP(firmware, Firmware);
+
+static const char *const confidential_computing_table[_COCO_MAX] = {
+ [COCO_NO] = "no",
+ [COCO_AMD_SEV_SNP] = "sev-snp",
+};
+
+DEFINE_STRING_TABLE_LOOKUP(confidential_computing, ConfidentialComputing);
diff --git a/src/vmspawn/vmspawn-settings.h b/src/vmspawn/vmspawn-settings.h
index 596a66cecd..9be3afdef4 100644
--- a/src/vmspawn/vmspawn-settings.h
+++ b/src/vmspawn/vmspawn-settings.h
@@ -42,6 +42,13 @@ typedef enum Firmware {
_FIRMWARE_INVALID = -EINVAL,
} Firmware;
+typedef enum ConfidentialComputing {
+ COCO_NO,
+ COCO_AMD_SEV_SNP,
+ _COCO_MAX,
+ _COCO_INVALID = -EINVAL,
+} ConfidentialComputing;
+
typedef enum SettingsMask {
SETTING_START_MODE = UINT64_C(1) << 0,
SETTING_MACHINE_ID = UINT64_C(1) << 6,
@@ -55,3 +62,4 @@ typedef enum SettingsMask {
DECLARE_STRING_TABLE_LOOKUP(console_mode, ConsoleMode);
DECLARE_STRING_TABLE_LOOKUP(console_transport, ConsoleTransport);
DECLARE_STRING_TABLE_LOOKUP(firmware, Firmware);
+DECLARE_STRING_TABLE_LOOKUP(confidential_computing, ConfidentialComputing);
diff --git a/src/vmspawn/vmspawn.c b/src/vmspawn/vmspawn.c
index f39122e481..6cb77dfadd 100644
--- a/src/vmspawn/vmspawn.c
+++ b/src/vmspawn/vmspawn.c
@@ -162,6 +162,7 @@ static Firmware arg_firmware_type = _FIRMWARE_INVALID;
static bool arg_firmware_describe = false;
static Set *arg_firmware_features_include = NULL;
static Set *arg_firmware_features_exclude = NULL;
+static ConfidentialComputing arg_confidential_computing = COCO_NO;
static char *arg_forward_journal = NULL;
static uint64_t arg_forward_journal_max_use = UINT64_MAX;
static uint64_t arg_forward_journal_keep_free = UINT64_MAX;
@@ -637,6 +638,14 @@ static int parse_argv(int argc, char *argv[]) {
break;
}
+ OPTION_LONG("coco", "no|sev-snp", "Run the guest as a confidential VM"): {
+ ConfidentialComputing cc = confidential_computing_from_string(opts.arg);
+ if (cc < 0)
+ return log_error_errno(cc, "Unknown --coco= value: %s", opts.arg);
+ arg_confidential_computing = cc;
+ break;
+ }
+
OPTION_LONG("discard-disk", "BOOL", "Control processing of discard requests"):
r = parse_boolean_argument("--discard-disk=", opts.arg, &arg_discard_disk);
if (r < 0)
@@ -2589,7 +2598,11 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) {
use_kvm = r;
}
- if (arg_firmware_type == FIRMWARE_UEFI) {
+ if (arg_confidential_computing == COCO_AMD_SEV_SNP && !use_kvm)
+ return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP),
+ "--coco=sev-snp requires KVM, but KVM is not available.");
+
+ if (arg_firmware_type == FIRMWARE_UEFI && arg_confidential_computing != COCO_AMD_SEV_SNP) {
if (arg_firmware)
r = load_ovmf_config(arg_firmware, &ovmf_config);
else
@@ -2663,13 +2676,19 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) {
if (r < 0)
return r;
+ if (arg_confidential_computing == COCO_AMD_SEV_SNP) {
+ r = qemu_config_key(config_file, "kernel-irqchip", "split");
+ if (r < 0)
+ return r;
+ }
+
if (ovmf_config && ARCHITECTURE_SUPPORTS_SMM) {
r = qemu_config_key(config_file, "smm", on_off(ovmf_config->supports_sb));
if (r < 0)
return r;
}
- if (ARCHITECTURE_SUPPORTS_CXL) {
+ if (ARCHITECTURE_SUPPORTS_CXL && arg_confidential_computing == COCO_NO) {
r = qemu_config_key(config_file, "cxl", "on");
if (r < 0)
return r;
@@ -2687,6 +2706,12 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) {
return r;
}
+ if (arg_confidential_computing == COCO_AMD_SEV_SNP) {
+ r = qemu_config_key(config_file, "confidential-guest-support", "snp0");
+ if (r < 0)
+ return r;
+ }
+
r = qemu_config_section(config_file, "smp-opts", /* id= */ NULL,
"cpus", arg_cpus ?: "1");
if (r < 0)
@@ -2719,11 +2744,13 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) {
if (r < 0)
return r;
- r = qemu_config_section(config_file, "device", "balloon0",
- "driver", "virtio-balloon",
- "free-page-reporting", "on");
- if (r < 0)
- return r;
+ if (arg_confidential_computing == COCO_NO) {
+ r = qemu_config_section(config_file, "device", "balloon0",
+ "driver", "virtio-balloon",
+ "free-page-reporting", "on");
+ if (r < 0)
+ return r;
+ }
if (ARCHITECTURE_SUPPORTS_VMGENID) {
sd_id128_t vmgenid;
@@ -2877,6 +2904,22 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) {
return r;
}
+ if (arg_confidential_computing == COCO_AMD_SEV_SNP) {
+ /* SNP marks encrypted guest pages via the "C-bit" in the page table entry. On all
+ * SNP-capable processors (Milan and later) the C-bit lives at bit 51, which reduces
+ * the usable guest physical address space by one bit.
+ * Embed the hashes of kernel, initrd and cmdline into the firmware
+ * so they are covered by the launch measurement and the guest's
+ * boot chain starts from a measured state. */
+ r = qemu_config_section(config_file, "object", "snp0",
+ "qom-type", "sev-snp-guest",
+ "cbitpos", "51",
+ "reduced-phys-bits", "1",
+ "kernel-hashes", "on");
+ if (r < 0)
+ return r;
+ }
+
unsigned child_cid = arg_vsock_cid;
if (use_vsock) {
config.vsock.fd = TAKE_FD(vhost_device_fd);
@@ -2894,14 +2937,17 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) {
config.vsock.cid = child_cid;
}
- /* -cpu stays on cmdline since not all flags are supported in config */
- r = strv_extend_many(&cmdline, "-cpu",
+ /* -cpu stays on cmdline since not all flags are supported in config. SNP needs a stable,
+ * named CPU model so the launch measurement is reproducible across hosts; EPYC-v4 is the
+ * baseline that covers all SNP-capable processors (Milan and later). */
+ const char *cpu_model =
#ifdef __x86_64__
- "max,hv_relaxed,hv-vapic,hv-time"
+ arg_confidential_computing == COCO_AMD_SEV_SNP ? "EPYC-v4"
+ : "max,hv_relaxed,hv-vapic,hv-time";
#else
- "max"
+ "max";
#endif
- );
+ r = strv_extend_many(&cmdline, "-cpu", cpu_model);
if (r < 0)
return log_oom();
@@ -3032,9 +3078,15 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) {
}
_cleanup_(unlink_and_freep) char *ovmf_vars = NULL;
- r = cmdline_add_ovmf(config_file, ovmf_config, &ovmf_vars);
- if (r < 0)
- return r;
+ if (arg_confidential_computing != COCO_NO) {
+ r = strv_extend_many(&cmdline, "-bios", arg_firmware);
+ if (r < 0)
+ return r;
+ } else {
+ r = cmdline_add_ovmf(config_file, ovmf_config, &ovmf_vars);
+ if (r < 0)
+ return r;
+ }
if (arg_linux) {
r = strv_extend_many(&cmdline, "-kernel", arg_linux);
@@ -4009,6 +4061,43 @@ static int verify_arguments(void) {
return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
"--grow-image is not supported for qcow2 images, use 'qemu-img resize FILE SIZE'.");
+ if (arg_confidential_computing == COCO_AMD_SEV_SNP) {
+ if (native_architecture() != ARCHITECTURE_X86_64)
+ return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP),
+ "--coco=sev-snp is only supported on x86_64.");
+ if (arg_kvm == 0)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+ "--coco=sev-snp requires KVM, remove --kvm=no.");
+ if (arg_firmware_type != FIRMWARE_UEFI)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+ "--coco can't be used with %s firmware",
+ firmware_to_string(arg_firmware_type));
+ /* SNP can't use pflash + NVRAM split, so the firmware-descriptor
+ * machinery doesn't apply. Require an explicit raw .fd path and
+ * use it verbatim with -bios later. */
+ if (!arg_firmware)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+ "--coco=sev-snp requires --firmware=PATH "
+ "pointing at a raw SNP-built OVMF .fd binary.");
+ log_debug("Using raw SNP firmware at %s (no NVRAM, no Secure Boot).", arg_firmware);
+ if (set_contains(arg_firmware_features_include, "secure-boot"))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+ "--secure-boot=yes cannot be combined with --coco.");
+ if (arg_credentials.n_credentials != 0)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+ "SMBIOS credentials aren't trusted by the confidential computing guest and will be rejected.");
+ if (arg_tpm > 0)
+ log_warning("TPM can't be trusted by the confidential computing guest");
+ /* kernel-hashes=on only covers what QEMU itself loads via -kernel/-initrd/-append.
+ * Without --linux= the kernel and initrd come off disk via OVMF and aren't part
+ * of the launch measurement, leaving the guest unattestable in any meaningful
+ * way. Require direct kernel boot so the boot chain starts from a measured state. */
+ if (!arg_linux)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+ "--coco=sev-snp requires --linux= "
+ "so kernel, initrd and cmdline are covered by the launch measurement.");
+ }
+
return 0;
}