Skip to content

rego: Fragment specialization#2789

Open
micromaomao wants to merge 9 commits into
microsoft:mainfrom
micromaomao:fragment-specialization-mainrebase
Open

rego: Fragment specialization#2789
micromaomao wants to merge 9 commits into
microsoft:mainfrom
micromaomao:fragment-specialization-mainrebase

Conversation

@micromaomao

Copy link
Copy Markdown
Member

This PR implements the "fragment specialization" aka. fragment with parameters feature, along with a new way to express env-rules using separate "name" / "value" fields.

We add a new parameters object in the fragment import statements. On fragment loads, the parameters will be passed to the fragment via an automatically injected object added to the end of the fragment's Rego source, and can be accessed using the parameter() function, which is also runtime injected.

The fragment itself also needs to define an additional parameters_api object, which declares the available parameters this fragment can consume, and optionally specify default values. It is expected that the policy tooling will use this object to validate the provided parameters when generating a policy with fragments.

Example fragment using this feature:

package fragment_parameter_test

svn := "1"
framework_version := "0.5.0"

parameters_api := {
	"infraListenerTitle": {
		"default": "Default Title"
	},
	"infraListenerPort": {
		"default": "8000"
	}
}

containers := [
	{
		"allow_elevated": false,
		"allow_stdio_access": true,
		"capabilities": null,
		"command": [
			"python3",
			"/infra.py"
		],
		"env_rules": [
			{
				"pattern": "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
				"required": false,
				"strategy": "string"
			},
			{
				"pattern": "PYTHONUNBUFFERED=1",
				"required": false,
				"strategy": "string"
			},
			{
				"pattern": "TERM=xterm",
				"required": false,
				"strategy": "string"
			},
			{
				"name": "INTRA_LISTENER_TITLE",
				"name_strategy": "string",
				"value": parameter("infraListenerTitle"),
				"value_strategy": "string"
			},
			{
				"name": "INFRA_LISTENER_PORT",
				"name_strategy": "string",
				"value": parameter("infraListenerPort"),
				"value_strategy": "string"
			}
		],
		"exec_processes": [],
		"id": "cacidashboardaci.azurecr.io/fragment-parameter/sidecar:latest",
		"layers": [
			"b8f2037e35c1ae3d5b32939a41d864bcf853d63ab2930e76b7d115a966134924",
			"40ae177d502bec7f43aeedee803eee8063228120ea0d36434d2f8afc9f3f057e",
			"11279bbe7668b1c5076c7a565b0c5888995dd5c938a4bb6a35f8869ebb696bbf"
		],
		"mounts": [],
		"name": "cacidashboardaci.azurecr.io/fragment-parameter/sidecar:latest",
		"no_new_privileges": false,
		"seccomp_profile_sha256": "",
		"signals": [
			9,
			15
		],
		"user": {
			"group_idnames": [
				{
					"pattern": "",
					"strategy": "any"
				}
			],
			"umask": "0022",
			"user_idname": {
				"pattern": "",
				"strategy": "any"
			}
		},
		"working_dir": "/"
	}
]

Example policy importing this fragment:

package policy

api_version := "0.11.0"
framework_version := "0.5.0"

fragments := [
	{
		"feed": "mcr.microsoft.com/aci/aci-cc-infra-fragment",
		"includes": [
			"containers",
			"fragments",
			"platform_rules"
		],
		"issuer": "did:x509:0:sha256:cjKyji1crjt6AGHVTkOmjXaZuI1d0rBB1kU8OgqRXfs::subject:CN:skr",
		"minimum_svn": "1"
	},
	{
		"feed": "cacidashboardaci.azurecr.io/fragment-parameter/sidecar",
		"includes": [
			"containers",
			"fragments"
		],
		"issuer": "did:x509:0:sha256:cjKyji1crjt6AGHVTkOmjXaZuI1d0rBB1kU8OgqRXfs::subject:CN:skr",
		"minimum_svn": "1",
		"parameters": {
			"infraListenerTitle": "Fragment specialization & platform rules"
		}
	}
]

