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") }