From ee837652ebc2d2ad2d91d11f0132263f4f707ba2 Mon Sep 17 00:00:00 2001 From: Mahati Chamarthy Date: Wed, 29 Oct 2025 10:23:22 +0000 Subject: [PATCH 1/9] C-WCOW: Unify data structures and reuse for C-LCOW and C-WCOW Signed-off-by: Mahati Chamarthy --- internal/gcs-sidecar/handlers.go | 8 +- internal/gcs-sidecar/host.go | 4 +- internal/gcs-sidecar/uvm.go | 2 +- internal/guest/prot/protocol.go | 8 +- internal/guest/runtime/hcsv2/uvm.go | 14 ++-- internal/protocol/guestresource/resources.go | 12 +-- internal/uvm/security_policy.go | 79 ++------------------ internal/uvm/start.go | 48 ++++++------ test/gcs/main_test.go | 2 +- 9 files changed, 52 insertions(+), 125 deletions(-) diff --git a/internal/gcs-sidecar/handlers.go b/internal/gcs-sidecar/handlers.go index 0ea47b79e3..d81c5fb11b 100644 --- a/internal/gcs-sidecar/handlers.go +++ b/internal/gcs-sidecar/handlers.go @@ -552,7 +552,7 @@ func (b *Bridge) modifySettings(req *request) (err error) { log.G(ctx).Tracef("hcsschema.MappedDirectory { %v }", settings) case guestresource.ResourceTypeSecurityPolicy: - securityPolicyRequest := modifyGuestSettingsRequest.Settings.(*guestresource.WCOWConfidentialOptions) + securityPolicyRequest := modifyGuestSettingsRequest.Settings.(*guestresource.ConfidentialOptions) log.G(ctx).Tracef("WCOWConfidentialOptions: { %v}", securityPolicyRequest) err := b.hostState.SetWCOWConfidentialUVMOptions(req.ctx, securityPolicyRequest, b.logWriter) if err != nil { @@ -569,10 +569,9 @@ func (b *Bridge) modifySettings(req *request) (err error) { } return nil case guestresource.ResourceTypePolicyFragment: - //Note: Reusing the same type LCOWSecurityPolicyFragment for CWCOW. - r, ok := modifyGuestSettingsRequest.Settings.(*guestresource.LCOWSecurityPolicyFragment) + r, ok := modifyGuestSettingsRequest.Settings.(*guestresource.SecurityPolicyFragment) if !ok { - return errors.New("the request settings are not of type LCOWSecurityPolicyFragment") + return errors.New("the request settings are not of type SecurityPolicyFragment") } return b.hostState.InjectFragment(ctx, r) case guestresource.ResourceTypeWCOWBlockCims: @@ -588,7 +587,6 @@ func (b *Bridge) modifySettings(req *request) (err error) { // The block device takes some time to show up. Wait for a few seconds. time.Sleep(2 * time.Second) - //TODO(Mahati) : test and verify CIM hashes var layerCIMs []*cimfs.BlockCIM layerHashes := make([]string, len(wcowBlockCimMounts.BlockCIMs)) layerDigests := make([][]byte, len(wcowBlockCimMounts.BlockCIMs)) diff --git a/internal/gcs-sidecar/host.go b/internal/gcs-sidecar/host.go index 0f7b195ecb..754da9688d 100644 --- a/internal/gcs-sidecar/host.go +++ b/internal/gcs-sidecar/host.go @@ -119,7 +119,7 @@ func (h *Host) SetupSecurityContextDir(ctx context.Context, spec *specs.Spec) er // (ie fingerprint of a non leaf cert and the subject matches the leaf cert) // 3 - Check that this issuer/feed match the requirement of the user provided // security policy (done in the regoby LoadFragment) -func (h *Host) InjectFragment(ctx context.Context, fragment *guestresource.LCOWSecurityPolicyFragment) (err error) { +func (h *Host) InjectFragment(ctx context.Context, fragment *guestresource.SecurityPolicyFragment) (err error) { log.G(ctx).WithField("fragment", fmt.Sprintf("%+v", fragment)).Debug("GCS Host.InjectFragment") issuer, feed, payloadString, err := securitypolicy.ExtractAndVerifyFragment(ctx, fragment) if err != nil { @@ -133,7 +133,7 @@ func (h *Host) InjectFragment(ctx context.Context, fragment *guestresource.LCOWS return nil } -func (h *Host) SetWCOWConfidentialUVMOptions(ctx context.Context, securityPolicyRequest *guestresource.WCOWConfidentialOptions, logWriter io.Writer) error { +func (h *Host) SetWCOWConfidentialUVMOptions(ctx context.Context, securityPolicyRequest *guestresource.ConfidentialOptions, logWriter io.Writer) error { h.policyMutex.Lock() defer h.policyMutex.Unlock() diff --git a/internal/gcs-sidecar/uvm.go b/internal/gcs-sidecar/uvm.go index 9578e0273c..b3a7792fd4 100644 --- a/internal/gcs-sidecar/uvm.go +++ b/internal/gcs-sidecar/uvm.go @@ -92,7 +92,7 @@ func unmarshalContainerModifySettings(req *request) (_ *prot.ContainerModifySett modifyGuestSettingsRequest.Settings = settings case guestresource.ResourceTypeSecurityPolicy: - securityPolicyRequest := &guestresource.WCOWConfidentialOptions{} + securityPolicyRequest := &guestresource.ConfidentialOptions{} if err := commonutils.UnmarshalJSONWithHresult(rawGuestRequest, securityPolicyRequest); err != nil { return nil, fmt.Errorf("invalid ResourceTypeSecurityPolicy request: %w", err) } diff --git a/internal/guest/prot/protocol.go b/internal/guest/prot/protocol.go index 576ac5e5f1..16e1f9daa3 100644 --- a/internal/guest/prot/protocol.go +++ b/internal/guest/prot/protocol.go @@ -583,15 +583,15 @@ func UnmarshalContainerModifySettings(b []byte) (*containerModifySettings, error } msr.Settings = cc case guestresource.ResourceTypeSecurityPolicy: - enforcer := &guestresource.LCOWConfidentialOptions{} + enforcer := &guestresource.ConfidentialOptions{} if err := commonutils.UnmarshalJSONWithHresult(msrRawSettings, enforcer); err != nil { - return &request, errors.Wrap(err, "failed to unmarshal settings as LCOWConfidentialOptions") + return &request, errors.Wrap(err, "failed to unmarshal settings as ConfidentialOptions") } msr.Settings = enforcer case guestresource.ResourceTypePolicyFragment: - fragment := &guestresource.LCOWSecurityPolicyFragment{} + fragment := &guestresource.SecurityPolicyFragment{} if err := commonutils.UnmarshalJSONWithHresult(msrRawSettings, fragment); err != nil { - return &request, errors.Wrap(err, "failed to unmarshal settings as LCOWSecurityPolicyFragment") + return &request, errors.Wrap(err, "failed to unmarshal settings as SecurityPolicyFragment") } msr.Settings = fragment default: diff --git a/internal/guest/runtime/hcsv2/uvm.go b/internal/guest/runtime/hcsv2/uvm.go index 5737f861d6..253586ae63 100644 --- a/internal/guest/runtime/hcsv2/uvm.go +++ b/internal/guest/runtime/hcsv2/uvm.go @@ -114,13 +114,13 @@ func NewHost(rtime runtime.Runtime, vsock transport.Transport, initialEnforcer s } } -// SetConfidentialUVMOptions takes guestresource.LCOWConfidentialOptions +// SetConfidentialUVMOptions takes guestresource.ConfidentialOptions // to set up our internal data structures we use to store and enforce // security policy. The options can contain security policy enforcer type, // encoded security policy and signed UVM reference information The security // policy and uvm reference information can be further presented to workload // containers for validation and attestation purposes. -func (h *Host) SetConfidentialUVMOptions(ctx context.Context, r *guestresource.LCOWConfidentialOptions) error { +func (h *Host) SetConfidentialUVMOptions(ctx context.Context, r *guestresource.ConfidentialOptions) error { h.policyMutex.Lock() defer h.policyMutex.Unlock() if h.securityPolicyEnforcerSet { @@ -182,7 +182,7 @@ func (h *Host) SetConfidentialUVMOptions(ctx context.Context, r *guestresource.L // (ie fingerprint of a non leaf cert and the subject matches the leaf cert) // 3 - Check that this issuer/feed match the requirement of the user provided // security policy (done in the regoby LoadFragment) -func (h *Host) InjectFragment(ctx context.Context, fragment *guestresource.LCOWSecurityPolicyFragment) (err error) { +func (h *Host) InjectFragment(ctx context.Context, fragment *guestresource.SecurityPolicyFragment) (err error) { log.G(ctx).WithField("fragment", fmt.Sprintf("%+v", fragment)).Debug("GCS Host.InjectFragment") issuer, feed, payloadString, err := securitypolicy.ExtractAndVerifyFragment(ctx, fragment) if err != nil { @@ -787,15 +787,15 @@ func (h *Host) modifyHostSettings(ctx context.Context, containerID string, req * } return c.modifyContainerConstraints(ctx, req.RequestType, req.Settings.(*guestresource.LCOWContainerConstraints)) case guestresource.ResourceTypeSecurityPolicy: - r, ok := req.Settings.(*guestresource.LCOWConfidentialOptions) + r, ok := req.Settings.(*guestresource.ConfidentialOptions) if !ok { - return errors.New("the request's settings are not of type LCOWConfidentialOptions") + return errors.New("the request's settings are not of type ConfidentialOptions") } return h.SetConfidentialUVMOptions(ctx, r) case guestresource.ResourceTypePolicyFragment: - r, ok := req.Settings.(*guestresource.LCOWSecurityPolicyFragment) + r, ok := req.Settings.(*guestresource.SecurityPolicyFragment) if !ok { - return errors.New("the request settings are not of type LCOWSecurityPolicyFragment") + return errors.New("the request settings are not of type SecurityPolicyFragment") } return h.InjectFragment(ctx, r) default: diff --git a/internal/protocol/guestresource/resources.go b/internal/protocol/guestresource/resources.go index b956069107..8a58949281 100644 --- a/internal/protocol/guestresource/resources.go +++ b/internal/protocol/guestresource/resources.go @@ -229,20 +229,14 @@ type SignalProcessOptionsWCOW struct { Signal guestrequest.SignalValueWCOW `json:",omitempty"` } -// LCOWConfidentialOptions is used to set various confidential container specific +// ConfidentialOptions is used to set various confidential container specific // options. -type LCOWConfidentialOptions struct { +type ConfidentialOptions struct { EnforcerType string `json:"EnforcerType,omitempty"` EncodedSecurityPolicy string `json:"EncodedSecurityPolicy,omitempty"` EncodedUVMReference string `json:"EncodedUVMReference,omitempty"` } -type LCOWSecurityPolicyFragment struct { +type SecurityPolicyFragment struct { Fragment string `json:"Fragment,omitempty"` } - -type WCOWConfidentialOptions struct { - EnforcerType string `json:"EnforcerType,omitempty"` - EncodedSecurityPolicy string `json:"EncodedSecurityPolicy,omitempty"` - EncodedUVMReference string `json:"EncodedUVMReference,omitempty"` -} diff --git a/internal/uvm/security_policy.go b/internal/uvm/security_policy.go index 0dcf4fe693..3fa47e87b1 100644 --- a/internal/uvm/security_policy.go +++ b/internal/uvm/security_policy.go @@ -16,11 +16,11 @@ import ( "github.com/Microsoft/hcsshim/pkg/ctrdtaskapi" ) -type ConfidentialUVMOpt func(ctx context.Context, r *guestresource.LCOWConfidentialOptions) error +type ConfidentialUVMOpt func(ctx context.Context, r *guestresource.ConfidentialOptions) error // WithSecurityPolicy sets the desired security policy for the resource. func WithSecurityPolicy(policy string) ConfidentialUVMOpt { - return func(ctx context.Context, r *guestresource.LCOWConfidentialOptions) error { + return func(ctx context.Context, r *guestresource.ConfidentialOptions) error { r.EncodedSecurityPolicy = policy return nil } @@ -28,75 +28,12 @@ func WithSecurityPolicy(policy string) ConfidentialUVMOpt { // WithSecurityPolicyEnforcer sets the desired enforcer type for the resource. func WithSecurityPolicyEnforcer(enforcer string) ConfidentialUVMOpt { - return func(ctx context.Context, r *guestresource.LCOWConfidentialOptions) error { + return func(ctx context.Context, r *guestresource.ConfidentialOptions) error { r.EnforcerType = enforcer return nil } } -// TODO (Mahati): Move this block out later -type WCOWConfidentialUVMOpt func(ctx context.Context, r *guestresource.WCOWConfidentialOptions) error - -// WithSecurityPolicy sets the desired security policy for the resource. -func WithWCOWSecurityPolicy(policy string) WCOWConfidentialUVMOpt { - return func(ctx context.Context, r *guestresource.WCOWConfidentialOptions) error { - r.EncodedSecurityPolicy = policy - return nil - } -} - -// WithSecurityPolicyEnforcer sets the desired enforcer type for the resource. -func WithWCOWSecurityPolicyEnforcer(enforcer string) WCOWConfidentialUVMOpt { - return func(ctx context.Context, r *guestresource.WCOWConfidentialOptions) error { - r.EnforcerType = enforcer - return nil - } -} - -// WithUVMReferenceInfo reads UVM reference info file and base64 encodes the -// content before setting it for the resource. This is no-op if the -// path is empty or the file doesn't exist. -func WithWCOWUVMReferenceInfo(path string) WCOWConfidentialUVMOpt { - return func(ctx context.Context, r *guestresource.WCOWConfidentialOptions) error { - encoded, err := base64EncodeFileContents(path) - if err != nil { - if os.IsNotExist(err) { - log.G(ctx).WithField("filePath", path).Debug("UVM reference info file not found") - return nil - } - return fmt.Errorf("failed to read UVM reference info file: %w", err) - } - r.EncodedUVMReference = encoded - return nil - } -} - -func (uvm *UtilityVM) SetWCOWConfidentialUVMOptions(ctx context.Context, opts ...WCOWConfidentialUVMOpt) error { - if uvm.operatingSystem != "windows" { - return errNotSupported - } - uvm.m.Lock() - defer uvm.m.Unlock() - confOpts := &guestresource.WCOWConfidentialOptions{} - for _, o := range opts { - if err := o(ctx, confOpts); err != nil { - return err - } - } - modification := &hcsschema.ModifySettingRequest{ - RequestType: guestrequest.RequestTypeAdd, - GuestRequest: guestrequest.ModificationRequest{ - ResourceType: guestresource.ResourceTypeSecurityPolicy, - RequestType: guestrequest.RequestTypeAdd, - Settings: *confOpts, - }, - } - if err := uvm.modify(ctx, modification); err != nil { - return fmt.Errorf("uvm::Policy: failed to modify utility VM configuration: %w", err) - } - return nil -} - func base64EncodeFileContents(filePath string) (string, error) { if filePath == "" { return "", nil @@ -112,7 +49,7 @@ func base64EncodeFileContents(filePath string) (string, error) { // content before setting it for the resource. This is no-op if the // `referenceName` is empty or the file doesn't exist. func WithUVMReferenceInfo(referenceRoot string, referenceName string) ConfidentialUVMOpt { - return func(ctx context.Context, r *guestresource.LCOWConfidentialOptions) error { + return func(ctx context.Context, r *guestresource.ConfidentialOptions) error { if referenceName == "" { return nil } @@ -137,14 +74,10 @@ func WithUVMReferenceInfo(referenceRoot string, referenceName string) Confidenti // This has to happen before we start mounting things or generally changing // the state of the UVM after is has been measured at startup func (uvm *UtilityVM) SetConfidentialUVMOptions(ctx context.Context, opts ...ConfidentialUVMOpt) error { - if uvm.operatingSystem != "linux" { - return errNotSupported - } - uvm.m.Lock() defer uvm.m.Unlock() - confOpts := &guestresource.LCOWConfidentialOptions{} + confOpts := &guestresource.ConfidentialOptions{} for _, o := range opts { if err := o(ctx, confOpts); err != nil { return err @@ -174,7 +107,7 @@ func (uvm *UtilityVM) InjectPolicyFragment(ctx context.Context, fragment *ctrdta GuestRequest: guestrequest.ModificationRequest{ ResourceType: guestresource.ResourceTypePolicyFragment, RequestType: guestrequest.RequestTypeAdd, - Settings: guestresource.LCOWSecurityPolicyFragment{ + Settings: guestresource.SecurityPolicyFragment{ Fragment: fragment.Fragment, }, }, diff --git a/internal/uvm/start.go b/internal/uvm/start.go index f2f7f4ef52..598baf819a 100644 --- a/internal/uvm/start.go +++ b/internal/uvm/start.go @@ -373,38 +373,40 @@ func (uvm *UtilityVM) Start(ctx context.Context) (err error) { } uvm.SCSIManager = mgr - if uvm.confidentialUVMOptions != nil && uvm.OS() == "linux" { + var policy, enforcer, referenceInfoFileRoot, referenceInfoFilePath string + + if uvm.confidentialUVMOptions != nil || uvm.HasConfidentialPolicy() { + if uvm.confidentialUVMOptions != nil && uvm.OS() == "linux" { + policy = uvm.confidentialUVMOptions.SecurityPolicy + enforcer = uvm.confidentialUVMOptions.SecurityPolicyEnforcer + referenceInfoFilePath = uvm.confidentialUVMOptions.UVMReferenceInfoFile + referenceInfoFileRoot = defaultLCOWOSBootFilesPath() + } else if uvm.HasConfidentialPolicy() && uvm.OS() == "windows" { + policy = uvm.createOpts.(*OptionsWCOW).SecurityPolicy + enforcer = uvm.createOpts.(*OptionsWCOW).SecurityPolicyEnforcer + referenceInfoFilePath = uvm.createOpts.(*OptionsWCOW).UVMReferenceInfoFile + } copts := []ConfidentialUVMOpt{ - WithSecurityPolicy(uvm.confidentialUVMOptions.SecurityPolicy), - WithSecurityPolicyEnforcer(uvm.confidentialUVMOptions.SecurityPolicyEnforcer), - WithUVMReferenceInfo(defaultLCOWOSBootFilesPath(), uvm.confidentialUVMOptions.UVMReferenceInfoFile), + WithSecurityPolicy(policy), + WithSecurityPolicyEnforcer(enforcer), + WithUVMReferenceInfo(referenceInfoFileRoot, referenceInfoFilePath), } if err := uvm.SetConfidentialUVMOptions(ctx, copts...); err != nil { return err } - } - if uvm.HasConfidentialPolicy() && uvm.OS() == "windows" { - copts := []WCOWConfidentialUVMOpt{ - WithWCOWSecurityPolicy(uvm.createOpts.(*OptionsWCOW).SecurityPolicy), - WithWCOWSecurityPolicyEnforcer(uvm.createOpts.(*OptionsWCOW).SecurityPolicyEnforcer), - WithWCOWUVMReferenceInfo(uvm.createOpts.(*OptionsWCOW).UVMReferenceInfoFile), - } - if err := uvm.SetWCOWConfidentialUVMOptions(ctx, copts...); err != nil { - return err + if uvm.OS() == "windows" && uvm.forwardLogs { + // If the UVM is Windows and log forwarding is enabled, set the log sources + // and start the log forwarding service. + if err := uvm.SetLogSources(ctx); err != nil { + e.WithError(err).Error("failed to set log sources") + } + if err := uvm.StartLogForwarding(ctx); err != nil { + e.WithError(err).Error("failed to start log forwarding") + } } } - if uvm.OS() == "windows" && uvm.forwardLogs { - // If the UVM is Windows and log forwarding is enabled, set the log sources - // and start the log forwarding service. - if err := uvm.SetLogSources(ctx); err != nil { - e.WithError(err).Error("failed to set log sources") - } - if err := uvm.StartLogForwarding(ctx); err != nil { - e.WithError(err).Error("failed to start log forwarding") - } - } return nil } diff --git a/test/gcs/main_test.go b/test/gcs/main_test.go index f4b32b34c8..564e11c4c4 100644 --- a/test/gcs/main_test.go +++ b/test/gcs/main_test.go @@ -169,7 +169,7 @@ func getHostErr(rt runtime.Runtime, tp transport.Transport) (*hcsv2.Host, error) h := hcsv2.NewHost(rt, tp, &securitypolicy.OpenDoorSecurityPolicyEnforcer{}, os.Stdout) if err := h.SetConfidentialUVMOptions( context.Background(), - &guestresource.LCOWConfidentialOptions{}, + &guestresource.ConfidentialOptions{}, ); err != nil { return nil, fmt.Errorf("could not set host security policy: %w", err) } From cf6b89e833158385cbf0e3781d3d16d20e63627f Mon Sep 17 00:00:00 2001 From: Mahati Chamarthy Date: Mon, 3 Nov 2025 21:27:21 +0000 Subject: [PATCH 2/9] C-WCOW: Move gcs-sidecar confidential options to securitypolicy pkg Signed-off-by: Mahati Chamarthy --- internal/gcs-sidecar/handlers.go | 61 +++++++++++++--- internal/gcs-sidecar/host.go | 49 ++++--------- internal/gcs-sidecar/policy.go | 19 ----- pkg/securitypolicy/securitypolicy_linux.go | 75 ++++++++++++++++++++ pkg/securitypolicy/securitypolicy_options.go | 60 ++++++++++++++++ pkg/securitypolicy/securitypolicy_windows.go | 11 +++ pkg/securitypolicy/securitypolicyenforcer.go | 1 - 7 files changed, 213 insertions(+), 63 deletions(-) delete mode 100644 internal/gcs-sidecar/policy.go create mode 100644 pkg/securitypolicy/securitypolicy_options.go diff --git a/internal/gcs-sidecar/handlers.go b/internal/gcs-sidecar/handlers.go index d81c5fb11b..5cc18cfbd3 100644 --- a/internal/gcs-sidecar/handlers.go +++ b/internal/gcs-sidecar/handlers.go @@ -18,9 +18,11 @@ import ( hcsschema "github.com/Microsoft/hcsshim/internal/hcs/schema2" "github.com/Microsoft/hcsshim/internal/log" "github.com/Microsoft/hcsshim/internal/oc" + oci "github.com/Microsoft/hcsshim/internal/oci" "github.com/Microsoft/hcsshim/internal/protocol/guestrequest" "github.com/Microsoft/hcsshim/internal/protocol/guestresource" "github.com/Microsoft/hcsshim/internal/windevice" + "github.com/Microsoft/hcsshim/pkg/annotations" "github.com/Microsoft/hcsshim/pkg/cimfs" "github.com/Microsoft/hcsshim/pkg/securitypolicy" "github.com/pkg/errors" @@ -80,7 +82,7 @@ func (b *Bridge) createContainer(req *request) (err error) { user := securitypolicy.IDName{ Name: spec.Process.User.Username, } - _, _, _, err := b.hostState.securityPolicyEnforcer.EnforceCreateContainerPolicyV2(req.ctx, containerID, spec.Process.Args, spec.Process.Env, spec.Process.Cwd, spec.Mounts, user, nil) + _, _, _, err := b.hostState.securityOptions.PolicyEnforcer.EnforceCreateContainerPolicyV2(req.ctx, containerID, spec.Process.Args, spec.Process.Env, spec.Process.Cwd, spec.Mounts, user, nil) if err != nil { return fmt.Errorf("CreateContainer operation is denied by policy: %w", err) @@ -108,6 +110,49 @@ func (b *Bridge) createContainer(req *request) (err error) { b.hostState.RemoveContainer(ctx, containerID) } }(err) + // Write security policy, signed UVM reference and host AMD certificate to + // container's rootfs, so that application and sidecar containers can have + // access to it. The security policy is required by containers which need to + // extract init-time claims found in the security policy. The directory path + // containing the files is exposed via UVM_SECURITY_CONTEXT_DIR env var. + // It may be an error to have a security policy but not expose it to the + // container as in that case it can never be checked as correct by a verifier. + if oci.ParseAnnotationsBool(ctx, spec.Annotations, annotations.WCOWSecurityPolicyEnv, true) { + encodedPolicy := b.hostState.securityOptions.PolicyEnforcer.EncodedSecurityPolicy() + hostAMDCert := spec.Annotations[annotations.WCOWHostAMDCertificate] + if len(encodedPolicy) > 0 || len(hostAMDCert) > 0 || len(b.hostState.securityOptions.UvmReferenceInfo) > 0 { + // Use os.MkdirTemp to make sure that the directory is unique. + securityContextDir, err := os.MkdirTemp(spec.Root.Path, securitypolicy.SecurityContextDirTemplate) + if err != nil { + return fmt.Errorf("failed to create security context directory: %w", err) + } + // Make sure that files inside directory are readable + if err := os.Chmod(securityContextDir, 0755); err != nil { + return fmt.Errorf("failed to chmod security context directory: %w", err) + } + + if len(encodedPolicy) > 0 { + if err := writeFileInDir(securityContextDir, securitypolicy.PolicyFilename, []byte(encodedPolicy), 0777); err != nil { + return fmt.Errorf("failed to write security policy: %w", err) + } + } + if len(b.hostState.securityOptions.UvmReferenceInfo) > 0 { + if err := writeFileInDir(securityContextDir, securitypolicy.ReferenceInfoFilename, []byte(b.hostState.securityOptions.UvmReferenceInfo), 0777); err != nil { + return fmt.Errorf("failed to write UVM reference info: %w", err) + } + } + + if len(hostAMDCert) > 0 { + if err := writeFileInDir(securityContextDir, securitypolicy.HostAMDCertFilename, []byte(hostAMDCert), 0777); err != nil { + return fmt.Errorf("failed to write host AMD certificate: %w", err) + } + } + + containerCtxDir := fmt.Sprintf("/%s", filepath.Base(securityContextDir)) + secCtxEnv := fmt.Sprintf("UVM_SECURITY_CONTEXT_DIR=%s", containerCtxDir) + spec.Process.Env = append(spec.Process.Env, secCtxEnv) + } + } // Strip the spec field hostedSystemBytes, err := json.Marshal(cwcowHostedSystem) @@ -203,7 +248,7 @@ func (b *Bridge) shutdownGraceful(req *request) (err error) { return fmt.Errorf("failed to unmarshal shutdownGraceful: %w", err) } - err = b.hostState.securityPolicyEnforcer.EnforceShutdownContainerPolicy(req.ctx, r.ContainerID) + err = b.hostState.securityOptions.PolicyEnforcer.EnforceShutdownContainerPolicy(req.ctx, r.ContainerID) if err != nil { return fmt.Errorf("rpcShudownGraceful operation not allowed: %w", err) } @@ -247,7 +292,7 @@ func (b *Bridge) executeProcess(req *request) (err error) { if containerID == UVMContainerID { log.G(req.ctx).Tracef("Enforcing policy on external exec process") - _, _, err := b.hostState.securityPolicyEnforcer.EnforceExecExternalProcessPolicy( + _, _, err := b.hostState.securityOptions.PolicyEnforcer.EnforceExecExternalProcessPolicy( req.ctx, commandLine, processParamEnvToOCIEnv(processParams.Environment), @@ -279,7 +324,7 @@ func (b *Bridge) executeProcess(req *request) (err error) { Name: processParams.User, } log.G(req.ctx).Tracef("Enforcing policy on exec in container") - _, _, _, err = b.hostState.securityPolicyEnforcer. + _, _, _, err = b.hostState.securityOptions.PolicyEnforcer. EnforceExecInContainerPolicyV2( req.ctx, containerID, @@ -385,7 +430,7 @@ func (b *Bridge) signalProcess(req *request) (err error) { WindowsSignal: wcowOptions.Signal, WindowsCommand: commandLine, } - err = b.hostState.securityPolicyEnforcer.EnforceSignalContainerProcessPolicyV2(req.ctx, containerID, opts) + err = b.hostState.securityOptions.PolicyEnforcer.EnforceSignalContainerProcessPolicyV2(req.ctx, containerID, opts) if err != nil { return err } @@ -414,7 +459,7 @@ func (b *Bridge) getProperties(req *request) (err error) { defer span.End() defer func() { oc.SetSpanStatus(span, err) }() - if err := b.hostState.securityPolicyEnforcer.EnforceGetPropertiesPolicy(req.ctx); err != nil { + if err := b.hostState.securityOptions.PolicyEnforcer.EnforceGetPropertiesPolicy(req.ctx); err != nil { return errors.Wrapf(err, "get properties denied due to policy") } @@ -622,7 +667,7 @@ func (b *Bridge) modifySettings(req *request) (err error) { hashesToVerify = layerHashes[1:] } - err := b.hostState.securityPolicyEnforcer.EnforceVerifiedCIMsPolicy(req.ctx, containerID, hashesToVerify) + err := b.hostState.securityOptions.PolicyEnforcer.EnforceVerifiedCIMsPolicy(req.ctx, containerID, hashesToVerify) if err != nil { return errors.Wrap(err, "CIM mount is denied by policy") } @@ -662,7 +707,7 @@ func (b *Bridge) modifySettings(req *request) (err error) { containerID, settings.CombinedLayers.ContainerRootPath, settings.CombinedLayers.Layers, settings.CombinedLayers.ScratchPath) //Since unencrypted scratch is not an option, always pass true - if err := b.hostState.securityPolicyEnforcer.EnforceScratchMountPolicy(ctx, settings.CombinedLayers.ContainerRootPath, true); err != nil { + if err := b.hostState.securityOptions.PolicyEnforcer.EnforceScratchMountPolicy(ctx, settings.CombinedLayers.ContainerRootPath, true); err != nil { return fmt.Errorf("scratch mounting denied by policy: %w", err) } // The following two folders are expected to be present in the scratch. diff --git a/internal/gcs-sidecar/host.go b/internal/gcs-sidecar/host.go index 754da9688d..d109d44aea 100644 --- a/internal/gcs-sidecar/host.go +++ b/internal/gcs-sidecar/host.go @@ -26,14 +26,9 @@ import ( ) type Host struct { + securityOptions *securitypolicy.SecurityOptions containersMutex sync.Mutex containers map[string]*Container - - // state required for the security policy enforcement - policyMutex sync.Mutex - securityPolicyEnforcer securitypolicy.SecurityPolicyEnforcer - securityPolicyEnforcerSet bool - uvmReferenceInfo string } type Container struct { @@ -55,10 +50,14 @@ type containerProcess struct { } func NewHost(initialEnforcer securitypolicy.SecurityPolicyEnforcer) *Host { + securityPolicyOptions := securitypolicy.NewSecurityOptions( + initialEnforcer, + false, + "", + ) return &Host{ - containers: make(map[string]*Container), - securityPolicyEnforcer: initialEnforcer, - securityPolicyEnforcerSet: false, + containers: make(map[string]*Container), + securityOptions: securityPolicyOptions, } } @@ -126,7 +125,7 @@ func (h *Host) InjectFragment(ctx context.Context, fragment *guestresource.Secur return err } // now offer the payload fragment to the policy - err = h.securityPolicyEnforcer.LoadFragment(ctx, issuer, feed, payloadString) + err = h.securityOptions.PolicyEnforcer.LoadFragment(ctx, issuer, feed, payloadString) if err != nil { return fmt.Errorf("error loading security policy fragment: %w", err) } @@ -134,13 +133,6 @@ func (h *Host) InjectFragment(ctx context.Context, fragment *guestresource.Secur } func (h *Host) SetWCOWConfidentialUVMOptions(ctx context.Context, securityPolicyRequest *guestresource.ConfidentialOptions, logWriter io.Writer) error { - h.policyMutex.Lock() - defer h.policyMutex.Unlock() - - if h.securityPolicyEnforcerSet { - return errors.New("security policy has already been set") - } - if err := pspdriver.GetPspDriverError(); err != nil { // For this case gcs-sidecar will keep initial deny policy. return errors.Wrapf(err, "an error occurred while using PSP driver") @@ -157,33 +149,20 @@ func (h *Host) SetWCOWConfidentialUVMOptions(ctx context.Context, securityPolicy return err } - // This limit ensures messages are below the character truncation limit that - // can be imposed by an orchestrator - maxErrorMessageLength := 3 * 1024 - - // Initialize security policy enforcer for a given enforcer type and - // encoded security policy. - p, err := securitypolicy.CreateSecurityPolicyEnforcer( + if err := h.securityOptions.SetConfidentialOptions(ctx, securityPolicyRequest.EnforcerType, securityPolicyRequest.EncodedSecurityPolicy, - DefaultCRIMounts(), - DefaultCRIPrivilegedMounts(), - maxErrorMessageLength, - ) - if err != nil { - return fmt.Errorf("error creating security policy enforcer: %w", err) + securityPolicyRequest.EncodedUVMReference, + ); err != nil { + return errors.Wrapf(err, "SetWCOWConfidentialUVMOptions failed to set security options") } - if err = p.EnforceRuntimeLoggingPolicy(ctx); err == nil { + if err = h.securityOptions.PolicyEnforcer.EnforceRuntimeLoggingPolicy(ctx); err == nil { logrus.SetOutput(logWriter) } else { logrus.SetOutput(io.Discard) } - h.securityPolicyEnforcer = p - h.securityPolicyEnforcerSet = true - h.uvmReferenceInfo = securityPolicyRequest.EncodedUVMReference - return nil } diff --git a/internal/gcs-sidecar/policy.go b/internal/gcs-sidecar/policy.go deleted file mode 100644 index 13b96ce64d..0000000000 --- a/internal/gcs-sidecar/policy.go +++ /dev/null @@ -1,19 +0,0 @@ -//go:build windows -// +build windows - -package bridge - -import ( - oci "github.com/opencontainers/runtime-spec/specs-go" -) - -// DefaultCRIMounts returns default mounts added to windows spec by containerD. -func DefaultCRIMounts() []oci.Mount { - return []oci.Mount{} -} - -// DefaultCRIPrivilegedMounts returns a slice of mounts which are added to the -// windows container spec when a container runs in a privileged mode. -func DefaultCRIPrivilegedMounts() []oci.Mount { - return []oci.Mount{} -} diff --git a/pkg/securitypolicy/securitypolicy_linux.go b/pkg/securitypolicy/securitypolicy_linux.go index cb04e03d92..449752d3ad 100644 --- a/pkg/securitypolicy/securitypolicy_linux.go +++ b/pkg/securitypolicy/securitypolicy_linux.go @@ -18,6 +18,81 @@ import ( //nolint:unused const osType = "linux" +func DefaultCRIMounts() []oci.Mount { + return []oci.Mount{ + { + Destination: "/proc", + Type: "proc", + Source: "proc", + Options: []string{"nosuid", "noexec", "nodev"}, + }, + { + Destination: "/dev", + Type: "tmpfs", + Source: "tmpfs", + Options: []string{"nosuid", "strictatime", "mode=755", "size=65536k"}, + }, + { + Destination: "/dev/pts", + Type: "devpts", + Source: "devpts", + Options: []string{"nosuid", "noexec", "newinstance", "ptmxmode=0666", "mode=0620", "gid=5"}, + }, + { + Destination: "/dev/shm", + Type: "tmpfs", + Source: "shm", + Options: []string{"nosuid", "noexec", "nodev", "mode=1777", "size=65536k"}, + }, + { + Destination: "/dev/mqueue", + Type: "mqueue", + Source: "mqueue", + Options: []string{"nosuid", "noexec", "nodev"}, + }, + { + Destination: "/sys", + Type: "sysfs", + Source: "sysfs", + Options: []string{"nosuid", "noexec", "nodev", "ro"}, + }, + { + Destination: "/run", + Type: "tmpfs", + Source: "tmpfs", + Options: []string{"nosuid", "strictatime", "mode=755", "size=65536k"}, + }, + // cgroup mount is always added by default, regardless if it is present + // in the mount constraints or not. If the user chooses to override it, + // then a corresponding mount constraint should be present. + { + Source: "cgroup", + Destination: "/sys/fs/cgroup", + Type: "cgroup", + Options: []string{"nosuid", "noexec", "nodev", "relatime", "ro"}, + }, + } +} + +// DefaultCRIPrivilegedMounts returns a slice of mounts which are added to the +// linux container spec when a container runs in a privileged mode. +func DefaultCRIPrivilegedMounts() []oci.Mount { + return []oci.Mount{ + { + Source: "cgroup", + Destination: "/sys/fs/cgroup", + Type: "cgroup", + Options: []string{"nosuid", "noexec", "nodev", "relatime", "rw"}, + }, + { + Destination: "/sys", + Type: "sysfs", + Source: "sysfs", + Options: []string{"nosuid", "noexec", "nodev", "rw"}, + }, + } +} + // SandboxMountsDir returns sandbox mounts directory inside UVM/host. func SandboxMountsDir(sandboxID string) string { return specInternal.SandboxMountsDir((sandboxID)) diff --git a/pkg/securitypolicy/securitypolicy_options.go b/pkg/securitypolicy/securitypolicy_options.go new file mode 100644 index 0000000000..44a8468019 --- /dev/null +++ b/pkg/securitypolicy/securitypolicy_options.go @@ -0,0 +1,60 @@ +package securitypolicy + +import ( + "context" + "fmt" + "sync" + + "github.com/pkg/errors" +) + +type SecurityOptions struct { + // state required for the security policy enforcement + PolicyEnforcer SecurityPolicyEnforcer + PolicyEnforcerSet bool + UvmReferenceInfo string + policyMutex sync.Mutex + EnforcerType string + EncodedSecurityPolicy string +} + +func NewSecurityOptions(enforcer SecurityPolicyEnforcer, enforcerSet bool, uvmReferenceInfo string) *SecurityOptions { + return &SecurityOptions{ + PolicyEnforcer: enforcer, + PolicyEnforcerSet: enforcerSet, + UvmReferenceInfo: uvmReferenceInfo, + EnforcerType: "", + EncodedSecurityPolicy: "", + } +} + +func (s *SecurityOptions) SetConfidentialOptions(ctx context.Context, enforcerType string, encodedSecurityPolicy string, encodedUVMReference string) error { + s.policyMutex.Lock() + defer s.policyMutex.Unlock() + + if s.PolicyEnforcerSet { + return errors.New("security policy has already been set") + } + // This limit ensures messages are below the character truncation limit that + // can be imposed by an orchestrator + maxErrorMessageLength := 3 * 1024 + + // Initialize security policy enforcer for a given enforcer type and + // encoded security policy. + p, err := CreateSecurityPolicyEnforcer( + enforcerType, + encodedSecurityPolicy, + DefaultCRIMounts(), + DefaultCRIPrivilegedMounts(), + maxErrorMessageLength, + ) + if err != nil { + return fmt.Errorf("error creating security policy enforcer: %w", err) + } + + s.PolicyEnforcer = p + s.PolicyEnforcerSet = true + s.UvmReferenceInfo = encodedUVMReference + + return nil +} diff --git a/pkg/securitypolicy/securitypolicy_windows.go b/pkg/securitypolicy/securitypolicy_windows.go index 6f873fef26..b8495f7385 100644 --- a/pkg/securitypolicy/securitypolicy_windows.go +++ b/pkg/securitypolicy/securitypolicy_windows.go @@ -21,3 +21,14 @@ func HugePagesMountsDir(sandboxID string) string { func GetAllUserInfo(process *oci.Process, rootPath string) (IDName, []IDName, string, error) { return IDName{}, []IDName{}, "", nil } + +// DefaultCRIMounts returns default mounts added to windows spec by containerD. +func DefaultCRIMounts() []oci.Mount { + return []oci.Mount{} +} + +// DefaultCRIPrivilegedMounts returns a slice of mounts which are added to the +// windows container spec when a container runs in a privileged mode. +func DefaultCRIPrivilegedMounts() []oci.Mount { + return []oci.Mount{} +} diff --git a/pkg/securitypolicy/securitypolicyenforcer.go b/pkg/securitypolicy/securitypolicyenforcer.go index 127b679ae7..588c28c5da 100644 --- a/pkg/securitypolicy/securitypolicyenforcer.go +++ b/pkg/securitypolicy/securitypolicyenforcer.go @@ -40,7 +40,6 @@ type CreateContainerOptions struct { Capabilities *oci.LinuxCapabilities SeccompProfileSHA256 string } - type SignalContainerOptions struct { IsInitProcess bool // One of these will be set depending on platform From e090d17dda7bb41894b6d89031bb8a63a1dff41b Mon Sep 17 00:00:00 2001 From: Mahati Chamarthy Date: Mon, 3 Nov 2025 22:08:35 +0000 Subject: [PATCH 3/9] C-WCOW: Move security policy config from LCOW gcs Signed-off-by: Mahati Chamarthy --- internal/gcs-sidecar/bridge.go | 6 +- internal/gcs-sidecar/handlers.go | 2 +- internal/gcs-sidecar/host.go | 11 +- internal/guest/policy/default.go | 98 --------------- internal/guest/policy/doc.go | 1 - internal/guest/runtime/hcsv2/uvm.go | 125 +++++++------------ pkg/securitypolicy/securitypolicy_linux.go | 11 ++ pkg/securitypolicy/securitypolicy_options.go | 36 ++++-- 8 files changed, 86 insertions(+), 204 deletions(-) delete mode 100644 internal/guest/policy/default.go delete mode 100644 internal/guest/policy/doc.go diff --git a/internal/gcs-sidecar/bridge.go b/internal/gcs-sidecar/bridge.go index b7bc2f9522..87472cc4a2 100644 --- a/internal/gcs-sidecar/bridge.go +++ b/internal/gcs-sidecar/bridge.go @@ -47,9 +47,6 @@ type Bridge struct { // and send responses back to hcsshim respectively. sendToGCSCh chan request sendToShimCh chan bridgeResponse - - // logging target - logWriter io.Writer } // SequenceID is used to correlate requests and responses. @@ -81,7 +78,7 @@ type request struct { } func NewBridge(shimConn io.ReadWriteCloser, inboxGCSConn io.ReadWriteCloser, initialEnforcer securitypolicy.SecurityPolicyEnforcer, logWriter io.Writer) *Bridge { - hostState := NewHost(initialEnforcer) + hostState := NewHost(initialEnforcer, logWriter) return &Bridge{ pending: make(map[sequenceID]chan *prot.ContainerExecuteProcessResponse), rpcHandlerList: make(map[prot.RPCProc]HandlerFunc), @@ -90,7 +87,6 @@ func NewBridge(shimConn io.ReadWriteCloser, inboxGCSConn io.ReadWriteCloser, ini inboxGCSConn: inboxGCSConn, sendToGCSCh: make(chan request), sendToShimCh: make(chan bridgeResponse), - logWriter: logWriter, } } diff --git a/internal/gcs-sidecar/handlers.go b/internal/gcs-sidecar/handlers.go index 5cc18cfbd3..ea6468ea48 100644 --- a/internal/gcs-sidecar/handlers.go +++ b/internal/gcs-sidecar/handlers.go @@ -599,7 +599,7 @@ func (b *Bridge) modifySettings(req *request) (err error) { case guestresource.ResourceTypeSecurityPolicy: securityPolicyRequest := modifyGuestSettingsRequest.Settings.(*guestresource.ConfidentialOptions) log.G(ctx).Tracef("WCOWConfidentialOptions: { %v}", securityPolicyRequest) - err := b.hostState.SetWCOWConfidentialUVMOptions(req.ctx, securityPolicyRequest, b.logWriter) + err := b.hostState.SetWCOWConfidentialUVMOptions(req.ctx, securityPolicyRequest) if err != nil { return errors.Wrap(err, "error creating enforcer") } diff --git a/internal/gcs-sidecar/host.go b/internal/gcs-sidecar/host.go index d109d44aea..27b688bbc1 100644 --- a/internal/gcs-sidecar/host.go +++ b/internal/gcs-sidecar/host.go @@ -49,11 +49,12 @@ type containerProcess struct { pid uint32 } -func NewHost(initialEnforcer securitypolicy.SecurityPolicyEnforcer) *Host { +func NewHost(initialEnforcer securitypolicy.SecurityPolicyEnforcer, logWriter io.Writer) *Host { securityPolicyOptions := securitypolicy.NewSecurityOptions( initialEnforcer, false, "", + logWriter, ) return &Host{ containers: make(map[string]*Container), @@ -132,7 +133,7 @@ func (h *Host) InjectFragment(ctx context.Context, fragment *guestresource.Secur return nil } -func (h *Host) SetWCOWConfidentialUVMOptions(ctx context.Context, securityPolicyRequest *guestresource.ConfidentialOptions, logWriter io.Writer) error { +func (h *Host) SetWCOWConfidentialUVMOptions(ctx context.Context, securityPolicyRequest *guestresource.ConfidentialOptions) error { if err := pspdriver.GetPspDriverError(); err != nil { // For this case gcs-sidecar will keep initial deny policy. return errors.Wrapf(err, "an error occurred while using PSP driver") @@ -157,12 +158,6 @@ func (h *Host) SetWCOWConfidentialUVMOptions(ctx context.Context, securityPolicy return errors.Wrapf(err, "SetWCOWConfidentialUVMOptions failed to set security options") } - if err = h.securityOptions.PolicyEnforcer.EnforceRuntimeLoggingPolicy(ctx); err == nil { - logrus.SetOutput(logWriter) - } else { - logrus.SetOutput(io.Discard) - } - return nil } diff --git a/internal/guest/policy/default.go b/internal/guest/policy/default.go deleted file mode 100644 index 98c909fb35..0000000000 --- a/internal/guest/policy/default.go +++ /dev/null @@ -1,98 +0,0 @@ -//go:build linux -// +build linux - -package policy - -import ( - oci "github.com/opencontainers/runtime-spec/specs-go" - - specGuest "github.com/Microsoft/hcsshim/internal/guest/spec" - "github.com/Microsoft/hcsshim/pkg/securitypolicy" -) - -func ExtendPolicyWithNetworkingMounts(sandboxID string, enforcer securitypolicy.SecurityPolicyEnforcer, spec *oci.Spec) error { - roSpec := &oci.Spec{ - Root: spec.Root, - } - networkingMounts := specGuest.GenerateWorkloadContainerNetworkMounts(sandboxID, roSpec) - if err := enforcer.ExtendDefaultMounts(networkingMounts); err != nil { - return err - } - return nil -} - -// DefaultCRIMounts returns default mounts added to linux spec by containerD. -func DefaultCRIMounts() []oci.Mount { - return []oci.Mount{ - { - Destination: "/proc", - Type: "proc", - Source: "proc", - Options: []string{"nosuid", "noexec", "nodev"}, - }, - { - Destination: "/dev", - Type: "tmpfs", - Source: "tmpfs", - Options: []string{"nosuid", "strictatime", "mode=755", "size=65536k"}, - }, - { - Destination: "/dev/pts", - Type: "devpts", - Source: "devpts", - Options: []string{"nosuid", "noexec", "newinstance", "ptmxmode=0666", "mode=0620", "gid=5"}, - }, - { - Destination: "/dev/shm", - Type: "tmpfs", - Source: "shm", - Options: []string{"nosuid", "noexec", "nodev", "mode=1777", "size=65536k"}, - }, - { - Destination: "/dev/mqueue", - Type: "mqueue", - Source: "mqueue", - Options: []string{"nosuid", "noexec", "nodev"}, - }, - { - Destination: "/sys", - Type: "sysfs", - Source: "sysfs", - Options: []string{"nosuid", "noexec", "nodev", "ro"}, - }, - { - Destination: "/run", - Type: "tmpfs", - Source: "tmpfs", - Options: []string{"nosuid", "strictatime", "mode=755", "size=65536k"}, - }, - // cgroup mount is always added by default, regardless if it is present - // in the mount constraints or not. If the user chooses to override it, - // then a corresponding mount constraint should be present. - { - Source: "cgroup", - Destination: "/sys/fs/cgroup", - Type: "cgroup", - Options: []string{"nosuid", "noexec", "nodev", "relatime", "ro"}, - }, - } -} - -// DefaultCRIPrivilegedMounts returns a slice of mounts which are added to the -// linux container spec when a container runs in a privileged mode. -func DefaultCRIPrivilegedMounts() []oci.Mount { - return []oci.Mount{ - { - Source: "cgroup", - Destination: "/sys/fs/cgroup", - Type: "cgroup", - Options: []string{"nosuid", "noexec", "nodev", "relatime", "rw"}, - }, - { - Destination: "/sys", - Type: "sysfs", - Source: "sysfs", - Options: []string{"nosuid", "noexec", "nodev", "rw"}, - }, - } -} diff --git a/internal/guest/policy/doc.go b/internal/guest/policy/doc.go deleted file mode 100644 index 8cbf7ee3fc..0000000000 --- a/internal/guest/policy/doc.go +++ /dev/null @@ -1 +0,0 @@ -package policy diff --git a/internal/guest/runtime/hcsv2/uvm.go b/internal/guest/runtime/hcsv2/uvm.go index 253586ae63..7c3c259c49 100644 --- a/internal/guest/runtime/hcsv2/uvm.go +++ b/internal/guest/runtime/hcsv2/uvm.go @@ -29,7 +29,6 @@ import ( "github.com/Microsoft/hcsshim/internal/bridgeutils/gcserr" "github.com/Microsoft/hcsshim/internal/debug" - "github.com/Microsoft/hcsshim/internal/guest/policy" "github.com/Microsoft/hcsshim/internal/guest/prot" "github.com/Microsoft/hcsshim/internal/guest/runtime" specGuest "github.com/Microsoft/hcsshim/internal/guest/spec" @@ -86,31 +85,30 @@ type Host struct { devNullTransport transport.Transport // state required for the security policy enforcement - policyMutex sync.Mutex - securityPolicyEnforcer securitypolicy.SecurityPolicyEnforcer - securityPolicyEnforcerSet bool - uvmReferenceInfo string + securityOptions *securitypolicy.SecurityOptions - // logging target - logWriter io.Writer // hostMounts keeps the state of currently mounted devices and file systems, // which is used for GCS hardening. hostMounts *hostMounts } func NewHost(rtime runtime.Runtime, vsock transport.Transport, initialEnforcer securitypolicy.SecurityPolicyEnforcer, logWriter io.Writer) *Host { + securityPolicyOptions := securitypolicy.NewSecurityOptions( + initialEnforcer, + false, + "", + logWriter, + ) return &Host{ - containers: make(map[string]*Container), - externalProcesses: make(map[int]*externalProcess), - virtualPods: make(map[string]*VirtualPod), - containerToVirtualPod: make(map[string]string), - rtime: rtime, - vsock: vsock, - devNullTransport: &transport.DevNullTransport{}, - securityPolicyEnforcerSet: false, - securityPolicyEnforcer: initialEnforcer, - logWriter: logWriter, - hostMounts: newHostMounts(), + containers: make(map[string]*Container), + externalProcesses: make(map[int]*externalProcess), + virtualPods: make(map[string]*VirtualPod), + containerToVirtualPod: make(map[string]string), + rtime: rtime, + vsock: vsock, + devNullTransport: &transport.DevNullTransport{}, + hostMounts: newHostMounts(), + securityOptions: securityPolicyOptions, } } @@ -121,41 +119,6 @@ func NewHost(rtime runtime.Runtime, vsock transport.Transport, initialEnforcer s // policy and uvm reference information can be further presented to workload // containers for validation and attestation purposes. func (h *Host) SetConfidentialUVMOptions(ctx context.Context, r *guestresource.ConfidentialOptions) error { - h.policyMutex.Lock() - defer h.policyMutex.Unlock() - if h.securityPolicyEnforcerSet { - return errors.New("security policy has already been set") - } - - // this limit ensures messages are below the character truncation limit that - // can be imposed by an orchestrator - maxErrorMessageLength := 3 * 1024 - - // Initialize security policy enforcer for a given enforcer type and - // encoded security policy. - p, err := securitypolicy.CreateSecurityPolicyEnforcer( - r.EnforcerType, - r.EncodedSecurityPolicy, - policy.DefaultCRIMounts(), - policy.DefaultCRIPrivilegedMounts(), - maxErrorMessageLength, - ) - if err != nil { - return err - } - - // This is one of two points at which we might change our logging. - // At this time, we now have a policy and can determine what the policy - // author put as policy around runtime logging. - // The other point is on startup where we take a flag to set the default - // policy enforcer to use before a policy arrives. After that flag is set, - // we use the enforcer in question to set up logging as well. - if err = p.EnforceRuntimeLoggingPolicy(ctx); err == nil { - logrus.SetOutput(h.logWriter) - } else { - logrus.SetOutput(io.Discard) - } - hostData, err := securitypolicy.NewSecurityPolicyDigest(r.EncodedSecurityPolicy) if err != nil { return err @@ -165,9 +128,13 @@ func (h *Host) SetConfidentialUVMOptions(ctx context.Context, r *guestresource.C return err } - h.securityPolicyEnforcer = p - h.securityPolicyEnforcerSet = true - h.uvmReferenceInfo = r.EncodedUVMReference + if err := h.securityOptions.SetConfidentialOptions(ctx, + r.EnforcerType, + r.EncodedSecurityPolicy, + r.EncodedUVMReference, + ); err != nil { + return errors.Wrapf(err, "SetWCOWConfidentialUVMOptions failed to set security options") + } return nil } @@ -189,7 +156,7 @@ func (h *Host) InjectFragment(ctx context.Context, fragment *guestresource.Secur return err } // now offer the payload fragment to the policy - err = h.securityPolicyEnforcer.LoadFragment(ctx, issuer, feed, payloadString) + err = h.securityOptions.PolicyEnforcer.LoadFragment(ctx, issuer, feed, payloadString) if err != nil { return fmt.Errorf("error loading security policy fragment: %w", err) } @@ -197,7 +164,7 @@ func (h *Host) InjectFragment(ctx context.Context, fragment *guestresource.Secur } func (h *Host) SecurityPolicyEnforcer() securitypolicy.SecurityPolicyEnforcer { - return h.securityPolicyEnforcer + return h.securityOptions.PolicyEnforcer } func (h *Host) Transport() transport.Transport { @@ -467,7 +434,7 @@ func (h *Host) CreateContainer(ctx context.Context, id string, settings *prot.VM return nil, err } - if err := policy.ExtendPolicyWithNetworkingMounts(id, h.securityPolicyEnforcer, settings.OCISpecification); err != nil { + if err := securitypolicy.ExtendPolicyWithNetworkingMounts(id, h.securityOptions.PolicyEnforcer, settings.OCISpecification); err != nil { return nil, err } case "container": @@ -482,7 +449,7 @@ func (h *Host) CreateContainer(ctx context.Context, id string, settings *prot.VM // Add SEV device when security policy is not empty, except when privileged annotation is // set to "true", in which case all UVMs devices are added. - if len(h.securityPolicyEnforcer.EncodedSecurityPolicy()) > 0 && !oci.ParseAnnotationsBool(ctx, + if len(h.securityOptions.PolicyEnforcer.EncodedSecurityPolicy()) > 0 && !oci.ParseAnnotationsBool(ctx, settings.OCISpecification.Annotations, annotations.LCOWPrivileged, false) { if err := specGuest.AddDevSev(ctx, settings.OCISpecification); err != nil { log.G(ctx).WithError(err).Debug("failed to add SEV device") @@ -494,7 +461,7 @@ func (h *Host) CreateContainer(ctx context.Context, id string, settings *prot.VM _ = os.RemoveAll(settings.OCIBundlePath) } }() - if err := policy.ExtendPolicyWithNetworkingMounts(sandboxID, h.securityPolicyEnforcer, settings.OCISpecification); err != nil { + if err := securitypolicy.ExtendPolicyWithNetworkingMounts(sandboxID, h.securityOptions.PolicyEnforcer, settings.OCISpecification); err != nil { return nil, err } default: @@ -511,7 +478,7 @@ func (h *Host) CreateContainer(ctx context.Context, id string, settings *prot.VM _ = os.RemoveAll(settings.OCIBundlePath) } }() - if err := policy.ExtendPolicyWithNetworkingMounts(id, h.securityPolicyEnforcer, + if err := securitypolicy.ExtendPolicyWithNetworkingMounts(id, h.securityOptions.PolicyEnforcer, settings.OCISpecification); err != nil { return nil, err } @@ -528,7 +495,7 @@ func (h *Host) CreateContainer(ctx context.Context, id string, settings *prot.VM }) } - user, groups, umask, err := h.securityPolicyEnforcer.GetUserInfo(settings.OCISpecification.Process, settings.OCISpecification.Root.Path) + user, groups, umask, err := h.securityOptions.PolicyEnforcer.GetUserInfo(settings.OCISpecification.Process, settings.OCISpecification.Root.Path) if err != nil { return nil, err } @@ -538,7 +505,7 @@ func (h *Host) CreateContainer(ctx context.Context, id string, settings *prot.VM return nil, err } - envToKeep, capsToKeep, allowStdio, err := h.securityPolicyEnforcer.EnforceCreateContainerPolicy( + envToKeep, capsToKeep, allowStdio, err := h.securityOptions.PolicyEnforcer.EnforceCreateContainerPolicy( ctx, sandboxID, id, @@ -604,9 +571,9 @@ func (h *Host) CreateContainer(ctx context.Context, id string, settings *prot.VM // It may be an error to have a security policy but not expose it to the // container as in that case it can never be checked as correct by a verifier. if oci.ParseAnnotationsBool(ctx, settings.OCISpecification.Annotations, annotations.LCOWSecurityPolicyEnv, true) { - encodedPolicy := h.securityPolicyEnforcer.EncodedSecurityPolicy() + encodedPolicy := h.securityOptions.PolicyEnforcer.EncodedSecurityPolicy() hostAMDCert := settings.OCISpecification.Annotations[annotations.LCOWHostAMDCertificate] - if len(encodedPolicy) > 0 || len(hostAMDCert) > 0 || len(h.uvmReferenceInfo) > 0 { + if len(encodedPolicy) > 0 || len(hostAMDCert) > 0 || len(h.securityOptions.UvmReferenceInfo) > 0 { // Use os.MkdirTemp to make sure that the directory is unique. securityContextDir, err := os.MkdirTemp(settings.OCISpecification.Root.Path, securitypolicy.SecurityContextDirTemplate) if err != nil { @@ -622,8 +589,8 @@ func (h *Host) CreateContainer(ctx context.Context, id string, settings *prot.VM return nil, fmt.Errorf("failed to write security policy: %w", err) } } - if len(h.uvmReferenceInfo) > 0 { - if err := writeFileInDir(securityContextDir, securitypolicy.ReferenceInfoFilename, []byte(h.uvmReferenceInfo), 0744); err != nil { + if len(h.securityOptions.UvmReferenceInfo) > 0 { + if err := writeFileInDir(securityContextDir, securitypolicy.ReferenceInfoFilename, []byte(h.securityOptions.UvmReferenceInfo), 0744); err != nil { return nil, fmt.Errorf("failed to write UVM reference info: %w", err) } } @@ -764,18 +731,18 @@ func (h *Host) modifyHostSettings(ctx context.Context, containerID string, req * }() } } - return modifyMappedVirtualDisk(ctx, req.RequestType, mvd, h.securityPolicyEnforcer) + return modifyMappedVirtualDisk(ctx, req.RequestType, mvd, h.securityOptions.PolicyEnforcer) case guestresource.ResourceTypeMappedDirectory: - return modifyMappedDirectory(ctx, h.vsock, req.RequestType, req.Settings.(*guestresource.LCOWMappedDirectory), h.securityPolicyEnforcer) + return modifyMappedDirectory(ctx, h.vsock, req.RequestType, req.Settings.(*guestresource.LCOWMappedDirectory), h.securityOptions.PolicyEnforcer) case guestresource.ResourceTypeVPMemDevice: - return modifyMappedVPMemDevice(ctx, req.RequestType, req.Settings.(*guestresource.LCOWMappedVPMemDevice), h.securityPolicyEnforcer) + return modifyMappedVPMemDevice(ctx, req.RequestType, req.Settings.(*guestresource.LCOWMappedVPMemDevice), h.securityOptions.PolicyEnforcer) case guestresource.ResourceTypeCombinedLayers: cl := req.Settings.(*guestresource.LCOWCombinedLayers) // when cl.ScratchPath == "", we mount overlay as read-only, in which case // we don't really care about scratch encryption, since the host already // knows about the layers and the overlayfs. encryptedScratch := cl.ScratchPath != "" && h.hostMounts.IsEncrypted(cl.ScratchPath) - return modifyCombinedLayers(ctx, req.RequestType, req.Settings.(*guestresource.LCOWCombinedLayers), encryptedScratch, h.securityPolicyEnforcer) + return modifyCombinedLayers(ctx, req.RequestType, req.Settings.(*guestresource.LCOWCombinedLayers), encryptedScratch, h.securityOptions.PolicyEnforcer) case guestresource.ResourceTypeNetwork: return modifyNetwork(ctx, req.RequestType, req.Settings.(*guestresource.LCOWNetworkAdapter)) case guestresource.ResourceTypeVPCIDevice: @@ -837,7 +804,7 @@ func (h *Host) ShutdownContainer(ctx context.Context, containerID string, gracef return err } - err = h.securityPolicyEnforcer.EnforceShutdownContainerPolicy(ctx, containerID) + err = h.securityOptions.PolicyEnforcer.EnforceShutdownContainerPolicy(ctx, containerID) if err != nil { return err } @@ -864,7 +831,7 @@ func (h *Host) SignalContainerProcess(ctx context.Context, containerID string, p signalingInitProcess := processID == c.initProcess.pid startupArgList := p.(*containerProcess).spec.Args - err = h.securityPolicyEnforcer.EnforceSignalContainerProcessPolicy(ctx, containerID, signal, signalingInitProcess, startupArgList) + err = h.securityOptions.PolicyEnforcer.EnforceSignalContainerProcessPolicy(ctx, containerID, signal, signalingInitProcess, startupArgList) if err != nil { return err } @@ -879,7 +846,7 @@ func (h *Host) ExecProcess(ctx context.Context, containerID string, params prot. if params.IsExternal || containerID == UVMContainerID { var envToKeep securitypolicy.EnvList var allowStdioAccess bool - envToKeep, allowStdioAccess, err = h.securityPolicyEnforcer.EnforceExecExternalProcessPolicy( + envToKeep, allowStdioAccess, err = h.securityOptions.PolicyEnforcer.EnforceExecExternalProcessPolicy( ctx, params.CommandArgs, processParamEnvToOCIEnv(params.Environment), @@ -921,12 +888,12 @@ func (h *Host) ExecProcess(ctx context.Context, containerID string, params prot. var umask string var allowStdioAccess bool - user, groups, umask, err = h.securityPolicyEnforcer.GetUserInfo(params.OCIProcess, c.spec.Root.Path) + user, groups, umask, err = h.securityOptions.PolicyEnforcer.GetUserInfo(params.OCIProcess, c.spec.Root.Path) if err != nil { return 0, err } - envToKeep, capsToKeep, allowStdioAccess, err = h.securityPolicyEnforcer.EnforceExecInContainerPolicy( + envToKeep, capsToKeep, allowStdioAccess, err = h.securityOptions.PolicyEnforcer.EnforceExecInContainerPolicy( ctx, containerID, params.OCIProcess.Args, @@ -975,7 +942,7 @@ func (h *Host) GetExternalProcess(pid int) (Process, error) { } func (h *Host) GetProperties(ctx context.Context, containerID string, query prot.PropertyQuery) (*prot.PropertiesV2, error) { - err := h.securityPolicyEnforcer.EnforceGetPropertiesPolicy(ctx) + err := h.securityOptions.PolicyEnforcer.EnforceGetPropertiesPolicy(ctx) if err != nil { return nil, errors.Wrapf(err, "get properties denied due to policy") } @@ -1031,7 +998,7 @@ func (h *Host) GetProperties(ctx context.Context, containerID string, query prot } func (h *Host) GetStacks(ctx context.Context) (string, error) { - err := h.securityPolicyEnforcer.EnforceDumpStacksPolicy(ctx) + err := h.securityOptions.PolicyEnforcer.EnforceDumpStacksPolicy(ctx) if err != nil { return "", errors.Wrapf(err, "dump stacks denied due to policy") } diff --git a/pkg/securitypolicy/securitypolicy_linux.go b/pkg/securitypolicy/securitypolicy_linux.go index 449752d3ad..54f8972de4 100644 --- a/pkg/securitypolicy/securitypolicy_linux.go +++ b/pkg/securitypolicy/securitypolicy_linux.go @@ -18,6 +18,17 @@ import ( //nolint:unused const osType = "linux" +func ExtendPolicyWithNetworkingMounts(sandboxID string, enforcer SecurityPolicyEnforcer, spec *oci.Spec) error { + roSpec := &oci.Spec{ + Root: spec.Root, + } + networkingMounts := specInternal.GenerateWorkloadContainerNetworkMounts(sandboxID, roSpec) + if err := enforcer.ExtendDefaultMounts(networkingMounts); err != nil { + return err + } + return nil +} + func DefaultCRIMounts() []oci.Mount { return []oci.Mount{ { diff --git a/pkg/securitypolicy/securitypolicy_options.go b/pkg/securitypolicy/securitypolicy_options.go index 44a8468019..a23a9286ee 100644 --- a/pkg/securitypolicy/securitypolicy_options.go +++ b/pkg/securitypolicy/securitypolicy_options.go @@ -3,28 +3,28 @@ package securitypolicy import ( "context" "fmt" + "io" "sync" "github.com/pkg/errors" + "github.com/sirupsen/logrus" ) type SecurityOptions struct { // state required for the security policy enforcement - PolicyEnforcer SecurityPolicyEnforcer - PolicyEnforcerSet bool - UvmReferenceInfo string - policyMutex sync.Mutex - EnforcerType string - EncodedSecurityPolicy string + PolicyEnforcer SecurityPolicyEnforcer + PolicyEnforcerSet bool + UvmReferenceInfo string + policyMutex sync.Mutex + logWriter io.Writer } -func NewSecurityOptions(enforcer SecurityPolicyEnforcer, enforcerSet bool, uvmReferenceInfo string) *SecurityOptions { +func NewSecurityOptions(enforcer SecurityPolicyEnforcer, enforcerSet bool, uvmReferenceInfo string, logWriter io.Writer) *SecurityOptions { return &SecurityOptions{ - PolicyEnforcer: enforcer, - PolicyEnforcerSet: enforcerSet, - UvmReferenceInfo: uvmReferenceInfo, - EnforcerType: "", - EncodedSecurityPolicy: "", + PolicyEnforcer: enforcer, + PolicyEnforcerSet: enforcerSet, + UvmReferenceInfo: uvmReferenceInfo, + logWriter: logWriter, } } @@ -52,6 +52,18 @@ func (s *SecurityOptions) SetConfidentialOptions(ctx context.Context, enforcerTy return fmt.Errorf("error creating security policy enforcer: %w", err) } + // This is one of two points at which we might change our logging. + // At this time, we now have a policy and can determine what the policy + // author put as policy around runtime logging. + // The other point is on startup where we take a flag to set the default + // policy enforcer to use before a policy arrives. After that flag is set, + // we use the enforcer in question to set up logging as well. + if err = s.PolicyEnforcer.EnforceRuntimeLoggingPolicy(ctx); err == nil { + logrus.SetOutput(s.logWriter) + } else { + logrus.SetOutput(io.Discard) + } + s.PolicyEnforcer = p s.PolicyEnforcerSet = true s.UvmReferenceInfo = encodedUVMReference From 93ec4ebed5e9a2c078690fdb6b39d4588a8bbad6 Mon Sep 17 00:00:00 2001 From: Mahati Chamarthy Date: Mon, 3 Nov 2025 22:22:33 +0000 Subject: [PATCH 4/9] C-WCOW: Move InjectFragment to securitypolicy pkg Signed-off-by: Mahati Chamarthy --- internal/gcs-sidecar/handlers.go | 2 +- internal/gcs-sidecar/host.go | 74 ------------------- internal/guest/runtime/hcsv2/uvm.go | 26 +------ pkg/securitypolicy/securitypolicy_options.go | 77 ++++++++++++++++++++ pkg/securitypolicy/securitypolicyenforcer.go | 73 ------------------- 5 files changed, 79 insertions(+), 173 deletions(-) diff --git a/internal/gcs-sidecar/handlers.go b/internal/gcs-sidecar/handlers.go index ea6468ea48..9b60ec6d94 100644 --- a/internal/gcs-sidecar/handlers.go +++ b/internal/gcs-sidecar/handlers.go @@ -618,7 +618,7 @@ func (b *Bridge) modifySettings(req *request) (err error) { if !ok { return errors.New("the request settings are not of type SecurityPolicyFragment") } - return b.hostState.InjectFragment(ctx, r) + return b.hostState.securityOptions.InjectFragment(ctx, r) case guestresource.ResourceTypeWCOWBlockCims: // This is request to mount the merged cim at given volumeGUID if modifyGuestSettingsRequest.RequestType == guestrequest.RequestTypeRemove { diff --git a/internal/gcs-sidecar/host.go b/internal/gcs-sidecar/host.go index 27b688bbc1..3419f51426 100644 --- a/internal/gcs-sidecar/host.go +++ b/internal/gcs-sidecar/host.go @@ -5,7 +5,6 @@ package bridge import ( "context" - "fmt" "io" "os" "path/filepath" @@ -15,10 +14,8 @@ import ( hcsschema "github.com/Microsoft/hcsshim/internal/hcs/schema2" "github.com/Microsoft/hcsshim/internal/log" "github.com/Microsoft/hcsshim/internal/logfields" - oci "github.com/Microsoft/hcsshim/internal/oci" "github.com/Microsoft/hcsshim/internal/protocol/guestresource" "github.com/Microsoft/hcsshim/internal/pspdriver" - "github.com/Microsoft/hcsshim/pkg/annotations" "github.com/Microsoft/hcsshim/pkg/securitypolicy" specs "github.com/opencontainers/runtime-spec/specs-go" "github.com/pkg/errors" @@ -62,77 +59,6 @@ func NewHost(initialEnforcer securitypolicy.SecurityPolicyEnforcer, logWriter io } } -// Write security policy, signed UVM reference and host AMD certificate to -// container's rootfs, so that application and sidecar containers can have -// access to it. The security policy is required by containers which need to -// extract init-time claims found in the security policy. The directory path -// containing the files is exposed via UVM_SECURITY_CONTEXT_DIR env var. -// It may be an error to have a security policy but not expose it to the -// container as in that case it can never be checked as correct by a verifier. -func (h *Host) SetupSecurityContextDir(ctx context.Context, spec *specs.Spec) error { - if oci.ParseAnnotationsBool(ctx, spec.Annotations, annotations.WCOWSecurityPolicyEnv, true) { - encodedPolicy := h.securityPolicyEnforcer.EncodedSecurityPolicy() - hostAMDCert := spec.Annotations[annotations.WCOWHostAMDCertificate] - if len(encodedPolicy) > 0 || len(hostAMDCert) > 0 || len(h.uvmReferenceInfo) > 0 { - // Use os.MkdirTemp to make sure that the directory is unique. - securityContextDir, err := os.MkdirTemp(spec.Root.Path, securitypolicy.SecurityContextDirTemplate) - if err != nil { - return fmt.Errorf("failed to create security context directory: %w", err) - } - // Make sure that files inside directory are readable - if err := os.Chmod(securityContextDir, 0755); err != nil { - return fmt.Errorf("failed to chmod security context directory: %w", err) - } - - if len(encodedPolicy) > 0 { - if err := writeFileInDir(securityContextDir, securitypolicy.PolicyFilename, []byte(encodedPolicy), 0777); err != nil { - return fmt.Errorf("failed to write security policy: %w", err) - } - } - if len(h.uvmReferenceInfo) > 0 { - if err := writeFileInDir(securityContextDir, securitypolicy.ReferenceInfoFilename, []byte(h.uvmReferenceInfo), 0777); err != nil { - return fmt.Errorf("failed to write UVM reference info: %w", err) - } - } - - if len(hostAMDCert) > 0 { - if err := writeFileInDir(securityContextDir, securitypolicy.HostAMDCertFilename, []byte(hostAMDCert), 0777); err != nil { - return fmt.Errorf("failed to write host AMD certificate: %w", err) - } - } - - containerCtxDir := fmt.Sprintf("/%s", filepath.Base(securityContextDir)) - secCtxEnv := fmt.Sprintf("UVM_SECURITY_CONTEXT_DIR=%s", containerCtxDir) - spec.Process.Env = append(spec.Process.Env, secCtxEnv) - } - } - return nil -} - -// InjectFragment extends current security policy with additional constraints -// from the incoming fragment. Note that it is base64 encoded over the bridge/ -// -// There are three checking steps: -// 1 - Unpack the cose document and check it was actually signed with the cert -// chain inside its header -// 2 - Check that the issuer field did:x509 identifier is for that cert chain -// (ie fingerprint of a non leaf cert and the subject matches the leaf cert) -// 3 - Check that this issuer/feed match the requirement of the user provided -// security policy (done in the regoby LoadFragment) -func (h *Host) InjectFragment(ctx context.Context, fragment *guestresource.SecurityPolicyFragment) (err error) { - log.G(ctx).WithField("fragment", fmt.Sprintf("%+v", fragment)).Debug("GCS Host.InjectFragment") - issuer, feed, payloadString, err := securitypolicy.ExtractAndVerifyFragment(ctx, fragment) - if err != nil { - return err - } - // now offer the payload fragment to the policy - err = h.securityOptions.PolicyEnforcer.LoadFragment(ctx, issuer, feed, payloadString) - if err != nil { - return fmt.Errorf("error loading security policy fragment: %w", err) - } - return nil -} - func (h *Host) SetWCOWConfidentialUVMOptions(ctx context.Context, securityPolicyRequest *guestresource.ConfidentialOptions) error { if err := pspdriver.GetPspDriverError(); err != nil { // For this case gcs-sidecar will keep initial deny policy. diff --git a/internal/guest/runtime/hcsv2/uvm.go b/internal/guest/runtime/hcsv2/uvm.go index 7c3c259c49..7212bac306 100644 --- a/internal/guest/runtime/hcsv2/uvm.go +++ b/internal/guest/runtime/hcsv2/uvm.go @@ -139,30 +139,6 @@ func (h *Host) SetConfidentialUVMOptions(ctx context.Context, r *guestresource.C return nil } -// InjectFragment extends current security policy with additional constraints -// from the incoming fragment. Note that it is base64 encoded over the bridge/ -// -// There are three checking steps: -// 1 - Unpack the cose document and check it was actually signed with the cert -// chain inside its header -// 2 - Check that the issuer field did:x509 identifier is for that cert chain -// (ie fingerprint of a non leaf cert and the subject matches the leaf cert) -// 3 - Check that this issuer/feed match the requirement of the user provided -// security policy (done in the regoby LoadFragment) -func (h *Host) InjectFragment(ctx context.Context, fragment *guestresource.SecurityPolicyFragment) (err error) { - log.G(ctx).WithField("fragment", fmt.Sprintf("%+v", fragment)).Debug("GCS Host.InjectFragment") - issuer, feed, payloadString, err := securitypolicy.ExtractAndVerifyFragment(ctx, fragment) - if err != nil { - return err - } - // now offer the payload fragment to the policy - err = h.securityOptions.PolicyEnforcer.LoadFragment(ctx, issuer, feed, payloadString) - if err != nil { - return fmt.Errorf("error loading security policy fragment: %w", err) - } - return nil -} - func (h *Host) SecurityPolicyEnforcer() securitypolicy.SecurityPolicyEnforcer { return h.securityOptions.PolicyEnforcer } @@ -764,7 +740,7 @@ func (h *Host) modifyHostSettings(ctx context.Context, containerID string, req * if !ok { return errors.New("the request settings are not of type SecurityPolicyFragment") } - return h.InjectFragment(ctx, r) + return h.securityOptions.InjectFragment(ctx, r) default: return errors.Errorf("the ResourceType %q is not supported for UVM", req.ResourceType) } diff --git a/pkg/securitypolicy/securitypolicy_options.go b/pkg/securitypolicy/securitypolicy_options.go index a23a9286ee..85048b5516 100644 --- a/pkg/securitypolicy/securitypolicy_options.go +++ b/pkg/securitypolicy/securitypolicy_options.go @@ -2,10 +2,19 @@ package securitypolicy import ( "context" + "crypto/sha256" + "encoding/base64" "fmt" "io" + "os" + "path/filepath" "sync" + "time" + "github.com/Microsoft/cosesign1go/pkg/cosesign1" + didx509resolver "github.com/Microsoft/didx509go/pkg/did-x509-resolver" + "github.com/Microsoft/hcsshim/internal/log" + "github.com/Microsoft/hcsshim/internal/protocol/guestresource" "github.com/pkg/errors" "github.com/sirupsen/logrus" ) @@ -70,3 +79,71 @@ func (s *SecurityOptions) SetConfidentialOptions(ctx context.Context, enforcerTy return nil } + +// Fragment extends current security policy with additional constraints +// from the incoming fragment. Note that it is base64 encoded over the bridge/ +// +// There are three checking steps: +// 1 - Unpack the cose document and check it was actually signed with the cert +// chain inside its header +// 2 - Check that the issuer field did:x509 identifier is for that cert chain +// (ie fingerprint of a non leaf cert and the subject matches the leaf cert) +// 3 - Check that this issuer/feed match the requirement of the user provided +// security policy (done in the regoby LoadFragment) +func (s *SecurityOptions) InjectFragment(ctx context.Context, fragment *guestresource.SecurityPolicyFragment) (err error) { + log.G(ctx).WithField("fragment", fmt.Sprintf("%+v", fragment)).Debug("VerifyAndExtractFragment") + + raw, err := base64.StdEncoding.DecodeString(fragment.Fragment) + if err != nil { + return fmt.Errorf("failed to decode fragment: %w", err) + } + blob := []byte(fragment.Fragment) + // keep a copy of the fragment, so we can manually figure out what went wrong + // will be removed eventually. Give it a unique name to avoid any potential + // race conditions. + sha := sha256.New() + sha.Write(blob) + timestamp := time.Now() + fragmentPath := fmt.Sprintf("fragment-%x-%d.blob", sha.Sum(nil), timestamp.UnixMilli()) + _ = os.WriteFile(filepath.Join(os.TempDir(), fragmentPath), blob, 0644) + + unpacked, err := cosesign1.UnpackAndValidateCOSE1CertChain(raw) + if err != nil { + return fmt.Errorf("InjectFragment failed COSE validation: %w", err) + } + + payloadString := string(unpacked.Payload[:]) + issuer := unpacked.Issuer + feed := unpacked.Feed + chainPem := unpacked.ChainPem + + log.G(ctx).WithFields(logrus.Fields{ + "issuer": issuer, // eg the DID:x509:blah.... + "feed": feed, + "cty": unpacked.ContentType, + "chainPem": chainPem, + }).Debugf("unpacked COSE1 cert chain") + + log.G(ctx).WithFields(logrus.Fields{ + "payload": payloadString, + }).Tracef("unpacked COSE1 payload") + + if len(issuer) == 0 || len(feed) == 0 { // must both be present + return fmt.Errorf("either issuer and feed must both be provided in the COSE_Sign1 protected header") + } + + // Resolve returns a did doc that we don't need + // we only care if there was an error or not + _, err = didx509resolver.Resolve(unpacked.ChainPem, issuer, true) + if err != nil { + log.G(ctx).Printf("Badly formed fragment - did resolver failed to match fragment did:x509 from chain with purported issuer %s, feed %s - err %s", issuer, feed, err.Error()) + return fmt.Errorf("failed to resolve DID: %w", err) + } + + // now offer the payload fragment to the policy + err = s.PolicyEnforcer.LoadFragment(ctx, issuer, feed, payloadString) + if err != nil { + return fmt.Errorf("error loading security policy fragment: %w", err) + } + return nil +} diff --git a/pkg/securitypolicy/securitypolicyenforcer.go b/pkg/securitypolicy/securitypolicyenforcer.go index 588c28c5da..59c3780638 100644 --- a/pkg/securitypolicy/securitypolicyenforcer.go +++ b/pkg/securitypolicy/securitypolicyenforcer.go @@ -2,22 +2,12 @@ package securitypolicy import ( "context" - "crypto/sha256" - "encoding/base64" "fmt" - "os" - "path/filepath" "syscall" - "time" - "github.com/Microsoft/cosesign1go/pkg/cosesign1" - didx509resolver "github.com/Microsoft/didx509go/pkg/did-x509-resolver" - "github.com/Microsoft/hcsshim/internal/log" "github.com/Microsoft/hcsshim/internal/protocol/guestrequest" - "github.com/Microsoft/hcsshim/internal/protocol/guestresource" oci "github.com/opencontainers/runtime-spec/specs-go" "github.com/pkg/errors" - "github.com/sirupsen/logrus" ) type createEnforcerFunc func(base64EncodedPolicy string, criMounts, criPrivilegedMounts []oci.Mount, maxErrorMessageLength int) (SecurityPolicyEnforcer, error) @@ -151,69 +141,6 @@ func (s stringSet) contains(item string) bool { return contains } -// Fragment extends current security policy with additional constraints -// from the incoming fragment. Note that it is base64 encoded over the bridge/ -// -// There are three checking steps: -// 1 - Unpack the cose document and check it was actually signed with the cert -// chain inside its header -// 2 - Check that the issuer field did:x509 identifier is for that cert chain -// (ie fingerprint of a non leaf cert and the subject matches the leaf cert) -// 3 - Check that this issuer/feed match the requirement of the user provided -// security policy (done in the regoby LoadFragment) -func ExtractAndVerifyFragment(ctx context.Context, fragment *guestresource.LCOWSecurityPolicyFragment) (issuer string, feed string, payloadString string, err error) { - log.G(ctx).WithField("fragment", fmt.Sprintf("%+v", fragment)).Debug("VerifyAndExtractFragment") - - raw, err := base64.StdEncoding.DecodeString(fragment.Fragment) - if err != nil { - return "", "", "", fmt.Errorf("failed to decode fragment: %w", err) - } - blob := []byte(fragment.Fragment) - // keep a copy of the fragment, so we can manually figure out what went wrong - // will be removed eventually. Give it a unique name to avoid any potential - // race conditions. - sha := sha256.New() - sha.Write(blob) - timestamp := time.Now() - fragmentPath := fmt.Sprintf("fragment-%x-%d.blob", sha.Sum(nil), timestamp.UnixMilli()) - _ = os.WriteFile(filepath.Join(os.TempDir(), fragmentPath), blob, 0644) - - unpacked, err := cosesign1.UnpackAndValidateCOSE1CertChain(raw) - if err != nil { - return "", "", "", fmt.Errorf("InjectFragment failed COSE validation: %w", err) - } - - payloadString = string(unpacked.Payload[:]) - issuer = unpacked.Issuer - feed = unpacked.Feed - chainPem := unpacked.ChainPem - - log.G(ctx).WithFields(logrus.Fields{ - "issuer": issuer, // eg the DID:x509:blah.... - "feed": feed, - "cty": unpacked.ContentType, - "chainPem": chainPem, - }).Debugf("unpacked COSE1 cert chain") - - log.G(ctx).WithFields(logrus.Fields{ - "payload": payloadString, - }).Tracef("unpacked COSE1 payload") - - if len(issuer) == 0 || len(feed) == 0 { // must both be present - return "", "", "", fmt.Errorf("either issuer and feed must both be provided in the COSE_Sign1 protected header") - } - - // Resolve returns a did doc that we don't need - // we only care if there was an error or not - _, err = didx509resolver.Resolve(unpacked.ChainPem, issuer, true) - if err != nil { - log.G(ctx).Printf("Badly formed fragment - did resolver failed to match fragment did:x509 from chain with purported issuer %s, feed %s - err %s", issuer, feed, err.Error()) - return "", "", "", err - } - - return issuer, feed, payloadString, nil -} - // CreateSecurityPolicyEnforcer returns an appropriate enforcer for input // parameters. Returns an error if the requested `enforcer` implementation // isn't registered. From 8809b5f1abe7f20c2d8fc206e8f3e0aa748b0b46 Mon Sep 17 00:00:00 2001 From: Mahati Chamarthy Date: Tue, 4 Nov 2025 16:50:35 +0000 Subject: [PATCH 5/9] C-WCOW: Move securitycontext dir to securitypolicy pkg Signed-off-by: Mahati Chamarthy --- internal/gcs-sidecar/handlers.go | 58 ++---------------- internal/guest/runtime/hcsv2/uvm.go | 56 +----------------- pkg/securitypolicy/securitypolicy_options.go | 62 ++++++++++++++++++++ 3 files changed, 68 insertions(+), 108 deletions(-) diff --git a/internal/gcs-sidecar/handlers.go b/internal/gcs-sidecar/handlers.go index 9b60ec6d94..c8c1b21dd5 100644 --- a/internal/gcs-sidecar/handlers.go +++ b/internal/gcs-sidecar/handlers.go @@ -110,48 +110,12 @@ func (b *Bridge) createContainer(req *request) (err error) { b.hostState.RemoveContainer(ctx, containerID) } }(err) - // Write security policy, signed UVM reference and host AMD certificate to - // container's rootfs, so that application and sidecar containers can have - // access to it. The security policy is required by containers which need to - // extract init-time claims found in the security policy. The directory path - // containing the files is exposed via UVM_SECURITY_CONTEXT_DIR env var. - // It may be an error to have a security policy but not expose it to the - // container as in that case it can never be checked as correct by a verifier. - if oci.ParseAnnotationsBool(ctx, spec.Annotations, annotations.WCOWSecurityPolicyEnv, true) { - encodedPolicy := b.hostState.securityOptions.PolicyEnforcer.EncodedSecurityPolicy() - hostAMDCert := spec.Annotations[annotations.WCOWHostAMDCertificate] - if len(encodedPolicy) > 0 || len(hostAMDCert) > 0 || len(b.hostState.securityOptions.UvmReferenceInfo) > 0 { - // Use os.MkdirTemp to make sure that the directory is unique. - securityContextDir, err := os.MkdirTemp(spec.Root.Path, securitypolicy.SecurityContextDirTemplate) - if err != nil { - return fmt.Errorf("failed to create security context directory: %w", err) - } - // Make sure that files inside directory are readable - if err := os.Chmod(securityContextDir, 0755); err != nil { - return fmt.Errorf("failed to chmod security context directory: %w", err) - } - if len(encodedPolicy) > 0 { - if err := writeFileInDir(securityContextDir, securitypolicy.PolicyFilename, []byte(encodedPolicy), 0777); err != nil { - return fmt.Errorf("failed to write security policy: %w", err) - } - } - if len(b.hostState.securityOptions.UvmReferenceInfo) > 0 { - if err := writeFileInDir(securityContextDir, securitypolicy.ReferenceInfoFilename, []byte(b.hostState.securityOptions.UvmReferenceInfo), 0777); err != nil { - return fmt.Errorf("failed to write UVM reference info: %w", err) - } - } - - if len(hostAMDCert) > 0 { - if err := writeFileInDir(securityContextDir, securitypolicy.HostAMDCertFilename, []byte(hostAMDCert), 0777); err != nil { - return fmt.Errorf("failed to write host AMD certificate: %w", err) - } - } - - containerCtxDir := fmt.Sprintf("/%s", filepath.Base(securityContextDir)) - secCtxEnv := fmt.Sprintf("UVM_SECURITY_CONTEXT_DIR=%s", containerCtxDir) - spec.Process.Env = append(spec.Process.Env, secCtxEnv) + if oci.ParseAnnotationsBool(ctx, spec.Annotations, annotations.WCOWSecurityPolicyEnv, true) { + if err := b.hostState.securityOptions.WriteSecurityContextDir(&spec); err != nil { + return fmt.Errorf("failed to write security context dir: %w", err) } + cwcowHostedSystemConfig.Spec = spec } // Strip the spec field @@ -196,20 +160,6 @@ func (b *Bridge) createContainer(req *request) (err error) { return nil } -func writeFileInDir(dir string, filename string, data []byte, perm os.FileMode) error { - st, err := os.Stat(dir) - if err != nil { - return err - } - - if !st.IsDir() { - return fmt.Errorf("not a directory %q", dir) - } - - targetFilename := filepath.Join(dir, filename) - return os.WriteFile(targetFilename, data, perm) -} - // processParamEnvToOCIEnv converts an Environment field from ProcessParameters // (a map from environment variable to value) into an array of environment // variable assignments (where each is in the form "=") which diff --git a/internal/guest/runtime/hcsv2/uvm.go b/internal/guest/runtime/hcsv2/uvm.go index 7212bac306..ea03577295 100644 --- a/internal/guest/runtime/hcsv2/uvm.go +++ b/internal/guest/runtime/hcsv2/uvm.go @@ -539,47 +539,9 @@ func (h *Host) CreateContainer(ctx context.Context, id string, settings *prot.VM settings.OCISpecification.Process.Capabilities = capsToKeep } - // Write security policy, signed UVM reference and host AMD certificate to - // container's rootfs, so that application and sidecar containers can have - // access to it. The security policy is required by containers which need to - // extract init-time claims found in the security policy. The directory path - // containing the files is exposed via UVM_SECURITY_CONTEXT_DIR env var. - // It may be an error to have a security policy but not expose it to the - // container as in that case it can never be checked as correct by a verifier. if oci.ParseAnnotationsBool(ctx, settings.OCISpecification.Annotations, annotations.LCOWSecurityPolicyEnv, true) { - encodedPolicy := h.securityOptions.PolicyEnforcer.EncodedSecurityPolicy() - hostAMDCert := settings.OCISpecification.Annotations[annotations.LCOWHostAMDCertificate] - if len(encodedPolicy) > 0 || len(hostAMDCert) > 0 || len(h.securityOptions.UvmReferenceInfo) > 0 { - // Use os.MkdirTemp to make sure that the directory is unique. - securityContextDir, err := os.MkdirTemp(settings.OCISpecification.Root.Path, securitypolicy.SecurityContextDirTemplate) - if err != nil { - return nil, fmt.Errorf("failed to create security context directory: %w", err) - } - // Make sure that files inside directory are readable - if err := os.Chmod(securityContextDir, 0755); err != nil { - return nil, fmt.Errorf("failed to chmod security context directory: %w", err) - } - - if len(encodedPolicy) > 0 { - if err := writeFileInDir(securityContextDir, securitypolicy.PolicyFilename, []byte(encodedPolicy), 0744); err != nil { - return nil, fmt.Errorf("failed to write security policy: %w", err) - } - } - if len(h.securityOptions.UvmReferenceInfo) > 0 { - if err := writeFileInDir(securityContextDir, securitypolicy.ReferenceInfoFilename, []byte(h.securityOptions.UvmReferenceInfo), 0744); err != nil { - return nil, fmt.Errorf("failed to write UVM reference info: %w", err) - } - } - - if len(hostAMDCert) > 0 { - if err := writeFileInDir(securityContextDir, securitypolicy.HostAMDCertFilename, []byte(hostAMDCert), 0744); err != nil { - return nil, fmt.Errorf("failed to write host AMD certificate: %w", err) - } - } - - containerCtxDir := fmt.Sprintf("/%s", filepath.Base(securityContextDir)) - secCtxEnv := fmt.Sprintf("UVM_SECURITY_CONTEXT_DIR=%s", containerCtxDir) - settings.OCISpecification.Process.Env = append(settings.OCISpecification.Process.Env, secCtxEnv) + if err := h.securityOptions.WriteSecurityContextDir(settings.OCISpecification); err != nil { + return nil, fmt.Errorf("failed to write security context dir: %w", err) } } @@ -1351,20 +1313,6 @@ func isPrivilegedContainerCreationRequest(ctx context.Context, spec *specs.Spec) return oci.ParseAnnotationsBool(ctx, spec.Annotations, annotations.LCOWPrivileged, false) } -func writeFileInDir(dir string, filename string, data []byte, perm os.FileMode) error { - st, err := os.Stat(dir) - if err != nil { - return err - } - - if !st.IsDir() { - return fmt.Errorf("not a directory %q", dir) - } - - targetFilename := filepath.Join(dir, filename) - return os.WriteFile(targetFilename, data, perm) -} - // Virtual Pod Management Methods // InitializeVirtualPodSupport sets up the parent cgroup for virtual pods diff --git a/pkg/securitypolicy/securitypolicy_options.go b/pkg/securitypolicy/securitypolicy_options.go index 85048b5516..33235ad880 100644 --- a/pkg/securitypolicy/securitypolicy_options.go +++ b/pkg/securitypolicy/securitypolicy_options.go @@ -15,6 +15,8 @@ import ( didx509resolver "github.com/Microsoft/didx509go/pkg/did-x509-resolver" "github.com/Microsoft/hcsshim/internal/log" "github.com/Microsoft/hcsshim/internal/protocol/guestresource" + "github.com/Microsoft/hcsshim/pkg/annotations" + "github.com/opencontainers/runtime-spec/specs-go" "github.com/pkg/errors" "github.com/sirupsen/logrus" ) @@ -147,3 +149,63 @@ func (s *SecurityOptions) InjectFragment(ctx context.Context, fragment *guestres } return nil } + +func writeFileInDir(dir string, filename string, data []byte, perm os.FileMode) error { + st, err := os.Stat(dir) + if err != nil { + return err + } + + if !st.IsDir() { + return fmt.Errorf("not a directory %q", dir) + } + + targetFilename := filepath.Join(dir, filename) + return os.WriteFile(targetFilename, data, perm) +} + +// Write security policy, signed UVM reference and host AMD certificate to +// container's rootfs, so that application and sidecar containers can have +// access to it. The security policy is required by containers which need to +// extract init-time claims found in the security policy. The directory path +// containing the files is exposed via UVM_SECURITY_CONTEXT_DIR env var. +// It may be an error to have a security policy but not expose it to the +// container as in that case it can never be checked as correct by a verifier. +func (s *SecurityOptions) WriteSecurityContextDir(spec *specs.Spec) error { + encodedPolicy := s.PolicyEnforcer.EncodedSecurityPolicy() + hostAMDCert := spec.Annotations[annotations.WCOWHostAMDCertificate] + if len(encodedPolicy) > 0 || len(hostAMDCert) > 0 || len(s.UvmReferenceInfo) > 0 { + // Use os.MkdirTemp to make sure that the directory is unique. + securityContextDir, err := os.MkdirTemp(spec.Root.Path, SecurityContextDirTemplate) + if err != nil { + return fmt.Errorf("failed to create security context directory: %w", err) + } + // Make sure that files inside directory are readable + if err := os.Chmod(securityContextDir, 0755); err != nil { + return fmt.Errorf("failed to chmod security context directory: %w", err) + } + + if len(encodedPolicy) > 0 { + if err := writeFileInDir(securityContextDir, PolicyFilename, []byte(encodedPolicy), 0777); err != nil { + return fmt.Errorf("failed to write security policy: %w", err) + } + } + if len(s.UvmReferenceInfo) > 0 { + if err := writeFileInDir(securityContextDir, ReferenceInfoFilename, []byte(s.UvmReferenceInfo), 0777); err != nil { + return fmt.Errorf("failed to write UVM reference info: %w", err) + } + } + + if len(hostAMDCert) > 0 { + if err := writeFileInDir(securityContextDir, HostAMDCertFilename, []byte(hostAMDCert), 0777); err != nil { + return fmt.Errorf("failed to write host AMD certificate: %w", err) + } + } + + containerCtxDir := fmt.Sprintf("/%s", filepath.Base(securityContextDir)) + secCtxEnv := fmt.Sprintf("UVM_SECURITY_CONTEXT_DIR=%s", containerCtxDir) + spec.Process.Env = append(spec.Process.Env, secCtxEnv) + + } + return nil +} From 83a71d4050af93e1efee69b40671427ba7272b18 Mon Sep 17 00:00:00 2001 From: Mahati Chamarthy Date: Tue, 4 Nov 2025 17:31:21 +0000 Subject: [PATCH 6/9] C-WCOW: PspDriver and hostdata changes in SecurityPolicy pkg Signed-off-by: Mahati Chamarthy --- cmd/gcs-sidecar/main.go | 3 +- internal/gcs-sidecar/handlers.go | 10 ++--- internal/gcs-sidecar/host.go | 37 +------------------ internal/guest/runtime/hcsv2/hostdata.go | 33 ----------------- internal/guest/runtime/hcsv2/uvm.go | 36 ++++-------------- .../securitypolicy}/pspdriver.go | 17 ++++----- pkg/securitypolicy/securitypolicy_linux.go | 24 ++++++++++++ pkg/securitypolicy/securitypolicy_options.go | 16 ++++++++ pkg/securitypolicy/securitypolicy_windows.go | 20 +++++++++- test/gcs/main_test.go | 5 +-- 10 files changed, 85 insertions(+), 116 deletions(-) delete mode 100644 internal/guest/runtime/hcsv2/hostdata.go rename {internal/pspdriver => pkg/securitypolicy}/pspdriver.go (94%) diff --git a/cmd/gcs-sidecar/main.go b/cmd/gcs-sidecar/main.go index e53ce3f636..1925621145 100644 --- a/cmd/gcs-sidecar/main.go +++ b/cmd/gcs-sidecar/main.go @@ -15,7 +15,6 @@ import ( "github.com/Microsoft/hcsshim/internal/gcs/prot" shimlog "github.com/Microsoft/hcsshim/internal/log" "github.com/Microsoft/hcsshim/internal/oc" - "github.com/Microsoft/hcsshim/internal/pspdriver" "github.com/Microsoft/hcsshim/pkg/securitypolicy" "github.com/sirupsen/logrus" "go.opencensus.io/trace" @@ -217,7 +216,7 @@ func main() { return } - if err := pspdriver.StartPSPDriver(ctx); err != nil { + if err := securitypolicy.StartPSPDriver(ctx); err != nil { // When error happens, pspdriver.GetPspDriverError() returns true. // In that case, gcs-sidecar should keep the initial "deny" policy // and reject all requests from the host. diff --git a/internal/gcs-sidecar/handlers.go b/internal/gcs-sidecar/handlers.go index c8c1b21dd5..f46dc8b985 100644 --- a/internal/gcs-sidecar/handlers.go +++ b/internal/gcs-sidecar/handlers.go @@ -88,9 +88,6 @@ func (b *Bridge) createContainer(req *request) (err error) { return fmt.Errorf("CreateContainer operation is denied by policy: %w", err) } - if err := b.hostState.SetupSecurityContextDir(ctx, &spec); err != nil { - return err - } commandLine := len(spec.Process.Args) > 0 c := &Container{ id: containerID, @@ -549,9 +546,12 @@ func (b *Bridge) modifySettings(req *request) (err error) { case guestresource.ResourceTypeSecurityPolicy: securityPolicyRequest := modifyGuestSettingsRequest.Settings.(*guestresource.ConfidentialOptions) log.G(ctx).Tracef("WCOWConfidentialOptions: { %v}", securityPolicyRequest) - err := b.hostState.SetWCOWConfidentialUVMOptions(req.ctx, securityPolicyRequest) + err := b.hostState.securityOptions.SetConfidentialOptions(ctx, + securityPolicyRequest.EnforcerType, + securityPolicyRequest.EncodedSecurityPolicy, + securityPolicyRequest.EncodedUVMReference) if err != nil { - return errors.Wrap(err, "error creating enforcer") + return errors.Wrap(err, "Failed to set Confidentia UVM Options") } // Send response back to shim resp := &prot.ResponseBase{ diff --git a/internal/gcs-sidecar/host.go b/internal/gcs-sidecar/host.go index 3419f51426..86fc98a30f 100644 --- a/internal/gcs-sidecar/host.go +++ b/internal/gcs-sidecar/host.go @@ -6,19 +6,14 @@ package bridge import ( "context" "io" - "os" - "path/filepath" "sync" "github.com/Microsoft/hcsshim/internal/bridgeutils/gcserr" hcsschema "github.com/Microsoft/hcsshim/internal/hcs/schema2" "github.com/Microsoft/hcsshim/internal/log" "github.com/Microsoft/hcsshim/internal/logfields" - "github.com/Microsoft/hcsshim/internal/protocol/guestresource" - "github.com/Microsoft/hcsshim/internal/pspdriver" "github.com/Microsoft/hcsshim/pkg/securitypolicy" - specs "github.com/opencontainers/runtime-spec/specs-go" - "github.com/pkg/errors" + oci "github.com/opencontainers/runtime-spec/specs-go" "github.com/sirupsen/logrus" ) @@ -30,7 +25,7 @@ type Host struct { type Container struct { id string - spec specs.Spec + spec oci.Spec processesMutex sync.Mutex processes map[uint32]*containerProcess commandLine bool @@ -59,34 +54,6 @@ func NewHost(initialEnforcer securitypolicy.SecurityPolicyEnforcer, logWriter io } } -func (h *Host) SetWCOWConfidentialUVMOptions(ctx context.Context, securityPolicyRequest *guestresource.ConfidentialOptions) error { - if err := pspdriver.GetPspDriverError(); err != nil { - // For this case gcs-sidecar will keep initial deny policy. - return errors.Wrapf(err, "an error occurred while using PSP driver") - } - - // Fetch report and validate host_data - hostData, err := securitypolicy.NewSecurityPolicyDigest(securityPolicyRequest.EncodedSecurityPolicy) - if err != nil { - return err - } - - if err := pspdriver.ValidateHostData(ctx, hostData[:]); err != nil { - // For this case gcs-sidecar will keep initial deny policy. - return err - } - - if err := h.securityOptions.SetConfidentialOptions(ctx, - securityPolicyRequest.EnforcerType, - securityPolicyRequest.EncodedSecurityPolicy, - securityPolicyRequest.EncodedUVMReference, - ); err != nil { - return errors.Wrapf(err, "SetWCOWConfidentialUVMOptions failed to set security options") - } - - return nil -} - func (h *Host) AddContainer(ctx context.Context, id string, c *Container) error { h.containersMutex.Lock() defer h.containersMutex.Unlock() diff --git a/internal/guest/runtime/hcsv2/hostdata.go b/internal/guest/runtime/hcsv2/hostdata.go deleted file mode 100644 index d75463fcda..0000000000 --- a/internal/guest/runtime/hcsv2/hostdata.go +++ /dev/null @@ -1,33 +0,0 @@ -//go:build linux -// +build linux - -package hcsv2 - -import ( - "bytes" - "fmt" - - "github.com/Microsoft/hcsshim/pkg/amdsevsnp" -) - -// validateHostData fetches SNP report (if applicable) and validates `hostData` against -// HostData set at UVM launch. -func validateHostData(hostData []byte) error { - // If the UVM is not SNP, then don't try to fetch an SNP report. - if !amdsevsnp.IsSNP() { - return nil - } - report, err := amdsevsnp.FetchParsedSNPReport(nil) - if err != nil { - return err - } - - if !bytes.Equal(hostData, report.HostData) { - return fmt.Errorf( - "security policy digest %q doesn't match HostData provided at launch %q", - hostData, - report.HostData, - ) - } - return nil -} diff --git a/internal/guest/runtime/hcsv2/uvm.go b/internal/guest/runtime/hcsv2/uvm.go index ea03577295..4436c73c4f 100644 --- a/internal/guest/runtime/hcsv2/uvm.go +++ b/internal/guest/runtime/hcsv2/uvm.go @@ -112,37 +112,14 @@ func NewHost(rtime runtime.Runtime, vsock transport.Transport, initialEnforcer s } } -// SetConfidentialUVMOptions takes guestresource.ConfidentialOptions -// to set up our internal data structures we use to store and enforce -// security policy. The options can contain security policy enforcer type, -// encoded security policy and signed UVM reference information The security -// policy and uvm reference information can be further presented to workload -// containers for validation and attestation purposes. -func (h *Host) SetConfidentialUVMOptions(ctx context.Context, r *guestresource.ConfidentialOptions) error { - hostData, err := securitypolicy.NewSecurityPolicyDigest(r.EncodedSecurityPolicy) - if err != nil { - return err - } - - if err := validateHostData(hostData[:]); err != nil { - return err - } - - if err := h.securityOptions.SetConfidentialOptions(ctx, - r.EnforcerType, - r.EncodedSecurityPolicy, - r.EncodedUVMReference, - ); err != nil { - return errors.Wrapf(err, "SetWCOWConfidentialUVMOptions failed to set security options") - } - - return nil -} - func (h *Host) SecurityPolicyEnforcer() securitypolicy.SecurityPolicyEnforcer { return h.securityOptions.PolicyEnforcer } +func (h *Host) SecurityOptions() *securitypolicy.SecurityOptions { + return h.securityOptions +} + func (h *Host) Transport() transport.Transport { return h.vsock } @@ -696,7 +673,10 @@ func (h *Host) modifyHostSettings(ctx context.Context, containerID string, req * if !ok { return errors.New("the request's settings are not of type ConfidentialOptions") } - return h.SetConfidentialUVMOptions(ctx, r) + return h.securityOptions.SetConfidentialOptions(ctx, + r.EnforcerType, + r.EncodedSecurityPolicy, + r.EncodedUVMReference) case guestresource.ResourceTypePolicyFragment: r, ok := req.Settings.(*guestresource.SecurityPolicyFragment) if !ok { diff --git a/internal/pspdriver/pspdriver.go b/pkg/securitypolicy/pspdriver.go similarity index 94% rename from internal/pspdriver/pspdriver.go rename to pkg/securitypolicy/pspdriver.go index db41384853..4a04805be3 100644 --- a/internal/pspdriver/pspdriver.go +++ b/pkg/securitypolicy/pspdriver.go @@ -1,7 +1,7 @@ //go:build windows // +build windows -package pspdriver +package securitypolicy import ( "bytes" @@ -217,7 +217,7 @@ func GetPspDriverError() error { } // IsSNPMode() returns true if it's in SNP mode. -func IsSNPMode(ctx context.Context) (bool, error) { +func IsSNPMode() (bool, error) { if pspDriverError != nil { return false, pspDriverError @@ -249,7 +249,7 @@ func IsSNPMode(ctx context.Context) (bool, error) { } // FetchRawSNPReport returns attestation report bytes. -func FetchRawSNPReport(ctx context.Context, reportData []byte) ([]byte, error) { +func FetchRawSNPReport(reportData []byte) ([]byte, error) { if pspDriverError != nil { return nil, pspDriverError } @@ -291,8 +291,8 @@ func FetchRawSNPReport(ctx context.Context, reportData []byte) ([]byte, error) { } // FetchParsedSNPReport parses raw attestation response into proper structs. -func FetchParsedSNPReport(ctx context.Context, reportData []byte) (Report, error) { - rawBytes, err := FetchRawSNPReport(ctx, reportData) +func FetchParsedSNPReport(reportData []byte) (Report, error) { + rawBytes, err := FetchRawSNPReport(reportData) if err != nil { return Report{}, err } @@ -305,19 +305,18 @@ func FetchParsedSNPReport(ctx context.Context, reportData []byte) (Report, error return r.report(), nil } -// TODO: Based on internal\guest\runtime\hcsv2\hostdata.go and it's duplicated. // ValidateHostData fetches SNP report (if applicable) and validates `hostData` against // HostData set at UVM launch. -func ValidateHostData(ctx context.Context, hostData []byte) error { +func ValidateHostDataPSP(hostData []byte) error { // If the UVM is not SNP, then don't try to fetch an SNP report. - isSnpMode, err := IsSNPMode(ctx) + isSnpMode, err := IsSNPMode() if err != nil { return err } if !isSnpMode { return nil } - report, err := FetchParsedSNPReport(ctx, nil) + report, err := FetchParsedSNPReport(nil) if err != nil { return err } diff --git a/pkg/securitypolicy/securitypolicy_linux.go b/pkg/securitypolicy/securitypolicy_linux.go index 54f8972de4..278038ac67 100644 --- a/pkg/securitypolicy/securitypolicy_linux.go +++ b/pkg/securitypolicy/securitypolicy_linux.go @@ -4,12 +4,14 @@ package securitypolicy import ( + "bytes" "fmt" "os" "path/filepath" "strconv" specInternal "github.com/Microsoft/hcsshim/internal/guest/spec" + "github.com/Microsoft/hcsshim/pkg/amdsevsnp" "github.com/moby/sys/user" oci "github.com/opencontainers/runtime-spec/specs-go" "github.com/pkg/errors" @@ -18,6 +20,28 @@ import ( //nolint:unused const osType = "linux" +// validateHostData fetches SNP report (if applicable) and validates `hostData` against +// HostData set at UVM launch. +func validateHostData(hostData []byte) error { + // If the UVM is not SNP, then don't try to fetch an SNP report. + if !amdsevsnp.IsSNP() { + return nil + } + report, err := amdsevsnp.FetchParsedSNPReport(nil) + if err != nil { + return err + } + + if !bytes.Equal(hostData, report.HostData) { + return fmt.Errorf( + "security policy digest %q doesn't match HostData provided at launch %q", + hostData, + report.HostData, + ) + } + return nil +} + func ExtendPolicyWithNetworkingMounts(sandboxID string, enforcer SecurityPolicyEnforcer, spec *oci.Spec) error { roSpec := &oci.Spec{ Root: spec.Root, diff --git a/pkg/securitypolicy/securitypolicy_options.go b/pkg/securitypolicy/securitypolicy_options.go index 33235ad880..b2b469e0bb 100644 --- a/pkg/securitypolicy/securitypolicy_options.go +++ b/pkg/securitypolicy/securitypolicy_options.go @@ -39,6 +39,12 @@ func NewSecurityOptions(enforcer SecurityPolicyEnforcer, enforcerSet bool, uvmRe } } +// SetConfidentialOptions takes guestresource.ConfidentialOptions +// to set up our internal data structures we use to store and enforce +// security policy. The options can contain security policy enforcer type, +// encoded security policy and signed UVM reference information The security +// policy and uvm reference information can be further presented to workload +// containers for validation and attestation purposes. func (s *SecurityOptions) SetConfidentialOptions(ctx context.Context, enforcerType string, encodedSecurityPolicy string, encodedUVMReference string) error { s.policyMutex.Lock() defer s.policyMutex.Unlock() @@ -46,6 +52,16 @@ func (s *SecurityOptions) SetConfidentialOptions(ctx context.Context, enforcerTy if s.PolicyEnforcerSet { return errors.New("security policy has already been set") } + + hostData, err := NewSecurityPolicyDigest(encodedSecurityPolicy) + if err != nil { + return err + } + + if err := validateHostData(hostData[:]); err != nil { + return err + } + // This limit ensures messages are below the character truncation limit that // can be imposed by an orchestrator maxErrorMessageLength := 3 * 1024 diff --git a/pkg/securitypolicy/securitypolicy_windows.go b/pkg/securitypolicy/securitypolicy_windows.go index b8495f7385..1582a756af 100644 --- a/pkg/securitypolicy/securitypolicy_windows.go +++ b/pkg/securitypolicy/securitypolicy_windows.go @@ -3,11 +3,29 @@ package securitypolicy -import oci "github.com/opencontainers/runtime-spec/specs-go" +import ( + oci "github.com/opencontainers/runtime-spec/specs-go" + "github.com/pkg/errors" +) //nolint:unused const osType = "windows" +// validateHostData fetches SNP report (if applicable) and validates `hostData` against +// HostData set at UVM launch. +func validateHostData(hostData []byte) error { + if err := GetPspDriverError(); err != nil { + // For this case gcs-sidecar will keep initial deny policy. + return errors.Wrapf(err, "an error occurred while using PSP driver") + } + + if err := ValidateHostDataPSP(hostData[:]); err != nil { + // For this case gcs-sidecar will keep initial deny policy. + return err + } + return nil +} + // SandboxMountsDir returns sandbox mounts directory inside UVM/host. func SandboxMountsDir(sandboxID string) string { return "" diff --git a/test/gcs/main_test.go b/test/gcs/main_test.go index 564e11c4c4..ce399e0767 100644 --- a/test/gcs/main_test.go +++ b/test/gcs/main_test.go @@ -22,7 +22,6 @@ import ( "github.com/Microsoft/hcsshim/internal/guest/transport" "github.com/Microsoft/hcsshim/internal/guestpath" "github.com/Microsoft/hcsshim/internal/oc" - "github.com/Microsoft/hcsshim/internal/protocol/guestresource" "github.com/Microsoft/hcsshim/pkg/securitypolicy" "github.com/Microsoft/hcsshim/test/internal/util" @@ -167,9 +166,9 @@ func getHost(_ context.Context, tb testing.TB, rt runtime.Runtime) *hcsv2.Host { func getHostErr(rt runtime.Runtime, tp transport.Transport) (*hcsv2.Host, error) { h := hcsv2.NewHost(rt, tp, &securitypolicy.OpenDoorSecurityPolicyEnforcer{}, os.Stdout) - if err := h.SetConfidentialUVMOptions( + if err := h.SecurityOptions().SetConfidentialOptions( context.Background(), - &guestresource.ConfidentialOptions{}, + "", "", "", ); err != nil { return nil, fmt.Errorf("could not set host security policy: %w", err) } From 1bf838e8c08db8f6afb04e63c52f72bf959c0296 Mon Sep 17 00:00:00 2001 From: Mahati Chamarthy Date: Thu, 11 Dec 2025 13:43:18 +0000 Subject: [PATCH 7/9] C-WCOW: Cleanup confidential options Signed-off-by: Mahati Chamarthy --- internal/uvm/create.go | 4 ++-- internal/uvm/create_lcow.go | 7 +++---- internal/uvm/create_wcow.go | 14 ++++++++------ internal/uvm/start.go | 33 ++++++++++++++++----------------- internal/uvm/types.go | 3 --- 5 files changed, 29 insertions(+), 32 deletions(-) diff --git a/internal/uvm/create.go b/internal/uvm/create.go index 472e19f437..c991d9ea79 100644 --- a/internal/uvm/create.go +++ b/internal/uvm/create.go @@ -323,8 +323,8 @@ func (uvm *UtilityVM) CloseCtx(ctx context.Context) (err error) { _ = uvm.WaitCtx(ctx) } - if uvm.confidentialUVMOptions != nil && uvm.confidentialUVMOptions.GuestStateFile != "" { - vmgsFullPath := filepath.Join(uvm.confidentialUVMOptions.BundleDirectory, uvm.confidentialUVMOptions.GuestStateFile) + if uvm.HasConfidentialPolicy() && uvm.createOpts.(*OptionsLCOW).GuestStateFile != "" { + vmgsFullPath := filepath.Join(uvm.createOpts.(*OptionsLCOW).BundleDirectory, uvm.createOpts.(*OptionsLCOW).GuestStateFile) e := log.G(ctx).WithField("VMGS file", vmgsFullPath) e.Debug("removing VMGS file") if err := os.Remove(vmgsFullPath); err != nil { diff --git a/internal/uvm/create_lcow.go b/internal/uvm/create_lcow.go index d0c0a93574..7e17e755e9 100644 --- a/internal/uvm/create_lcow.go +++ b/internal/uvm/create_lcow.go @@ -89,7 +89,7 @@ const ( UVMReferenceInfoFile = "reference_info.cose" ) -type ConfidentialOptions struct { +type ConfidentialLCOWOptions struct { GuestStateFile string // The vmgs file to load UseGuestStateFile bool // Use a vmgs file that contains a kernel and initrd, required for SNP SecurityPolicy string // Optional security policy @@ -105,7 +105,7 @@ type ConfidentialOptions struct { // OptionsLCOW are the set of options passed to CreateLCOW() to create a utility vm. type OptionsLCOW struct { *Options - *ConfidentialOptions + *ConfidentialLCOWOptions // Folder in which kernel and root file system reside. Defaults to \Program Files\Linux Containers. // @@ -176,7 +176,7 @@ func NewDefaultOptionsLCOW(id, owner string) *OptionsLCOW { VPCIEnabled: false, EnableScratchEncryption: false, DisableTimeSyncService: false, - ConfidentialOptions: &ConfidentialOptions{ + ConfidentialLCOWOptions: &ConfidentialLCOWOptions{ SecurityPolicyEnabled: false, UVMReferenceInfoFile: UVMReferenceInfoFile, }, @@ -951,7 +951,6 @@ func CreateLCOW(ctx context.Context, opts *OptionsLCOW) (_ *UtilityVM, err error vpmemMultiMapping: !opts.VPMemNoMultiMapping, encryptScratch: opts.EnableScratchEncryption, noWritableFileShares: opts.NoWritableFileShares, - confidentialUVMOptions: opts.ConfidentialOptions, policyBasedRouting: opts.PolicyBasedRouting, } diff --git a/internal/uvm/create_wcow.go b/internal/uvm/create_wcow.go index 3e9c1e044e..7658657a69 100644 --- a/internal/uvm/create_wcow.go +++ b/internal/uvm/create_wcow.go @@ -108,12 +108,14 @@ func GetDefaultReferenceInfoFilePath() string { // executable files name. func NewDefaultOptionsWCOW(id, owner string) *OptionsWCOW { return &OptionsWCOW{ - Options: newDefaultOptions(id, owner), - AdditionalRegistryKeys: []hcsschema.RegistryValue{}, - ConfidentialWCOWOptions: &ConfidentialWCOWOptions{}, - OutputHandlerCreator: parseLogrus, - ForwardLogs: true, // Default to true for WCOW, and set to false for CWCOW in internal/oci/uvm.go SpecToUVMCreateOpts - LogSources: "", + Options: newDefaultOptions(id, owner), + AdditionalRegistryKeys: []hcsschema.RegistryValue{}, + ConfidentialWCOWOptions: &ConfidentialWCOWOptions{ + SecurityPolicyEnabled: false, + }, + OutputHandlerCreator: parseLogrus, + ForwardLogs: true, // Default to true for WCOW, and set to false for CWCOW in internal/oci/uvm.go SpecToUVMCreateOpts + LogSources: "", } } diff --git a/internal/uvm/start.go b/internal/uvm/start.go index 598baf819a..781bc3c417 100644 --- a/internal/uvm/start.go +++ b/internal/uvm/start.go @@ -373,15 +373,14 @@ func (uvm *UtilityVM) Start(ctx context.Context) (err error) { } uvm.SCSIManager = mgr - var policy, enforcer, referenceInfoFileRoot, referenceInfoFilePath string - - if uvm.confidentialUVMOptions != nil || uvm.HasConfidentialPolicy() { - if uvm.confidentialUVMOptions != nil && uvm.OS() == "linux" { - policy = uvm.confidentialUVMOptions.SecurityPolicy - enforcer = uvm.confidentialUVMOptions.SecurityPolicyEnforcer - referenceInfoFilePath = uvm.confidentialUVMOptions.UVMReferenceInfoFile + if uvm.HasConfidentialPolicy() { + var policy, enforcer, referenceInfoFileRoot, referenceInfoFilePath string + if uvm.OS() == "linux" { + policy = uvm.createOpts.(*OptionsLCOW).SecurityPolicy + enforcer = uvm.createOpts.(*OptionsLCOW).SecurityPolicyEnforcer + referenceInfoFilePath = uvm.createOpts.(*OptionsLCOW).UVMReferenceInfoFile referenceInfoFileRoot = defaultLCOWOSBootFilesPath() - } else if uvm.HasConfidentialPolicy() && uvm.OS() == "windows" { + } else if uvm.OS() == "windows" { policy = uvm.createOpts.(*OptionsWCOW).SecurityPolicy enforcer = uvm.createOpts.(*OptionsWCOW).SecurityPolicyEnforcer referenceInfoFilePath = uvm.createOpts.(*OptionsWCOW).UVMReferenceInfoFile @@ -394,16 +393,16 @@ func (uvm *UtilityVM) Start(ctx context.Context) (err error) { if err := uvm.SetConfidentialUVMOptions(ctx, copts...); err != nil { return err } + } - if uvm.OS() == "windows" && uvm.forwardLogs { - // If the UVM is Windows and log forwarding is enabled, set the log sources - // and start the log forwarding service. - if err := uvm.SetLogSources(ctx); err != nil { - e.WithError(err).Error("failed to set log sources") - } - if err := uvm.StartLogForwarding(ctx); err != nil { - e.WithError(err).Error("failed to start log forwarding") - } + if uvm.OS() == "windows" && uvm.forwardLogs { + // If the UVM is Windows and log forwarding is enabled, set the log sources + // and start the log forwarding service. + if err := uvm.SetLogSources(ctx); err != nil { + e.WithError(err).Error("failed to set log sources") + } + if err := uvm.StartLogForwarding(ctx); err != nil { + e.WithError(err).Error("failed to start log forwarding") } } diff --git a/internal/uvm/types.go b/internal/uvm/types.go index a4b38720cb..a5fbb8014b 100644 --- a/internal/uvm/types.go +++ b/internal/uvm/types.go @@ -137,9 +137,6 @@ type UtilityVM struct { // This only applies for WCOW. noInheritHostTimezone bool - // confidentialUVMOptions hold confidential UVM specific options - confidentialUVMOptions *ConfidentialOptions - // LCOW only. Indicates whether to use policy based routing when configuring net interfaces in the guest. policyBasedRouting bool From 948c16f97b3bbc9c1c7ff5e2e4136041b32fe956 Mon Sep 17 00:00:00 2001 From: Mahati Chamarthy Date: Fri, 12 Dec 2025 18:40:07 +0000 Subject: [PATCH 8/9] C-WCOW: Hardware checks and reports in separate pkg Signed-off-by: Mahati Chamarthy --- cmd/gcs-sidecar/main.go | 3 +- pkg/amdsevsnp/report.go | 192 ---------------- pkg/amdsevsnp/report_linux.go | 213 ++++++++++++++++++ pkg/amdsevsnp/report_test.go | 3 - .../report_windows.go} | 128 +---------- pkg/amdsevsnp/validate.go | 39 ++++ pkg/securitypolicy/securitypolicy_linux.go | 24 -- pkg/securitypolicy/securitypolicy_options.go | 3 +- pkg/securitypolicy/securitypolicy_windows.go | 16 -- 9 files changed, 261 insertions(+), 360 deletions(-) create mode 100644 pkg/amdsevsnp/report_linux.go rename pkg/{securitypolicy/pspdriver.go => amdsevsnp/report_windows.go} (60%) create mode 100644 pkg/amdsevsnp/validate.go diff --git a/cmd/gcs-sidecar/main.go b/cmd/gcs-sidecar/main.go index 1925621145..4a23c93c28 100644 --- a/cmd/gcs-sidecar/main.go +++ b/cmd/gcs-sidecar/main.go @@ -15,6 +15,7 @@ import ( "github.com/Microsoft/hcsshim/internal/gcs/prot" shimlog "github.com/Microsoft/hcsshim/internal/log" "github.com/Microsoft/hcsshim/internal/oc" + "github.com/Microsoft/hcsshim/pkg/amdsevsnp" "github.com/Microsoft/hcsshim/pkg/securitypolicy" "github.com/sirupsen/logrus" "go.opencensus.io/trace" @@ -216,7 +217,7 @@ func main() { return } - if err := securitypolicy.StartPSPDriver(ctx); err != nil { + if err := amdsevsnp.StartPSPDriver(ctx); err != nil { // When error happens, pspdriver.GetPspDriverError() returns true. // In that case, gcs-sidecar should keep the initial "deny" policy // and reject all requests from the host. diff --git a/pkg/amdsevsnp/report.go b/pkg/amdsevsnp/report.go index e296a7eeb0..e1a3fa000e 100644 --- a/pkg/amdsevsnp/report.go +++ b/pkg/amdsevsnp/report.go @@ -1,76 +1,11 @@ -//go:build linux -// +build linux - package amdsevsnp import ( "bytes" "encoding/binary" "encoding/hex" - "errors" - "fmt" - "os" - "unsafe" - - "github.com/Microsoft/hcsshim/internal/guest/linux" -) - -const ( - msgTypeInvalid = iota - msgCPUIDRequest - msgCPUIDResponse - msgKeyRequest - msgKeyResponse - msgReportRequest - msgReportResponse - msgExportRequest - msgExportResponse - msgImportRequest - msgImportResponse - msgAbsorbRequest - msgAbsorbResponse - msgVMRKRequest - msgVMRKResponse - msgTypeMax -) - -type guestRequest5 struct { - RequestMsgType byte - ResponseMsgType byte - MsgVersion byte - RequestLength uint16 - RequestUAddr unsafe.Pointer - ResponseLength uint16 - ResponseUAddr unsafe.Pointer - Error uint32 -} - -type guestRequest6 struct { - MsgVersion byte - RequestData unsafe.Pointer - ResponseData unsafe.Pointer - Error uint64 -} - -// AMD SEV ioctl definitions for kernel 5.x. -const ( - snpGetReportIoctlCode5 = 3223868161 ) -// AMD SEV ioctl definitions for kernel 6.x. -const ( - snpGetReportIoctlCode6 = 3223343872 -) - -// reportRequest used to issue SEV-SNP request -// https://www.amd.com/system/files/TechDocs/56860.pdf -// MSG_REPORT_REQ Table 20. -type reportRequest struct { - ReportData [64]byte - VMPL uint32 - _ [28]byte -} - // report is an internal representation of SEV-SNP report // https://www.amd.com/system/files/TechDocs/56860.pdf // ATTESTATION_REPORT Table 21. @@ -131,133 +66,6 @@ func (sr *report) report() Report { } } -// reportResponse is the attestation response struct -// https://www.amd.com/system/files/TechDocs/56860.pdf -// MSG_REPORT_RSP Table 23. -// NOTE: reportResponse.Report is a byte slice, to have the original -// response in bytes. The conversion to internal struct happens inside -// convertRawReport. -// -// NOTE: the additional 64 bytes are reserved, without them, the ioctl fails. -type reportResponse struct { - Status uint32 - ReportSize uint32 - reserved1 [24]byte - Report [1184]byte - reserved2 [64]byte // padding to the size of SEV_SNP_REPORT_RSP_BUF_SZ (i.e., 1280 bytes) -} - -// Size of `snp_report_resp` in include/uapi/linux/sev-guest.h. -// It's used only for Linux 6.x. -// It will have the conteints of reportResponse in the first unsafe.Sizeof(reportResponse{}) bytes. -const reportResponseContainerLength6 = 4000 - -const snpDevicePath5 = "/dev/sev" -const snpDevicePath6 = "/dev/sev-guest" - -func IsSNP() bool { - return isSNPVM5() || isSNPVM6() -} - -// Check if the code is being run in SNP VM for Linux kernel version 5.x. -func isSNPVM5() bool { - _, err := os.Stat(snpDevicePath5) - return !errors.Is(err, os.ErrNotExist) -} - -// Check if the code is being run in SNP VM for Linux kernel version 6.x. -func isSNPVM6() bool { - _, err := os.Stat(snpDevicePath6) - return !errors.Is(err, os.ErrNotExist) -} - -// FetchRawSNPReport returns attestation report bytes. -func FetchRawSNPReport(reportData []byte) ([]byte, error) { - if isSNPVM5() { - return fetchRawSNPReport5(reportData) - } else if isSNPVM6() { - return fetchRawSNPReport6(reportData) - } else { - return nil, fmt.Errorf("SEV device is not found") - } -} - -func fetchRawSNPReport5(reportData []byte) ([]byte, error) { - f, err := os.OpenFile(snpDevicePath5, os.O_RDWR, 0) - if err != nil { - return nil, err - } - defer func() { - _ = f.Close() - }() - - var ( - msgReportIn reportRequest - msgReportOut reportResponse - ) - - if reportData != nil { - if len(reportData) > len(msgReportIn.ReportData) { - return nil, fmt.Errorf("reportData too large: %s", reportData) - } - copy(msgReportIn.ReportData[:], reportData) - } - - payload := &guestRequest5{ - RequestMsgType: msgReportRequest, - ResponseMsgType: msgReportResponse, - MsgVersion: 1, - RequestLength: uint16(unsafe.Sizeof(msgReportIn)), - RequestUAddr: unsafe.Pointer(&msgReportIn), - ResponseLength: uint16(unsafe.Sizeof(msgReportOut)), - ResponseUAddr: unsafe.Pointer(&msgReportOut), - Error: 0, - } - - if err := linux.Ioctl(f, snpGetReportIoctlCode5, unsafe.Pointer(payload)); err != nil { - return nil, err - } - return msgReportOut.Report[:], nil -} - -func fetchRawSNPReport6(reportData []byte) ([]byte, error) { - f, err := os.OpenFile(snpDevicePath6, os.O_RDWR, 0) - if err != nil { - return nil, err - } - defer func() { - _ = f.Close() - }() - - var ( - msgReportIn reportRequest - msgReportOut reportResponse - ) - reportOutContainer := [reportResponseContainerLength6]byte{} - - if reportData != nil { - if len(reportData) > len(msgReportIn.ReportData) { - return nil, fmt.Errorf("reportData too large: %s", reportData) - } - copy(msgReportIn.ReportData[:], reportData) - } - - payload := &guestRequest6{ - MsgVersion: 1, - RequestData: unsafe.Pointer(&msgReportIn), - ResponseData: unsafe.Pointer(&reportOutContainer), - Error: 0, - } - - if err := linux.Ioctl(f, snpGetReportIoctlCode6, unsafe.Pointer(payload)); err != nil { - return nil, err - } - - msgReportOut = *(*reportResponse)(unsafe.Pointer(&reportOutContainer[0])) - - return msgReportOut.Report[:], nil -} - // Report represents parsed attestation report. type Report struct { Version uint32 diff --git a/pkg/amdsevsnp/report_linux.go b/pkg/amdsevsnp/report_linux.go new file mode 100644 index 0000000000..7c7f398795 --- /dev/null +++ b/pkg/amdsevsnp/report_linux.go @@ -0,0 +1,213 @@ +//go:build linux +// +build linux + +package amdsevsnp + +import ( + "errors" + "fmt" + "os" + "unsafe" + + "github.com/Microsoft/hcsshim/internal/guest/linux" +) + +const ( + msgTypeInvalid = iota + msgCPUIDRequest + msgCPUIDResponse + msgKeyRequest + msgKeyResponse + msgReportRequest + msgReportResponse + msgExportRequest + msgExportResponse + msgImportRequest + msgImportResponse + msgAbsorbRequest + msgAbsorbResponse + msgVMRKRequest + msgVMRKResponse + msgTypeMax +) + +// AMD SEV ioctl definitions for kernel 5.x. +const ( + snpGetReportIoctlCode5 = 3223868161 +) + +// AMD SEV ioctl definitions for kernel 6.x. +const ( + snpGetReportIoctlCode6 = 3223343872 +) + +// reportRequest used to issue SEV-SNP request +// https://www.amd.com/system/files/TechDocs/56860.pdf +// MSG_REPORT_REQ Table 20. +type reportRequest struct { + ReportData [64]byte + VMPL uint32 + _ [28]byte +} + +// reportResponse is the attestation response struct +// https://www.amd.com/system/files/TechDocs/56860.pdf +// MSG_REPORT_RSP Table 23. +// NOTE: reportResponse.Report is a byte slice, to have the original +// response in bytes. The conversion to internal struct happens inside +// convertRawReport. +// +// NOTE: the additional 64 bytes are reserved, without them, the ioctl fails. +type reportResponse struct { + Status uint32 + ReportSize uint32 + reserved1 [24]byte + Report [1184]byte + reserved2 [64]byte // padding to the size of SEV_SNP_REPORT_RSP_BUF_SZ (i.e., 1280 bytes) +} + +// Size of `snp_report_resp` in include/uapi/linux/sev-guest.h. +// It's used only for Linux 6.x. +// It will have the conteints of reportResponse in the first unsafe.Sizeof(reportResponse{}) bytes. +const reportResponseContainerLength6 = 4000 + +type guestRequest5 struct { + RequestMsgType byte + ResponseMsgType byte + MsgVersion byte + RequestLength uint16 + RequestUAddr unsafe.Pointer + ResponseLength uint16 + ResponseUAddr unsafe.Pointer + Error uint32 +} + +type guestRequest6 struct { + MsgVersion byte + RequestData unsafe.Pointer + ResponseData unsafe.Pointer + Error uint64 +} + +const snpDevicePath5 = "/dev/sev" +const snpDevicePath6 = "/dev/sev-guest" + +func IsSNP() (bool, error) { + isVM5, err := isSNPVM5() + if err != nil { + return false, err + } + if isVM5 { + return true, nil + } + isVM6, err := isSNPVM6() + if err != nil { + return false, err + } + return isVM6, nil +} + +// Check if the code is being run in SNP VM for Linux kernel version 5.x. +func isSNPVM5() (bool, error) { + _, err := os.Stat(snpDevicePath5) + return !errors.Is(err, os.ErrNotExist), nil +} + +// Check if the code is being run in SNP VM for Linux kernel version 6.x. +func isSNPVM6() (bool, error) { + _, err := os.Stat(snpDevicePath6) + return !errors.Is(err, os.ErrNotExist), nil +} + +// FetchRawSNPReport returns attestation report bytes. +func FetchRawSNPReport(reportData []byte) ([]byte, error) { + isVM5, _ := isSNPVM5() + if isVM5 { + return fetchRawSNPReport5(reportData) + } + isVM6, _ := isSNPVM6() + if isVM6 { + return fetchRawSNPReport6(reportData) + } + return nil, fmt.Errorf("SEV device is not found") +} + +func fetchRawSNPReport5(reportData []byte) ([]byte, error) { + f, err := os.OpenFile(snpDevicePath5, os.O_RDWR, 0) + if err != nil { + return nil, err + } + defer func() { + _ = f.Close() + }() + + var ( + msgReportIn reportRequest + msgReportOut reportResponse + ) + + if reportData != nil { + if len(reportData) > len(msgReportIn.ReportData) { + return nil, fmt.Errorf("reportData too large: %s", reportData) + } + copy(msgReportIn.ReportData[:], reportData) + } + + payload := &guestRequest5{ + RequestMsgType: msgReportRequest, + ResponseMsgType: msgReportResponse, + MsgVersion: 1, + RequestLength: uint16(unsafe.Sizeof(msgReportIn)), + RequestUAddr: unsafe.Pointer(&msgReportIn), + ResponseLength: uint16(unsafe.Sizeof(msgReportOut)), + ResponseUAddr: unsafe.Pointer(&msgReportOut), + Error: 0, + } + + if err := linux.Ioctl(f, snpGetReportIoctlCode5, unsafe.Pointer(payload)); err != nil { + return nil, err + } + return msgReportOut.Report[:], nil +} + +func fetchRawSNPReport6(reportData []byte) ([]byte, error) { + f, err := os.OpenFile(snpDevicePath6, os.O_RDWR, 0) + if err != nil { + return nil, err + } + defer func() { + _ = f.Close() + }() + + var ( + msgReportIn reportRequest + msgReportOut reportResponse + ) + reportOutContainer := [reportResponseContainerLength6]byte{} + + if reportData != nil { + if len(reportData) > len(msgReportIn.ReportData) { + return nil, fmt.Errorf("reportData too large: %s", reportData) + } + copy(msgReportIn.ReportData[:], reportData) + } + + payload := &guestRequest6{ + MsgVersion: 1, + RequestData: unsafe.Pointer(&msgReportIn), + ResponseData: unsafe.Pointer(&reportOutContainer), + Error: 0, + } + + if err := linux.Ioctl(f, snpGetReportIoctlCode6, unsafe.Pointer(payload)); err != nil { + return nil, err + } + + msgReportOut = *(*reportResponse)(unsafe.Pointer(&reportOutContainer[0])) + + return msgReportOut.Report[:], nil +} + +func CheckDriverError() error { + return nil +} diff --git a/pkg/amdsevsnp/report_test.go b/pkg/amdsevsnp/report_test.go index e42af6de1f..d1949121b0 100644 --- a/pkg/amdsevsnp/report_test.go +++ b/pkg/amdsevsnp/report_test.go @@ -1,6 +1,3 @@ -//go:build linux -// +build linux - package amdsevsnp import ( diff --git a/pkg/securitypolicy/pspdriver.go b/pkg/amdsevsnp/report_windows.go similarity index 60% rename from pkg/securitypolicy/pspdriver.go rename to pkg/amdsevsnp/report_windows.go index 4a04805be3..ed5bbd8f01 100644 --- a/pkg/securitypolicy/pspdriver.go +++ b/pkg/amdsevsnp/report_windows.go @@ -1,13 +1,11 @@ //go:build windows // +build windows -package securitypolicy +package amdsevsnp import ( "bytes" "context" - "encoding/binary" - "encoding/hex" "fmt" "time" @@ -44,107 +42,6 @@ type SNPPSPGuestRequestResult struct { PspStatus uint64 } -// Type used by FetchParsedSNPReport. -// This it converted to the public type `Report` -// by `func (sr *report) report() Report`. -type report struct { - Version uint32 - GuestSVN uint32 - Policy uint64 - FamilyID [16]byte - ImageID [16]byte - VMPL uint32 - SignatureAlgo uint32 - PlatformVersion uint64 - PlatformInfo uint64 - AuthorKeyEn uint32 - Reserved1 uint32 - ReportData [SnpPspReportDataSize]byte - Measurement [48]byte - HostData [SnpPspReportHostDataSize]byte - IDKeyDigest [48]byte - AuthorKeyDigest [48]byte - ReportID [32]byte - ReportIDMA [32]byte - ReportTCB uint64 - Reserved2 [24]byte - ChipID [64]byte - CommittedSVN [8]byte - CommittedVersion [8]byte - LaunchSVN [8]byte - Reserved3 [168]byte - Signature [512]byte -} - -// Report represents parsed attestation report. -// Fields with string type is hex-encoded values of the corresponding byte arrays. -// Based on Table 23 of 'SEV-ES Guest-Hypervisor Communication Block Standardization'. -// -// https://www.amd.com/content/dam/amd/en/documents/epyc-technical-docs/specifications/56421.pdf -type Report struct { - Version uint32 - GuestSVN uint32 - Policy uint64 - FamilyID string - ImageID string - VMPL uint32 - SignatureAlgo uint32 - PlatformVersion uint64 - PlatformInfo uint64 - AuthorKeyEn uint32 - ReportData string - Measurement string - HostData []byte - IDKeyDigest string - AuthorKeyDigest string - ReportID string - ReportIDMA string - ReportTCB uint64 - ChipID string - CommittedSVN string - CommittedVersion string - LaunchSVN string - Signature string -} - -func (sr *report) report() Report { - return Report{ - Version: sr.Version, - GuestSVN: sr.GuestSVN, - Policy: sr.Policy, - FamilyID: hex.EncodeToString(mirrorBytes(sr.FamilyID[:])[:]), - ImageID: hex.EncodeToString(mirrorBytes(sr.ImageID[:])[:]), - VMPL: sr.VMPL, - SignatureAlgo: sr.SignatureAlgo, - PlatformVersion: sr.PlatformVersion, - PlatformInfo: sr.PlatformInfo, - AuthorKeyEn: sr.AuthorKeyEn, - ReportData: hex.EncodeToString(sr.ReportData[:]), - Measurement: hex.EncodeToString(sr.Measurement[:]), - HostData: sr.HostData[:], - IDKeyDigest: hex.EncodeToString(sr.IDKeyDigest[:]), - AuthorKeyDigest: hex.EncodeToString(sr.AuthorKeyDigest[:]), - ReportID: hex.EncodeToString(sr.ReportID[:]), - ReportIDMA: hex.EncodeToString(sr.ReportIDMA[:]), - ReportTCB: sr.ReportTCB, - ChipID: hex.EncodeToString(sr.ChipID[:]), - CommittedSVN: hex.EncodeToString(sr.CommittedSVN[:]), - CommittedVersion: hex.EncodeToString(sr.CommittedVersion[:]), - LaunchSVN: hex.EncodeToString(sr.LaunchSVN[:]), - Signature: hex.EncodeToString(sr.Signature[:]), - } -} - -// mirrorBytes mirrors the byte ordering so that hex-encoding little endian -// ordered bytes come out in the readable order. -func mirrorBytes(b []byte) []byte { - for i := 0; i < len(b)/2; i++ { - mirrorIndex := len(b) - i - 1 - b[i], b[mirrorIndex] = b[mirrorIndex], b[i] - } - return b -} - var ( pspDriverStarted = false // The error needs to be stored to be retrieved later. @@ -212,12 +109,12 @@ func IsPspDriverStarted() bool { // Return an error from the PSP driver dll // when it fails to use the dll at all. // Otherwise it returns nil. -func GetPspDriverError() error { +func CheckDriverError() error { return pspDriverError } -// IsSNPMode() returns true if it's in SNP mode. -func IsSNPMode() (bool, error) { +// IsSNP() returns true if it's in SNP mode. +func IsSNP() (bool, error) { if pspDriverError != nil { return false, pspDriverError @@ -290,26 +187,11 @@ func FetchRawSNPReport(reportData []byte) ([]byte, error) { return report[:], nil } -// FetchParsedSNPReport parses raw attestation response into proper structs. -func FetchParsedSNPReport(reportData []byte) (Report, error) { - rawBytes, err := FetchRawSNPReport(reportData) - if err != nil { - return Report{}, err - } - - var r report - buf := bytes.NewBuffer(rawBytes) - if err := binary.Read(buf, binary.LittleEndian, &r); err != nil { - return Report{}, err - } - return r.report(), nil -} - // ValidateHostData fetches SNP report (if applicable) and validates `hostData` against // HostData set at UVM launch. func ValidateHostDataPSP(hostData []byte) error { // If the UVM is not SNP, then don't try to fetch an SNP report. - isSnpMode, err := IsSNPMode() + isSnpMode, err := IsSNP() if err != nil { return err } diff --git a/pkg/amdsevsnp/validate.go b/pkg/amdsevsnp/validate.go new file mode 100644 index 0000000000..1566f8f018 --- /dev/null +++ b/pkg/amdsevsnp/validate.go @@ -0,0 +1,39 @@ +package amdsevsnp + +import ( + "bytes" + "fmt" + + "github.com/pkg/errors" +) + +// validateHostData fetches SNP report (if applicable) and validates `hostData` against +// HostData set at UVM launch. +func ValidateHostData(hostData []byte) error { + + if err := CheckDriverError(); err != nil { + // For this case gcs-sidecar will keep initial deny policy. + return errors.Wrapf(err, "an error occurred while using PSP driver") + } + // If the UVM is not SNP, then don't try to fetch an SNP report. + isSNP, err := IsSNP() + if err != nil { + return err + } + if !isSNP { + return nil + } + report, err := FetchParsedSNPReport(nil) + if err != nil { + return err + } + + if !bytes.Equal(hostData, report.HostData) { + return fmt.Errorf( + "security policy digest %q doesn't match HostData provided at launch %q", + hostData, + report.HostData, + ) + } + return nil +} diff --git a/pkg/securitypolicy/securitypolicy_linux.go b/pkg/securitypolicy/securitypolicy_linux.go index 278038ac67..54f8972de4 100644 --- a/pkg/securitypolicy/securitypolicy_linux.go +++ b/pkg/securitypolicy/securitypolicy_linux.go @@ -4,14 +4,12 @@ package securitypolicy import ( - "bytes" "fmt" "os" "path/filepath" "strconv" specInternal "github.com/Microsoft/hcsshim/internal/guest/spec" - "github.com/Microsoft/hcsshim/pkg/amdsevsnp" "github.com/moby/sys/user" oci "github.com/opencontainers/runtime-spec/specs-go" "github.com/pkg/errors" @@ -20,28 +18,6 @@ import ( //nolint:unused const osType = "linux" -// validateHostData fetches SNP report (if applicable) and validates `hostData` against -// HostData set at UVM launch. -func validateHostData(hostData []byte) error { - // If the UVM is not SNP, then don't try to fetch an SNP report. - if !amdsevsnp.IsSNP() { - return nil - } - report, err := amdsevsnp.FetchParsedSNPReport(nil) - if err != nil { - return err - } - - if !bytes.Equal(hostData, report.HostData) { - return fmt.Errorf( - "security policy digest %q doesn't match HostData provided at launch %q", - hostData, - report.HostData, - ) - } - return nil -} - func ExtendPolicyWithNetworkingMounts(sandboxID string, enforcer SecurityPolicyEnforcer, spec *oci.Spec) error { roSpec := &oci.Spec{ Root: spec.Root, diff --git a/pkg/securitypolicy/securitypolicy_options.go b/pkg/securitypolicy/securitypolicy_options.go index b2b469e0bb..dbebb58239 100644 --- a/pkg/securitypolicy/securitypolicy_options.go +++ b/pkg/securitypolicy/securitypolicy_options.go @@ -15,6 +15,7 @@ import ( didx509resolver "github.com/Microsoft/didx509go/pkg/did-x509-resolver" "github.com/Microsoft/hcsshim/internal/log" "github.com/Microsoft/hcsshim/internal/protocol/guestresource" + "github.com/Microsoft/hcsshim/pkg/amdsevsnp" "github.com/Microsoft/hcsshim/pkg/annotations" "github.com/opencontainers/runtime-spec/specs-go" "github.com/pkg/errors" @@ -58,7 +59,7 @@ func (s *SecurityOptions) SetConfidentialOptions(ctx context.Context, enforcerTy return err } - if err := validateHostData(hostData[:]); err != nil { + if err := amdsevsnp.ValidateHostData(hostData[:]); err != nil { return err } diff --git a/pkg/securitypolicy/securitypolicy_windows.go b/pkg/securitypolicy/securitypolicy_windows.go index 1582a756af..cbb6b731c8 100644 --- a/pkg/securitypolicy/securitypolicy_windows.go +++ b/pkg/securitypolicy/securitypolicy_windows.go @@ -5,27 +5,11 @@ package securitypolicy import ( oci "github.com/opencontainers/runtime-spec/specs-go" - "github.com/pkg/errors" ) //nolint:unused const osType = "windows" -// validateHostData fetches SNP report (if applicable) and validates `hostData` against -// HostData set at UVM launch. -func validateHostData(hostData []byte) error { - if err := GetPspDriverError(); err != nil { - // For this case gcs-sidecar will keep initial deny policy. - return errors.Wrapf(err, "an error occurred while using PSP driver") - } - - if err := ValidateHostDataPSP(hostData[:]); err != nil { - // For this case gcs-sidecar will keep initial deny policy. - return err - } - return nil -} - // SandboxMountsDir returns sandbox mounts directory inside UVM/host. func SandboxMountsDir(sandboxID string) string { return "" From 62697f3b7cdbe1085df4919188ad3678bca9d6a0 Mon Sep 17 00:00:00 2001 From: Hamza El-Saawy Date: Mon, 22 Dec 2025 12:08:15 -0500 Subject: [PATCH 9/9] Prevent panic from interface case --- internal/uvm/create.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/uvm/create.go b/internal/uvm/create.go index c991d9ea79..59352f7794 100644 --- a/internal/uvm/create.go +++ b/internal/uvm/create.go @@ -323,8 +323,8 @@ func (uvm *UtilityVM) CloseCtx(ctx context.Context) (err error) { _ = uvm.WaitCtx(ctx) } - if uvm.HasConfidentialPolicy() && uvm.createOpts.(*OptionsLCOW).GuestStateFile != "" { - vmgsFullPath := filepath.Join(uvm.createOpts.(*OptionsLCOW).BundleDirectory, uvm.createOpts.(*OptionsLCOW).GuestStateFile) + if lopts, ok := uvm.createOpts.(*OptionsLCOW); ok && uvm.HasConfidentialPolicy() && lopts.GuestStateFile != "" { + vmgsFullPath := filepath.Join(lopts.BundleDirectory, lopts.GuestStateFile) e := log.G(ctx).WithField("VMGS file", vmgsFullPath) e.Debug("removing VMGS file") if err := os.Remove(vmgsFullPath); err != nil {