containers := [
	{
		"allow_elevated": false,
		"allow_stdio_access": true,
		"capabilities": null,
		"command": [
			"/pause"
		],
		"env_rules": [
			{
				"pattern": "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
				"required": true,
				"strategy": "string"
			},
			{
				"pattern": "TERM=xterm",
				"required": false,
				"strategy": "string"
			}
		],
		"exec_processes": [],
		"layers": [
			"16b514057a06ad665f92c02863aca074fd5976c755d26bff16365299169e8415"
		],
		"mounts": [],
		"name": "pause-container",
		"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 := true
allow_runtime_logging := true
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
mount_overlay := data.framework.mount_overlay
unmount_overlay := data.framework.unmount_overlay
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 := {"errors": data.framework.errors}

If multiple fragment parameters are specified, all combinations of values are considered "allowed", and therefore the fragment injection is repeated, passing in different parameter combinations, one for each combination. This means that if a fragment defines, for example:

containers := [
	{
		...
		"env_rules": [
			{
				"name": "SOME_KEY",
				"name_strategy": "string",
				"value": parameter("my_param"),
				"value_strategy": "string"
			}
		]
	}
]

and we have two fragment import statement for this fragment, e.g.:

fragments := [
	{
		"feed": ...,
		"issuer": ...,
		"minimum_svn": ...,
		"parameters": {
			"my_param": "value1"
		}
	},
	{
		"feed": ...,
		"issuer": ...,
		"minimum_svn": ...,
		"parameters": {
			"my_param": "value2"
		}
	}
]

Then the container can be started with either SOME_KEY=value1 or SOME_KEY=value2.

In implementing the fragment parameters feature, we need a way to store, for
each issuer and feed tuple, which parameters should be used for fragments
matching it.  Consider this list of fragments, which a parent fragment might
define:

fragments := [
    {
        "feed": "mcr.microsoft.com/maa/enclavehost",
        "issuer": "did:x509:0:sha256:...",
        "includes": [
            "containers",
            "fragments"
        ],
        "minimum_svn": "1",
        "parameters": {
            "region": "australiacentral2",
            "cloud": "Public"
        }
    },
    // ...
]

When we load the fragment containing this, we need to iterate through its
data.<namespace>.fragments array, and for each entry, append the parameters
object to an array that is keyed by the issuer and feed, since we can have
multiple such fragment import entries for the same issuer and feed, but with
different parameters.

This basically means that we need the equivalent of the following code in Rego,
which I've failed to come up with a way to write:

    for f in fragment_fragments {
        issuer = data.metadata.issuers[f.issuer] or {}
        feed = issuer.feeds[f.feed] or {}
        feed.parameters = feed.parameters or []
        feed.parameters = array_append(feed.parameters, f.parameters)
        issuer.feeds[f.feed] = feed
        data.metadata.issuers[f.issuer] = issuer
    }

While we can dynamically lookup or store based on a key that is from a variable
access, and we can send multiple metadata updates, those updates cannot express
the semantic of "merging" objects or arrays.

To make this simpler, we introduce a new metadata data type - "set", which
supports storing an unordered list of arbitrary objects (which themselves might
be either a string, or an object containing key-value pairs), that can be
inserted to via metadata operations one at a time.  This means that we can
simply append to the metadata update operations array once per each fragment
import statement, and the outcome would be the union of all the parameter
objects.  This is also very easy to query in Rego, given an issuer and feed:

    parameters := [
        fp.parameters |
            fp = data.metadata.fragment_parameters[_]
            fp.issuer == input.issuer
            fp.feed == input.feed
    ]

It is likely that the existing code that extracts included containers/fragments
from a fragment can be simplified by using this feature, but that is outside of
the scope of this PR.

Signed-off-by: Tingmao Wang <tingmaowang@microsoft.com>
…and improve comments

Signed-off-by: Tingmao Wang <tingmaowang@microsoft.com>
…ching 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 <tingmaowang@microsoft.com>
Signed-off-by: Tingmao Wang <tingmaowang@microsoft.com>
This commit implements the support for the "parameters" feature for fragments.
This is achieved by having the framework extract the parameters object from
fragment import statements, and storing them in a set, tagged with the
applicable (issuer, feed) pair.  On future fragment loads, if the fragment's
issuer and feed pair matches with any previous entry from this set, the
parameters will be passed to the fragment via an automatically injected object
added to the end of the fragment's Rego source (__fragment_parameters).

(Note that in reality, we need to combine this set with the import statements
defined in the main policy. c.f. candidate_fragments.  This is done in
fragment_parameters_for)

In order for fragments to use this parameter object, it is expected that all
fragments will now have an additional "stub" inserted at runtime to define the
parameter() function, which will, under the hood, reference a "hidden" variable
__fragment_parameters, also inserted dynamically at runtime, containing the
actual parameter values.

If multiple fragment parameters are specified, all combinations of values are
considered "allowed", and therefore the fragment injection is repeated, passing
in different parameter combinations, one for each combination.  This means that
if a fragment defines, for example:

	containers := [
		{
			...
			"env_rules": [
				{
					"name": "SOME_KEY",
					"name_strategy": "string",
					"value": parameter("my_param"),
					"value_strategy": "string"
				}
			]
		}
	]

and we have two fragment import statement for this fragment, e.g.:

fragments := [
	{
		"feed": ...,
		"issuer": ...,
		"minimum_svn": ...,
		"parameters": {
			"my_param": "value1"
		}
	},
	{
		"feed": ...,
		"issuer": ...,
		"minimum_svn": ...,
		"parameters": {
			"my_param": "value2"
		}
	}
]

Then the container can be started with either SOME_KEY=value1 or
SOME_KEY=value2.

Signed-off-by: Tingmao Wang <tingmaowang@microsoft.com>
---

Changes:
- Fix missing [_]
- fix wrong usage of defer
- Inject fragment parameter function definitions at runtime:

  We must inject this at runtime so as to avoid having to support this exact
  implementation (eg __fragment_parameters[name]) of fetching the parameters
  forever (as it will essentially be hard-coded in the generated policy).

- Move the actual implementation of parameter() into the framework so that
  fragments doesn't have to say 'import future.keywords.every', 'import
  future.keywords.in'.

- Use indirection with default when accessing fragment.parameters to not break
  older fragments.

  If we don't do this, loading a fragment without the parameters definition
  would fail due to "unsafe" rego variable references.

- Fix broken tests caused by the new extract_parameter framework function

Signed-off-by: Tingmao Wang <tingmaowang@microsoft.com>
Signed-off-by: Tingmao Wang <tingmaowang@microsoft.com>
…ing env list

If every element in the input env list matches some env rule in some container,
but there is no container that matches the entire env list, we currently deny
correctly but returns no error message.

Signed-off-by: Tingmao Wang <tingmaowang@microsoft.com>
This will test for scenarios like different fragments using the same parameter
name, multiple parameter combinations, correct parameter passing for nested
fragments, and make sure container creation is denied if the parameter and the
given environment / command doesn't match.

Signed-off-by: Tingmao Wang <tingmaowang@microsoft.com>
Name still provisional, might change later.

Signed-off-by: Tingmao Wang <tingmaowang@microsoft.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds “fragment specialization” (fragment parameters) to the Rego security policy framework by allowing fragment import statements to provide a parameters object, injecting those parameters (and a parameter() helper) into fragment Rego at load time, and extending fragment metadata handling to support nested fragments and parameter propagation.

Changes:

  • Injects fragment parameter definitions into fragment Rego at load time and loads fragments once per allowed parameter set.
  • Extends framework.rego to (a) support env-rules expressed as either pattern/strategy or name/value with separate strategies, and (b) propagate fragment parameters via metadata.
  • Adds “set” support to the rego policy interpreter metadata operations and introduces extensive Linux tests + embedded fragment policy fixtures.

Reviewed changes

Copilot reviewed 20 out of 20 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
pkg/securitypolicy/version_framework Bumps framework version to 0.5.0.
pkg/securitypolicy/securitypolicyenforcer_rego.go Injects parameter support into fragment modules and iterates fragment loading across parameter possibilities.
pkg/securitypolicy/securitypolicy.go Extends EnvRuleConfig with name/value-based env-rule fields.
pkg/securitypolicy/securitypolicy_marshal.go Marshals new env-rule form and adds fragment parameters to fragment import emission.
pkg/securitypolicy/securitypolicy_internal.go Extends internal fragment representation to carry parameters.
pkg/securitypolicy/framework.rego Adds env-rule dual-form matching and fragment-parameter metadata plumbing + helper extraction rule.
pkg/securitypolicy/fragment_definition.rego Defines injected parameter() helper and fragment parameter metadata accessor.
pkg/securitypolicy/regopolicy_linux_test.go Updates existing tests and adds comprehensive fragment-parameter test coverage with embedded Rego fixtures.
pkg/securitypolicy/rego_utils_test.go Updates env var generators/helpers to better model NAME=VALUE strings and env-rule variations.
pkg/securitypolicy/fragment_test_policies/_container_common.rego.inc Shared fixture include for fragment tests.
pkg/securitypolicy/fragment_test_policies/simple_env_rule_param.rego Fixture fragment consuming an env-rule parameter.
pkg/securitypolicy/fragment_test_policies/env_rule_param.rego Fixture fragment consuming multiple parameter shapes (object + string).
pkg/securitypolicy/fragment_test_policies/env_rule_param_another_fragment.rego Fixture fragment for multi-fragment/parameter name collision tests.
pkg/securitypolicy/fragment_test_policies/nested_importer.rego Fixture fragment that imports nested fragments with parameterized parameters.
pkg/securitypolicy/fragment_test_policies/nested_importer_2.rego Second nested-importer fixture to expand parameter combinations.
pkg/securitypolicy/fragment_test_policies/nested_fragment.rego Nested fragment fixture consuming parameters with defaults.
pkg/securitypolicy/fragment_test_policies/param_on_command.rego Fixture fragment parameterizing command arrays.
internal/regopolicyinterpreter/regopolicyinterpreter.go Adds metadata “set” type support and an Array accessor on query results.
internal/regopolicyinterpreter/test.rego Adds test helpers for set add/remove/contains/get.
internal/regopolicyinterpreter/regopolicyinterpreter_test.go Adds property tests covering set metadata semantics and updates metadata copy assertions.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +113 to +117
UseNameValue bool
Name string
NameStrategy EnvVarRule
Value string
ValueStrategy EnvVarRule
Comment on lines +8449 to +8454
for i_fragment, f := range fragmentsToLoad {
err = policy.LoadFragment(gc.ctx, topFragmentIssuer, fmt.Sprintf(topFragmentFeedFmt, i_fragment), paramTestTemplateFragmentCode(f.fragmentCode))
}
if err != nil {
t.Fatalf("failed to load fragment: %v", err)
}
Comment on lines +52 to +56
maxGeneratedEnvironmentVariables = 16
maxGeneratedEnvironmentVariableNameLength = 31
maxGeneratedEnvironmentVariableValueLength = 32
maxGeneratedEnvironmentVariableRules = 8
maxGeneratedFragmentNamespaceLength = 32
Comment on lines +580 to +583
paramsJson, err := json.Marshal(f.parameters)
if err != nil {
panic(fmt.Errorf("failed to marshal fragment parameters object to JSON: %w", err))
}
Comment on lines +237 to +238
# A env rule can be of two form:
# {
@micromaomao

Copy link
Copy Markdown
Member Author

@KenGordon I was discussing with Takuro on whether this scheme would allow a main policy writer to say "allow any values from set A for parameter A, and allow any values from set B for parameter B". Currently this requires specifying all combinations of allowed parameters, so they would have to have len(set A)*len(set B) fragment allow rules, first allowing paramA=a1, paramB=b1, then paramA=a1, paramB=b2, ..., then paramA=a2, paramB=b1, ...

Do you think the current scheme is flexible enough as it is? In theory they can allow any combination of parameter values they specify, but if their rule is very lax and they have more parameters, the number of combinations they need to allow grows exponentially.

If this case would be rare then we can get away with not worrying about it, but wanted to check with you.

@micromaomao

Copy link
Copy Markdown
Member Author

@takuro-sato Ken was happy with not doing anything special for allowing set of values for now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants