Skip to content

Commit dd4b213

Browse files
authored
CWCOW: Enforce registry entries on containers (microsoft#2611)
* CWCOW: Enforce registry entries on containers Signed-off-by: Mahati Chamarthy <mahati.chamarthy@gmail.com> * CWCOW: Use builtin go APIs Signed-off-by: Mahati Chamarthy <mahati.chamarthy@gmail.com> * CWCOW: Rename registry test annotation Signed-off-by: Mahati Chamarthy <mahati.chamarthy@gmail.com> --------- Signed-off-by: Mahati Chamarthy <mahati.chamarthy@gmail.com>
1 parent 4612d59 commit dd4b213

12 files changed

Lines changed: 381 additions & 3 deletions

internal/annotations/annotations.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,3 +67,9 @@ const (
6767
// [HCS RegistryValue]: https://learn.microsoft.com/en-us/virtualization/api/hcs/schemareference#registryvalue
6868
AdditionalRegistryValues = "io.microsoft.virtualmachine.wcow.additional-reg-keys"
6969
)
70+
71+
// WCOW container annotations.
72+
const (
73+
// This is for testing and debugging registry entries that can be set in the WCOW containers
74+
WCOWRegistryAnnotationTest = "io.microsoft.container.wcow.test-registry-annotation"
75+
)
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
//go:build windows
2+
// +build windows
3+
4+
package bridge
5+
6+
import (
7+
"math"
8+
"reflect"
9+
"slices"
10+
"strconv"
11+
12+
hcsschema "github.com/Microsoft/hcsshim/internal/hcs/schema2"
13+
)
14+
15+
// DefaultRegistryValues contains the registry values that are always allowed
16+
// without requiring policy validation. These are common system settings needed
17+
// for proper UVM operation.
18+
var defaultRegistryValues = []hcsschema.RegistryValue{
19+
{
20+
Key: &hcsschema.RegistryKey{
21+
Hive: hcsschema.RegistryHive_SYSTEM,
22+
Name: "ControlSet001\\Control",
23+
},
24+
Name: "WaitToKillServiceTimeout",
25+
StringValue: strconv.Itoa(math.MaxInt32),
26+
Type_: hcsschema.RegistryValueType_STRING,
27+
},
28+
}
29+
30+
// isDefaultRegistryValue checks if the given registry value matches one of the default allowed values
31+
func isDefaultRegistryValue(value hcsschema.RegistryValue) bool {
32+
return slices.ContainsFunc(defaultRegistryValues, func(rv hcsschema.RegistryValue) bool {
33+
return registryValuesMatch(rv, value)
34+
})
35+
}
36+
37+
// registryValuesMatch checks if two registry values are equivalent.
38+
// Assumes registry values are well-formed (only relevant value fields are populated for each Type_).
39+
func registryValuesMatch(a, b hcsschema.RegistryValue) bool {
40+
return reflect.DeepEqual(a, b)
41+
}

internal/gcs-sidecar/handlers.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,45 @@ func (b *Bridge) createContainer(req *request) (err error) {
8282
containerID := createContainerRequest.ContainerID
8383
log.G(ctx).Tracef("rpcCreate: CWCOWHostedSystemConfig {spec: %v, schemaVersion: %v, container: %v}}", string(req.message), schemaVersion, container)
8484

85+
// Enforce registry changes policy
86+
if container != nil && container.RegistryChanges != nil {
87+
log.G(ctx).Trace("Container has registry changes, validating against policy")
88+
89+
// First, separate default values from non-default values
90+
var defaultValues []hcsschema.RegistryValue
91+
var nonDefaultValues []hcsschema.RegistryValue
92+
93+
if container.RegistryChanges.AddValues != nil {
94+
for _, value := range container.RegistryChanges.AddValues {
95+
if isDefaultRegistryValue(value) {
96+
defaultValues = append(defaultValues, value)
97+
log.G(ctx).WithField("name", value.Name).Trace("Registry value matches default, accepting without policy check")
98+
} else {
99+
nonDefaultValues = append(nonDefaultValues, value)
100+
}
101+
}
102+
}
103+
104+
// If there are non-default values, validate them against policy
105+
if len(nonDefaultValues) > 0 {
106+
log.G(ctx).Tracef("Validating %d registry values against policy", len(nonDefaultValues))
107+
108+
nonDefaultChanges := &hcsschema.RegistryChanges{
109+
AddValues: nonDefaultValues,
110+
}
111+
112+
err := b.hostState.securityOptions.PolicyEnforcer.EnforceRegistryChangesPolicy(ctx, containerID, nonDefaultChanges)
113+
if err != nil {
114+
log.G(ctx).WithError(err).Warn("Registry changes validation failed - rejecting")
115+
return fmt.Errorf("registry entry operation is denied by policy: %w", err)
116+
}
117+
log.G(ctx).Tracef("All container registry values validated successfully")
118+
}
119+
120+
log.G(ctx).Infof("Registry validation complete: %d total values (%d defaults + %d validated)",
121+
len(container.RegistryChanges.AddValues), len(defaultValues), len(nonDefaultValues))
122+
}
123+
85124
user := securitypolicy.IDName{
86125
Name: spec.Process.User.Username,
87126
}

internal/hcsoci/hcsdoc_wcow.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -487,6 +487,15 @@ func createWindowsContainerDocument(ctx context.Context, coi *createOptionsInter
487487
}...)
488488
}
489489

