From afb63a3078669cad3eea98719e90bed79577bf10 Mon Sep 17 00:00:00 2001 From: Tingmao Wang Date: Thu, 30 Oct 2025 09:00:00 +0000 Subject: [PATCH 1/9] rego: Support env rules with separate name and value patterns and matching strategies This is to make it easier to parameterize environment rules. Currently, name and value for an environment rule are actually combined into one "pattern" field, and there is only one strategy for the combined pattern. This presents a problem when a fragment wants to delegate the decision of e.g. whether to match the value (but only the value, not the key) with a regex or with a fixed string. We split "pattern" and "strategy" out into "name", "name_strategy", "value" and "value_strategy" in order to allow more flexibility when fragment exposes env-var parameters. Signed-off-by: Tingmao Wang --- pkg/securitypolicy/framework.rego | 63 +++++++++++++++++++++++++++---- 1 file changed, 55 insertions(+), 8 deletions(-) diff --git a/pkg/securitypolicy/framework.rego b/pkg/securitypolicy/framework.rego index 6af20467be..9e2995a00c 100644 --- a/pkg/securitypolicy/framework.rego +++ b/pkg/securitypolicy/framework.rego @@ -234,21 +234,68 @@ command_ok(command) { } } -env_ok(pattern, "string", value) { +# An env rule can be of two forms: +# { +# "pattern": "name=value", +# "strategy": "string" | "re2" +# } +# or +# { +# "name": "name_pattern", +# "name_strategy": "string" | "re2", +# "value": "value_pattern", +# "value_strategy": "string" | "re2" +# } + +# env_pattern_ok(pattern, strategy, value) tests whether the given string +# pattern matches the input value. + +env_pattern_ok(pattern, "string", value) { pattern == value } -env_ok(pattern, "re2", value) { +env_pattern_ok(pattern, "re2", value) { regex.match(anchor_pattern(pattern), value) } +# env_rule_ok accepts both forms of env rules described above, and matches it +# against the given env string (of form name=value). + +env_rule_ok(rule, env) { + pattern := object.get(rule, "pattern", null) + strategy := object.get(rule, "strategy", null) + pattern != null + strategy != null + env_pattern_ok(pattern, strategy, env) +} + +env_rule_ok(rule, env) { + rule_name := object.get(rule, "name", null) + name_strategy := object.get(rule, "name_strategy", null) + rule_value := object.get(rule, "value", null) + value_strategy := object.get(rule, "value_strategy", null) + rule_name != null + name_strategy != null + rule_value != null + value_strategy != null + + # Split the env into name and value (value can contain '=', name cannot) + eq_idx := indexof(env, "=") + eq_idx >= 0 + env_name := substring(env, 0, eq_idx) + env_value := substring(env, eq_idx + 1, -1) + + env_pattern_ok(rule_name, name_strategy, env_name) + env_pattern_ok(rule_value, value_strategy, env_value) +} + rule_ok(rule, env) { not rule.required } rule_ok(rule, env) { rule.required - env_ok(rule.pattern, rule.strategy, env) + env_rule_ok(rule, env) } envList_ok(env_rules, envList) { @@ -259,7 +306,7 @@ envList_ok(env_rules, envList) { every env in envList { some rule in env_rules - env_ok(rule.pattern, rule.strategy, env) + env_rule_ok(rule, env) } } @@ -267,7 +314,7 @@ valid_envs_subset(env_rules) := envs { envs := {env | some env in input.envList some rule in env_rules - env_ok(rule.pattern, rule.strategy, env) + env_rule_ok(rule, env) } } @@ -1594,14 +1641,14 @@ env_matches(env) { input.rule in ["create_container", "exec_in_container"] some container in data.metadata.matches[input.containerID] some rule in container.env_rules - env_ok(rule.pattern, rule.strategy, env) + env_rule_ok(rule, env) } env_matches(env) { input.rule in ["exec_external"] some process in candidate_external_processes some rule in process.env_rules - env_ok(rule.pattern, rule.strategy, env) + env_rule_ok(rule, env) } errors[envError] { @@ -1619,7 +1666,7 @@ errors[envError] { env_rule_matches(rule) { some env in input.envList - env_ok(rule.pattern, rule.strategy, env) + env_rule_ok(rule, env) } errors["missing required environment variable"] { From 7c0ef62aa071292a77afb7bd0424520687ad55ca Mon Sep 17 00:00:00 2001 From: Tingmao Wang Date: Wed, 29 Oct 2025 15:39:31 +0000 Subject: [PATCH 2/9] regopolicy_test: Add tests for name/value env rules Signed-off-by: Tingmao Wang --- pkg/securitypolicy/rego_utils_test.go | 100 +++++++++++------- pkg/securitypolicy/regopolicy_linux_test.go | 27 ++--- pkg/securitypolicy/regopolicy_windows_test.go | 11 +- pkg/securitypolicy/securitypolicy.go | 8 ++ pkg/securitypolicy/securitypolicy_marshal.go | 6 +- 5 files changed, 93 insertions(+), 59 deletions(-) diff --git a/pkg/securitypolicy/rego_utils_test.go b/pkg/securitypolicy/rego_utils_test.go index 6afe7de6cc..3736d54f6a 100644 --- a/pkg/securitypolicy/rego_utils_test.go +++ b/pkg/securitypolicy/rego_utils_test.go @@ -44,31 +44,32 @@ const ( maxPlan9MountIndex = 16 // variables that influence generated test fixtures - minStringLength = 10 - maxContainersInGeneratedConstraints = 32 - maxLayersInGeneratedContainer = 32 - maxGeneratedCommandLength = 128 - maxGeneratedCommandArgs = 12 - maxGeneratedEnvironmentVariables = 16 - maxGeneratedEnvironmentVariableRuleLength = 64 - maxGeneratedEnvironmentVariableRules = 8 - maxGeneratedFragmentNamespaceLength = 32 - maxGeneratedMountTargetLength = 256 - maxGeneratedVersion = 10 - rootHashLength = 64 - maxGeneratedMounts = 4 - maxGeneratedMountSourceLength = 32 - maxGeneratedMountDestinationLength = 32 - maxGeneratedMountOptions = 5 - maxGeneratedMountOptionLength = 32 - maxGeneratedExecProcesses = 4 - maxGeneratedWorkingDirLength = 128 - maxSignalNumber = 64 - maxGeneratedNameLength = 8 - maxGeneratedGroupNames = 4 - maxGeneratedCapabilities = 12 - maxGeneratedCapabilitesLength = 24 - maxWindowsSignalLength = 64 + minStringLength = 10 + maxContainersInGeneratedConstraints = 32 + maxLayersInGeneratedContainer = 32 + maxGeneratedCommandLength = 128 + maxGeneratedCommandArgs = 12 + maxGeneratedEnvironmentVariables = 16 + maxGeneratedEnvironmentVariableNameLength = 31 + maxGeneratedEnvironmentVariableValueLength = 32 + maxGeneratedEnvironmentVariableRules = 8 + maxGeneratedFragmentNamespaceLength = 32 + maxGeneratedMountTargetLength = 256 + maxGeneratedVersion = 10 + rootHashLength = 64 + maxGeneratedMounts = 4 + maxGeneratedMountSourceLength = 32 + maxGeneratedMountDestinationLength = 32 + maxGeneratedMountOptions = 5 + maxGeneratedMountOptionLength = 32 + maxGeneratedExecProcesses = 4 + maxGeneratedWorkingDirLength = 128 + maxSignalNumber = 64 + maxGeneratedNameLength = 8 + maxGeneratedGroupNames = 4 + maxGeneratedCapabilities = 12 + maxGeneratedCapabilitesLength = 24 + maxWindowsSignalLength = 64 // additional consts // the standard enforcer tests don't do anything with the encoded policy // string. this const exists to make that explicit @@ -2232,7 +2233,7 @@ func (*SecurityPolicy) Generate(r *rand.Rand, _ int) reflect.Value { for j := 0; j < numEnvRules; j++ { rule := EnvRuleConfig{ Strategy: "string", - Rule: randVariableString(r, maxGeneratedEnvironmentVariableRuleLength), + Rule: generateRandomEnvironmentVariable(r), Required: false, } c.EnvRules.Elements[strconv.Itoa(j)] = rule @@ -2428,9 +2429,18 @@ func generateEnvironmentVariableRules(r *rand.Rand) []EnvRuleConfig { numArgs := atLeastOneAtMost(r, maxGeneratedEnvironmentVariableRules) for i := 0; i < int(numArgs); i++ { - rule := EnvRuleConfig{ - Strategy: "string", - Rule: randVariableString(r, maxGeneratedEnvironmentVariableRuleLength), + var rule EnvRuleConfig + rule.UseNameValue = randBool(r) + name := randVariableString(r, maxGeneratedEnvironmentVariableNameLength) + value := randVariableString(r, maxGeneratedEnvironmentVariableValueLength) + if rule.UseNameValue { + rule.Name = name + rule.NameStrategy = EnvVarRuleString + rule.Value = value + rule.ValueStrategy = EnvVarRuleString + } else { + rule.Rule = fmt.Sprintf("%s=%s", name, value) + rule.Strategy = EnvVarRuleString } rules = append(rules, rule) } @@ -2438,6 +2448,12 @@ func generateEnvironmentVariableRules(r *rand.Rand) []EnvRuleConfig { return rules } +func generateRandomEnvironmentVariable(r *rand.Rand) string { + name := randVariableString(r, maxGeneratedEnvironmentVariableNameLength) + value := randVariableString(r, maxGeneratedEnvironmentVariableValueLength) + return fmt.Sprintf("%s=%s", name, value) +} + func generateExecProcesses(r *rand.Rand) []containerExecProcess { var processes []containerExecProcess @@ -2507,15 +2523,26 @@ func generateEnvironmentVariables(r *rand.Rand) []string { numVars := atLeastOneAtMost(r, maxGeneratedEnvironmentVariables) for i := 0; i < int(numVars); i++ { - variable := randVariableString(r, maxGeneratedEnvironmentVariableRuleLength) + variable := generateRandomEnvironmentVariable(r) envVars = append(envVars, variable) } return envVars } -func generateNeverMatchingEnvironmentVariable(r *rand.Rand) string { - return randString(r, maxGeneratedEnvironmentVariableRuleLength+1) +func envRuleToStr(rule EnvRuleConfig) string { + if rule.UseNameValue { + if strings.Contains(rule.Name, "=") { + panic(fmt.Sprintf("expected env rule name %q to not contain '='", rule.Name)) + } + return fmt.Sprintf("%s=%s", rule.Name, rule.Value) + } else { + return rule.Rule + } +} + +func hasRegexInRule(rule EnvRuleConfig) bool { + return rule.Strategy == EnvVarRuleRegex || rule.NameStrategy == EnvVarRuleRegex || rule.ValueStrategy == EnvVarRuleRegex } func buildEnvironmentVariablesFromEnvRules(rules []EnvRuleConfig, r *rand.Rand) []string { @@ -2533,8 +2560,8 @@ func buildEnvironmentVariablesFromEnvRules(rules []EnvRuleConfig, r *rand.Rand) // tests for _, rule := range rules { if rule.Required { - if rule.Strategy != EnvVarRuleRegex { - vars = append(vars, rule.Rule) + if !hasRegexInRule(rule) { + vars = append(vars, envRuleToStr(rule)) } numberOfMatches-- } @@ -2562,9 +2589,8 @@ func buildEnvironmentVariablesFromEnvRules(rules []EnvRuleConfig, r *rand.Rand) } } - // include it if it's not regex - if rules[anIndex].Strategy != EnvVarRuleRegex { - vars = append(vars, rules[anIndex].Rule) + if !hasRegexInRule(rules[anIndex]) { + vars = append(vars, envRuleToStr(rules[anIndex])) usedIndexes[anIndex] = struct{}{} } numberOfMatches-- diff --git a/pkg/securitypolicy/regopolicy_linux_test.go b/pkg/securitypolicy/regopolicy_linux_test.go index e35883edcb..9b6034289f 100644 --- a/pkg/securitypolicy/regopolicy_linux_test.go +++ b/pkg/securitypolicy/regopolicy_linux_test.go @@ -8,7 +8,6 @@ import ( "encoding/json" "errors" "fmt" - "math/rand" "os" "path" "path/filepath" @@ -1233,7 +1232,8 @@ func Test_Rego_EnforceEnvironmentVariablePolicy_NotAllMatches(t *testing.T) { return false } - envList := append(tc.envList, generateNeverMatchingEnvironmentVariable(testRand)) + // Generate a new random env var that will not be in the allowed list + envList := append(tc.envList, generateRandomEnvironmentVariable(testRand)) _, _, _, err = tc.policy.EnforceCreateContainerPolicy(p.ctx, tc.sandboxID, tc.containerID, tc.argList, envList, tc.workingDir, tc.mounts, false, tc.noNewPrivileges, tc.user, tc.groups, tc.umask, tc.capabilities, tc.seccomp) // not getting an error means something is broken @@ -1241,7 +1241,8 @@ func Test_Rego_EnforceEnvironmentVariablePolicy_NotAllMatches(t *testing.T) { return false } - return assertDecisionJSONContains(t, err, "invalid env list", envList[0]) + anyKeyInConstraints := strings.Split(envList[0], "=")[0] + return assertDecisionJSONContains(t, err, "invalid env list", anyKeyInConstraints) } if err := quick.Check(f, &quick.Config{MaxCount: 50, Rand: testRand}); err != nil { @@ -1481,7 +1482,11 @@ func Test_Rego_EnforceCreateContainer(t *testing.T) { _, _, _, err = tc.policy.EnforceCreateContainerPolicy(p.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 - return err == nil + if err != nil { + t.Error(err) + return false + } + return true } if err := quick.Check(f, &quick.Config{MaxCount: 50, Rand: testRand}); err != nil { @@ -3053,13 +3058,9 @@ exec_external := { "env_list": ["%s"] }` - generateEnv := func(r *rand.Rand) string { - return randVariableString(r, maxGeneratedEnvironmentVariableRuleLength) - } - generateEnvs := func(envSet stringSet) []string { numVars := atLeastOneAtMost(testRand, maxGeneratedEnvironmentVariableRules) - return envSet.randUniqueArray(testRand, generateEnv, numVars) + return envSet.randUniqueArray(testRand, generateRandomEnvironmentVariable, numVars) } testFunc := func(gc *generatedConstraints) bool { @@ -3217,7 +3218,7 @@ func Test_Rego_EnforceEnvironmentVariablePolicy_MissingRequired(t *testing.T) { // add a rule to re2 match requiredRule := EnvRuleConfig{ Strategy: "string", - Rule: randVariableString(testRand, maxGeneratedEnvironmentVariableRuleLength), + Rule: generateRandomEnvironmentVariable(testRand), Required: true, } @@ -6214,7 +6215,7 @@ func Test_Rego_Enforce_CreateContainer_RequiredEnvMissingHasErrorMessage(t *test container := selectContainerFromContainerList(constraints.containers, testRand) requiredRule := EnvRuleConfig{ Strategy: "string", - Rule: randVariableString(testRand, maxGeneratedEnvironmentVariableRuleLength), + Rule: generateRandomEnvironmentVariable(testRand), Required: true, } @@ -6468,7 +6469,7 @@ func Test_Rego_EnforceCreateContainer_RetryEverything(t *testing.T) { func Test_Rego_ExecInContainerPolicy_RequiredEnvMissingHasErrorMessage(t *testing.T) { constraints := generateConstraints(testRand, 1) container := selectContainerFromContainerList(constraints.containers, testRand) - neededEnv := randVariableString(testRand, maxGeneratedEnvironmentVariableRuleLength) + neededEnv := generateRandomEnvironmentVariable(testRand) requiredRule := EnvRuleConfig{ Strategy: "string", Rule: neededEnv, @@ -6514,7 +6515,7 @@ func Test_Rego_ExecInContainerPolicy_RequiredEnvMissingHasErrorMessage(t *testin func Test_Rego_ExecExternalProcessPolicy_RequiredEnvMissingHasErrorMessage(t *testing.T) { constraints := generateConstraints(testRand, 1) process := generateExternalProcess(testRand) - neededEnv := randVariableString(testRand, maxGeneratedEnvironmentVariableRuleLength) + neededEnv := generateRandomEnvironmentVariable(testRand) requiredRule := EnvRuleConfig{ Strategy: "string", Rule: neededEnv, diff --git a/pkg/securitypolicy/regopolicy_windows_test.go b/pkg/securitypolicy/regopolicy_windows_test.go index 33b49a64f8..eaa8fa85b5 100644 --- a/pkg/securitypolicy/regopolicy_windows_test.go +++ b/pkg/securitypolicy/regopolicy_windows_test.go @@ -7,7 +7,6 @@ import ( "context" _ "embed" "fmt" - "math/rand" "strconv" "strings" "testing" @@ -89,7 +88,7 @@ func Test_Rego_EnforceEnvironmentVariablePolicy_NotAllMatches_Windows(t *testing return false } - envList := append(tc.envList, generateNeverMatchingEnvironmentVariable(testRand)) + envList := append(tc.envList, generateRandomEnvironmentVariable(testRand)) _, _, _, err = tc.policy.EnforceCreateContainerPolicyV2(p.ctx, tc.containerID, tc.argList, envList, tc.workingDir, tc.mounts, tc.user, nil) @@ -570,13 +569,9 @@ exec_external := { "env_list": ["%s"] }` - generateEnv := func(r *rand.Rand) string { - return randVariableString(r, maxGeneratedEnvironmentVariableRuleLength) - } - generateEnvs := func(envSet stringSet) []string { numVars := atLeastOneAtMost(testRand, maxGeneratedEnvironmentVariableRules) - return envSet.randUniqueArray(testRand, generateEnv, numVars) + return envSet.randUniqueArray(testRand, generateRandomEnvironmentVariable, numVars) } testFunc := func(gc *generatedWindowsConstraints) bool { @@ -729,7 +724,7 @@ func Test_Rego_EnforceEnvironmentVariablePolicy_MissingRequired_Windows(t *testi // add a rule to re2 match requiredRule := EnvRuleConfig{ Strategy: "string", - Rule: randVariableString(testRand, maxGeneratedEnvironmentVariableRuleLength), + Rule: generateRandomEnvironmentVariable(testRand), Required: true, } diff --git a/pkg/securitypolicy/securitypolicy.go b/pkg/securitypolicy/securitypolicy.go index f1d761d439..ebc439ba51 100644 --- a/pkg/securitypolicy/securitypolicy.go +++ b/pkg/securitypolicy/securitypolicy.go @@ -107,6 +107,14 @@ type EnvRuleConfig struct { Strategy EnvVarRule `json:"strategy" toml:"strategy"` Rule string `json:"rule" toml:"rule"` Required bool `json:"required" toml:"required"` + + // If UseNameValue is true, the marshalled Rego will use rules with name and + // value separately, and ignore .Rule and .Strategy. + UseNameValue bool + Name string + NameStrategy EnvVarRule + Value string + ValueStrategy EnvVarRule } type IDNameConfig struct { diff --git a/pkg/securitypolicy/securitypolicy_marshal.go b/pkg/securitypolicy/securitypolicy_marshal.go index 665dc9e4f0..01adc59cb9 100644 --- a/pkg/securitypolicy/securitypolicy_marshal.go +++ b/pkg/securitypolicy/securitypolicy_marshal.go @@ -370,7 +370,11 @@ func writeCommand(builder *strings.Builder, command []string, indent string) { } func (e EnvRuleConfig) marshalRego() string { - return fmt.Sprintf("{\"pattern\": `%s`, \"strategy\": \"%s\", \"required\": %v}", e.Rule, e.Strategy, e.Required) + if e.UseNameValue { + return fmt.Sprintf("{\"name\": `%s`, \"name_strategy\": \"%s\", \"value\": `%s`, \"value_strategy\": \"%s\", \"required\": %v}", e.Name, e.NameStrategy, e.Value, e.ValueStrategy, e.Required) + } else { + return fmt.Sprintf("{\"pattern\": `%s`, \"strategy\": \"%s\", \"required\": %v}", e.Rule, e.Strategy, e.Required) + } } type envRuleArray []EnvRuleConfig From 78cf88921362bbb4a876400bfdcb746293f5e449 Mon Sep 17 00:00:00 2001 From: Tingmao Wang Date: Tue, 4 Nov 2025 10:05:38 +0000 Subject: [PATCH 3/9] rego: Increment framework version to 0.5.0 Signed-off-by: Tingmao Wang --- pkg/securitypolicy/version_framework | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/securitypolicy/version_framework b/pkg/securitypolicy/version_framework index 267577d47e..8f0916f768 100644 --- a/pkg/securitypolicy/version_framework +++ b/pkg/securitypolicy/version_framework @@ -1 +1 @@ -0.4.1 +0.5.0 From 958756731c944c12ad62c345f8e24417b2ee3b6f Mon Sep 17 00:00:00 2001 From: Tingmao Wang Date: Wed, 4 Mar 2026 22:22:07 +0000 Subject: [PATCH 4/9] --------------------------------------------- From 37e5cbf8c8b70b021102c2084749539b974cfa83 Mon Sep 17 00:00:00 2001 From: Tingmao Wang Date: Tue, 18 Nov 2025 15:46:37 +0000 Subject: [PATCH 5/9] 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 9e2995a00c..7bca54e591 100644 --- a/pkg/securitypolicy/framework.rego +++ b/pkg/securitypolicy/framework.rego @@ -289,21 +289,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 b36099023ca8702f54603914326d4f2c65434733 Mon Sep 17 00:00:00 2001 From: Tingmao Wang Date: Tue, 18 Nov 2025 20:38:07 +0000 Subject: [PATCH 6/9] 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 7bca54e591..807e92038f 100644 --- a/pkg/securitypolicy/framework.rego +++ b/pkg/securitypolicy/framework.rego @@ -138,25 +138,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 d59538ed2f73a12864f05468d93c55ffca27fdda Mon Sep 17 00:00:00 2001 From: Tingmao Wang Date: Tue, 18 Nov 2025 23:38:56 +0000 Subject: [PATCH 7/9] 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 | 104 ++++++++++++++++++++++++++++-- 1 file changed, 99 insertions(+), 5 deletions(-) diff --git a/pkg/securitypolicy/framework.rego b/pkg/securitypolicy/framework.rego index 807e92038f..e03389a163 100644 --- a/pkg/securitypolicy/framework.rego +++ b/pkg/securitypolicy/framework.rego @@ -160,7 +160,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} @@ -1188,18 +1199,25 @@ 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_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 @@ -1229,6 +1247,22 @@ apply_defaults("fragment", raw_values, framework_version) := 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 @@ -1237,7 +1271,8 @@ extract_fragment_includes(includes) := fragment { objects := { "containers": apply_defaults("container", fragment_containers, framework_version), "fragments": apply_defaults("fragment", fragment_fragments, framework_version), - "external_processes": apply_defaults("external_process", fragment_external_processes, framework_version) + "external_processes": apply_defaults("external_process", fragment_external_processes, framework_version), + "platform_rules": apply_defaults("platform_rules", fragment_platform_rules, framework_version), } fragment := { @@ -1502,6 +1537,65 @@ registry_changes := {"allowed": true} { } } +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 f9db39ff88467a4af9a7e87764bd28a7edcdb434 Mon Sep 17 00:00:00 2001 From: Tingmao Wang Date: Wed, 19 Nov 2025 15:02:29 +0000 Subject: [PATCH 8/9] 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 54f68b9adf..1e3dbd81f8 100644 --- a/pkg/securitypolicy/securitypolicyenforcer_rego.go +++ b/pkg/securitypolicy/securitypolicyenforcer_rego.go @@ -374,9 +374,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 389a86a629b2ea618df7fba305edac76a98c70a4 Mon Sep 17 00:00:00 2001 From: Tingmao Wang Date: Fri, 21 Nov 2025 01:11:35 +0000 Subject: [PATCH 9/9] 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 | 387 ++++++++++++++++++ 4 files changed, 549 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 3736d54f6a..7a728c1caf 100644 --- a/pkg/securitypolicy/rego_utils_test.go +++ b/pkg/securitypolicy/rego_utils_test.go @@ -876,6 +876,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)) @@ -899,27 +923,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 9b6034289f..3018693255 100644 --- a/pkg/securitypolicy/regopolicy_linux_test.go +++ b/pkg/securitypolicy/regopolicy_linux_test.go @@ -4,6 +4,8 @@ package securitypolicy import ( + _ "embed" + "context" "encoding/json" "errors" @@ -7643,6 +7645,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, platformFragment.issuer, platformFragment.feed, 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, platformFragment.issuer, platformFragment.feed, platformRulesFragmentPolicyCode) + if err != nil { + t.Fatalf("failed to load platform rules fragment: %v", err) + } + } + for _, containerFragment := range containerFragments { + err = policy.LoadFragment(gc.ctx, containerFragment.info.issuer, containerFragment.info.feed, containerFragment.code) + if err != nil { + t.Fatalf("failed to load container fragment: %v", err) + } + } + } else { + for _, containerFragment := range containerFragments { + err = policy.LoadFragment(gc.ctx, containerFragment.info.issuer, containerFragment.info.feed, containerFragment.code) + if err != nil { + t.Fatalf("failed to load container fragment: %v", err) + } + } + if !skipLoadPlatformRulesFragment { + err = policy.LoadFragment(gc.ctx, platformFragment.issuer, platformFragment.feed, 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(), containerFragment.info.issuer, containerFragment.info.feed, 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 @@ -7897,3 +8267,20 @@ func Test_Rego_SandboxSysfsCarveOut_PrivilegedRequestDenied(t *testing.T) { } assertDecisionJSONContains(t, err, "privileged escalation not allowed") } + +//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 +} + +const paramTestImageBaseLayer = "0000000000000000000000000000000000000000000000000000000000000000"