From 75300bba012a0f4d3f152548e1ce2ba3ca6cf7ed Mon Sep 17 00:00:00 2001 From: Tingmao Wang Date: Tue, 18 Nov 2025 15:46:37 +0000 Subject: [PATCH 1/5] rego: Fix unable to start container with empty env list when non-required rules present This refactors rule_ok (and renames it) to fix the `some env in envList` being applied at the wrong level. Signed-off-by: Tingmao Wang --- pkg/securitypolicy/framework.rego | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/pkg/securitypolicy/framework.rego b/pkg/securitypolicy/framework.rego index c92f83f73d..6552e8bad8 100644 --- a/pkg/securitypolicy/framework.rego +++ b/pkg/securitypolicy/framework.rego @@ -297,21 +297,26 @@ env_rule_ok(rule, env) { env_pattern_ok(rule_value, value_strategy, env_value) } -rule_ok(rule, env) { - not rule.required -} - -rule_ok(rule, env) { +# For a required env rule, check that envList contains a matching env var for +# it. +env_required_rule_ok(rule, envList) { rule.required + some env in envList env_rule_ok(rule, env) } +# If it's not required, skip the check +env_required_rule_ok(rule, envList) { + not rule.required +} + envList_ok(env_rules, envList) { + # Check that all required rules are satisfied every rule in env_rules { - some env in envList - rule_ok(rule, env) + env_required_rule_ok(rule, envList) } + # Check that any env provided is allowed every env in envList { some rule in env_rules env_rule_ok(rule, env) From 8839e484b0373856a8d47c5b56284f04040ade12 Mon Sep 17 00:00:00 2001 From: Tingmao Wang Date: Tue, 18 Nov 2025 20:38:07 +0000 Subject: [PATCH 2/5] framework.rego: Refactor policy_containers to remove code duplication Signed-off-by: Tingmao Wang --- pkg/securitypolicy/framework.rego | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/pkg/securitypolicy/framework.rego b/pkg/securitypolicy/framework.rego index 6552e8bad8..a59be39b25 100644 --- a/pkg/securitypolicy/framework.rego +++ b/pkg/securitypolicy/framework.rego @@ -146,25 +146,22 @@ overlay_mounted(target) { data.metadata.overlayTargets[target] } -default candidate_containers := [] +# Note that a valid policy might not even define (data.policy.)containers, if +# all its containers are coming from fragments. This default rule prevents +# breaking other rules in this case. +default policy_containers := [] -candidate_containers := containers { +policy_containers := pc { semver.compare(policy_framework_version, version) == 0 - - policy_containers := [c | c := data.policy.containers[_]] - fragment_containers := [c | - feed := data.metadata.issuers[_].feeds[_] - fragment := feed[_] - c := fragment.containers[_] - ] - - containers := array.concat(policy_containers, fragment_containers) + pc := data.policy.containers } -candidate_containers := containers { +policy_containers := pc { semver.compare(policy_framework_version, version) < 0 + pc := apply_defaults("container", data.policy.containers, policy_framework_version) +} - policy_containers := apply_defaults("container", data.policy.containers, policy_framework_version) +candidate_containers := containers { fragment_containers := [c | feed := data.metadata.issuers[_].feeds[_] fragment := feed[_] From a249f98713842a083773b100f973655cf5edc333 Mon Sep 17 00:00:00 2001 From: Tingmao Wang Date: Tue, 18 Nov 2025 23:38:56 +0000 Subject: [PATCH 3/5] rego: Implement platform_rules support This currently support containers[_].env_rules and containers[_].mounts. If multiple platform_rules are defined, a container matching either one can be started (but in a consistent manner - e.g. if two platforms have different environment variables or mounts, a container can't "mix and match" between them). In order to achieve the above consistency, we "patch" the container objects instead of adding logic to e.g. env_rule_ok or envList_ok. This also means that the error_objects of a denial message will reflect the platform rules inserted. Signed-off-by: Tingmao Wang --- pkg/securitypolicy/framework.rego | 102 ++++++++++++++++++++++++++++-- 1 file changed, 98 insertions(+), 4 deletions(-) diff --git a/pkg/securitypolicy/framework.rego b/pkg/securitypolicy/framework.rego index a59be39b25..ef47564d83 100644 --- a/pkg/securitypolicy/framework.rego +++ b/pkg/securitypolicy/framework.rego @@ -168,7 +168,18 @@ candidate_containers := containers { c := fragment.containers[_] ] - containers := array.concat(policy_containers, fragment_containers) + containers_raw := array.concat(policy_containers, fragment_containers) + + # Each container definition applied with platform_rules might turn into + # multiple containers if multiple platforms are allowed. We flatten the + # result here. + after_platform_rules := [c2 | + c1 := containers_raw[_] + applied := apply_platform_rules("container", c1) + c2 := applied[_] + ] + + containers := after_platform_rules } default mount_cims := {"allowed": false} @@ -1196,21 +1207,28 @@ runtime_logging := {"allowed": true} { allow_runtime_logging } -default fragment_containers := [] +# Helpers to get data from the fragment that is currently being loaded. Since +# input.namespace is the package name the fragment loaded as, +# data[input.namespace] can be used to access the fragment. This is only valid +# during a load_fragment call - the content exported by the fragment needs to be +# persisted into the metadata for later enforcement use. (c.f. +# extract_fragment_includes) +default fragment_containers := [] fragment_containers := data[input.namespace].containers default fragment_fragments := [] - fragment_fragments := data[input.namespace].fragments default fragment_external_processes := [] - fragment_external_processes := data[input.namespace].external_processes default fragment_transparency_trust_lists := [] fragment_transparency_trust_lists := data[input.namespace].transparency_trust_lists +default fragment_platform_rules := [] +fragment_platform_rules := data[input.namespace].platform_rules + apply_defaults(name, raw_values, framework_version) := values { semver.compare(framework_version, version) == 0 values := raw_values @@ -1255,6 +1273,22 @@ apply_defaults("transparency_trust_lists", raw_values, framework_version) := val values := [] } +# platform_rules is introduced in framework version 0.5.0. If an old policy has it, +# silently ignore as it might be using the name for something else. + +apply_defaults("platform_rules", raw_values, framework_version) := values { + semver.compare(framework_version, version) < 0 + semver.compare(framework_version, "0.5.0") >= 0 + # This is currently unreachable, otherwise we would call something like + # check_platform_rule here, like above (not defined yet). + values := raw_values +} + +apply_defaults("platform_rules", raw_values, framework_version) := values { + semver.compare(framework_version, "0.5.0") < 0 + values := [] +} + default fragment_framework_version := null fragment_framework_version := data[input.namespace].framework_version @@ -1265,6 +1299,7 @@ extract_fragment_includes(includes) := fragment { "fragments": apply_defaults("fragment", fragment_fragments, framework_version), "external_processes": apply_defaults("external_process", fragment_external_processes, framework_version), "transparency_trust_lists": apply_defaults("transparency_trust_lists", fragment_transparency_trust_lists, framework_version), + "platform_rules": apply_defaults("platform_rules", fragment_platform_rules, framework_version), } fragment := { @@ -1770,6 +1805,65 @@ extract_parameter(name, fragment_parameters_obj, parameters_metadata) := fragmen "default" in object.keys(parameters_metadata[name]) } +default policy_platform_rules := [] + +policy_platform_rules := platform_rules { + semver.compare(policy_framework_version, version) == 0 + platform_rules := data.policy.platform_rules +} + +# For policy with framework_version < 0.5.0, apply_defaults will ignore +# platform_rules and return []. + +policy_platform_rules := platform_rules { + semver.compare(policy_framework_version, version) < 0 + platform_rules := apply_defaults("platform_rules", data.policy.platform_rules, policy_framework_version) +} + +default candidate_platform_rules := [] + +candidate_platform_rules := platform_rules { + fragment_platform_rules := [r | + feed := data.metadata.issuers[_].feeds[_] + fragment := feed[_] + r := fragment.platform_rules[_] + ] + + platform_rules := array.concat(policy_platform_rules, fragment_platform_rules) +} + +# apply_platform_rules("container", c) applies "platform_rules" to the container +# object c, and returns a list of containers which are the input container with +# platform rules applied. The return value may contain more than one container +# if multiple platform rules are defined. + +# No platform rules - return as-is. +apply_platform_rules("container", container) := updated_containers { + count(candidate_platform_rules) == 0 + updated_containers := [container] +} + +apply_platform_rules("container", container) := updated_containers { + count(candidate_platform_rules) > 0 + updated_containers := [updated_container | + platform_rule := candidate_platform_rules[_] + updated_container := apply_single_platform_rule("container", container, platform_rule) + ] +} + +apply_single_platform_rule("container", container, platform_rule) := updated_container { + container_env_rules := object.get(container, "env_rules", []) + updated_env_rules := array.concat(container_env_rules, object.get(platform_rule, "env_rules", [])) + + container_mounts := object.get(container, "mounts", []) + updated_mounts := array.concat(container_mounts, object.get(platform_rule, "mounts", [])) + + updated_container := object.union(container, { + "env_rules": updated_env_rules, + "mounts": updated_mounts, + }) +} + reason := { "errors": errors, "error_objects": error_objects From 3577113a8002002f087f6d89f1854fecd6e921e8 Mon Sep 17 00:00:00 2001 From: Tingmao Wang Date: Wed, 19 Nov 2025 15:02:29 +0000 Subject: [PATCH 4/5] rego: Fix create_container enforcement deny message missing input.rule field Currently we only add input.rule to the original input, not the redacted input. This results in the case of create_container not having the "rule" field in the final deny message, but other enforcement points do have it since in those cases the redactSensitiveData return the original input map. Signed-off-by: Tingmao Wang --- pkg/securitypolicy/securitypolicyenforcer_rego.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/securitypolicy/securitypolicyenforcer_rego.go b/pkg/securitypolicy/securitypolicyenforcer_rego.go index f041c246aa..af9d59227c 100644 --- a/pkg/securitypolicy/securitypolicyenforcer_rego.go +++ b/pkg/securitypolicy/securitypolicyenforcer_rego.go @@ -408,9 +408,9 @@ func (policy *regoEnforcer) denyWithError(ctx context.Context, policyError error } func (policy *regoEnforcer) denyWithReason(ctx context.Context, enforcementPoint string, input inputData) error { + input["rule"] = enforcementPoint cleaned_input := policy.redactSensitiveData(input) cleaned_input = replaceCapabilitiesWithPlaceholders(cleaned_input) - input["rule"] = enforcementPoint policyDecision := map[string]interface{}{ "input": cleaned_input, "decision": "deny", From 4a42b64788920dc59dae19e6ee2f3b91e3761ddd Mon Sep 17 00:00:00 2001 From: Tingmao Wang Date: Fri, 21 Nov 2025 01:11:35 +0000 Subject: [PATCH 5/5] regopolicy_linux_test: Test for platform_rules Signed-off-by: Tingmao Wang --- .../platform_rules.rego | 29 ++ .../policy_with_platform_rules.rego | 108 +++++ pkg/securitypolicy/rego_utils_test.go | 46 ++- pkg/securitypolicy/regopolicy_linux_test.go | 383 ++++++++++++++++++ 4 files changed, 545 insertions(+), 21 deletions(-) create mode 100644 pkg/securitypolicy/fragment_test_policies/platform_rules.rego create mode 100644 pkg/securitypolicy/policy_with_platform_rules.rego diff --git a/pkg/securitypolicy/fragment_test_policies/platform_rules.rego b/pkg/securitypolicy/fragment_test_policies/platform_rules.rego new file mode 100644 index 0000000000..b8d8e096a4 --- /dev/null +++ b/pkg/securitypolicy/fragment_test_policies/platform_rules.rego @@ -0,0 +1,29 @@ +package fragment + +svn := "1" +framework_version := "0.5.0" + +platform_rules := [ + { + "env_rules": [ + { + "name": "(?i)(FABRIC)_.+", + "name_strategy": "re2", + "value": ".+", + "value_strategy": "re2" + } + ], + "mounts": [ + { + "destination": "/var/run/secrets/kubernetes.io/serviceaccount", + "options": [ + "rbind", + "rshared", + "ro" + ], + "source": "sandbox:///tmp/atlas/emptydir/.+", + "type": "bind" + } + ] + } +] diff --git a/pkg/securitypolicy/policy_with_platform_rules.rego b/pkg/securitypolicy/policy_with_platform_rules.rego new file mode 100644 index 0000000000..a250992398 --- /dev/null +++ b/pkg/securitypolicy/policy_with_platform_rules.rego @@ -0,0 +1,108 @@ +package policy + +api_version := "@@API_VERSION@@" +framework_version := "@@FRAMEWORK_VERSION@@" + +fragments := [ + { + "feed": "@@FRAGMENT_FEED@@", + "includes": [ + "containers", + "fragments" + ], + "issuer": "@@FRAGMENT_ISSUER@@", + "minimum_svn": "0" + } +] + +platform_rules := [ + { + "env_rules": [ + { + "name": "(?i)(FABRIC)_.+", + "name_strategy": "re2", + "value": ".+", + "value_strategy": "re2" + } + ], + "mounts": [ + { + "destination": "/var/run/secrets/kubernetes.io/serviceaccount", + "options": [ + "rbind", + "rshared", + "ro" + ], + "source": "sandbox:///tmp/atlas/emptydir/.+", + "type": "bind" + } + ] + } +] + +containers := [ + { + "allow_elevated": false, + "allow_stdio_access": true, + "capabilities": { + "ambient": [], + "bounding": [], + "effective": [], + "inheritable": [], + "permitted": [] + }, + "command": [ "bash" ], + "env_rules": [], + "exec_processes": [], + "layers": [ + "0000000000000000000000000000000000000000000000000000000000000000", + ], + "mounts": [], + "no_new_privileges": false, + "seccomp_profile_sha256": "", + "signals": [], + "user": { + "group_idnames": [ + { + "pattern": "", + "strategy": "any" + } + ], + "umask": "0022", + "user_idname": { + "pattern": "", + "strategy": "any" + } + }, + "working_dir": "/" + } +] + +allow_properties_access := true +allow_dump_stacks := false +allow_runtime_logging := false +allow_environment_variable_dropping := true +allow_unencrypted_scratch := false +allow_capability_dropping := true + +mount_device := data.framework.mount_device +rw_mount_device := data.framework.rw_mount_device +unmount_device := data.framework.unmount_device +rw_unmount_device := data.framework.rw_unmount_device +mount_overlay := data.framework.mount_overlay +unmount_overlay := data.framework.unmount_overlay +mount_cims := data.framework.mount_cims +create_container := data.framework.create_container +exec_in_container := data.framework.exec_in_container +exec_external := data.framework.exec_external +shutdown_container := data.framework.shutdown_container +signal_container_process := data.framework.signal_container_process +plan9_mount := data.framework.plan9_mount +plan9_unmount := data.framework.plan9_unmount +get_properties := data.framework.get_properties +dump_stacks := data.framework.dump_stacks +runtime_logging := data.framework.runtime_logging +load_fragment := data.framework.load_fragment +scratch_mount := data.framework.scratch_mount +scratch_unmount := data.framework.scratch_unmount +reason := data.framework.reason diff --git a/pkg/securitypolicy/rego_utils_test.go b/pkg/securitypolicy/rego_utils_test.go index 4ece06a815..64f0ff2574 100644 --- a/pkg/securitypolicy/rego_utils_test.go +++ b/pkg/securitypolicy/rego_utils_test.go @@ -879,6 +879,30 @@ func setupRegoFragmentSVNMismatchTestConfig(gc *generatedConstraints) (*regoFrag return setupRegoFragmentTestConfig(gc, 2, []string{"containers"}, []string{}, false, false, false, true) } +func makeContainerFromGeneratedFragment(fragment *regoFragment) *regoFragmentContainer { + container := fragment.selectContainer() + + envList := buildEnvironmentVariablesFromEnvRules(container.EnvRules, testRand) + sandboxID := testDataGenerator.uniqueSandboxID() + user := buildIDNameFromConfig(container.User.UserIDName, testRand) + groups := buildGroupIDNamesFromUser(container.User, testRand) + capabilities := copyLinuxCapabilities(container.Capabilities.toExternal()) + seccomp := container.SeccompProfileSHA256 + + mounts := container.Mounts + mountSpec := buildMountSpecFromMountArray(mounts, sandboxID, testRand) + return ®oFragmentContainer{ + container: container, + envList: envList, + sandboxID: sandboxID, + mounts: mountSpec.Mounts, + user: user, + groups: groups, + capabilities: &capabilities, + seccomp: seccomp, + } +} + func setupRegoFragmentTestConfig(gc *generatedConstraints, numFragments int, includes []string, excludes []string, svnError bool, sameIssuer bool, sameFeed bool, svnMismatch bool) (tc *regoFragmentTestConfig, err error) { gc.fragments = generateFragments(testRand, int32(numFragments)) @@ -902,27 +926,7 @@ func setupRegoFragmentTestConfig(gc *generatedConstraints, numFragments int, inc externalProcesses := make([]*externalProcess, numFragments) plan9Mounts := make([]string, numFragments) for i, fragment := range fragments { - container := fragment.selectContainer() - - envList := buildEnvironmentVariablesFromEnvRules(container.EnvRules, testRand) - sandboxID := testDataGenerator.uniqueSandboxID() - user := buildIDNameFromConfig(container.User.UserIDName, testRand) - groups := buildGroupIDNamesFromUser(container.User, testRand) - capabilities := copyLinuxCapabilities(container.Capabilities.toExternal()) - seccomp := container.SeccompProfileSHA256 - - mounts := container.Mounts - mountSpec := buildMountSpecFromMountArray(mounts, sandboxID, testRand) - containers[i] = ®oFragmentContainer{ - container: container, - envList: envList, - sandboxID: sandboxID, - mounts: mountSpec.Mounts, - user: user, - groups: groups, - capabilities: &capabilities, - seccomp: seccomp, - } + containers[i] = makeContainerFromGeneratedFragment(fragment) for _, include := range fragment.info.includes { switch include { diff --git a/pkg/securitypolicy/regopolicy_linux_test.go b/pkg/securitypolicy/regopolicy_linux_test.go index 5a36461070..967b2076ef 100644 --- a/pkg/securitypolicy/regopolicy_linux_test.go +++ b/pkg/securitypolicy/regopolicy_linux_test.go @@ -9345,6 +9345,374 @@ func Test_Rego_GetUserInfo_EtcPasswdOnly(t *testing.T) { } } +// Test platform rules in fragment, container in main policy +func Test_Rego_PlatformRules_InFragment1(t *testing.T) { + f := func(gc *generatedConstraints, skipLoadFragment bool, fragmentDontIncludePlatformRules bool) bool { + platformFragment := fragment{ + issuer: testDataGenerator.uniqueFragmentIssuer(), + feed: "infra", + minimumSVN: "1", + includes: []string{ + "platform_rules", + }, + } + + if fragmentDontIncludePlatformRules { + platformFragment.includes = []string{ + "containers", + } + } + + gc.fragments = []*fragment{&platformFragment} + + securityPolicy := gc.toPolicy() + defaultMounts := generateMounts(testRand) + privilegedMounts := generateMounts(testRand) + + policy, err := newRegoPolicy(securityPolicy.marshalRego(), + toOCIMounts(defaultMounts), + toOCIMounts(privilegedMounts), testOSType) + if err != nil { + t.Fatalf("failed to create rego policy: %v", err) + } + + if !skipLoadFragment { + err = policy.LoadFragment(gc.ctx, LoadFragmentOptions{Issuer: platformFragment.issuer, Feed: platformFragment.feed, Rego: platformRulesFragmentPolicyCode}) + if err != nil { + t.Fatalf("failed to load infra fragment: %v", err) + } + } + + container := selectContainerFromContainerList(gc.containers, testRand) + containerID, err := mountImageForContainer(policy, container) + if err != nil { + t.Errorf("failed to mount image for container: %v", err) + return false + } + + tc, err := createTestContainerSpec(gc, containerID, container, false, policy, defaultMounts, privilegedMounts) + if err != nil { + t.Fatalf("failed to create test container spec: %v", err) + } + tc.envList = append(tc.envList, "Fabric_NodeIPOrFqdn=10.0.0.1") + tc.mounts = append(tc.mounts, oci.Mount{ + Source: fmt.Sprintf("/run/gcs/c/%s/sandboxMounts/tmp/atlas/emptydir/serviceaccount", tc.sandboxID), + Destination: "/var/run/secrets/kubernetes.io/serviceaccount", + Type: "bind", + Options: []string{"rbind", "rshared", "ro"}, + }) + + envsToKeep, _, _, err := tc.policy.EnforceCreateContainerPolicy(gc.ctx, tc.sandboxID, tc.containerID, tc.argList, tc.envList, tc.workingDir, tc.mounts, false, tc.noNewPrivileges, tc.user, tc.groups, tc.umask, tc.capabilities, tc.seccomp) + + if skipLoadFragment || fragmentDontIncludePlatformRules { + if err == nil { + t.Error("expected error due to missing platform rules, got nil") + return false + } + assertDecisionJSONContains(t, err, "invalid env list: Fabric_NodeIPOrFqdn") + assertDecisionJSONContains(t, err, "invalid mount list: /var/run/secrets/kubernetes.io/serviceaccount") + return true + } + + // getting an error means something is broken + if err != nil { + t.Error(err) + return false + } + + slices.Sort(tc.envList) + slices.Sort(envsToKeep) + if !slices.Equal(tc.envList, envsToKeep) { + t.Errorf("expected envs to keep = %v, got %v", tc.envList, envsToKeep) + return false + } + + return true + } + + if err := quick.Check(f, &quick.Config{MaxCount: 25, Rand: testRand}); err != nil { + t.Errorf("Test_Rego_PlatformRules_InFragment1 failed: %v", err) + } +} + +// Test platform rule and container both in separate fragment +func Test_Rego_PlatformRules_InFragment2(t *testing.T) { + f := func(gc *generatedConstraints, loadPlatformRulesFragmentFirst bool, skipLoadPlatformRulesFragment bool) bool { + // Generate some random fragments in the policy + gc.fragments = generateFragments(testRand, maxFragmentsInGeneratedConstraints) + // Pick one or two to use for container create test + containerFragments := selectFragmentsFromConstraints(gc, testRand.Intn(2)+1, []string{"containers"}, []string{}, false, frameworkVersion, false) + // Now add platform rules fragment in a random position + platformFragment := fragment{ + issuer: testDataGenerator.uniqueFragmentIssuer(), + feed: "infra", + minimumSVN: "1", + includes: []string{ + "platform_rules", + }, + } + gc.fragments = append(gc.fragments, &platformFragment) + testRand.Shuffle(len(gc.fragments), func(i, j int) { + gc.fragments[i], gc.fragments[j] = gc.fragments[j], gc.fragments[i] + }) + + containers := make([]*regoFragmentContainer, len(containerFragments)) + + // c.f. setupRegoFragmentTestConfig + for i, fragment := range containerFragments { + containers[i] = makeContainerFromGeneratedFragment(fragment) + + containers[i].envList = append(containers[i].envList, "Fabric_NodeIPOrFqdn=10.0.0.1") + containers[i].mounts = append(containers[i].mounts, oci.Mount{ + Source: fmt.Sprintf("/run/gcs/c/%s/sandboxMounts/tmp/atlas/emptydir/serviceaccount", containers[i].sandboxID), + Destination: "/var/run/secrets/kubernetes.io/serviceaccount", + Type: "bind", + Options: []string{"rbind", "rshared", "ro"}, + }) + + code := fragment.constraints.toFragment().marshalRego() + fragment.code = setFrameworkVersion(code, frameworkVersion) + } + + securityPolicy := gc.toPolicy() + defaultMounts := generateMounts(testRand) + privilegedMounts := generateMounts(testRand) + + policy, err := newRegoPolicy(securityPolicy.marshalRego(), + toOCIMounts(defaultMounts), + toOCIMounts(privilegedMounts), testOSType) + if err != nil { + t.Fatalf("failed to create rego policy: %v", err) + } + + if loadPlatformRulesFragmentFirst { + if !skipLoadPlatformRulesFragment { + err = policy.LoadFragment(gc.ctx, LoadFragmentOptions{Issuer: platformFragment.issuer, Feed: platformFragment.feed, Rego: platformRulesFragmentPolicyCode}) + if err != nil { + t.Fatalf("failed to load platform rules fragment: %v", err) + } + } + for _, containerFragment := range containerFragments { + err = policy.LoadFragment(gc.ctx, LoadFragmentOptions{Issuer: containerFragment.info.issuer, Feed: containerFragment.info.feed, Rego: containerFragment.code}) + if err != nil { + t.Fatalf("failed to load container fragment: %v", err) + } + } + } else { + for _, containerFragment := range containerFragments { + err = policy.LoadFragment(gc.ctx, LoadFragmentOptions{Issuer: containerFragment.info.issuer, Feed: containerFragment.info.feed, Rego: containerFragment.code}) + if err != nil { + t.Fatalf("failed to load container fragment: %v", err) + } + } + if !skipLoadPlatformRulesFragment { + err = policy.LoadFragment(gc.ctx, LoadFragmentOptions{Issuer: platformFragment.issuer, Feed: platformFragment.feed, Rego: platformRulesFragmentPolicyCode}) + if err != nil { + t.Fatalf("failed to load platform rules fragment: %v", err) + } + } + } + + for _, container := range containers { + containerID, err := mountImageForContainer(policy, container.container) + if err != nil { + t.Errorf("failed to mount image for container: %v", err) + return false + } + + _, _, _, err = policy.EnforceCreateContainerPolicy(gc.ctx, + container.sandboxID, + containerID, + copyStrings(container.container.Command), + copyStrings(container.envList), + container.container.WorkingDir, + copyMounts(container.mounts), + false, + container.container.NoNewPrivileges, + container.user, + container.groups, + container.container.User.Umask, + container.capabilities, + container.seccomp, + ) + + if !skipLoadPlatformRulesFragment { + if err != nil { + t.Errorf("unable to create container from fragment: %v", err) + return false + } + } else { + if err == nil { + t.Error("expected error due to missing platform rules, got nil") + return false + } + assertDecisionJSONContains(t, err, "invalid env list: Fabric_NodeIPOrFqdn") + assertDecisionJSONContains(t, err, "invalid mount list: /var/run/secrets/kubernetes.io/serviceaccount") + } + } + + return true + } + if err := quick.Check(f, &quick.Config{MaxCount: 25, Rand: testRand}); err != nil { + t.Errorf("Test_Rego_PlatformRules_InFragment2 failed: %v", err) + } +} + +// Test platform rules and container both in main policy +func Test_Rego_PlatformRules_InPolicy1(t *testing.T) { + // We don't actually use fragment in this test, but need some value as + // placeholder. + fragmentFeed := testDataGenerator.uniqueFragmentFeed() + fragmentIssuer := testDataGenerator.uniqueFragmentIssuer() + + rego := getPolicyCode_PolicyWithPlatformRules(fragmentFeed, fragmentIssuer) + defaultMounts := generateMounts(testRand) + privilegedMounts := generateMounts(testRand) + + p, err := newRegoPolicy(rego, + toOCIMounts(defaultMounts), + toOCIMounts(privilegedMounts), testOSType) + if err != nil { + t.Errorf("unable to create policy with platform rules: %v", err) + } + + container := &securityPolicyContainer{ + Command: []string{"bash"}, + EnvRules: []EnvRuleConfig{ + { + Required: true, + UseNameValue: true, + Name: "Fabric_NodeIPOrFqdn", + NameStrategy: EnvVarRuleString, + Value: "10.0.0.1", + ValueStrategy: EnvVarRuleString, + }, + }, + Layers: []string{ + paramTestImageBaseLayer, + }, + WorkingDir: "/", + Mounts: []mountInternal{ + { + Source: "sandbox:///tmp/atlas/emptydir/.+", + Destination: "/var/run/secrets/kubernetes.io/serviceaccount", + Type: "bind", + Options: []string{"rbind", "rshared", "ro"}, + }, + }, + User: UserConfig{ + UserIDName: generateIDNameConfig(testRand), + GroupIDNames: []IDNameConfig{ + generateIDNameConfig(testRand), + }, + Umask: "0022", + }, + Capabilities: &capabilitiesInternal{ + Ambient: []string{}, + Bounding: []string{}, + Effective: []string{}, + Inheritable: []string{}, + Permitted: []string{}, + }, + } + containerID, err := mountImageForContainer(p, container) + if err != nil { + t.Fatalf("failed to mount image for container: %v", err) + } + + tc, err := createTestContainerSpec(&generatedConstraints{ + ctx: context.Background(), + }, containerID, container, false, p, defaultMounts, privilegedMounts) + if err != nil { + t.Fatalf("failed to create test container spec: %v", err) + } + + envsToKeep, _, _, err := tc.policy.EnforceCreateContainerPolicy(tc.ctx, tc.sandboxID, tc.containerID, tc.argList, tc.envList, tc.workingDir, tc.mounts, false, tc.noNewPrivileges, tc.user, tc.groups, tc.umask, tc.capabilities, tc.seccomp) + + // getting an error means something is broken + if err != nil { + t.Fatal(err) + } + + if !slices.Equal(tc.envList, envsToKeep) { + t.Fatalf("expected envs to keep = %v, got %v", tc.envList, envsToKeep) + } +} + +// Test platform rule in main policy, container in fragment +func Test_Rego_PlatformRules_InPolicy2(t *testing.T) { + // Generate a random fragment. We only have one "slot" in + // policy_with_platform_rules.rego for an external fragment so we only do + // one. generateFragments' argument is only a minimum, so truncate to one to + // keep selectFragmentsFromConstraints' random pick aligned with + // fragments[0] below. + fragments := generateFragments(testRand, 1)[:1] + containerFragments := selectFragmentsFromConstraints(&generatedConstraints{ + fragments: fragments, + }, 1, []string{"containers"}, []string{}, false, frameworkVersion, false) + containers := make([]*regoFragmentContainer, len(containerFragments)) + + for i, fragment := range containerFragments { + containers[i] = makeContainerFromGeneratedFragment(fragment) + + containers[i].envList = append(containers[i].envList, "Fabric_NodeIPOrFqdn=10.0.0.1") + containers[i].mounts = append(containers[i].mounts, oci.Mount{ + Source: fmt.Sprintf("/run/gcs/c/%s/sandboxMounts/tmp/atlas/emptydir/serviceaccount", containers[i].sandboxID), + Destination: "/var/run/secrets/kubernetes.io/serviceaccount", + Type: "bind", + Options: []string{"rbind", "rshared", "ro"}, + }) + + code := fragment.constraints.toFragment().marshalRego() + fragment.code = setFrameworkVersion(code, frameworkVersion) + } + + rego := getPolicyCode_PolicyWithPlatformRules(fragments[0].feed, fragments[0].issuer) + defaultMounts := generateMounts(testRand) + privilegedMounts := generateMounts(testRand) + + p, err := newRegoPolicy(rego, + toOCIMounts(defaultMounts), + toOCIMounts(privilegedMounts), testOSType) + if err != nil { + t.Fatalf("unable to create policy with platform rules: %v", err) + } + + for _, containerFragment := range containerFragments { + err = p.LoadFragment(context.Background(), LoadFragmentOptions{Issuer: containerFragment.info.issuer, Feed: containerFragment.info.feed, Rego: containerFragment.code}) + if err != nil { + t.Fatalf("failed to load container fragment: %v", err) + } + } + + for _, container := range containers { + containerID, err := mountImageForContainer(p, container.container) + if err != nil { + t.Fatalf("failed to mount image for container: %v", err) + } + + _, _, _, err = p.EnforceCreateContainerPolicy(context.Background(), + container.sandboxID, + containerID, + copyStrings(container.container.Command), + copyStrings(container.envList), + container.container.WorkingDir, + copyMounts(container.mounts), + false, + container.container.NoNewPrivileges, + container.user, + container.groups, + container.container.User.Umask, + container.capabilities, + container.seccomp, + ) + + if err != nil { + t.Fatalf("unable to create container from fragment: %v", err) + } + } +} + type getUserInfoTestCase struct { userStrs []string additionalGIDs []uint32 @@ -9871,3 +10239,18 @@ func fragmentParameterTestCheckOneEnvWithLayer( assertDecisionJSONContains(t, err, "invalid env list") } } + +//go:embed fragment_test_policies/platform_rules.rego +var platformRulesFragmentPolicyCode string + +//go:embed policy_with_platform_rules.rego +var policyWithPlatformRules string + +func getPolicyCode_PolicyWithPlatformRules(fragmentFeed, fragmentIssuer string) string { + s := policyWithPlatformRules + s = strings.ReplaceAll(s, "@@API_VERSION@@", apiVersion) + s = strings.ReplaceAll(s, "@@FRAMEWORK_VERSION@@", frameworkVersion) + s = strings.ReplaceAll(s, "@@FRAGMENT_FEED@@", fragmentFeed) + s = strings.ReplaceAll(s, "@@FRAGMENT_ISSUER@@", fragmentIssuer) + return s +}