490+
// Parse and add test annotation registry values if present (for testing/debugging)
491+
testAnnotationValues := oci.ParseTestAnnotationRegistryValues(ctx, coi.Spec.Annotations)
492+
if len(testAnnotationValues) > 0 {
493+
log.G(ctx).WithField("count", len(testAnnotationValues)).Info("adding test annotation registry values to container")
494+
registryAdd = append(registryAdd, testAnnotationValues...)
495+
} else {
496+
log.G(ctx).Debug("no test annotation registry values found in container annotations")
497+
}
498+
490499
v2Container.RegistryChanges = &hcsschema.RegistryChanges{
491500
AddValues: registryAdd,
492501
}

internal/oci/annotations.go

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,12 +90,17 @@ func ParseAnnotationsDisableGMSA(ctx context.Context, s *specs.Spec) bool {
9090
//
9191
// Like the [parseAnnotation*] functions, this logs errors but does not return them.
9292
func parseAdditionalRegistryValues(ctx context.Context, a map[string]string) []hcsschema.RegistryValue {
93+
return parseRegistryValues(ctx, a, iannotations.AdditionalRegistryValues)
94+
}
95+
96+
// parseRegistryValues is a generic function to parse registry values from annotations.
97+
func parseRegistryValues(ctx context.Context, a map[string]string, annotationKey string) []hcsschema.RegistryValue {
9398
// rather than have users deal with nil vs []hcsschema.RegistryValue as returns, always
9499
// return the latter.
95100
// this is mostly to make testing easier, since its awkward to have to differentiate between
96101
// situations where one is returned vs the other.
97102

98-
k := iannotations.AdditionalRegistryValues
103+
k := annotationKey
99104
v := a[k]
100105
if v == "" {
101106
return []hcsschema.RegistryValue{}
@@ -204,7 +209,13 @@ func parseAdditionalRegistryValues(ctx context.Context, a map[string]string) []h
204209
return slices.Clip(rvs)
205210
}
206211

207-
// ParseHVSocketServiceTable extracts any additional Hyper-V socket service configurations from annotations.
212+
// ParseTestAnnotationRegistryValues extracts registry values from the WCOW test annotation.
213+
// This is for testing and debugging purposes only.
214+
func ParseTestAnnotationRegistryValues(ctx context.Context, a map[string]string) []hcsschema.RegistryValue {
215+
return parseRegistryValues(ctx, a, iannotations.WCOWRegistryAnnotationTest)
216+
}
217+
218+
// parseHVSocketServiceTable extracts any additional Hyper-V socket service configurations from annotations.
208219
//
209220
// Like the [parseAnnotation*] functions, this logs errors but does not return them.
210221
func ParseHVSocketServiceTable(ctx context.Context, a map[string]string) map[string]hcsschema.HvSocketServiceConfig {

pkg/securitypolicy/api.rego

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ version := "@@API_VERSION@@"
55
enforcement_points := {
66
"mount_device": {"introducedVersion": "0.1.0", "default_results": {"allowed": false}},
77
"mount_overlay": {"introducedVersion": "0.1.0", "default_results": {"allowed": false}},
8-
"mount_cims": {"introducedVersion": "0.11.0", "default_results": {"allowed": false}},
8+
"mount_cims": {"introducedVersion": "0.11.0", "default_results": {"allowed": false}},
9+
"registry_changes": {"introducedVersion": "0.10.0", "default_results": {"allowed": false}},
910
"create_container": {"introducedVersion": "0.1.0", "default_results": {"allowed": false, "env_list": null, "allow_stdio_access": false}},
1011
"unmount_device": {"introducedVersion": "0.2.0", "default_results": {"allowed": true}},
1112
"unmount_overlay": {"introducedVersion": "0.6.0", "default_results": {"allowed": true}},

pkg/securitypolicy/framework.rego

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1231,6 +1231,116 @@ scratch_unmount := {"metadata": [remove_scratch_mount], "allowed": true} {
12311231
}
12321232
}
12331233

1234+
# Registry changes validation
1235+
default registry_changes := {"allowed": false}
1236+
1237+
# Helper function to compare registry keys
1238+
registry_keys_match(policy_key, input_key) {
1239+
policy_key.hive == input_key.Hive
1240+
policy_key.name == input_key.Name
1241+
# Volatile field comparison (default to false if not specified)
1242+
policy_volatile := object.get(policy_key, "volatile", false)
1243+
input_volatile := object.get(input_key, "Volatile", false)
1244+
policy_volatile == input_volatile
1245+
}
1246+
1247+
# Helper function to compare registry values
1248+
# STRING type
1249+
registry_value_matches(policy_value, input_value) {
1250+
registry_keys_match(policy_value.key, input_value.Key)
1251+
policy_value.name == input_value.Name
1252+
policy_value.type == input_value.Type
1253+
policy_value.type == "String"
1254+
policy_value.string_value == input_value.StringValue
1255+
}
1256+
1257+
# EXPANDED_STRING type (uses StringValue field)
1258+
registry_value_matches(policy_value, input_value) {
1259+
registry_keys_match(policy_value.key, input_value.Key)
1260+
policy_value.name == input_value.Name
1261+
policy_value.type == input_value.Type
1262+
policy_value.type == "ExpandedString"
1263+
policy_value.string_value == input_value.StringValue
1264+
}
1265+
1266+
# MULTI_STRING type (uses StringValue field)
1267+
registry_value_matches(policy_value, input_value) {
1268+
registry_keys_match(policy_value.key, input_value.Key)
1269+
policy_value.name == input_value.Name
1270+
policy_value.type == input_value.Type
1271+
policy_value.type == "MultiString"
1272+
policy_value.string_value == input_value.StringValue
1273+
}
1274+
1275+
# D_WORD type
1276+
registry_value_matches(policy_value, input_value) {
1277+
registry_keys_match(policy_value.key, input_value.Key)
1278+
policy_value.name == input_value.Name
1279+
policy_value.type == input_value.Type
1280+
policy_value.type == "DWord"
1281+
policy_value.dword_value == input_value.DWordValue
1282+
}
1283+
1284+
# Q_WORD type
1285+
registry_value_matches(policy_value, input_value) {
1286+
registry_keys_match(policy_value.key, input_value.Key)
1287+
policy_value.name == input_value.Name
1288+
policy_value.type == input_value.Type
1289+
policy_value.type == "QWord"
1290+
policy_value.qword_value == input_value.QWordValue
1291+
}
1292+
1293+
# BINARY type
1294+
registry_value_matches(policy_value, input_value) {
1295+
registry_keys_match(policy_value.key, input_value.Key)
1296+
policy_value.name == input_value.Name
1297+
policy_value.type == input_value.Type
1298+
policy_value.type == "Binary"
1299+
policy_value.binary_value == input_value.BinaryValue
1300+
}
1301+
1302+
# CUSTOM_TYPE - both CustomType field and BinaryValue must match
1303+
registry_value_matches(policy_value, input_value) {
1304+
registry_keys_match(policy_value.key, input_value.Key)
1305+
policy_value.name == input_value.Name
1306+
policy_value.type == input_value.Type
1307+
policy_value.type == "CustomType"
1308+
policy_value.custom_type == input_value.CustomType
1309+
policy_value.binary_value == input_value.BinaryValue
1310+
}
1311+
1312+
# NONE type - no value to compare, just key and name
1313+
registry_value_matches(policy_value, input_value) {
1314+
registry_keys_match(policy_value.key, input_value.Key)
1315+
policy_value.name == input_value.Name
1316+
policy_value.type == input_value.Type
1317+
policy_value.type == "None"
1318+
}
1319+
1320+
# Filter input registry values to only include those that match policy
1321+
filtered_registry_values(input_values, policy_values) := [input_val |
1322+
input_val := input_values[_]
1323+
some policy_val in policy_values
1324+
registry_value_matches(policy_val, input_val)
1325+
]
1326+
1327+
registry_changes := {"allowed": true} {
1328+
containers := data.metadata.matches[input.containerID]
1329+
container := containers[_]
1330+
1331+
# Check if container has registry_changes defined in policy
1332+
container.registry_changes
1333+
1334+
# If input has registry changes, filter to only matching ones
1335+
input.registryChanges.AddValues
1336+
matched_values := filtered_registry_values(input.registryChanges.AddValues, container.registry_changes.add_values)
1337+
1338+
# Build result with filtered AddValues
1339+
result := {
1340+
"AddValues": matched_values
1341+
}
1342+
}
1343+
12341344
reason := {
12351345
"errors": errors,
12361346
"error_objects": error_objects

pkg/securitypolicy/open_door.rego

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ mount_device := {"allowed": true}
66
mount_overlay := {"allowed": true}
77
create_container := {"allowed": true, "env_list": null, "allow_stdio_access": true}
88
mount_cims := {"allowed": true}
9+
registry_changes := {"allowed": true}
910
unmount_device := {"allowed": true}
1011
unmount_overlay := {"allowed": true}
1112
exec_in_container := {"allowed": true, "env_list": null}

pkg/securitypolicy/policy.rego

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ unmount_device := data.framework.unmount_device
1010
mount_overlay := data.framework.mount_overlay
1111
unmount_overlay := data.framework.unmount_overlay
1212
mount_cims:= data.framework.mount_cims
13+
registry_changes := data.framework.registry_changes
1314
create_container := data.framework.create_container
1415
exec_in_container := data.framework.exec_in_container
1516
exec_external := data.framework.exec_external

0 commit comments

Comments
 (0)