Skip to content

Commit

Permalink
Allow to setup SEV session for a scheduled guest
Browse files Browse the repository at this point in the history
Introduce sev/setupsession API endpoint for VMI. It can be used to
provide the session launch blob and the Diffie-Hellman key for a guest
that has been scheduled to run on a particular node.

Signed-off-by: Vasiliy Ulyanov <[email protected]>
  • Loading branch information
vasiliy-ul committed Jun 23, 2023
1 parent a66769a commit 3913f87
Show file tree
Hide file tree
Showing 23 changed files with 431 additions and 2 deletions.
124 changes: 124 additions & 0 deletions api/openapi-spec/swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -11708,6 +11708,57 @@
}
]
},
"/apis/subresources.kubevirt.io/v1/namespaces/{namespace:[a-z0-9][a-z0-9\\-]*}/virtualmachineinstances/{name:[a-z0-9][a-z0-9\\-]*}/sev/setupsession": {
"put": {
"description": "Setup SEV session parameters for a Virtual Machine",
"operationId": "v1SEVSetupSession",
"parameters": [
{
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/v1.SEVSessionOptions"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "string"
}
},
"400": {
"description": "Bad Request",
"schema": {
"type": "string"
}
},
"401": {
"description": "Unauthorized"
}
}
},
"parameters": [
{
"uniqueItems": true,
"type": "string",
"description": "Name of the resource",
"name": "name",
"in": "path",
"required": true
},
{
"uniqueItems": true,
"type": "string",
"description": "Object name and auth scope, such as for teams and projects",
"name": "namespace",
"in": "path",
"required": true
}
]
},
"/apis/subresources.kubevirt.io/v1/namespaces/{namespace:[a-z0-9][a-z0-9\\-]*}/virtualmachineinstances/{name:[a-z0-9][a-z0-9\\-]*}/softreboot": {
"put": {
"description": "Soft reboot a VirtualMachineInstance object.",
Expand Down Expand Up @@ -13296,6 +13347,57 @@
}
]
},
"/apis/subresources.kubevirt.io/v1alpha3/namespaces/{namespace:[a-z0-9][a-z0-9\\-]*}/virtualmachineinstances/{name:[a-z0-9][a-z0-9\\-]*}/sev/setupsession": {
"put": {
"description": "Setup SEV session parameters for a Virtual Machine",
"operationId": "v1alpha3SEVSetupSession",
"parameters": [
{
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/v1.SEVSessionOptions"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "string"
}
},
"400": {
"description": "Bad Request",
"schema": {
"type": "string"
}
},
"401": {
"description": "Unauthorized"
}
}
},
"parameters": [
{
"uniqueItems": true,
"type": "string",
"description": "Name of the resource",
"name": "name",
"in": "path",
"required": true
},
{
"uniqueItems": true,
"type": "string",
"description": "Object name and auth scope, such as for teams and projects",
"name": "namespace",
"in": "path",
"required": true
}
]
},
"/apis/subresources.kubevirt.io/v1alpha3/namespaces/{namespace:[a-z0-9][a-z0-9\\-]*}/virtualmachineinstances/{name:[a-z0-9][a-z0-9\\-]*}/softreboot": {
"put": {
"description": "Soft reboot a VirtualMachineInstance object.",
Expand Down Expand Up @@ -18685,9 +18787,17 @@
"description": "If specified, run the attestation process for a vmi.",
"$ref": "#/definitions/v1.SEVAttestation"
},
"dhCert": {
"description": "Base64 encoded guest owner's Diffie-Hellman key.",
"type": "string"
},
"policy": {
"description": "Guest policy flags as defined in AMD SEV API specification. Note: due to security reasons it is not allowed to enable guest debugging. Therefore NoDebug flag is not exposed to users and is always true.",
"$ref": "#/definitions/v1.SEVPolicy"
},
"session": {
"description": "Base64 encoded session blob.",
"type": "string"
}
}
},
Expand Down Expand Up @@ -18767,6 +18877,20 @@
}
}
},
"v1.SEVSessionOptions": {
"description": "SEVSessionOptions is used to provide SEV session parameters.",
"type": "object",
"properties": {
"dhCert": {
"description": "Base64 encoded guest owner's Diffie-Hellman key.",
"type": "string"
},
"session": {
"description": "Base64 encoded session blob.",
"type": "string"
}
}
},
"v1.SMBiosConfiguration": {
"type": "object",
"properties": {
Expand Down
3 changes: 3 additions & 0 deletions manifests/generated/operator-csv.yaml.in
Original file line number Diff line number Diff line change
Expand Up @@ -651,6 +651,7 @@ spec:
- virtualmachineinstances/freeze
- virtualmachineinstances/unfreeze
- virtualmachineinstances/softreboot
- virtualmachineinstances/sev/setupsession
verbs:
- update
- apiGroups:
Expand Down Expand Up @@ -898,6 +899,7 @@ spec:
- virtualmachineinstances/freeze
- virtualmachineinstances/unfreeze
- virtualmachineinstances/softreboot
- virtualmachineinstances/sev/setupsession
verbs:
- update
- apiGroups:
Expand Down Expand Up @@ -1045,6 +1047,7 @@ spec:
- virtualmachineinstances/freeze
- virtualmachineinstances/unfreeze
- virtualmachineinstances/softreboot
- virtualmachineinstances/sev/setupsession
verbs:
- update
- apiGroups:
Expand Down
3 changes: 3 additions & 0 deletions manifests/generated/rbac-operator.authorization.k8s.yaml.in
Original file line number Diff line number Diff line change
Expand Up @@ -579,6 +579,7 @@ rules:
- virtualmachineinstances/freeze
- virtualmachineinstances/unfreeze
- virtualmachineinstances/softreboot
- virtualmachineinstances/sev/setupsession
verbs:
- update
- apiGroups:
Expand Down Expand Up @@ -826,6 +827,7 @@ rules:
- virtualmachineinstances/freeze
- virtualmachineinstances/unfreeze
- virtualmachineinstances/softreboot
- virtualmachineinstances/sev/setupsession
verbs:
- update
- apiGroups:
Expand Down Expand Up @@ -973,6 +975,7 @@ rules:
- virtualmachineinstances/freeze
- virtualmachineinstances/unfreeze
- virtualmachineinstances/softreboot
- virtualmachineinstances/sev/setupsession
verbs:
- update
- apiGroups:
Expand Down
13 changes: 13 additions & 0 deletions pkg/virt-api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -543,6 +543,15 @@ func (app *virtAPIApp) composeSubresources() {
Writes(v1.SEVMeasurementInfo{}).
Returns(http.StatusOK, "OK", v1.SEVMeasurementInfo{}))

subws.Route(subws.PUT(definitions.NamespacedResourcePath(subresourcesvmiGVR)+definitions.SubResourcePath("sev/setupsession")).
To(subresourceApp.SEVSetupSessionHandler).
Reads(v1.SEVSessionOptions{}).
Param(definitions.NamespaceParam(subws)).Param(definitions.NameParam(subws)).
Operation(version.Version+"SEVSetupSession").
Doc("Setup SEV session parameters for a Virtual Machine").
Returns(http.StatusOK, "OK", "").
Returns(http.StatusBadRequest, httpStatusBadRequestMessage, ""))

// Return empty api resource list.
// K8s expects to be able to retrieve a resource list for each aggregated
// app in order to discover what resources it provides. Without returning
Expand Down Expand Up @@ -649,6 +658,10 @@ func (app *virtAPIApp) composeSubresources() {
Name: "virtualmachineinstances/sev/querylaunchmeasurement",
Namespaced: true,
},
{
Name: "virtualmachineinstances/sev/setupsession",
Namespaced: true,
},
}

response.WriteAsJson(list)
Expand Down
72 changes: 72 additions & 0 deletions pkg/virt-api/rest/subresource.go
Original file line number Diff line number Diff line change
Expand Up @@ -1649,3 +1649,75 @@ func (app *SubresourceAPIApp) SEVQueryLaunchMeasurementHandler(request *restful.

app.httpGetRequestHandler(request, response, validate, getURL, v1.SEVMeasurementInfo{})
}

func (app *SubresourceAPIApp) SEVSetupSessionHandler(request *restful.Request, response *restful.Response) {
if !app.ensureSEVEnabled(response) {
return
}

if request.Request.Body == nil {
writeError(errors.NewBadRequest("Request with no body: SEV session parameters are required"), response)
return
}

opts := &v1.SEVSessionOptions{}
err := yaml.NewYAMLOrJSONDecoder(request.Request.Body, 1024).Decode(opts)
switch err {
case io.EOF, nil:
break
default:
writeError(errors.NewBadRequest(fmt.Sprintf(unmarshalRequestErrFmt, err)), response)
return
}

if opts.Session == "" {
writeError(errors.NewBadRequest("Session blob is required"), response)
return
}

if opts.DHCert == "" {
writeError(errors.NewBadRequest("DH cert is required"), response)
return
}

validate := func(vmi *v1.VirtualMachineInstance) *errors.StatusError {
if !vmi.IsScheduled() {
return errors.NewConflict(v1.Resource("virtualmachineinstance"), vmi.Name, fmt.Errorf("VMI is not in %s phase", v1.Scheduled))
}
if !kutil.IsSEVAttestationRequested(vmi) {
return errors.NewConflict(v1.Resource("virtualmachineinstance"), vmi.Name, fmt.Errorf(vmiNoAttestationErr))
}
sev := vmi.Spec.Domain.LaunchSecurity.SEV
if sev.Session != "" || sev.DHCert != "" {
return errors.NewConflict(v1.Resource("virtualmachineinstance"), vmi.Name, fmt.Errorf("Session already defined"))
}
return nil
}

name := request.PathParameter("name")
namespace := request.PathParameter("namespace")
vmi, statusError := app.fetchAndValidateVirtualMachineInstance(namespace, name, validate)
if statusError != nil {
writeError(statusError, response)
return
}

oldSEV := vmi.Spec.Domain.LaunchSecurity.SEV
newSEV := oldSEV.DeepCopy()
newSEV.Session = opts.Session
newSEV.DHCert = opts.DHCert
patch, err := patch.GenerateTestReplacePatch("/spec/domain/launchSecurity/sev", oldSEV, newSEV)
if err != nil {
writeError(errors.NewInternalError(err), response)
return
}

log.Log.Object(vmi).Infof("Patching vmi: %s", string(patch))
if _, err := app.virtCli.VirtualMachineInstance(vmi.Namespace).Patch(context.Background(), vmi.Name, types.JSONPatchType, patch, &k8smetav1.PatchOptions{}); err != nil {
log.Log.Object(vmi).Reason(err).Errorf("Failed to patch vmi")
writeError(errors.NewInternalError(err), response)
return
}

response.WriteHeader(http.StatusAccepted)
}
27 changes: 27 additions & 0 deletions pkg/virt-api/rest/subresource_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2234,6 +2234,10 @@ var _ = Describe("VirtualMachineInstance Subresources", func() {
}
}

withScheduledPhase := func(vmi *v1.VirtualMachineInstance) {
vmi.Status.Phase = v1.Scheduled
}

BeforeEach(func() {
enableFeatureGate(virtconfig.WorkloadEncryptionSEV)
})
Expand Down Expand Up @@ -2293,6 +2297,29 @@ var _ = Describe("VirtualMachineInstance Subresources", func() {
Entry("when VMI is not paused", Running, UnPaused, withSEVAttestation),
Entry("when attestation is not requested ", Running, Paused),
)

It("Should allow to setup SEV session parameters for a paused VMI", func() {
sevSessionOptions := &v1.SEVSessionOptions{
Session: "AAABBB",
DHCert: "CCCDDD",
}
body, err := json.Marshal(sevSessionOptions)
Expect(err).ToNot(HaveOccurred())
request.Request.Body = &readCloserWrapper{bytes.NewReader(body)}

expectVMI(NotRunning, UnPaused, withSEVAttestation, withScheduledPhase)
vmiClient.EXPECT().Patch(context.Background(), testVMIName, types.JSONPatchType, gomock.Any(), gomock.Any()).DoAndReturn(
func(ctx context.Context, name string, patchType types.PatchType, body interface{}, opts *k8smetav1.PatchOptions, _ ...string) (interface{}, interface{}) {
patch := []byte(`[{"op":"test","path":"/spec/domain/launchSecurity/sev","value":{"attestation":{}}},{"op":"replace","path":"/spec/domain/launchSecurity/sev","value":{"attestation":{},"session":"AAABBB","dhCert":"CCCDDD"}}]`)
Expect(body).To(Equal(patch))
return nil, nil
},
)

app.SEVSetupSessionHandler(request, response)
Expect(response.Error()).ToNot(HaveOccurred())
Expect(response.StatusCode()).To(Equal(http.StatusAccepted))
})
})

