Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions internal/annotations/annotations.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,9 @@ const (
// [HCS RegistryValue]: https://learn.microsoft.com/en-us/virtualization/api/hcs/schemareference#registryvalue
AdditionalRegistryValues = "io.microsoft.virtualmachine.wcow.additional-reg-keys"
)

// WCOW container annotations.
const (
// This is for testing and debugging annotations that can be set in the WCOW containers
WCOWAnnotationsTest = "io.microsoft.container.wcow.testannotation"
)
89 changes: 89 additions & 0 deletions internal/gcs-sidecar/default_registry_values.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
//go:build windows
// +build windows

package bridge

import (
"math"
"strconv"

hcsschema "github.com/Microsoft/hcsshim/internal/hcs/schema2"
)

// DefaultRegistryValues contains the registry values that are always allowed
// without requiring policy validation. These are common system settings needed
// for proper UVM operation.
var DefaultRegistryValues = []hcsschema.RegistryValue{
{
Key: &hcsschema.RegistryKey{
Hive: hcsschema.RegistryHive_SYSTEM,
Name: "ControlSet001\\Control",
},
Name: "WaitToKillServiceTimeout",
StringValue: strconv.Itoa(math.MaxInt32),
Type_: hcsschema.RegistryValueType_STRING,
},
}

// isDefaultRegistryValue checks if the given registry value matches one of the default allowed values
func isDefaultRegistryValue(value hcsschema.RegistryValue) bool {
for _, defaultVal := range DefaultRegistryValues {
if registryValuesMatch(defaultVal, value) {
return true
}
}
return false
}

// registryValuesMatch checks if two registry values are equivalent
func registryValuesMatch(a, b hcsschema.RegistryValue) bool {
// Check if keys match
if !registryKeysMatch(a.Key, b.Key) {
return false
}

// Check if names match
if a.Name != b.Name {
return false
}

// Check if types match
if a.Type_ != b.Type_ {
return false
}

// Check type-specific values
switch a.Type_ {
case hcsschema.RegistryValueType_STRING:
return a.StringValue == b.StringValue
case hcsschema.RegistryValueType_EXPANDED_STRING:
return a.StringValue == b.StringValue
case hcsschema.RegistryValueType_MULTI_STRING:
return a.StringValue == b.StringValue
case hcsschema.RegistryValueType_D_WORD:
return a.DWordValue == b.DWordValue
case hcsschema.RegistryValueType_Q_WORD:
return a.QWordValue == b.QWordValue
case hcsschema.RegistryValueType_BINARY:
return a.BinaryValue == b.BinaryValue
case hcsschema.RegistryValueType_CUSTOM_TYPE:
// For CustomType, both CustomType field and BinaryValue must match
return a.CustomType == b.CustomType && a.BinaryValue == b.BinaryValue
case hcsschema.RegistryValueType_NONE:
// NONE type has no value to compare
return true
default:
return false
}
}

// registryKeysMatch checks if two registry keys are equivalent
func registryKeysMatch(a, b *hcsschema.RegistryKey) bool {
if a == nil && b == nil {
return true
}
if a == nil || b == nil {
return false
}
return a.Hive == b.Hive && a.Name == b.Name && a.Volatile == b.Volatile
}
39 changes: 39 additions & 0 deletions internal/gcs-sidecar/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,45 @@ func (b *Bridge) createContainer(req *request) (err error) {
containerID := createContainerRequest.ContainerID
log.G(ctx).Tracef("rpcCreate: CWCOWHostedSystemConfig {spec: %v, schemaVersion: %v, container: %v}}", string(req.message), schemaVersion, container)

// Enforce registry changes policy
if container != nil && container.RegistryChanges != nil {
log.G(ctx).Trace("Container has registry changes, validating against policy")

// First, separate default values from non-default values
var defaultValues []hcsschema.RegistryValue
var nonDefaultValues []hcsschema.RegistryValue

if container.RegistryChanges.AddValues != nil {
for _, value := range container.RegistryChanges.AddValues {
if isDefaultRegistryValue(value) {
defaultValues = append(defaultValues, value)
log.G(ctx).WithField("name", value.Name).Trace("Registry value matches default, accepting without policy check")
} else {
nonDefaultValues = append(nonDefaultValues, value)
}
}
}

// If there are non-default values, validate them against policy
if len(nonDefaultValues) > 0 {
log.G(ctx).Tracef("Validating %d registry values against policy", len(nonDefaultValues))

nonDefaultChanges := &hcsschema.RegistryChanges{
AddValues: nonDefaultValues,
}

err := b.hostState.securityOptions.PolicyEnforcer.EnforceRegistryChangesPolicy(ctx, containerID, nonDefaultChanges)
if err != nil {
log.G(ctx).WithError(err).Warn("Registry changes validation failed - rejecting")
return fmt.Errorf("registry entry operation is denied by policy: %w", err)
}
log.G(ctx).Tracef("All container registry values validated successfully")
}

log.G(ctx).Infof("Registry validation complete: %d total values (%d defaults + %d validated)",
len(container.RegistryChanges.AddValues), len(defaultValues), len(nonDefaultValues))
}

user := securitypolicy.IDName{
Name: spec.Process.User.Username,
}
Expand Down
9 changes: 9 additions & 0 deletions internal/hcsoci/hcsdoc_wcow.go
Original file line number Diff line number Diff line change
Expand Up @@ -487,6 +487,15 @@ func createWindowsContainerDocument(ctx context.Context, coi *createOptionsInter
}...)
}

