From c0cd5e8ff089b6cc67a44227a2db565b9cb6781a Mon Sep 17 00:00:00 2001 From: "Sean T. Allen" Date: Fri, 3 Sep 2021 10:08:46 -0400 Subject: [PATCH] Add security policy enforcement of environment variables Supports two different matching schemes: - string This is a direct string match. All characters must be equal. - re2 The rule is an re2 regular expression that will be matched against the environment variable. Environment variables are in the form "KEY=VALUE" as a single string. The securitypolicy tool has been updated to automatically include any environment variables defined in the image spec for an image to the allowed environment variables in the generated policy. Signed-off-by: Sean T. Allen --- internal/guest/runtime/hcsv2/uvm.go | 3 +- .../mountmonitoringsecuritypolicyenforcer.go | 2 +- internal/tools/securitypolicy/README.md | 49 ++- internal/tools/securitypolicy/main.go | 93 ++++- pkg/securitypolicy/securitypolicy.go | 7 + pkg/securitypolicy/securitypolicy_test.go | 319 +++++++++++++++++- pkg/securitypolicy/securitypolicyenforcer.go | 104 +++++- test/go.sum | 3 + .../pkg/securitypolicy/securitypolicy.go | 7 + .../securitypolicy/securitypolicyenforcer.go | 104 +++++- 10 files changed, 620 insertions(+), 71 deletions(-) diff --git a/internal/guest/runtime/hcsv2/uvm.go b/internal/guest/runtime/hcsv2/uvm.go index e494118cc1..e60532260e 100644 --- a/internal/guest/runtime/hcsv2/uvm.go +++ b/internal/guest/runtime/hcsv2/uvm.go @@ -159,7 +159,8 @@ func (h *Host) CreateContainer(ctx context.Context, id string, settings *prot.VM return nil, gcserr.NewHresultError(gcserr.HrVmcomputeSystemAlreadyExists) } - err = h.securityPolicyEnforcer.EnforceCommandPolicy(id, settings.OCISpecification.Process.Args) + err = h.securityPolicyEnforcer.EnforceStartContainerPolicy(id, settings.OCISpecification.Process.Args, settings.OCISpecification.Process.Env) + if err != nil { return nil, errors.Wrapf(err, "container creation denied due to policy") } diff --git a/internal/guest/storage/test/policy/mountmonitoringsecuritypolicyenforcer.go b/internal/guest/storage/test/policy/mountmonitoringsecuritypolicyenforcer.go index 44bdab94c6..4ac690d5e8 100644 --- a/internal/guest/storage/test/policy/mountmonitoringsecuritypolicyenforcer.go +++ b/internal/guest/storage/test/policy/mountmonitoringsecuritypolicyenforcer.go @@ -23,6 +23,6 @@ func (p *MountMonitoringSecurityPolicyEnforcer) EnforceOverlayMountPolicy(contai return nil } -func (p *MountMonitoringSecurityPolicyEnforcer) EnforceCommandPolicy(containerID string, argList []string) (err error) { +func (p *MountMonitoringSecurityPolicyEnforcer) EnforceStartContainerPolicy(containerID string, argList []string, envList []string) (err error) { return nil } diff --git a/internal/tools/securitypolicy/README.md b/internal/tools/securitypolicy/README.md index a6a4ce05bb..24aff7425c 100644 --- a/internal/tools/securitypolicy/README.md +++ b/internal/tools/securitypolicy/README.md @@ -20,6 +20,10 @@ be downloaded, turned into an ext4, and finally a dm-verity root hash calculated [[image]] name = "rust:1.52.1" command = ["rustc", "--help"] + +[[image.env_rule]] +strategy = "re2" +rule = "PREFIX_.+=.+" ``` ### Converted to JSON @@ -32,13 +36,54 @@ represented in JSON. "allow_all": false, "containers": [ { - "command": ["/pause"], + "command": [ + "/pause" + ], + "env_rules": [ + { + "strategy": "string", + "rule": "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + }, + { + "strategy": "string", + "rule": "TERM=xterm" + } + ], "layers": [ "16b514057a06ad665f92c02863aca074fd5976c755d26bff16365299169e8415" ] }, { - "command": ["rustc", "--help"], + "command": [ + "rustc", + "--help" + ], + "env_rules": [ + { + "strategy": "re2", + "rule": "PREFIX_.+=.+" + }, + { + "strategy": "string", + "rule": "TERM=xterm" + }, + { + "strategy": "string", + "rule": "PATH=/usr/local/cargo/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + }, + { + "strategy": "string", + "rule": "RUSTUP_HOME=/usr/local/rustup" + }, + { + "strategy": "string", + "rule": "CARGO_HOME=/usr/local/cargo" + }, + { + "strategy": "string", + "rule": "RUST_VERSION=1.52.1" + } + ], "layers": [ "fe84c9d5bfddd07a2624d00333cf13c1a9c941f3a261f13ead44fc6a93bc0e7a", "4dedae42847c704da891a28c25d32201a1ae440bce2aecccfa8e6f03b97a6a6c", diff --git a/internal/tools/securitypolicy/main.go b/internal/tools/securitypolicy/main.go index 27cef7ff08..f8a360951d 100644 --- a/internal/tools/securitypolicy/main.go +++ b/internal/tools/securitypolicy/main.go @@ -7,6 +7,7 @@ import ( "fmt" "io/ioutil" "os" + "regexp" "github.com/BurntSushi/toml" "github.com/Microsoft/hcsshim/ext4/dmverity" @@ -78,9 +79,15 @@ func main() { } } +type EnvironmentVariableRule struct { + Strategy string `toml:"strategy"` + Rule string `toml:"rule"` +} + type Image struct { - Name string `toml:"name"` - Command []string `toml:"command"` + Name string `toml:"name"` + Command []string `toml:"command"` + EnvRules []EnvironmentVariableRule `toml:"env_rule"` } type Config struct { @@ -97,16 +104,6 @@ func createOpenDoorPolicy() sp.SecurityPolicy { func createPolicyFromConfig(config Config) (sp.SecurityPolicy, error) { p := sp.SecurityPolicy{} - // for now, we hardcode the pause container version 3.1 here - // in a final end user tool, we would not do it this way. - // as this is a tool for use by developers currently working - // on security policy implementation code - pausec := sp.SecurityPolicyContainer{ - Command: []string{"/pause"}, - Layers: []string{"16b514057a06ad665f92c02863aca074fd5976c755d26bff16365299169e8415"}, - } - p.Containers = append(p.Containers, pausec) - var imageOptions []remote.Option if len(*username) != 0 && len(*password) != 0 { auth := authn.Basic{ @@ -117,10 +114,25 @@ func createPolicyFromConfig(config Config) (sp.SecurityPolicy, error) { imageOptions = append(imageOptions, authOption) } + // Hardcode the pause container version and command. We still pull it + // to get the root hash and any environment variable rules we might need. + pause := Image{ + Name: "k8s.gcr.io/pause:3.1", + Command: []string{"/pause"}, + EnvRules: []EnvironmentVariableRule{}} + config.Images = append(config.Images, pause) + for _, image := range config.Images { + // validate EnvRules + err := validateEnvRules(image.EnvRules) + if err != nil { + return p, err + } + container := sp.SecurityPolicyContainer{ - Command: image.Command, - Layers: []string{}, + Command: image.Command, + EnvRules: convertEnvironmentVariableRules(image.EnvRules), + Layers: []string{}, } ref, err := name.ParseReference(image.Name) if err != nil { @@ -172,8 +184,61 @@ func createPolicyFromConfig(config Config) (sp.SecurityPolicy, error) { container.Layers = append(container.Layers, hashString) } + // add rules for all known environment variables from the configuration + // these are in addition to "other rules" from the policy definition file + config, err := img.ConfigFile() + if err != nil { + return p, err + } + for _, env := range config.Config.Env { + rule := sp.SecurityPolicyEnvironmentVariableRule{ + Strategy: "string", + Rule: env, + } + + container.EnvRules = append(container.EnvRules, rule) + } + + // cri adds TERM=xterm for all workload containers. we add to all containers + // to prevent any possble erroring + rule := sp.SecurityPolicyEnvironmentVariableRule{ + Strategy: "string", + Rule: "TERM=xterm", + } + + container.EnvRules = append(container.EnvRules, rule) + p.Containers = append(p.Containers, container) } return p, nil } + +func validateEnvRules(rules []EnvironmentVariableRule) error { + for _, rule := range rules { + switch rule.Strategy { + case "re2": + _, err := regexp.Compile(rule.Rule) + if err != nil { + return err + } + } + } + + return nil +} + +func convertEnvironmentVariableRules(toml []EnvironmentVariableRule) []sp.SecurityPolicyEnvironmentVariableRule { + json := make([]sp.SecurityPolicyEnvironmentVariableRule, len(toml)) + + for i, rule := range toml { + jsonRule := sp.SecurityPolicyEnvironmentVariableRule{ + Strategy: rule.Strategy, + Rule: rule.Rule, + } + + json[i] = jsonRule + } + + return json +} diff --git a/pkg/securitypolicy/securitypolicy.go b/pkg/securitypolicy/securitypolicy.go index 18b64f3468..a62a09273c 100644 --- a/pkg/securitypolicy/securitypolicy.go +++ b/pkg/securitypolicy/securitypolicy.go @@ -23,6 +23,8 @@ type SecurityPolicy struct { type SecurityPolicyContainer struct { // The command that we will allow the container to execute Command []string `json:"command"` + // The rules for determining if a given environment variable is allowed + EnvRules []SecurityPolicyEnvironmentVariableRule `json:"env_rules"` // An ordered list of dm-verity root hashes for each layer that makes up // "a container". Containers are constructed as an overlay file system. The // order that the layers are overlayed is important and needs to be enforced @@ -30,6 +32,11 @@ type SecurityPolicyContainer struct { Layers []string `json:"layers"` } +type SecurityPolicyEnvironmentVariableRule struct { + Strategy string `json:"strategy"` + Rule string `json:"rule"` +} + // EncodedSecurityPolicy is a JSON representation of SecurityPolicy that has // been base64 encoded for storage in an annotation embedded within another // JSON configuration diff --git a/pkg/securitypolicy/securitypolicy_test.go b/pkg/securitypolicy/securitypolicy_test.go index 6eccecd4e7..294d78e2e3 100644 --- a/pkg/securitypolicy/securitypolicy_test.go +++ b/pkg/securitypolicy/securitypolicy_test.go @@ -13,13 +13,16 @@ import ( ) const ( - maxContainersInGeneratedPolicy = 32 - maxLayersInGeneratedContainer = 32 - maxGeneratedContainerID = 1000000 - maxGeneratedCommandLength = 128 - maxGeneratedCommandArgs = 12 - maxGeneratedMountTargetLength = 256 - rootHashLength = 64 + maxContainersInGeneratedPolicy = 32 + maxLayersInGeneratedContainer = 32 + maxGeneratedContainerID = 1000000 + maxGeneratedCommandLength = 128 + maxGeneratedCommandArgs = 12 + maxGeneratedEnvironmentVariables = 24 + maxGeneratedEnvironmentVariableRuleLength = 64 + maxGeneratedEnvironmentVariableRules = 12 + maxGeneratedMountTargetLength = 256 + rootHashLength = 64 ) // Do we correctly set up the data structures that are part of creating a new @@ -303,7 +306,7 @@ func Test_EnforceCommandPolicy_Matches(t *testing.T) { return false } - err = policy.EnforceCommandPolicy(containerID, container.Command) + err = policy.enforceCommandPolicy(containerID, container.Command) // getting an error means something is broken return err == nil @@ -335,7 +338,7 @@ func Test_EnforceCommandPolicy_NoMatches(t *testing.T) { return false } - err = policy.EnforceCommandPolicy(containerID, generateCommand(r)) + err = policy.enforceCommandPolicy(containerID, generateCommand(r)) // not getting an error means something is broken return err != nil @@ -372,8 +375,8 @@ func Test_EnforceCommandPolicy_NarrowingMatches(t *testing.T) { return false } - testcontainerOneID := "" - testcontainerTwoID := "" + testContainerOneID := "" + testContainerTwoID := "" indexForContainerOne := -1 indexForContainerTwo := -1 @@ -392,11 +395,11 @@ func Test_EnforceCommandPolicy_NarrowingMatches(t *testing.T) { } if cmp.Equal(container, testContainerOne) { - testcontainerOneID = containerID + testContainerOneID = containerID indexForContainerOne = index } if cmp.Equal(container, testContainerTwo) { - testcontainerTwoID = containerID + testContainerTwoID = containerID indexForContainerTwo = index } } @@ -407,7 +410,7 @@ func Test_EnforceCommandPolicy_NarrowingMatches(t *testing.T) { return false } for _, id := range containerOneMapping { - if (id != testcontainerOneID) && (id != testcontainerTwoID) { + if (id != testContainerOneID) && (id != testContainerTwoID) { return false } } @@ -417,14 +420,14 @@ func Test_EnforceCommandPolicy_NarrowingMatches(t *testing.T) { return false } for _, id := range containerTwoMapping { - if (id != testcontainerOneID) && (id != testcontainerTwoID) { + if (id != testContainerOneID) && (id != testContainerTwoID) { return false } } // enforce command policy for containerOne // this will narrow our list of possible ids down - err = policy.EnforceCommandPolicy(testcontainerOneID, testContainerOne.Command) + err = policy.enforceCommandPolicy(testContainerOneID, testContainerOne.Command) if err != nil { return false } @@ -436,7 +439,7 @@ func Test_EnforceCommandPolicy_NarrowingMatches(t *testing.T) { return false } for _, id := range updatedMapping { - if id != testcontainerTwoID { + if id != testContainerTwoID { return false } } @@ -451,6 +454,218 @@ func Test_EnforceCommandPolicy_NarrowingMatches(t *testing.T) { } } +func Test_EnforceEnvironmentVariablePolicy_Matches(t *testing.T) { + f := func(p *SecurityPolicy) bool { + policy, err := NewStandardSecurityPolicyEnforcer(p) + if err != nil { + return false + } + + r := rand.New(rand.NewSource(time.Now().UnixNano())) + containerID := generateContainerID(r) + container := selectContainerFromPolicy(p, r) + + layerPaths, err := createValidOverlayForContainer(policy, container, r) + if err != nil { + return false + } + + err = policy.EnforceOverlayMountPolicy(containerID, layerPaths) + if err != nil { + return false + } + + envVars := buildEnvironmentVariablesFromContainerRules(container, r) + err = policy.enforceEnvironmentVariablePolicy(containerID, envVars) + + // getting an error means something is broken + return err == nil + } + + if err := quick.Check(f, &quick.Config{MaxCount: 1000}); err != nil { + t.Errorf("Test_EnforceEnvironmentVariablePolicy_Matches: %v", err) + } +} + +func Test_EnforceEnvironmentVariablePolicy_Re2Match(t *testing.T) { + r := rand.New(rand.NewSource(time.Now().UnixNano())) + p := generateSecurityPolicy(r, 1) + + container := generateSecurityPolicyContainer(r, 1) + // add a rule to re2 match + re2MatchRule := SecurityPolicyEnvironmentVariableRule{ + Strategy: "re2", + Rule: "PREFIX_.+=.+"} + container.EnvRules = append(container.EnvRules, re2MatchRule) + p.Containers = append(p.Containers, container) + + policy, err := NewStandardSecurityPolicyEnforcer(p) + if err != nil { + t.Fatalf("expected nil error got: %v", err) + } + + containerID := generateContainerID(r) + + layerPaths, err := createValidOverlayForContainer(policy, container, r) + if err != nil { + t.Fatalf("expected nil error got: %v", err) + } + + err = policy.EnforceOverlayMountPolicy(containerID, layerPaths) + if err != nil { + t.Fatalf("expected nil error got: %v", err) + } + + envVars := []string{"PREFIX_FOO=BAR"} + err = policy.enforceEnvironmentVariablePolicy(containerID, envVars) + + // getting an error means something is broken + if err != nil { + t.Fatalf("expected nil error got: %v", err) + } +} + +func Test_EnforceEnvironmentVariablePolicy_NotAllMatches(t *testing.T) { + f := func(p *SecurityPolicy) bool { + policy, err := NewStandardSecurityPolicyEnforcer(p) + if err != nil { + return false + } + + r := rand.New(rand.NewSource(time.Now().UnixNano())) + containerID := generateContainerID(r) + container := selectContainerFromPolicy(p, r) + + layerPaths, err := createValidOverlayForContainer(policy, container, r) + if err != nil { + return false + } + + err = policy.EnforceOverlayMountPolicy(containerID, layerPaths) + if err != nil { + return false + } + + envVars := generateEnvironmentVariables(r) + envVars = append(envVars, generateNeverMatchingEnvironmentVariable(r)) + err = policy.enforceEnvironmentVariablePolicy(containerID, envVars) + + // not getting an error means something is broken + return err != nil + } + + if err := quick.Check(f, &quick.Config{MaxCount: 1000}); err != nil { + t.Errorf("Test_EnforceEnvironmentVariablePolicy_NotAllMatches: %v", err) + } +} + +// This is a tricky test. +// The key to understanding it is, that when we have multiple containers +// with the same base aka same mounts and overlay, then we don't know at the +// time of overlay which container from policy is a given container id refers +// to. Instead we have a list of possible container ids for the so far matching +// containers in policy. We can narrow down the list of possible containers +// at the time that we enforce environment variables, the same as we do with +// commands. +// +// This test verifies the "narrowing possible container ids that could be +// the container in our policy" functionality works correctly. +func Test_EnforceEnvironmentVariablePolicy_NarrowingMatches(t *testing.T) { + f := func(p *SecurityPolicy) bool { + r := rand.New(rand.NewSource(time.Now().UnixNano())) + // create two additional containers that "share everything" + // except that they have different environment variables + testContainerOne := generateSecurityPolicyContainer(r, 5) + testContainerTwo := testContainerOne + testContainerTwo.EnvRules = generateEnvironmentVariableRules(r) + // add new containers to policy before creating enforcer + p.Containers = append(p.Containers, testContainerOne, testContainerTwo) + + policy, err := NewStandardSecurityPolicyEnforcer(p) + if err != nil { + return false + } + + testContainerOneID := "" + testContainerTwoID := "" + indexForContainerOne := -1 + indexForContainerTwo := -1 + + // mount and overlay all our containers + for index, container := range p.Containers { + containerID := generateContainerID(r) + + layerPaths, err := createValidOverlayForContainer(policy, container, r) + if err != nil { + return false + } + + err = policy.EnforceOverlayMountPolicy(containerID, layerPaths) + if err != nil { + return false + } + + if cmp.Equal(container, testContainerOne) { + testContainerOneID = containerID + indexForContainerOne = index + } + if cmp.Equal(container, testContainerTwo) { + testContainerTwoID = containerID + indexForContainerTwo = index + } + } + + // validate our expectations prior to enforcing command policy + containerOneMapping := policy.ContainerIndexToContainerIds[indexForContainerOne] + if len(containerOneMapping) != 2 { + return false + } + for _, id := range containerOneMapping { + if (id != testContainerOneID) && (id != testContainerTwoID) { + return false + } + } + + containerTwoMapping := policy.ContainerIndexToContainerIds[indexForContainerTwo] + if len(containerTwoMapping) != 2 { + return false + } + for _, id := range containerTwoMapping { + if (id != testContainerOneID) && (id != testContainerTwoID) { + return false + } + } + + // enforce command policy for containerOne + // this will narrow our list of possible ids down + envVars := buildEnvironmentVariablesFromContainerRules(testContainerOne, r) + err = policy.enforceEnvironmentVariablePolicy(testContainerOneID, envVars) + if err != nil { + return false + } + + // Ok, we have full setup and we can now verify that when we enforced + // command policy above that it correctly narrowed down containerTwo + updatedMapping := policy.ContainerIndexToContainerIds[indexForContainerTwo] + if len(updatedMapping) != 1 { + return false + } + for _, id := range updatedMapping { + if id != testContainerTwoID { + return false + } + } + + return true + } + + // This is a more expensive test to run than others, so we run fewer times + // for each run, + if err := quick.Check(f, &quick.Config{MaxCount: 100}); err != nil { + t.Errorf("Test_EnforceEnvironmentVariablePolicy_NarrowingMatches: %v", err) + } +} + // // Setup and "fixtures" follow... // @@ -474,6 +689,7 @@ func generateSecurityPolicy(r *rand.Rand, numContainers int32) *SecurityPolicy { func generateSecurityPolicyContainer(r *rand.Rand, size int32) SecurityPolicyContainer { c := SecurityPolicyContainer{} c.Command = generateCommand(r) + c.EnvRules = generateEnvironmentVariableRules(r) layers := int(atLeastOneAtMost(r, size)) for i := 0; i < layers; i++ { c.Layers = append(c.Layers, generateRootHash(r)) @@ -497,6 +713,75 @@ func generateCommand(r *rand.Rand) []string { return args } +func generateEnvironmentVariableRules(r *rand.Rand) []SecurityPolicyEnvironmentVariableRule { + rules := []SecurityPolicyEnvironmentVariableRule{} + + numArgs := atLeastOneAtMost(r, maxGeneratedEnvironmentVariableRules) + for i := 0; i < int(numArgs); i++ { + rule := SecurityPolicyEnvironmentVariableRule{ + Strategy: "string", + Rule: randVariableString(r, maxGeneratedEnvironmentVariableRuleLength), + } + rules = append(rules, rule) + } + + return rules +} + +func generateEnvironmentVariables(r *rand.Rand) []string { + envVars := []string{} + + numVars := atLeastOneAtMost(r, maxGeneratedEnvironmentVariables) + for i := 0; i < int(numVars); i++ { + variable := randVariableString(r, maxGeneratedEnvironmentVariableRuleLength) + envVars = append(envVars, variable) + } + + return envVars +} + +func generateNeverMatchingEnvironmentVariable(r *rand.Rand) string { + return randString(r, maxGeneratedEnvironmentVariableRuleLength+1) +} + +func buildEnvironmentVariablesFromContainerRules(c SecurityPolicyContainer, r *rand.Rand) []string { + vars := make([]string, 0) + + // Select some number of the valid, matching rules to be environment + // variable + numberOfRules := int32(len(c.EnvRules)) + numberOfMatches := randMinMax(r, 1, numberOfRules) + usedIndexes := map[int]struct{}{} + for numberOfMatches > 0 { + anIndex := -1 + if (numberOfMatches * 2) > numberOfRules { + // if we have a lot of matches, randomly select + exists := true + + for exists { + anIndex = int(randMinMax(r, 0, numberOfRules-1)) + _, exists = usedIndexes[anIndex] + } + } else { + // we have a "smaller set of rules. we'll just iterate and select from + // available + exists := true + + for exists { + anIndex++ + _, exists = usedIndexes[anIndex] + } + } + + vars = append(vars, c.EnvRules[anIndex].Rule) + usedIndexes[anIndex] = struct{}{} + + numberOfMatches-- + } + + return vars +} + func generateMountTarget(r *rand.Rand) string { return randVariableString(r, maxGeneratedMountTargetLength) } diff --git a/pkg/securitypolicy/securitypolicyenforcer.go b/pkg/securitypolicy/securitypolicyenforcer.go index a3513009e4..97ffa980c9 100644 --- a/pkg/securitypolicy/securitypolicyenforcer.go +++ b/pkg/securitypolicy/securitypolicyenforcer.go @@ -3,6 +3,7 @@ package securitypolicy import ( "errors" "fmt" + "regexp" "sync" "github.com/google/go-cmp/cmp" @@ -11,7 +12,7 @@ import ( type SecurityPolicyEnforcer interface { EnforcePmemMountPolicy(target string, deviceHash string) (err error) EnforceOverlayMountPolicy(containerID string, layerPaths []string) (err error) - EnforceCommandPolicy(containerID string, argList []string) (err error) + EnforceStartContainerPolicy(containerID string, argList []string, envList []string) (err error) } func NewSecurityPolicyEnforcer(policy *SecurityPolicy) (SecurityPolicyEnforcer, error) { @@ -72,14 +73,16 @@ type StandardSecurityPolicyEnforcer struct { // Most of the work that this security policy enforcer does it around managing // state needed to map from a container definition in the SecurityPolicy to // a specfic container ID as we bring up each container. See - // EnforceCommandPolicy where most of the functionality is handling the case + // enforceCommandPolicy where most of the functionality is handling the case // were policy containers share an overlay and have to try to distinguish them - // based on the command line arguments. + // based on the command line arguments. enforceEnvironmentVariablePolicy can + // further narrow based on environment variables if required. // - // implementation details are availanle in: + // implementation details are available in: // - EnforcePmemMountPolicy // - EnforceOverlayMountPolicy - // - EnforceCommandPolicy + // - enforceCommandPolicy + // - enforceEnvironmentVariablePolicy // - NewStandardSecurityPolicyEnforcer Devices [][]string ContainerIndexToContainerIds map[int][]string @@ -187,7 +190,7 @@ func (policyState *StandardSecurityPolicyEnforcer) EnforceOverlayMountPolicy(con return nil } -func (policyState *StandardSecurityPolicyEnforcer) EnforceCommandPolicy(containerID string, argList []string) (err error) { +func (policyState *StandardSecurityPolicyEnforcer) EnforceStartContainerPolicy(containerID string, argList []string, envList []string) (err error) { policyState.mutex.Lock() defer policyState.mutex.Unlock() @@ -199,6 +202,23 @@ func (policyState *StandardSecurityPolicyEnforcer) EnforceCommandPolicy(containe return errors.New("container has already been started") } + err = policyState.enforceCommandPolicy(containerID, argList) + if err != nil { + return err + } + + err = policyState.enforceEnvironmentVariablePolicy(containerID, envList) + if err != nil { + return err + } + + // record that we've allowed this container to start + policyState.startedContainers[containerID] = struct{}{} + + return nil +} + +func (policyState *StandardSecurityPolicyEnforcer) enforceCommandPolicy(containerID string, argList []string) (err error) { // Get a list of all the indexes into our security policy's list of // containers that are possible matches for this containerID based // on the image overlay layout @@ -217,14 +237,7 @@ func (policyState *StandardSecurityPolicyEnforcer) EnforceCommandPolicy(containe } else { // a possible matching index turned out not to match, so we // need to update that list and remove it - updatedContainerIds := []string{} - existingContainerIds := policyState.ContainerIndexToContainerIds[possibleIndex] - for _, id := range existingContainerIds { - if id != containerID { - updatedContainerIds = append(updatedContainerIds, id) - } - } - policyState.ContainerIndexToContainerIds[possibleIndex] = updatedContainerIds + policyState.narrowMatchesForContainerIndex(possibleIndex, containerID) } } @@ -233,12 +246,67 @@ func (policyState *StandardSecurityPolicyEnforcer) EnforceCommandPolicy(containe return errors.New(errmsg) } - // record that we've allowed this container to start - policyState.startedContainers[containerID] = struct{}{} + return nil +} + +func (policyState *StandardSecurityPolicyEnforcer) enforceEnvironmentVariablePolicy(containerID string, envList []string) (err error) { + // Get a list of all the indexes into our security policy's list of + // containers that are possible matches for this containerID based + // on the image overlay layout and command line + possibleIndexes := possibleIndexesForID(containerID, policyState.ContainerIndexToContainerIds) + + for _, envVariable := range envList { + matchingRuleFoundForSomeContainer := false + for _, possibleIndex := range possibleIndexes { + envRules := policyState.SecurityPolicy.Containers[possibleIndex].EnvRules + ok := envIsMatchedByRule(envVariable, envRules) + if ok { + matchingRuleFoundForSomeContainer = true + } else { + // a possible matching index turned out not to match, so we + // need to update that list and remove it + policyState.narrowMatchesForContainerIndex(possibleIndex, containerID) + } + } + + if !matchingRuleFoundForSomeContainer { + return fmt.Errorf("env variable %s unmatched by policy rule", envVariable) + } + } return nil } +func envIsMatchedByRule(envVariable string, rules []SecurityPolicyEnvironmentVariableRule) bool { + for _, rule := range rules { + switch rule.Strategy { + case "string": + if rule.Rule == envVariable { + return true + } + case "re2": + // if the match errors out, we don't care. it's not a match + matched, _ := regexp.MatchString(rule.Rule, envVariable) + if matched { + return true + } + } + } + + return false +} + +func (policyState *StandardSecurityPolicyEnforcer) narrowMatchesForContainerIndex(index int, idToRemove string) { + updatedContainerIds := []string{} + existingContainerIds := policyState.ContainerIndexToContainerIds[index] + for _, id := range existingContainerIds { + if id != idToRemove { + updatedContainerIds = append(updatedContainerIds, id) + } + } + policyState.ContainerIndexToContainerIds[index] = updatedContainerIds +} + func equalForOverlay(a1 []string, a2 []string) bool { // We've stored the layers from bottom to topl they are in layerPaths as // top to bottom (the order a string gets concatenated for the unix mount @@ -281,7 +349,7 @@ func (p *OpenDoorSecurityPolicyEnforcer) EnforceOverlayMountPolicy(containerID s return nil } -func (p *OpenDoorSecurityPolicyEnforcer) EnforceCommandPolicy(containerID string, argList []string) (err error) { +func (p *OpenDoorSecurityPolicyEnforcer) EnforceStartContainerPolicy(containerID string, argList []string, envList []string) (err error) { return nil } @@ -297,6 +365,6 @@ func (p *ClosedDoorSecurityPolicyEnforcer) EnforceOverlayMountPolicy(containerID return errors.New("creating an overlay fs is denied by policy") } -func (p *ClosedDoorSecurityPolicyEnforcer) EnforceCommandPolicy(containerID string, argList []string) (err error) { +func (p *ClosedDoorSecurityPolicyEnforcer) EnforceStartContainerPolicy(containerID string, argList []string, envList []string) (err error) { return errors.New("running commands is denied by policy") } diff --git a/test/go.sum b/test/go.sum index 9eceb2542c..cdf1310235 100644 --- a/test/go.sum +++ b/test/go.sum @@ -390,6 +390,7 @@ github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfn github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/linuxkit/virtsock v0.0.0-20201010232012-f8cee7dfc7a3/go.mod h1:3r6x7q95whyfWQpmGZTu3gk3v2YkMi05HEzl7Tf7YEo= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= @@ -429,6 +430,7 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8m github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= github.com/ncw/swift v1.0.47/go.mod h1:23YIA4yWVnGwv2dQlN4bB7egfYX6YLn0Yo/S6zZO/ZM= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= @@ -855,6 +857,7 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20141024133853-64131543e789/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= diff --git a/test/vendor/github.com/Microsoft/hcsshim/pkg/securitypolicy/securitypolicy.go b/test/vendor/github.com/Microsoft/hcsshim/pkg/securitypolicy/securitypolicy.go index 18b64f3468..a62a09273c 100644 --- a/test/vendor/github.com/Microsoft/hcsshim/pkg/securitypolicy/securitypolicy.go +++ b/test/vendor/github.com/Microsoft/hcsshim/pkg/securitypolicy/securitypolicy.go @@ -23,6 +23,8 @@ type SecurityPolicy struct { type SecurityPolicyContainer struct { // The command that we will allow the container to execute Command []string `json:"command"` + // The rules for determining if a given environment variable is allowed + EnvRules []SecurityPolicyEnvironmentVariableRule `json:"env_rules"` // An ordered list of dm-verity root hashes for each layer that makes up // "a container". Containers are constructed as an overlay file system. The // order that the layers are overlayed is important and needs to be enforced @@ -30,6 +32,11 @@ type SecurityPolicyContainer struct { Layers []string `json:"layers"` } +type SecurityPolicyEnvironmentVariableRule struct { + Strategy string `json:"strategy"` + Rule string `json:"rule"` +} + // EncodedSecurityPolicy is a JSON representation of SecurityPolicy that has // been base64 encoded for storage in an annotation embedded within another // JSON configuration diff --git a/test/vendor/github.com/Microsoft/hcsshim/pkg/securitypolicy/securitypolicyenforcer.go b/test/vendor/github.com/Microsoft/hcsshim/pkg/securitypolicy/securitypolicyenforcer.go index a3513009e4..97ffa980c9 100644 --- a/test/vendor/github.com/Microsoft/hcsshim/pkg/securitypolicy/securitypolicyenforcer.go +++ b/test/vendor/github.com/Microsoft/hcsshim/pkg/securitypolicy/securitypolicyenforcer.go @@ -3,6 +3,7 @@ package securitypolicy import ( "errors" "fmt" + "regexp" "sync" "github.com/google/go-cmp/cmp" @@ -11,7 +12,7 @@ import ( type SecurityPolicyEnforcer interface { EnforcePmemMountPolicy(target string, deviceHash string) (err error) EnforceOverlayMountPolicy(containerID string, layerPaths []string) (err error) - EnforceCommandPolicy(containerID string, argList []string) (err error) + EnforceStartContainerPolicy(containerID string, argList []string, envList []string) (err error) } func NewSecurityPolicyEnforcer(policy *SecurityPolicy) (SecurityPolicyEnforcer, error) { @@ -72,14 +73,16 @@ type StandardSecurityPolicyEnforcer struct { // Most of the work that this security policy enforcer does it around managing // state needed to map from a container definition in the SecurityPolicy to // a specfic container ID as we bring up each container. See - // EnforceCommandPolicy where most of the functionality is handling the case + // enforceCommandPolicy where most of the functionality is handling the case // were policy containers share an overlay and have to try to distinguish them - // based on the command line arguments. + // based on the command line arguments. enforceEnvironmentVariablePolicy can + // further narrow based on environment variables if required. // - // implementation details are availanle in: + // implementation details are available in: // - EnforcePmemMountPolicy // - EnforceOverlayMountPolicy - // - EnforceCommandPolicy + // - enforceCommandPolicy + // - enforceEnvironmentVariablePolicy // - NewStandardSecurityPolicyEnforcer Devices [][]string ContainerIndexToContainerIds map[int][]string @@ -187,7 +190,7 @@ func (policyState *StandardSecurityPolicyEnforcer) EnforceOverlayMountPolicy(con return nil } -func (policyState *StandardSecurityPolicyEnforcer) EnforceCommandPolicy(containerID string, argList []string) (err error) { +func (policyState *StandardSecurityPolicyEnforcer) EnforceStartContainerPolicy(containerID string, argList []string, envList []string) (err error) { policyState.mutex.Lock() defer policyState.mutex.Unlock() @@ -199,6 +202,23 @@ func (policyState *StandardSecurityPolicyEnforcer) EnforceCommandPolicy(containe return errors.New("container has already been started") } + err = policyState.enforceCommandPolicy(containerID, argList) + if err != nil { + return err + } + + err = policyState.enforceEnvironmentVariablePolicy(containerID, envList) + if err != nil { + return err + } + + // record that we've allowed this container to start + policyState.startedContainers[containerID] = struct{}{} + + return nil +} + +func (policyState *StandardSecurityPolicyEnforcer) enforceCommandPolicy(containerID string, argList []string) (err error) { // Get a list of all the indexes into our security policy's list of // containers that are possible matches for this containerID based // on the image overlay layout @@ -217,14 +237,7 @@ func (policyState *StandardSecurityPolicyEnforcer) EnforceCommandPolicy(containe } else { // a possible matching index turned out not to match, so we // need to update that list and remove it - updatedContainerIds := []string{} - existingContainerIds := policyState.ContainerIndexToContainerIds[possibleIndex] - for _, id := range existingContainerIds { - if id != containerID { - updatedContainerIds = append(updatedContainerIds, id) - } - } - policyState.ContainerIndexToContainerIds[possibleIndex] = updatedContainerIds + policyState.narrowMatchesForContainerIndex(possibleIndex, containerID) } } @@ -233,12 +246,67 @@ func (policyState *StandardSecurityPolicyEnforcer) EnforceCommandPolicy(containe return errors.New(errmsg) } - // record that we've allowed this container to start - policyState.startedContainers[containerID] = struct{}{} + return nil +} + +func (policyState *StandardSecurityPolicyEnforcer) enforceEnvironmentVariablePolicy(containerID string, envList []string) (err error) { + // Get a list of all the indexes into our security policy's list of + // containers that are possible matches for this containerID based + // on the image overlay layout and command line + possibleIndexes := possibleIndexesForID(containerID, policyState.ContainerIndexToContainerIds) + + for _, envVariable := range envList { + matchingRuleFoundForSomeContainer := false + for _, possibleIndex := range possibleIndexes { + envRules := policyState.SecurityPolicy.Containers[possibleIndex].EnvRules + ok := envIsMatchedByRule(envVariable, envRules) + if ok { + matchingRuleFoundForSomeContainer = true + } else { + // a possible matching index turned out not to match, so we + // need to update that list and remove it + policyState.narrowMatchesForContainerIndex(possibleIndex, containerID) + } + } + + if !matchingRuleFoundForSomeContainer { + return fmt.Errorf("env variable %s unmatched by policy rule", envVariable) + } + } return nil } +func envIsMatchedByRule(envVariable string, rules []SecurityPolicyEnvironmentVariableRule) bool { + for _, rule := range rules { + switch rule.Strategy { + case "string": + if rule.Rule == envVariable { + return true + } + case "re2": + // if the match errors out, we don't care. it's not a match + matched, _ := regexp.MatchString(rule.Rule, envVariable) + if matched { + return true + } + } + } + + return false +} + +func (policyState *StandardSecurityPolicyEnforcer) narrowMatchesForContainerIndex(index int, idToRemove string) { + updatedContainerIds := []string{} + existingContainerIds := policyState.ContainerIndexToContainerIds[index] + for _, id := range existingContainerIds { + if id != idToRemove { + updatedContainerIds = append(updatedContainerIds, id) + } + } + policyState.ContainerIndexToContainerIds[index] = updatedContainerIds +} + func equalForOverlay(a1 []string, a2 []string) bool { // We've stored the layers from bottom to topl they are in layerPaths as // top to bottom (the order a string gets concatenated for the unix mount @@ -281,7 +349,7 @@ func (p *OpenDoorSecurityPolicyEnforcer) EnforceOverlayMountPolicy(containerID s return nil } -func (p *OpenDoorSecurityPolicyEnforcer) EnforceCommandPolicy(containerID string, argList []string) (err error) { +func (p *OpenDoorSecurityPolicyEnforcer) EnforceStartContainerPolicy(containerID string, argList []string, envList []string) (err error) { return nil } @@ -297,6 +365,6 @@ func (p *ClosedDoorSecurityPolicyEnforcer) EnforceOverlayMountPolicy(containerID return errors.New("creating an overlay fs is denied by policy") } -func (p *ClosedDoorSecurityPolicyEnforcer) EnforceCommandPolicy(containerID string, argList []string) (err error) { +func (p *ClosedDoorSecurityPolicyEnforcer) EnforceStartContainerPolicy(containerID string, argList []string, envList []string) (err error) { return errors.New("running commands is denied by policy") }