From 28ae95bd096353522af261b0219e7331beebbad2 Mon Sep 17 00:00:00 2001 From: Anton Iacobaeus <46004494+antoniacobaeus@users.noreply.github.com> Date: Tue, 13 May 2025 03:43:15 +0200 Subject: [PATCH] feat(vm): add support for AMD SEV (#1952) Signed-off-by: Anton Iacobaeus --- docs/resources/virtual_environment_vm.md | 35 +++++ proxmox/nodes/vms/custom_amdsev.go | 110 +++++++++++++ proxmox/nodes/vms/vms_types.go | 2 + proxmoxtf/resource/vm/validators.go | 5 + proxmoxtf/resource/vm/vm.go | 189 +++++++++++++++++++++++ 5 files changed, 341 insertions(+) create mode 100644 proxmox/nodes/vms/custom_amdsev.go diff --git a/docs/resources/virtual_environment_vm.md b/docs/resources/virtual_environment_vm.md index f6f4ad47..ea479abe 100755 --- a/docs/resources/virtual_environment_vm.md +++ b/docs/resources/virtual_environment_vm.md @@ -134,6 +134,20 @@ output "ubuntu_vm_public_key" { - `type` - (Optional) The QEMU agent interface type (defaults to `virtio`). - `isa` - ISA Serial Port. - `virtio` - VirtIO (paravirtualized). +- `amd_sev` - (Optional) Secure Encrypted Virtualization (SEV) features by AMD CPUs. + - `type` - (Optional) Enable standard SEV with `std` or enable experimental + SEV-ES with the `es` option or enable experimental SEV-SNP with the `snp` option + (defaults to `std`). + - `allow_smt` - (Optional) Sets policy bit to allow Simultaneous Multi Threading (SMT) + (Ignored unless for SEV-SNP) (defaults to `true`). + - `kernel_hashes` - (Optional) Add kernel hashes to guest firmware for measured + linux kernel launch (defaults to `false`). + - `no_debug` - (Optional) Sets policy bit to disallow debugging of guest (defaults + to `false`). + - `no_key_sharing` - (Optional) Sets policy bit to disallow key sharing with + other guests (Ignored for SEV-SNP) (defaults to `false`). + + The `amd_sev` setting is only allowed for a `root@pam` authenticated user. - `audio_device` - (Optional) An audio device. - `device` - (Optional) The device (defaults to `intel-hda`). - `AC97` - Intel 82801AA AC97 Audio. @@ -642,6 +656,27 @@ and when refreshing resources. The provider has no way to distinguish between trusts the user to set `agent.enabled` correctly and waits for `qemu-guest-agent` to start. +## AMD SEV +AMD SEV (-ES, -SNP) are security features for AMD processors. SEV-SNP support +is included in Proxmox version **8.4**, see [Proxmox Wiki]( +https://pve.proxmox.com/wiki/Qemu/KVM_Virtual_Machines#qm_virtual_machines_settings) +and [Proxmox Documentation](https://pve.proxmox.com/pve-docs/pve-admin-guide.html#qm_memory_encryption) +for more information. + +`amd-sev` requires root and therefore `root@pam` auth. + +SEV-SNP requires `bios = OVMF` and a supported AMD CPU (`EPYC-v4` for instance), +`machine = q35` is also advised. No EFI disk is required since SEV-SNP uses +consolidated read-only firmware. A configured EFI will be ignored. + +All changes made to `amd_sev` will trigger reboots. Removing or adding the +`amd_sev` block will force a replacement of the resource. Modifying the `amd_sev` +block will not trigger replacements. + +`allow_smt` is by default set to `true` even if `snp` is not the selected type. +Proxmox will ignore this value when `snp` is not in use. Likewise `no_key_sharing` +is `false` by default but ignored by Proxmox when `snp` is in use. + ## Important Notes ### `local-lvm` Datastore diff --git a/proxmox/nodes/vms/custom_amdsev.go b/proxmox/nodes/vms/custom_amdsev.go new file mode 100644 index 00000000..4b128cdd --- /dev/null +++ b/proxmox/nodes/vms/custom_amdsev.go @@ -0,0 +1,110 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package vms + +import ( + "encoding/json" + "fmt" + "net/url" + "strings" + + "github.com/bpg/terraform-provider-proxmox/proxmox/types" +) + +// CustomAMDSEV handles AMDSEV parameters. +type CustomAMDSEV struct { + Type string `json:"type" url:"type"` + AllowSMT *types.CustomBool `json:"allow-smt" url:"allow-smt,int"` + KernelHashes *types.CustomBool `json:"kernel-hashes" url:"kernel-hashes,int"` + NoDebug *types.CustomBool `json:"no-debug" url:"no-debug,int"` + NoKeySharing *types.CustomBool `json:"no-key-sharing" url:"no-key-sharing,int"` +} + +// EncodeValues converts a CustomAMDSEV struct to a URL value. +func (r *CustomAMDSEV) EncodeValues(key string, v *url.Values) error { + values := []string{ + fmt.Sprintf("type=%s", r.Type), + } + + if r.AllowSMT != nil { + if *r.AllowSMT { + values = append(values, "allow-smt=1") + } else { + values = append(values, "allow-smt=0") + } + } + + if r.KernelHashes != nil { + if *r.KernelHashes { + values = append(values, "kernel-hashes=1") + } else { + values = append(values, "kernel-hashes=0") + } + } + + if r.NoDebug != nil { + if *r.NoDebug { + values = append(values, "no-debug=1") + } else { + values = append(values, "no-debug=0") + } + } + + if r.NoKeySharing != nil { + if *r.NoKeySharing { + values = append(values, "no-key-sharing=1") + } else { + values = append(values, "no-key-sharing=0") + } + } + + if len(values) > 0 { + v.Add(key, strings.Join(values, ",")) + } + + return nil +} + +// UnmarshalJSON converts a CustomAMDSEV string to an object. +func (r *CustomAMDSEV) UnmarshalJSON(b []byte) error { + var s string + + if err := json.Unmarshal(b, &s); err != nil { + return fmt.Errorf("error unmarshalling CustomAMDSEV: %w", err) + } + + pairs := strings.Split(s, ",") + + for i, p := range pairs { + v := strings.Split(strings.TrimSpace(p), "=") + + if len(v) == 1 && i == 0 { + r.Type = v[0] + } + + if len(v) == 2 { + switch v[0] { + case "type": + r.Type = v[1] + case "allow-smt": + allow_smt := types.CustomBool(v[1] == "1") + r.AllowSMT = &allow_smt + case "kernel-hashes": + kernel_hashes := types.CustomBool(v[1] == "1") + r.KernelHashes = &kernel_hashes + case "no-debug": + no_debug := types.CustomBool(v[1] == "1") + r.NoDebug = &no_debug + case "no-key-sharing": + no_key_sharing := types.CustomBool(v[1] == "1") + r.NoKeySharing = &no_key_sharing + } + } + } + + return nil +} diff --git a/proxmox/nodes/vms/vms_types.go b/proxmox/nodes/vms/vms_types.go index 27dc5b87..d8211b5f 100644 --- a/proxmox/nodes/vms/vms_types.go +++ b/proxmox/nodes/vms/vms_types.go @@ -47,6 +47,7 @@ type CloneRequestBody struct { type CreateRequestBody struct { ACPI *types.CustomBool `json:"acpi,omitempty" url:"acpi,omitempty,int"` Agent *CustomAgent `json:"agent,omitempty" url:"agent,omitempty"` + AMDSEV *CustomAMDSEV `json:"amd-sev,omitempty" url:"amd-sev,omitempty"` AllowReboot *types.CustomBool `json:"reboot,omitempty" url:"reboot,omitempty,int"` AudioDevices CustomAudioDevices `json:"audio,omitempty" url:"audio,omitempty"` Autostart *types.CustomBool `json:"autostart,omitempty" url:"autostart,omitempty,int"` @@ -183,6 +184,7 @@ type GetResponseBody struct { type GetResponseData struct { ACPI *types.CustomBool `json:"acpi,omitempty"` Agent *CustomAgent `json:"agent,omitempty"` + AMDSEV *CustomAMDSEV `json:"amd-sev,omitempty"` AllowReboot *types.CustomBool `json:"reboot,omitempty"` AudioDevice *CustomAudioDevice `json:"audio0,omitempty"` Autostart *types.CustomBool `json:"autostart,omitempty"` diff --git a/proxmoxtf/resource/vm/validators.go b/proxmoxtf/resource/vm/validators.go index d551fd47..f19648a0 100755 --- a/proxmoxtf/resource/vm/validators.go +++ b/proxmoxtf/resource/vm/validators.go @@ -158,6 +158,11 @@ func QEMUAgentTypeValidator() schema.SchemaValidateDiagFunc { return validation.ToDiagFunc(validation.StringInSlice([]string{"isa", "virtio"}, false)) } +// AMDSEVTypeValidator is a schema validation function for AMDSEV types. +func AMDSEVTypeValidator() schema.SchemaValidateDiagFunc { + return validation.ToDiagFunc(validation.StringInSlice([]string{"std", "es", "snp"}, false)) +} + // KeyboardLayoutValidator is a schema validation function for keyboard layouts. func KeyboardLayoutValidator() schema.SchemaValidateDiagFunc { return validation.ToDiagFunc(validation.StringInSlice([]string{ diff --git a/proxmoxtf/resource/vm/vm.go b/proxmoxtf/resource/vm/vm.go index aae8eb0f..f0cc8f09 100644 --- a/proxmoxtf/resource/vm/vm.go +++ b/proxmoxtf/resource/vm/vm.go @@ -49,6 +49,11 @@ const ( dvAgentTimeout = "15m" dvAgentTrim = false dvAgentType = "virtio" + dvAMDSEVType = "std" + dvAMDSEVAllowSMT = true + dvAMDSEVKernelHashes = false + dvAMDSEVNoDebug = false + dvAMDSEVNoKeySharing = false dvAudioDeviceDevice = "intel-hda" dvAudioDeviceDriver = "spice" dvAudioDeviceEnabled = true @@ -153,6 +158,12 @@ const ( mkAgentTimeout = "timeout" mkAgentTrim = "trim" mkAgentType = "type" + mkAMDSEV = "amd_sev" + mkAMDSEVType = "type" + mkAMDSEVAllowSMT = "allow_smt" + mkAMDSEVKernelHashes = "kernel_hashes" + mkAMDSEVNoDebug = "no_debug" + mkAMDSEVNoKeySharing = "no_key_sharing" mkAudioDevice = "audio_device" mkAudioDeviceDevice = "device" mkAudioDeviceDriver = "driver" @@ -389,6 +400,53 @@ func VM() *schema.Resource { MaxItems: 1, MinItems: 0, }, + mkAMDSEV: { + Type: schema.TypeList, + Description: "Secure Encrypted Virtualization (SEV) features by AMD CPUs", + Optional: true, + ForceNew: true, + DefaultFunc: func() (interface{}, error) { + return []interface{}{}, nil + }, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + mkAMDSEVType: { + Type: schema.TypeString, + Description: "Enable standard SEV with type=std or enable experimental SEV-ES with the es option" + + "or enable experimental SEV-SNP with the snp option.", + Optional: true, + Default: dvAMDSEVType, + ValidateDiagFunc: AMDSEVTypeValidator(), + }, + mkAMDSEVAllowSMT: { + Type: schema.TypeBool, + Description: "Sets policy bit to allow Simultaneous Multi Threading (SMT) (Ignored unless for SEV-SNP)", + Optional: true, + Default: dvAMDSEVAllowSMT, + }, + mkAMDSEVKernelHashes: { + Type: schema.TypeBool, + Description: "Add kernel hashes to guest firmware for measured linux kernel launch", + Optional: true, + Default: dvAMDSEVKernelHashes, + }, + mkAMDSEVNoDebug: { + Type: schema.TypeBool, + Description: "Sets policy bit to disallow debugging of guest", + Optional: true, + Default: dvAMDSEVNoDebug, + }, + mkAMDSEVNoKeySharing: { + Type: schema.TypeBool, + Description: "Sets policy bit to disallow key sharing with other guests (Ignored for SEV-SNP)", + Optional: true, + Default: dvAMDSEVNoKeySharing, + }, + }, + }, + MaxItems: 1, + MinItems: 0, + }, mkKVMArguments: { Type: schema.TypeString, Description: "The args implementation", @@ -1932,6 +1990,7 @@ func vmCreateClone(ctx context.Context, d *schema.ResourceData, m interface{}) d acpi := types.CustomBool(d.Get(mkACPI).(bool)) agent := d.Get(mkAgent).([]interface{}) + amdsev := d.Get(mkAMDSEV).([]interface{}) bios := d.Get(mkBIOS).(string) cdrom := d.Get(mkCDROM).([]interface{}) cpu := d.Get(mkCPU).([]interface{}) @@ -1982,6 +2041,32 @@ func vmCreateClone(ctx context.Context, d *schema.ResourceData, m interface{}) d } } + if len(amdsev) > 0 && amdsev[0] != nil { + amdsevBlock := amdsev[0].(map[string]interface{}) + + amdsevType := amdsevBlock[mkAMDSEVType].(string) + amdsevAllowSMT := types.CustomBool( + amdsevBlock[mkAMDSEVAllowSMT].(bool), + ) + amdsevKernelHashes := types.CustomBool( + amdsevBlock[mkAMDSEVKernelHashes].(bool), + ) + amdsevNoDebug := types.CustomBool( + amdsevBlock[mkAMDSEVNoDebug].(bool), + ) + amdsevNoKeySharing := types.CustomBool( + amdsevBlock[mkAMDSEVNoKeySharing].(bool), + ) + + updateBody.AMDSEV = &vms.CustomAMDSEV{ + Type: amdsevType, + AllowSMT: &amdsevAllowSMT, + KernelHashes: &amdsevKernelHashes, + NoDebug: &amdsevNoDebug, + NoKeySharing: &amdsevNoKeySharing, + } + } + if kvmArguments != dvKVMArguments { updateBody.KVMArguments = &kvmArguments } @@ -2437,6 +2522,8 @@ func vmCreateCustom(ctx context.Context, d *schema.ResourceData, m interface{}) agentTrim := types.CustomBool(agentBlock[mkAgentTrim].(bool)) agentType := agentBlock[mkAgentType].(string) + amdsev := vmGetAMDSEVObject(d) + kvmArguments := d.Get(mkKVMArguments).(string) audioDevices := vmGetAudioDeviceList(d) @@ -2718,6 +2805,7 @@ func vmCreateCustom(ctx context.Context, d *schema.ResourceData, m interface{}) TrimClonedDisks: &agentTrim, Type: &agentType, }, + AMDSEV: amdsev, AudioDevices: audioDevices, BIOS: &bios, Boot: &vms.CustomBoot{ @@ -2869,6 +2957,39 @@ func vmCreateStart(ctx context.Context, d *schema.ResourceData, m interface{}) d return vmRead(ctx, d, m) } +func vmGetAMDSEVObject(d *schema.ResourceData) *vms.CustomAMDSEV { + var amdsev *vms.CustomAMDSEV + + amdsevBlock := d.Get(mkAMDSEV).([]interface{}) + if len(amdsevBlock) > 0 && amdsevBlock[0] != nil { + block := amdsevBlock[0].(map[string]interface{}) + + amdsevType := block[mkAMDSEVType].(string) + amdsevAllowSMT := types.CustomBool( + block[mkAMDSEVAllowSMT].(bool), + ) + amdsevKernelHashes := types.CustomBool( + block[mkAMDSEVKernelHashes].(bool), + ) + amdsevNoDebug := types.CustomBool( + block[mkAMDSEVNoDebug].(bool), + ) + amdsevNoKeySharing := types.CustomBool( + block[mkAMDSEVNoKeySharing].(bool), + ) + + amdsev = &vms.CustomAMDSEV{ + Type: amdsevType, + AllowSMT: &amdsevAllowSMT, + KernelHashes: &amdsevKernelHashes, + NoDebug: &amdsevNoDebug, + NoKeySharing: &amdsevNoKeySharing, + } + } + + return amdsev +} + func vmGetAudioDeviceList(d *schema.ResourceData) vms.CustomAudioDevices { devices := d.Get(mkAudioDevice).([]interface{}) list := make(vms.CustomAudioDevices, len(devices)) @@ -3628,6 +3749,65 @@ func vmReadCustom( } } + // Compare the amdsev configuration to the one stored in the state. + currentAMDSEV := d.Get(mkAMDSEV).([]interface{}) + + //nolint:gocritic + if len(clone) == 0 || len(currentAMDSEV) > 0 { + if vmConfig.AMDSEV != nil { + amdsev := map[string]interface{}{} + + amdsev[mkAMDSEVType] = vmConfig.AMDSEV.Type + + if vmConfig.AMDSEV.AllowSMT != nil { + amdsev[mkAMDSEVAllowSMT] = bool(*vmConfig.AMDSEV.AllowSMT) + } else { + amdsev[mkAMDSEVAllowSMT] = false + } + + if vmConfig.AMDSEV.KernelHashes != nil { + amdsev[mkAMDSEVKernelHashes] = bool(*vmConfig.AMDSEV.KernelHashes) + } else { + amdsev[mkAMDSEVKernelHashes] = false + } + + if vmConfig.AMDSEV.NoDebug != nil { + amdsev[mkAMDSEVNoDebug] = bool(*vmConfig.AMDSEV.NoDebug) + } else { + amdsev[mkAMDSEVNoDebug] = false + } + + if vmConfig.AMDSEV.NoKeySharing != nil { + amdsev[mkAMDSEVNoKeySharing] = bool(*vmConfig.AMDSEV.NoKeySharing) + } else { + amdsev[mkAMDSEVNoKeySharing] = false + } + + if len(clone) > 0 { + if len(currentAMDSEV) > 0 { + err := d.Set(mkAMDSEV, []interface{}{amdsev}) + diags = append(diags, diag.FromErr(err)...) + } + } else if len(currentAMDSEV) > 0 || + amdsev[mkAMDSEVType] != dvAMDSEVType || + amdsev[mkAMDSEVAllowSMT] != dvAMDSEVAllowSMT || + amdsev[mkAMDSEVKernelHashes] != dvAMDSEVKernelHashes || + amdsev[mkAMDSEVNoDebug] != dvAMDSEVNoDebug || + amdsev[mkAMDSEVNoKeySharing] != dvAMDSEVNoKeySharing { + err := d.Set(mkAMDSEV, []interface{}{amdsev}) + diags = append(diags, diag.FromErr(err)...) + } + } else if len(clone) > 0 { + if len(currentAMDSEV) > 0 { + err := d.Set(mkAMDSEV, []interface{}{}) + diags = append(diags, diag.FromErr(err)...) + } + } else { + err := d.Set(mkAMDSEV, []interface{}{}) + diags = append(diags, diag.FromErr(err)...) + } + } + // Compare the audio devices to those stored in the state. currentAudioDevice := d.Get(mkAudioDevice).([]interface{}) @@ -5061,6 +5241,15 @@ func vmUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.D rebootRequired = true } + // Prepare the new amdsev configuration. + if d.HasChange(mkAMDSEV) { + amdsev := vmGetAMDSEVObject(d) + + updateBody.AMDSEV = amdsev + + rebootRequired = true + } + // Prepare the new audio devices. if d.HasChange(mkAudioDevice) { updateBody.AudioDevices = vmGetAudioDeviceList(d)