AfterEach(func() {
Expand Down
8 changes: 8 additions & 0 deletions pkg/virt-handler/vm.go
Original file line number Diff line number Diff line change
Expand Up @@ -2960,6 +2960,14 @@ func (d *VirtualMachineController) vmUpdateHelperDefault(origVMI *v1.VirtualMach
if err != nil {
return fmt.Errorf("failed to adjust resources: %v", err)
}

if util.IsSEVAttestationRequested(vmi) {
sev := vmi.Spec.Domain.LaunchSecurity.SEV
if sev.Session == "" || sev.DHCert == "" {
// Wait for the session parameters to be provided
return nil
}
}
} else if vmi.IsRunning() {
if err := d.hotplugSriovInterfaces(vmi); err != nil {
log.Log.Object(vmi).Error(err.Error())
Expand Down
2 changes: 2 additions & 0 deletions pkg/virt-launcher/virtwrap/api/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -949,6 +949,8 @@ type LaunchSecurity struct {
Cbitpos string `xml:"cbitpos,omitempty"`
ReducedPhysBits string `xml:"reducedPhysBits,omitempty"`
Policy string `xml:"policy,omitempty"`
DHCert string `xml:"dhCert,omitempty"`
Session string `xml:"session,omitempty"`
}

//END LaunchSecurity --------------------
Expand Down
6 changes: 4 additions & 2 deletions pkg/virt-launcher/virtwrap/converter/converter.go
Original file line number Diff line number Diff line change
Expand Up @@ -1327,8 +1327,10 @@ func Convert_v1_VirtualMachineInstance_To_api_Domain(vmi *v1.VirtualMachineInsta
sevPolicyBits := launchsecurity.SEVPolicyToBits(vmi.Spec.Domain.LaunchSecurity.SEV.Policy)
// Cbitpos and ReducedPhysBits will be filled automatically by libvirt from the domain capabilities
domain.Spec.LaunchSecurity = &api.LaunchSecurity{
Type: "sev",
Policy: "0x" + strconv.FormatUint(uint64(sevPolicyBits), 16),
Type: "sev",
Policy: "0x" + strconv.FormatUint(uint64(sevPolicyBits), 16),
DHCert: vmi.Spec.Domain.LaunchSecurity.SEV.DHCert,
Session: vmi.Spec.Domain.LaunchSecurity.SEV.Session,
}
controllerDriver = &api.ControllerDriver{
IOMMU: "on",
Expand Down
Loading

0 comments on commit 3913f87

Please sign in to comment.