// Parse and add test annotation registry values if present (for testing/debugging)
testAnnotationValues := oci.ParseTestAnnotationRegistryValues(ctx, coi.Spec.Annotations)
if len(testAnnotationValues) > 0 {
log.G(ctx).WithField("count", len(testAnnotationValues)).Info("adding test annotation registry values to container")
registryAdd = append(registryAdd, testAnnotationValues...)
} else {
log.G(ctx).Debug("no test annotation registry values found in container annotations")
}

v2Container.RegistryChanges = &hcsschema.RegistryChanges{
AddValues: registryAdd,
}
Expand Down
15 changes: 13 additions & 2 deletions internal/oci/annotations.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,12 +90,17 @@ func ParseAnnotationsDisableGMSA(ctx context.Context, s *specs.Spec) bool {
//
// Like the [parseAnnotation*] functions, this logs errors but does not return them.
func parseAdditionalRegistryValues(ctx context.Context, a map[string]string) []hcsschema.RegistryValue {
return parseRegistryValues(ctx, a, iannotations.AdditionalRegistryValues)
}

// parseRegistryValues is a generic function to parse registry values from annotations.
func parseRegistryValues(ctx context.Context, a map[string]string, annotationKey string) []hcsschema.RegistryValue {
// rather than have users deal with nil vs []hcsschema.RegistryValue as returns, always
// return the latter.
// this is mostly to make testing easier, since its awkward to have to differentiate between
// situations where one is returned vs the other.

k := iannotations.AdditionalRegistryValues
k := annotationKey
v := a[k]
if v == "" {
return []hcsschema.RegistryValue{}
Expand Down Expand Up @@ -204,7 +209,13 @@ func parseAdditionalRegistryValues(ctx context.Context, a map[string]string) []h
return slices.Clip(rvs)
}

// ParseHVSocketServiceTable extracts any additional Hyper-V socket service configurations from annotations.
// ParseTestAnnotationRegistryValues extracts registry values from the WCOW test annotation.
// This is for testing and debugging purposes only.
func ParseTestAnnotationRegistryValues(ctx context.Context, a map[string]string) []hcsschema.RegistryValue {
return parseRegistryValues(ctx, a, iannotations.WCOWAnnotationsTest)
}

// parseHVSocketServiceTable extracts any additional Hyper-V socket service configurations from annotations.
//
// Like the [parseAnnotation*] functions, this logs errors but does not return them.
func ParseHVSocketServiceTable(ctx context.Context, a map[string]string) map[string]hcsschema.HvSocketServiceConfig {
Expand Down
3 changes: 2 additions & 1 deletion pkg/securitypolicy/api.rego
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ version := "@@API_VERSION@@"
enforcement_points := {
"mount_device": {"introducedVersion": "0.1.0", "default_results": {"allowed": false}},
"mount_overlay": {"introducedVersion": "0.1.0", "default_results": {"allowed": false}},
"mount_cims": {"introducedVersion": "0.11.0", "default_results": {"allowed": false}},
"mount_cims": {"introducedVersion": "0.11.0", "default_results": {"allowed": false}},
"registry_changes": {"introducedVersion": "0.10.0", "default_results": {"allowed": false}},
"create_container": {"introducedVersion": "0.1.0", "default_results": {"allowed": false, "env_list": null, "allow_stdio_access": false}},
"unmount_device": {"introducedVersion": "0.2.0", "default_results": {"allowed": true}},
"unmount_overlay": {"introducedVersion": "0.6.0", "default_results": {"allowed": true}},
Expand Down
110 changes: 110 additions & 0 deletions pkg/securitypolicy/framework.rego
Original file line number Diff line number Diff line change
Expand Up @@ -1231,6 +1231,116 @@ scratch_unmount := {"metadata": [remove_scratch_mount], "allowed": true} {
}
}

# Registry changes validation
default registry_changes := {"allowed": false}

# Helper function to compare registry keys
registry_keys_match(policy_key, input_key) {
policy_key.hive == input_key.Hive
policy_key.name == input_key.Name
# Volatile field comparison (default to false if not specified)
policy_volatile := object.get(policy_key, "volatile", false)
input_volatile := object.get(input_key, "Volatile", false)
policy_volatile == input_volatile
}

# Helper function to compare registry values
# STRING type
registry_value_matches(policy_value, input_value) {
registry_keys_match(policy_value.key, input_value.Key)
policy_value.name == input_value.Name
policy_value.type == input_value.Type_
policy_value.type == "STRING"
policy_value.string_value == input_value.StringValue
}

# EXPANDED_STRING type (uses StringValue field)
registry_value_matches(policy_value, input_value) {
registry_keys_match(policy_value.key, input_value.Key)
policy_value.name == input_value.Name
policy_value.type == input_value.Type_
policy_value.type == "EXPANDED_STRING"
policy_value.string_value == input_value.StringValue
}

# MULTI_STRING type (uses StringValue field)
registry_value_matches(policy_value, input_value) {
registry_keys_match(policy_value.key, input_value.Key)
policy_value.name == input_value.Name
policy_value.type == input_value.Type_
policy_value.type == "MULTI_STRING"
policy_value.string_value == input_value.StringValue
}

# D_WORD type
registry_value_matches(policy_value, input_value) {
registry_keys_match(policy_value.key, input_value.Key)
policy_value.name == input_value.Name
policy_value.type == input_value.Type_
policy_value.type == "D_WORD"
policy_value.dword_value == input_value.DWordValue
}

# Q_WORD type
registry_value_matches(policy_value, input_value) {
registry_keys_match(policy_value.key, input_value.Key)
policy_value.name == input_value.Name
policy_value.type == input_value.Type_
policy_value.type == "Q_WORD"
policy_value.qword_value == input_value.QWordValue
}

# BINARY type
registry_value_matches(policy_value, input_value) {
registry_keys_match(policy_value.key, input_value.Key)
policy_value.name == input_value.Name
policy_value.type == input_value.Type_
policy_value.type == "BINARY"
policy_value.binary_value == input_value.BinaryValue
}

# CUSTOM_TYPE - both CustomType field and BinaryValue must match
registry_value_matches(policy_value, input_value) {
registry_keys_match(policy_value.key, input_value.Key)
policy_value.name == input_value.Name
policy_value.type == input_value.Type_
policy_value.type == "CUSTOM_TYPE"
policy_value.custom_type == input_value.CustomType
policy_value.binary_value == input_value.BinaryValue
}

# NONE type - no value to compare, just key and name
registry_value_matches(policy_value, input_value) {
registry_keys_match(policy_value.key, input_value.Key)
policy_value.name == input_value.Name
policy_value.type == input_value.Type_
policy_value.type == "NONE"
}

# Filter input registry values to only include those that match policy
filtered_registry_values(input_values, policy_values) := [input_val |
input_val := input_values[_]
some policy_val in policy_values
registry_value_matches(policy_val, input_val)
]

registry_changes := {"allowed": true} {
containers := data.metadata.matches[input.containerID]
container := containers[_]

# Check if container has registry_changes defined in policy
container.registry_changes

# If input has registry changes, filter to only matching ones
input.registryChanges.AddValues
matched_values := filtered_registry_values(input.registryChanges.AddValues, container.registry_changes.add_values)

# Build result with filtered AddValues
result := {
"AddValues": matched_values
}
}

reason := {
"errors": errors,
"error_objects": error_objects
Expand Down
1 change: 1 addition & 0 deletions pkg/securitypolicy/open_door.rego
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ mount_device := {"allowed": true}
mount_overlay := {"allowed": true}
create_container := {"allowed": true, "env_list": null, "allow_stdio_access": true}
mount_cims := {"allowed": true}
registry_changes := {"allowed": true}
unmount_device := {"allowed": true}
unmount_overlay := {"allowed": true}
exec_in_container := {"allowed": true, "env_list": null}
Expand Down
1 change: 1 addition & 0 deletions pkg/securitypolicy/policy.rego
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ unmount_device := data.framework.unmount_device
mount_overlay := data.framework.mount_overlay
unmount_overlay := data.framework.unmount_overlay
mount_cims:= data.framework.mount_cims
registry_changes := data.framework.registry_changes
create_container := data.framework.create_container
exec_in_container := data.framework.exec_in_container
exec_external := data.framework.exec_external
Expand Down
Loading