diff --git a/internal/guest/runtime/hcsv2/uvm.go b/internal/guest/runtime/hcsv2/uvm.go index e60532260e..d16c12f52f 100644 --- a/internal/guest/runtime/hcsv2/uvm.go +++ b/internal/guest/runtime/hcsv2/uvm.go @@ -5,7 +5,6 @@ package hcsv2 import ( "bufio" "context" - "encoding/base64" "encoding/json" "fmt" "os" @@ -80,22 +79,13 @@ func (h *Host) SetSecurityPolicy(base64Policy string) error { return errors.New("security policy has already been set") } - // base64 decode the incoming policy string - // its base64 encoded because it is coming from an annotation - // annotations are a map of string to string - // we want to store a complex json object so.... base64 it is - jsonPolicy, err := base64.StdEncoding.DecodeString(base64Policy) + // construct security policy state + securityPolicyState, err := securitypolicy.NewSecurityPolicyState(base64Policy) if err != nil { - return errors.Wrap(err, "unable to decode policy from Base64 format") - } - - // json unmarshall the decoded to a SecurityPolicy - var securityPolicy securitypolicy.SecurityPolicy - if err := json.Unmarshal(jsonPolicy, &securityPolicy); err != nil { - return errors.Wrap(err, "unable to unmarshal policy") + return err } - p, err := securitypolicy.NewSecurityPolicyEnforcer(&securityPolicy) + p, err := securitypolicy.NewSecurityPolicyEnforcer(*securityPolicyState) if err != nil { return err } diff --git a/internal/tools/securitypolicy/README.md b/internal/tools/securitypolicy/README.md index 24aff7425c..f4c4738b43 100644 --- a/internal/tools/securitypolicy/README.md +++ b/internal/tools/securitypolicy/README.md @@ -34,66 +34,73 @@ represented in JSON. ```json { "allow_all": false, - "containers": [ - { - "command": [ - "/pause" - ], - "env_rules": [ - { + "num_containers": 2, + "containers": { + "0": { + "num_commands": 2, + "command": { + "0": "rustc", + "1": "--help" + }, + "num_env_rules": 6, + "env_rules": { + "0": { "strategy": "string", - "rule": "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + "rule": "PATH=/usr/local/cargo/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" }, - { + "1": { "strategy": "string", - "rule": "TERM=xterm" - } - ], - "layers": [ - "16b514057a06ad665f92c02863aca074fd5976c755d26bff16365299169e8415" - ] - }, - { - "command": [ - "rustc", - "--help" - ], - "env_rules": [ - { - "strategy": "re2", - "rule": "PREFIX_.+=.+" + "rule": "RUSTUP_HOME=/usr/local/rustup" }, - { + "2": { "strategy": "string", - "rule": "TERM=xterm" + "rule": "CARGO_HOME=/usr/local/cargo" }, - { + "3": { "strategy": "string", - "rule": "PATH=/usr/local/cargo/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + "rule": "RUST_VERSION=1.52.1" }, - { + "4": { "strategy": "string", - "rule": "RUSTUP_HOME=/usr/local/rustup" + "rule": "TERM=xterm" }, - { + "5": { + "strategy": "re2", + "rule": "PREFIX_.+=.+" + } + }, + "num_layers": 6, + "layers": { + "0": "fe84c9d5bfddd07a2624d00333cf13c1a9c941f3a261f13ead44fc6a93bc0e7a", + "1": "4dedae42847c704da891a28c25d32201a1ae440bce2aecccfa8e6f03b97a6a6c", + "2": "41d64cdeb347bf236b4c13b7403b633ff11f1cf94dbc7cf881a44d6da88c5156", + "3": "eb36921e1f82af46dfe248ef8f1b3afb6a5230a64181d960d10237a08cd73c79", + "4": "e769d7487cc314d3ee748a4440805317c19262c7acd2fdbdb0d47d2e4613a15c", + "5": "1b80f120dbd88e4355d6241b519c3e25290215c469516b49dece9cf07175a766" + } + }, + "1": { + "num_commands": 1, + "command": { + "0": "/pause" + }, + "num_env_rules": 2, + "env_rules": { + "0": { "strategy": "string", - "rule": "CARGO_HOME=/usr/local/cargo" + "rule": "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" }, - { + "1": { "strategy": "string", - "rule": "RUST_VERSION=1.52.1" + "rule": "TERM=xterm" } - ], - "layers": [ - "fe84c9d5bfddd07a2624d00333cf13c1a9c941f3a261f13ead44fc6a93bc0e7a", - "4dedae42847c704da891a28c25d32201a1ae440bce2aecccfa8e6f03b97a6a6c", - "41d64cdeb347bf236b4c13b7403b633ff11f1cf94dbc7cf881a44d6da88c5156", - "eb36921e1f82af46dfe248ef8f1b3afb6a5230a64181d960d10237a08cd73c79", - "e769d7487cc314d3ee748a4440805317c19262c7acd2fdbdb0d47d2e4613a15c", - "1b80f120dbd88e4355d6241b519c3e25290215c469516b49dece9cf07175a766" - ] + }, + "num_layers": 1, + "layers": { + "0": "16b514057a06ad665f92c02863aca074fd5976c755d26bff16365299169e8415" + } } - ] + } } ``` diff --git a/internal/tools/securitypolicy/main.go b/internal/tools/securitypolicy/main.go index f8a360951d..3aba848601 100644 --- a/internal/tools/securitypolicy/main.go +++ b/internal/tools/securitypolicy/main.go @@ -8,6 +8,7 @@ import ( "io/ioutil" "os" "regexp" + "strconv" "github.com/BurntSushi/toml" "github.com/Microsoft/hcsshim/ext4/dmverity" @@ -102,7 +103,9 @@ func createOpenDoorPolicy() sp.SecurityPolicy { } func createPolicyFromConfig(config Config) (sp.SecurityPolicy, error) { - p := sp.SecurityPolicy{} + p := sp.SecurityPolicy{ + Containers: map[string]sp.SecurityPolicyContainer{}, + } var imageOptions []remote.Option if len(*username) != 0 && len(*password) != 0 { @@ -129,10 +132,13 @@ func createPolicyFromConfig(config Config) (sp.SecurityPolicy, error) { return p, err } + command := convertCommand(image.Command) + envRules := convertEnvironmentVariableRules(image.EnvRules) container := sp.SecurityPolicyContainer{ - Command: image.Command, - EnvRules: convertEnvironmentVariableRules(image.EnvRules), - Layers: []string{}, + NumCommands: len(command), + Command: command, + EnvRules: envRules, + Layers: map[string]string{}, } ref, err := name.ParseReference(image.Name) if err != nil { @@ -181,9 +187,11 @@ func createPolicyFromConfig(config Config) (sp.SecurityPolicy, error) { } hash := dmverity.RootHash(tree) hashString := fmt.Sprintf("%x", hash) - container.Layers = append(container.Layers, hashString) + container.Layers = addLayer(container.Layers, hashString) } + container.NumLayers = len(layers) + // 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() @@ -196,7 +204,7 @@ func createPolicyFromConfig(config Config) (sp.SecurityPolicy, error) { Rule: env, } - container.EnvRules = append(container.EnvRules, rule) + container.EnvRules = addEnvRule(container.EnvRules, rule) } // cri adds TERM=xterm for all workload containers. we add to all containers @@ -206,11 +214,14 @@ func createPolicyFromConfig(config Config) (sp.SecurityPolicy, error) { Rule: "TERM=xterm", } - container.EnvRules = append(container.EnvRules, rule) + container.EnvRules = addEnvRule(container.EnvRules, rule) + container.NumEnvRules = len(container.EnvRules) - p.Containers = append(p.Containers, container) + p.Containers = addContainer(p.Containers, container) } + p.NumContainers = len(p.Containers) + return p, nil } @@ -228,8 +239,18 @@ func validateEnvRules(rules []EnvironmentVariableRule) error { return nil } -func convertEnvironmentVariableRules(toml []EnvironmentVariableRule) []sp.SecurityPolicyEnvironmentVariableRule { - json := make([]sp.SecurityPolicyEnvironmentVariableRule, len(toml)) +func convertCommand(toml []string) map[string]string { + json := map[string]string{} + + for i, arg := range toml { + json[strconv.Itoa(i)] = arg + } + + return json +} + +func convertEnvironmentVariableRules(toml []EnvironmentVariableRule) map[string]sp.SecurityPolicyEnvironmentVariableRule { + json := map[string]sp.SecurityPolicyEnvironmentVariableRule{} for i, rule := range toml { jsonRule := sp.SecurityPolicyEnvironmentVariableRule{ @@ -237,8 +258,32 @@ func convertEnvironmentVariableRules(toml []EnvironmentVariableRule) []sp.Securi Rule: rule.Rule, } - json[i] = jsonRule + json[strconv.Itoa(i)] = jsonRule } return json } + +func addContainer(containers map[string]sp.SecurityPolicyContainer, container sp.SecurityPolicyContainer) map[string]sp.SecurityPolicyContainer { + index := strconv.Itoa(len(containers)) + + containers[index] = container + + return containers +} + +func addLayer(layers map[string]string, layer string) map[string]string { + index := strconv.Itoa(len(layers)) + + layers[index] = layer + + return layers +} + +func addEnvRule(rules map[string]sp.SecurityPolicyEnvironmentVariableRule, rule sp.SecurityPolicyEnvironmentVariableRule) map[string]sp.SecurityPolicyEnvironmentVariableRule { + index := strconv.Itoa(len(rules)) + + rules[index] = rule + + return rules +} diff --git a/pkg/securitypolicy/securitypolicy.go b/pkg/securitypolicy/securitypolicy.go index a62a09273c..86aae0b84a 100644 --- a/pkg/securitypolicy/securitypolicy.go +++ b/pkg/securitypolicy/securitypolicy.go @@ -1,6 +1,47 @@ package securitypolicy -// SecurityPolicy is the user supplied security policy to enforce. +import ( + "encoding/base64" + "encoding/json" + + "github.com/pkg/errors" +) + +// Internal version of SecurityPolicyContainer +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 + // as part of policy. + Layers []string `json:"layers"` +} + +// Internal versino of SecurityPolicyEnvironmentVariableRule +type securityPolicyEnvironmentVariableRule struct { + Strategy string `json:"type"` + Rule string `json:"rule"` +} + +// SecurityPolicyState is a structure that holds user supplied policy to enforce +// we keep both the encoded representation and the unmarshalled representation +// because different components need to have access to either of these +type SecurityPolicyState struct { + EncodedSecurityPolicy EncodedSecurityPolicy `json:"EncodedSecurityPolicy,omitempty"` + SecurityPolicy `json:"SecurityPolicy,omitempty"` +} + +// EncodedSecurityPolicy is a JSON representation of SecurityPolicy that has +// been base64 encoded for storage in an annotation embedded within another +// JSON configuration +type EncodedSecurityPolicy struct { + SecurityPolicy string `json:"SecurityPolicy,omitempty"` +} + +// JSON transport version type SecurityPolicy struct { // Flag that when set to true allows for all checks to pass. Currently used // to run with security policy enforcement "running dark"; checks can be in @@ -9,8 +50,10 @@ type SecurityPolicy struct { // standpoint. Policy enforcement isn't actually off as the policy is "allow // everything:. AllowAll bool `json:"allow_all"` + // Total number of containers in our map + NumContainers int `json:"num_containers"` // One or more containers that are allowed to run - Containers []SecurityPolicyContainer `json:"containers"` + Containers map[string]SecurityPolicyContainer `json:"containers"` } // SecurityPolicyContainer contains information about a container that should be @@ -21,15 +64,22 @@ type SecurityPolicy struct { // entries. Once that overlay creation is allowed, the command could not match // policy and running the command would be rejected. type SecurityPolicyContainer struct { + // Number of entries that should be in the "Command" map + NumCommands int `json:"num_commands"` // The command that we will allow the container to execute - Command []string `json:"command"` + Command map[string]string `json:"command"` + // Number of entries that should be in the "EnvRules" map + NumEnvRules int `json:"num_env_rules"` // 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 + EnvRules map[string]SecurityPolicyEnvironmentVariableRule `json:"env_rules"` + // Number of entries that should in the "Layers" map + NumLayers int `json:"num_layers"` + // 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 - // as part of policy. - Layers []string `json:"layers"` + // as part of policy. The map is interpreted as an ordered list by arranging + // the keys of the map as indexes like 0,1,2,3 to establish the order. + Layers map[string]string `json:"layers"` } type SecurityPolicyEnvironmentVariableRule struct { @@ -37,9 +87,36 @@ type SecurityPolicyEnvironmentVariableRule struct { 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 -type EncodedSecurityPolicy struct { - SecurityPolicy string `json:"SecurityPolicy,omitempty"` +// Constructs SecurityPolicyState from base64Policy string. It first decodes +// base64 policy and returns the structs security policy struct and encoded +// security policy for given policy. The security policy is transmitted as json +// in an annotation, so we first have to remove the base64 encoding that allows +// the JSON based policy to be passed as a string. From there, we decode the +// JSONand setup our security policy struct +func NewSecurityPolicyState(base64Policy string) (*SecurityPolicyState, error) { + // construct an encoded security policy that holds the base64 representation + encodedSecurityPolicy := EncodedSecurityPolicy{ + SecurityPolicy: base64Policy, + } + + // base64 decode the incoming policy string + // its base64 encoded because it is coming from an annotation + // annotations are a map of string to string + // we want to store a complex json object so.... base64 it is + jsonPolicy, err := base64.StdEncoding.DecodeString(base64Policy) + if err != nil { + return nil, errors.Wrap(err, "unable to decode policy from Base64 format") + } + + // json unmarshall the decoded to a SecurityPolicy + securityPolicy := SecurityPolicy{} + err = json.Unmarshal(jsonPolicy, &securityPolicy) + if err != nil { + return nil, errors.Wrap(err, "unable to unmarshal JSON policy") + } + + return &SecurityPolicyState{ + SecurityPolicy: securityPolicy, + EncodedSecurityPolicy: encodedSecurityPolicy, + }, nil } diff --git a/pkg/securitypolicy/securitypolicy_test.go b/pkg/securitypolicy/securitypolicy_test.go index 294d78e2e3..5325d76f8d 100644 --- a/pkg/securitypolicy/securitypolicy_test.go +++ b/pkg/securitypolicy/securitypolicy_test.go @@ -13,6 +13,7 @@ import ( ) const ( + // variables that influence generated test fixtures maxContainersInGeneratedPolicy = 32 maxLayersInGeneratedContainer = 32 maxGeneratedContainerID = 1000000 @@ -23,26 +24,92 @@ const ( maxGeneratedEnvironmentVariableRules = 12 maxGeneratedMountTargetLength = 256 rootHashLength = 64 + // additional consts + // the standard enforcer tests don't do anything with the encoded policy + // string. this const exists to make that explicit + ignoredEncodedPolicyString = "" ) -// Do we correctly set up the data structures that are part of creating a new -// StandardSecurityPolicyEnforcer -func Test_StandardSecurityPolicyEnforcer_Devices_Initialization(t *testing.T) { +// Validate that our conversion from the external SecurityPolicy representation +// to our internal format is done correctly. +func Test_StandardSecurityPolicyEnforcer_From_Security_Policy_Conversion(t *testing.T) { f := func(p *SecurityPolicy) bool { - policy, err := NewStandardSecurityPolicyEnforcer(p) + + containers, err := toInternal(p) if err != nil { + t.Logf("unexpected setup error. this might mean test fixture setup has a bug: %v", err) + return false + } + + if len(containers) != p.NumContainers { + t.Errorf("numContainers don't match. internal: %d, external: %d", len(containers), p.NumContainers) return false } + // do by index comparison of containers + for i := 0; i < len(containers); i++ { + internal := containers[i] + external := p.Containers[strconv.Itoa(i)] + + // verify sanity with size + if len(internal.Command) != external.NumCommands { + t.Errorf("numCommands don't match for container %d. internal: %d, external: %d", i, len(internal.Command), external.NumCommands) + } + + if len(internal.EnvRules) != external.NumEnvRules { + t.Errorf("numEnvRules don't match for container %d. internal: %d, external: %d", i, len(internal.EnvRules), external.NumEnvRules) + } + + if len(internal.Layers) != external.NumLayers { + t.Errorf("numLayers don't match for container %d. internal: %d, external: %d", i, len(internal.Layers), external.NumLayers) + } + + // do by index comparison of sub-items + for j := 0; j < len(internal.Command); j++ { + if internal.Command[j] != external.Command[strconv.Itoa(j)] { + t.Errorf("command entries at index %d for for container %d don't match. internal: %s, external: %s", j, i, internal.Command[j], external.Command[strconv.Itoa(j)]) + } + } + + for j := 0; j < len(internal.EnvRules); j++ { + irule := internal.EnvRules[j] + erule := external.EnvRules[strconv.Itoa(j)] + if (irule.Strategy != erule.Strategy) || + (irule.Rule != erule.Rule) { + t.Errorf("env rule entries at index %d for for container %d don't match. internal: %v, external: %v", j, i, irule, erule) + } + } + + for j := 0; j < len(internal.Layers); j++ { + if internal.Layers[j] != external.Layers[strconv.Itoa(j)] { + t.Errorf("layer entries at index %d for for container %d don't match. internal: %s, external: %s", j, i, internal.Layers[j], external.Layers[strconv.Itoa(j)]) + } + } + } + + return !t.Failed() + } + + if err := quick.Check(f, &quick.Config{MaxCount: 1000}); err != nil { + t.Errorf("Test_StandardSecurityPolicyEnforcer_From_Security_Policy_Conversion failed: %v", err) + } +} + +// Do we correctly set up the data structures that are part of creating a new +// StandardSecurityPolicyEnforcer +func Test_StandardSecurityPolicyEnforcer_Devices_Initialization(t *testing.T) { + f := func(p *generatedContainers) bool { + policy := NewStandardSecurityPolicyEnforcer(p.containers, ignoredEncodedPolicyString) + // there should be a device entry for each container - if len(p.Containers) != len(policy.Devices) { + if len(p.containers) != len(policy.Devices) { return false } // in each device entry that corresponds to a container, // the array should have space for all the root hashes - for i := 0; i < len(p.Containers); i++ { - if len(p.Containers[i].Layers) != len(policy.Devices[i]) { + for i := 0; i < len(p.containers); i++ { + if len(p.containers[i].Layers) != len(policy.Devices[i]) { return false } } @@ -58,18 +125,15 @@ func Test_StandardSecurityPolicyEnforcer_Devices_Initialization(t *testing.T) { // Verify that StandardSecurityPolicyEnforcer.EnforcePmemMountPolicy will return // an error when there's no matching root hash in the policy func Test_EnforcePmemMountPolicy_No_Matches(t *testing.T) { - f := func(p *SecurityPolicy) bool { + f := func(p *generatedContainers) bool { - policy, err := NewStandardSecurityPolicyEnforcer(p) - if err != nil { - return false - } + policy := NewStandardSecurityPolicyEnforcer(p.containers, ignoredEncodedPolicyString) r := rand.New(rand.NewSource(time.Now().UnixNano())) target := generateMountTarget(r) rootHash := generateInvalidRootHash(r) - err = policy.EnforcePmemMountPolicy(target, rootHash) + err := policy.EnforcePmemMountPolicy(target, rootHash) // we expect an error, not getting one means something is broken return err != nil @@ -83,18 +147,15 @@ func Test_EnforcePmemMountPolicy_No_Matches(t *testing.T) { // Verify that StandardSecurityPolicyEnforcer.EnforcePmemMountPolicy doesn't return // an error when there's a matching root hash in the policy func Test_EnforcePmemMountPolicy_Matches(t *testing.T) { - f := func(p *SecurityPolicy) bool { + f := func(p *generatedContainers) bool { - policy, err := NewStandardSecurityPolicyEnforcer(p) - if err != nil { - return false - } + policy := NewStandardSecurityPolicyEnforcer(p.containers, ignoredEncodedPolicyString) r := rand.New(rand.NewSource(time.Now().UnixNano())) target := generateMountTarget(r) - rootHash := selectRootHashFromPolicy(p, r) + rootHash := selectRootHashFromContainers(p, r) - err = policy.EnforcePmemMountPolicy(target, rootHash) + err := policy.EnforcePmemMountPolicy(target, rootHash) // getting an error means something is broken return err == nil @@ -108,16 +169,13 @@ func Test_EnforcePmemMountPolicy_Matches(t *testing.T) { // Verify that StandardSecurityPolicyEnforcer.EnforceOverlayMountPolicy will return // an error when there's no matching overlay targets. func Test_EnforceOverlayMountPolicy_No_Matches(t *testing.T) { - f := func(p *SecurityPolicy) bool { + f := func(p *generatedContainers) bool { - policy, err := NewStandardSecurityPolicyEnforcer(p) - if err != nil { - return false - } + policy := NewStandardSecurityPolicyEnforcer(p.containers, ignoredEncodedPolicyString) r := rand.New(rand.NewSource(time.Now().UnixNano())) containerID := generateContainerID(r) - container := selectContainerFromPolicy(p, r) + container := selectContainerFromContainers(p, r) layerPaths, err := createInvalidOverlayForContainer(policy, container, r) if err != nil { @@ -138,16 +196,13 @@ func Test_EnforceOverlayMountPolicy_No_Matches(t *testing.T) { // Verify that StandardSecurityPolicyEnforcer.EnforceOverlayMountPolicy doesn't // return an error when there's a valid overlay target. func Test_EnforceOverlayMountPolicy_Matches(t *testing.T) { - f := func(p *SecurityPolicy) bool { + f := func(p *generatedContainers) bool { - policy, err := NewStandardSecurityPolicyEnforcer(p) - if err != nil { - return false - } + policy := NewStandardSecurityPolicyEnforcer(p.containers, ignoredEncodedPolicyString) r := rand.New(rand.NewSource(time.Now().UnixNano())) containerID := generateContainerID(r) - container := selectContainerFromPolicy(p, r) + container := selectContainerFromContainers(p, r) layerPaths, err := createValidOverlayForContainer(policy, container, r) if err != nil { @@ -168,15 +223,12 @@ func Test_EnforceOverlayMountPolicy_Matches(t *testing.T) { // Tests the specific case of trying to mount the same overlay twice using the /// same container id. This should be disallowed. func Test_EnforceOverlayMountPolicy_Overlay_Single_Container_Twice(t *testing.T) { r := rand.New(rand.NewSource(time.Now().UnixNano())) - p := generateSecurityPolicy(r, 1) + p := generateContainers(r, 1) - policy, err := NewStandardSecurityPolicyEnforcer(p) - if err != nil { - t.Fatalf("expected nil error got: %v", err) - } + policy := NewStandardSecurityPolicyEnforcer(p.containers, ignoredEncodedPolicyString) containerID := generateContainerID(r) - container := selectContainerFromPolicy(p, r) + container := selectContainerFromContainers(p, r) layerPaths, err := createValidOverlayForContainer(policy, container, r) if err != nil { @@ -201,11 +253,11 @@ func Test_EnforceOverlayMountPolicy_Overlay_Single_Container_Twice(t *testing.T) func Test_EnforceOverlayMountPolicy_Multiple_Instances_Same_Container(t *testing.T) { for containersToCreate := 2; containersToCreate <= maxContainersInGeneratedPolicy; containersToCreate++ { r := rand.New(rand.NewSource(time.Now().UnixNano())) - var containers []SecurityPolicyContainer + var containers []securityPolicyContainer for i := 1; i <= int(containersToCreate); i++ { arg := "command " + strconv.Itoa(i) - c := SecurityPolicyContainer{ + c := securityPolicyContainer{ Command: []string{arg}, Layers: []string{"1", "2"}, } @@ -213,19 +265,11 @@ func Test_EnforceOverlayMountPolicy_Multiple_Instances_Same_Container(t *testing containers = append(containers, c) } - p := &SecurityPolicy{ - AllowAll: false, - Containers: containers, - } - - policy, err := NewStandardSecurityPolicyEnforcer(p) - if err != nil { - t.Fatal("unexpected error on test setup") - } + policy := NewStandardSecurityPolicyEnforcer(containers, "") idsUsed := map[string]bool{} - for i := 0; i < len(p.Containers); i++ { - layerPaths, err := createValidOverlayForContainer(policy, p.Containers[i], r) + for i := 0; i < len(containers); i++ { + layerPaths, err := createValidOverlayForContainer(policy, containers[i], r) if err != nil { t.Fatal("unexpected error on test setup") } @@ -254,12 +298,9 @@ func Test_EnforceOverlayMountPolicy_Multiple_Instances_Same_Container(t *testing // but no more than that one. func Test_EnforceOverlayMountPolicy_Overlay_Single_Container_Twice_With_Different_IDs(t *testing.T) { r := rand.New(rand.NewSource(time.Now().UnixNano())) - p := generateSecurityPolicy(r, 1) + p := generateContainers(r, 1) - policy, err := NewStandardSecurityPolicyEnforcer(p) - if err != nil { - t.Fatalf("expected nil error got: %v", err) - } + policy := NewStandardSecurityPolicyEnforcer(p.containers, ignoredEncodedPolicyString) var containerIDOne, containerIDTwo string @@ -267,7 +308,7 @@ func Test_EnforceOverlayMountPolicy_Overlay_Single_Container_Twice_With_Differen containerIDOne = generateContainerID(r) containerIDTwo = generateContainerID(r) } - container := selectContainerFromPolicy(p, r) + container := selectContainerFromContainers(p, r) layerPaths, err := createValidOverlayForContainer(policy, container, r) if err != nil { @@ -286,15 +327,12 @@ func Test_EnforceOverlayMountPolicy_Overlay_Single_Container_Twice_With_Differen } func Test_EnforceCommandPolicy_Matches(t *testing.T) { - f := func(p *SecurityPolicy) bool { - policy, err := NewStandardSecurityPolicyEnforcer(p) - if err != nil { - return false - } + f := func(p *generatedContainers) bool { + policy := NewStandardSecurityPolicyEnforcer(p.containers, ignoredEncodedPolicyString) r := rand.New(rand.NewSource(time.Now().UnixNano())) containerID := generateContainerID(r) - container := selectContainerFromPolicy(p, r) + container := selectContainerFromContainers(p, r) layerPaths, err := createValidOverlayForContainer(policy, container, r) if err != nil { @@ -318,15 +356,12 @@ func Test_EnforceCommandPolicy_Matches(t *testing.T) { } func Test_EnforceCommandPolicy_NoMatches(t *testing.T) { - f := func(p *SecurityPolicy) bool { - policy, err := NewStandardSecurityPolicyEnforcer(p) - if err != nil { - return false - } + f := func(p *generatedContainers) bool { + policy := NewStandardSecurityPolicyEnforcer(p.containers, ignoredEncodedPolicyString) r := rand.New(rand.NewSource(time.Now().UnixNano())) containerID := generateContainerID(r) - container := selectContainerFromPolicy(p, r) + container := selectContainerFromContainers(p, r) layerPaths, err := createValidOverlayForContainer(policy, container, r) if err != nil { @@ -360,20 +395,17 @@ func Test_EnforceCommandPolicy_NoMatches(t *testing.T) { // This test verifies the "narrowing possible container ids that could be // the container in our policy" functionality works correctly. func Test_EnforceCommandPolicy_NarrowingMatches(t *testing.T) { - f := func(p *SecurityPolicy) bool { + f := func(p *generatedContainers) bool { r := rand.New(rand.NewSource(time.Now().UnixNano())) // create two additional containers that "share everything" // except that they have different commands - testContainerOne := generateSecurityPolicyContainer(r, 5) + testContainerOne := generateContainersContainer(r, 5) testContainerTwo := testContainerOne testContainerTwo.Command = generateCommand(r) // add new containers to policy before creating enforcer - p.Containers = append(p.Containers, testContainerOne, testContainerTwo) + p.containers = append(p.containers, testContainerOne, testContainerTwo) - policy, err := NewStandardSecurityPolicyEnforcer(p) - if err != nil { - return false - } + policy := NewStandardSecurityPolicyEnforcer(p.containers, ignoredEncodedPolicyString) testContainerOneID := "" testContainerTwoID := "" @@ -381,7 +413,7 @@ func Test_EnforceCommandPolicy_NarrowingMatches(t *testing.T) { indexForContainerTwo := -1 // mount and overlay all our containers - for index, container := range p.Containers { + for index, container := range p.containers { containerID := generateContainerID(r) layerPaths, err := createValidOverlayForContainer(policy, container, r) @@ -427,7 +459,7 @@ func Test_EnforceCommandPolicy_NarrowingMatches(t *testing.T) { // 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 } @@ -455,15 +487,12 @@ 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 - } + f := func(p *generatedContainers) bool { + policy := NewStandardSecurityPolicyEnforcer(p.containers, ignoredEncodedPolicyString) r := rand.New(rand.NewSource(time.Now().UnixNano())) containerID := generateContainerID(r) - container := selectContainerFromPolicy(p, r) + container := selectContainerFromContainers(p, r) layerPaths, err := createValidOverlayForContainer(policy, container, r) if err != nil { @@ -489,20 +518,17 @@ func Test_EnforceEnvironmentVariablePolicy_Matches(t *testing.T) { func Test_EnforceEnvironmentVariablePolicy_Re2Match(t *testing.T) { r := rand.New(rand.NewSource(time.Now().UnixNano())) - p := generateSecurityPolicy(r, 1) + p := generateContainers(r, 1) - container := generateSecurityPolicyContainer(r, 1) + container := generateContainersContainer(r, 1) // add a rule to re2 match - re2MatchRule := SecurityPolicyEnvironmentVariableRule{ + re2MatchRule := securityPolicyEnvironmentVariableRule{ Strategy: "re2", Rule: "PREFIX_.+=.+"} container.EnvRules = append(container.EnvRules, re2MatchRule) - p.Containers = append(p.Containers, container) + p.containers = append(p.containers, container) - policy, err := NewStandardSecurityPolicyEnforcer(p) - if err != nil { - t.Fatalf("expected nil error got: %v", err) - } + policy := NewStandardSecurityPolicyEnforcer(p.containers, ignoredEncodedPolicyString) containerID := generateContainerID(r) @@ -526,15 +552,12 @@ func Test_EnforceEnvironmentVariablePolicy_Re2Match(t *testing.T) { } func Test_EnforceEnvironmentVariablePolicy_NotAllMatches(t *testing.T) { - f := func(p *SecurityPolicy) bool { - policy, err := NewStandardSecurityPolicyEnforcer(p) - if err != nil { - return false - } + f := func(p *generatedContainers) bool { + policy := NewStandardSecurityPolicyEnforcer(p.containers, ignoredEncodedPolicyString) r := rand.New(rand.NewSource(time.Now().UnixNano())) containerID := generateContainerID(r) - container := selectContainerFromPolicy(p, r) + container := selectContainerFromContainers(p, r) layerPaths, err := createValidOverlayForContainer(policy, container, r) if err != nil { @@ -571,20 +594,17 @@ func Test_EnforceEnvironmentVariablePolicy_NotAllMatches(t *testing.T) { // 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 { + f := func(p *generatedContainers) 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) + testContainerOne := generateContainersContainer(r, 5) testContainerTwo := testContainerOne testContainerTwo.EnvRules = generateEnvironmentVariableRules(r) // add new containers to policy before creating enforcer - p.Containers = append(p.Containers, testContainerOne, testContainerTwo) + p.containers = append(p.containers, testContainerOne, testContainerTwo) - policy, err := NewStandardSecurityPolicyEnforcer(p) - if err != nil { - return false - } + policy := NewStandardSecurityPolicyEnforcer(p.containers, ignoredEncodedPolicyString) testContainerOneID := "" testContainerTwoID := "" @@ -592,7 +612,7 @@ func Test_EnforceEnvironmentVariablePolicy_NarrowingMatches(t *testing.T) { indexForContainerTwo := -1 // mount and overlay all our containers - for index, container := range p.Containers { + for index, container := range p.containers { containerID := generateContainerID(r) layerPaths, err := createValidOverlayForContainer(policy, container, r) @@ -639,7 +659,7 @@ func Test_EnforceEnvironmentVariablePolicy_NarrowingMatches(t *testing.T) { // enforce command policy for containerOne // this will narrow our list of possible ids down envVars := buildEnvironmentVariablesFromContainerRules(testContainerOne, r) - err = policy.enforceEnvironmentVariablePolicy(testContainerOneID, envVars) + err := policy.enforceEnvironmentVariablePolicy(testContainerOneID, envVars) if err != nil { return false } @@ -671,23 +691,75 @@ func Test_EnforceEnvironmentVariablePolicy_NarrowingMatches(t *testing.T) { // func (*SecurityPolicy) Generate(r *rand.Rand, size int) reflect.Value { - p := generateSecurityPolicy(r, maxContainersInGeneratedPolicy) + // This fixture setup is used from 1 test. Given the limited scope it is + // used from, all functionality is in this single function. That saves having + // confusing fixture name functions where we have generate* for both internal + // and external versions + p := &SecurityPolicy{ + Containers: map[string]SecurityPolicyContainer{}, + } + p.AllowAll = false + numContainers := int(atLeastOneAtMost(r, maxContainersInGeneratedPolicy)) + for i := 0; i < numContainers; i++ { + c := SecurityPolicyContainer{ + Command: map[string]string{}, + EnvRules: map[string]SecurityPolicyEnvironmentVariableRule{}, + Layers: map[string]string{}, + } + + // command + numArgs := int(atLeastOneAtMost(r, maxGeneratedCommandArgs)) + for i := 0; i < numArgs; i++ { + c.Command[strconv.Itoa(i)] = randVariableString(r, maxGeneratedCommandLength) + } + c.NumCommands = numArgs + + // layers + numLayers := int(atLeastOneAtMost(r, maxLayersInGeneratedContainer)) + for i := 0; i < numLayers; i++ { + c.Layers[strconv.Itoa(i)] = generateRootHash(r) + } + c.NumLayers = numLayers + + // env variable rules + numEnvRules := int(atMost(r, maxGeneratedEnvironmentVariableRules)) + for i := 0; i < numEnvRules; i++ { + rule := SecurityPolicyEnvironmentVariableRule{ + Strategy: "string", + Rule: randVariableString(r, maxGeneratedEnvironmentVariableRuleLength), + } + c.EnvRules[strconv.Itoa(i)] = rule + } + c.NumEnvRules = numEnvRules + + p.Containers[strconv.Itoa(i)] = c + } + + p.NumContainers = numContainers + return reflect.ValueOf(p) } -func generateSecurityPolicy(r *rand.Rand, numContainers int32) *SecurityPolicy { - p := &SecurityPolicy{} - p.AllowAll = false - containers := atLeastOneAtMost(r, numContainers) - for i := 0; i < (int)(containers); i++ { - p.Containers = append(p.Containers, generateSecurityPolicyContainer(r, maxLayersInGeneratedContainer)) +func (*generatedContainers) Generate(r *rand.Rand, size int) reflect.Value { + c := generateContainers(r, maxContainersInGeneratedPolicy) + return reflect.ValueOf(c) +} + +func generateContainers(r *rand.Rand, upTo int32) *generatedContainers { + containers := []securityPolicyContainer{} + + numContainers := (int)(atLeastOneAtMost(r, upTo)) + for i := 0; i < numContainers; i++ { + containers = append(containers, generateContainersContainer(r, maxLayersInGeneratedContainer)) } - return p + return &generatedContainers{ + containers: containers, + } } -func generateSecurityPolicyContainer(r *rand.Rand, size int32) SecurityPolicyContainer { - c := SecurityPolicyContainer{} +func generateContainersContainer(r *rand.Rand, size int32) securityPolicyContainer { + c := securityPolicyContainer{} c.Command = generateCommand(r) c.EnvRules = generateEnvironmentVariableRules(r) layers := int(atLeastOneAtMost(r, size)) @@ -713,12 +785,12 @@ func generateCommand(r *rand.Rand) []string { return args } -func generateEnvironmentVariableRules(r *rand.Rand) []SecurityPolicyEnvironmentVariableRule { - rules := []SecurityPolicyEnvironmentVariableRule{} +func generateEnvironmentVariableRules(r *rand.Rand) []securityPolicyEnvironmentVariableRule { + rules := []securityPolicyEnvironmentVariableRule{} numArgs := atLeastOneAtMost(r, maxGeneratedEnvironmentVariableRules) for i := 0; i < int(numArgs); i++ { - rule := SecurityPolicyEnvironmentVariableRule{ + rule := securityPolicyEnvironmentVariableRule{ Strategy: "string", Rule: randVariableString(r, maxGeneratedEnvironmentVariableRuleLength), } @@ -744,7 +816,7 @@ func generateNeverMatchingEnvironmentVariable(r *rand.Rand) string { return randString(r, maxGeneratedEnvironmentVariableRuleLength+1) } -func buildEnvironmentVariablesFromContainerRules(c SecurityPolicyContainer, r *rand.Rand) []string { +func buildEnvironmentVariablesFromContainerRules(c securityPolicyContainer, r *rand.Rand) []string { vars := make([]string, 0) // Select some number of the valid, matching rules to be environment @@ -793,10 +865,10 @@ func generateInvalidRootHash(r *rand.Rand) string { return randVariableString(r, rootHashLength-1) } -func selectRootHashFromPolicy(policy *SecurityPolicy, r *rand.Rand) string { +func selectRootHashFromContainers(containers *generatedContainers, r *rand.Rand) string { - numberOfContainersInPolicy := len(policy.Containers) - container := policy.Containers[r.Intn(numberOfContainersInPolicy)] + numberOfContainersInPolicy := len(containers.containers) + container := containers.containers[r.Intn(numberOfContainersInPolicy)] numberOfLayersInContainer := len(container.Layers) return container.Layers[r.Intn(numberOfLayersInContainer)] @@ -807,12 +879,12 @@ func generateContainerID(r *rand.Rand) string { return strconv.FormatInt(int64(id), 10) } -func selectContainerFromPolicy(policy *SecurityPolicy, r *rand.Rand) SecurityPolicyContainer { - numberOfContainersInPolicy := len(policy.Containers) - return policy.Containers[r.Intn(numberOfContainersInPolicy)] +func selectContainerFromContainers(containers *generatedContainers, r *rand.Rand) securityPolicyContainer { + numberOfContainersInPolicy := len(containers.containers) + return containers.containers[r.Intn(numberOfContainersInPolicy)] } -func createValidOverlayForContainer(enforcer SecurityPolicyEnforcer, container SecurityPolicyContainer, r *rand.Rand) ([]string, error) { +func createValidOverlayForContainer(enforcer SecurityPolicyEnforcer, container securityPolicyContainer, r *rand.Rand) ([]string, error) { // storage for our mount paths overlay := make([]string, len(container.Layers)) @@ -829,7 +901,7 @@ func createValidOverlayForContainer(enforcer SecurityPolicyEnforcer, container S return overlay, nil } -func createInvalidOverlayForContainer(enforcer SecurityPolicyEnforcer, container SecurityPolicyContainer, r *rand.Rand) ([]string, error) { +func createInvalidOverlayForContainer(enforcer SecurityPolicyEnforcer, container securityPolicyContainer, r *rand.Rand) ([]string, error) { method := r.Intn(3) if method == 0 { return invalidOverlaySameSizeWrongMounts(enforcer, container, r) @@ -840,7 +912,7 @@ func createInvalidOverlayForContainer(enforcer SecurityPolicyEnforcer, container } } -func invalidOverlaySameSizeWrongMounts(enforcer SecurityPolicyEnforcer, container SecurityPolicyContainer, r *rand.Rand) ([]string, error) { +func invalidOverlaySameSizeWrongMounts(enforcer SecurityPolicyEnforcer, container securityPolicyContainer, r *rand.Rand) ([]string, error) { // storage for our mount paths overlay := make([]string, len(container.Layers)) @@ -858,7 +930,7 @@ func invalidOverlaySameSizeWrongMounts(enforcer SecurityPolicyEnforcer, containe return overlay, nil } -func invalidOverlayCorrectDevicesWrongOrderSomeMissing(enforcer SecurityPolicyEnforcer, container SecurityPolicyContainer, r *rand.Rand) ([]string, error) { +func invalidOverlayCorrectDevicesWrongOrderSomeMissing(enforcer SecurityPolicyEnforcer, container securityPolicyContainer, r *rand.Rand) ([]string, error) { if len(container.Layers) == 1 { // won't work with only 1, we need to bail out to another method return invalidOverlayRandomJunk(enforcer, container, r) @@ -881,7 +953,7 @@ func invalidOverlayCorrectDevicesWrongOrderSomeMissing(enforcer SecurityPolicyEn return overlay, nil } -func invalidOverlayRandomJunk(enforcer SecurityPolicyEnforcer, container SecurityPolicyContainer, r *rand.Rand) ([]string, error) { +func invalidOverlayRandomJunk(enforcer SecurityPolicyEnforcer, container securityPolicyContainer, r *rand.Rand) ([]string, error) { // create "junk" for entry layersToCreate := r.Int31n(maxLayersInGeneratedContainer) overlay := make([]string, layersToCreate) @@ -922,3 +994,12 @@ func randMinMax(r *rand.Rand, min int32, max int32) int32 { func atLeastOneAtMost(r *rand.Rand, most int32) int32 { return randMinMax(r, 1, most) } + +func atMost(r *rand.Rand, most int32) int32 { + return randMinMax(r, 0, most) +} + +// a type to hold a list of generated containers +type generatedContainers struct { + containers []securityPolicyContainer +} diff --git a/pkg/securitypolicy/securitypolicyenforcer.go b/pkg/securitypolicy/securitypolicyenforcer.go index 97ffa980c9..8b5d8378c1 100644 --- a/pkg/securitypolicy/securitypolicyenforcer.go +++ b/pkg/securitypolicy/securitypolicyenforcer.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "regexp" + "strconv" "sync" "github.com/google/go-cmp/cmp" @@ -15,21 +16,23 @@ type SecurityPolicyEnforcer interface { EnforceStartContainerPolicy(containerID string, argList []string, envList []string) (err error) } -func NewSecurityPolicyEnforcer(policy *SecurityPolicy) (SecurityPolicyEnforcer, error) { - if policy == nil { - return nil, errors.New("security policy can't be nil") - } - - if policy.AllowAll { +func NewSecurityPolicyEnforcer(state SecurityPolicyState) (SecurityPolicyEnforcer, error) { + if state.SecurityPolicy.AllowAll { return &OpenDoorSecurityPolicyEnforcer{}, nil } else { - return NewStandardSecurityPolicyEnforcer(policy) + containers, err := toInternal(&state.SecurityPolicy) + if err != nil { + return nil, err + } + return NewStandardSecurityPolicyEnforcer(containers, state.EncodedSecurityPolicy.SecurityPolicy), nil } } type StandardSecurityPolicyEnforcer struct { - // The user supplied security policy. - SecurityPolicy SecurityPolicy + // EncodedSecurityPolicy state is needed for key release + EncodedSecurityPolicy string + // Containers from the user supplied security policy. + Containers []securityPolicyContainer // Devices and ContainerIndexToContainerIds are used to build up an // understanding of the containers running with a UVM as they come up and // map them back to a container definition from the user supplied @@ -95,36 +98,97 @@ type StandardSecurityPolicyEnforcer struct { var _ SecurityPolicyEnforcer = (*StandardSecurityPolicyEnforcer)(nil) -func NewStandardSecurityPolicyEnforcer(policy *SecurityPolicy) (*StandardSecurityPolicyEnforcer, error) { - if policy == nil { - return nil, errors.New("security policy can't be nil") - } - - // create new StandardSecurityPolicyEnforcer and add the new SecurityPolicy +func NewStandardSecurityPolicyEnforcer(containers []securityPolicyContainer, encoded string) *StandardSecurityPolicyEnforcer { + // create new StandardSecurityPolicyEnforcer and add the expected containers // to it // fill out corresponding devices structure by creating a "same shapped" // devices listing that corresponds to our container root hash lists // the devices list will get filled out as layers are mounted - devices := make([][]string, len(policy.Containers)) + devices := make([][]string, len(containers)) - for i, container := range policy.Containers { + for i, container := range containers { devices[i] = make([]string, len(container.Layers)) } return &StandardSecurityPolicyEnforcer{ - SecurityPolicy: *policy, + EncodedSecurityPolicy: encoded, + Containers: containers, Devices: devices, ContainerIndexToContainerIds: map[int][]string{}, startedContainers: map[string]struct{}{}, mutex: &sync.Mutex{}, - }, nil + } +} + +func toInternal(external *SecurityPolicy) ([]securityPolicyContainer, error) { + containerMapLength := len(external.Containers) + if external.NumContainers != containerMapLength { + errmsg := fmt.Sprintf("container numbers don't match in policy. expected: %d, actual: %d", external.NumContainers, containerMapLength) + return nil, errors.New(errmsg) + } + + internal := make([]securityPolicyContainer, containerMapLength) + + for i := 0; i < containerMapLength; i++ { + iContainer := securityPolicyContainer{} + + eContainer := external.Containers[strconv.Itoa(i)] + + // Command conversion + if eContainer.NumCommands != len(eContainer.Command) { + errmsg := fmt.Sprintf("command argument numbers don't match in policy. expected: %d, actual: %d", eContainer.NumCommands, len(eContainer.Command)) + return nil, errors.New(errmsg) + } + iContainer.Command = stringMapToStringArray(eContainer.Command) + + // Layers conversion + if eContainer.NumLayers != len(eContainer.Layers) { + errmsg := fmt.Sprintf("layer numbers don't match in policy. expected: %d, actual: %d", eContainer.NumLayers, len(eContainer.Layers)) + return nil, errors.New(errmsg) + } + iContainer.Layers = stringMapToStringArray(eContainer.Layers) + + // EnvRules conversion + envRulesMapLength := len(eContainer.EnvRules) + if eContainer.NumEnvRules != envRulesMapLength { + errmsg := fmt.Sprintf("env rule numbers don't match in policy. expected: %d, actual: %d", eContainer.NumEnvRules, envRulesMapLength) + return nil, errors.New(errmsg) + } + + envRules := make([]securityPolicyEnvironmentVariableRule, envRulesMapLength) + for i := 0; i < envRulesMapLength; i++ { + eIndex := strconv.Itoa(i) + rule := securityPolicyEnvironmentVariableRule{ + Strategy: eContainer.EnvRules[eIndex].Strategy, + Rule: eContainer.EnvRules[eIndex].Rule, + } + envRules[i] = rule + } + iContainer.EnvRules = envRules + + // save off new container + internal[i] = iContainer + } + + return internal, nil +} + +func stringMapToStringArray(in map[string]string) []string { + inLength := len(in) + out := make([]string, inLength) + + for i := 0; i < inLength; i++ { + out[i] = in[strconv.Itoa(i)] + } + + return out } func (policyState *StandardSecurityPolicyEnforcer) EnforcePmemMountPolicy(target string, deviceHash string) (err error) { policyState.mutex.Lock() defer policyState.mutex.Unlock() - if len(policyState.SecurityPolicy.Containers) < 1 { + if len(policyState.Containers) < 1 { return errors.New("policy doesn't allow mounting containers") } @@ -134,7 +198,7 @@ func (policyState *StandardSecurityPolicyEnforcer) EnforcePmemMountPolicy(target found := false - for i, container := range policyState.SecurityPolicy.Containers { + for i, container := range policyState.Containers { for ii, layer := range container.Layers { if deviceHash == layer { policyState.Devices[i][ii] = target @@ -154,7 +218,7 @@ func (policyState *StandardSecurityPolicyEnforcer) EnforceOverlayMountPolicy(con policyState.mutex.Lock() defer policyState.mutex.Unlock() - if len(policyState.SecurityPolicy.Containers) < 1 { + if len(policyState.Containers) < 1 { return errors.New("policy doesn't allow mounting containers") } @@ -194,7 +258,7 @@ func (policyState *StandardSecurityPolicyEnforcer) EnforceStartContainerPolicy(c policyState.mutex.Lock() defer policyState.mutex.Unlock() - if len(policyState.SecurityPolicy.Containers) < 1 { + if len(policyState.Containers) < 1 { return errors.New("policy doesn't allow mounting containers") } @@ -231,7 +295,7 @@ func (policyState *StandardSecurityPolicyEnforcer) enforceCommandPolicy(containe // security policy whose command line isn't a match. matchingCommandFound := false for _, possibleIndex := range possibleIndexes { - cmd := policyState.SecurityPolicy.Containers[possibleIndex].Command + cmd := policyState.Containers[possibleIndex].Command if cmp.Equal(cmd, argList) { matchingCommandFound = true } else { @@ -258,7 +322,7 @@ func (policyState *StandardSecurityPolicyEnforcer) enforceEnvironmentVariablePol for _, envVariable := range envList { matchingRuleFoundForSomeContainer := false for _, possibleIndex := range possibleIndexes { - envRules := policyState.SecurityPolicy.Containers[possibleIndex].EnvRules + envRules := policyState.Containers[possibleIndex].EnvRules ok := envIsMatchedByRule(envVariable, envRules) if ok { matchingRuleFoundForSomeContainer = true @@ -277,7 +341,7 @@ func (policyState *StandardSecurityPolicyEnforcer) enforceEnvironmentVariablePol return nil } -func envIsMatchedByRule(envVariable string, rules []SecurityPolicyEnvironmentVariableRule) bool { +func envIsMatchedByRule(envVariable string, rules []securityPolicyEnvironmentVariableRule) bool { for _, rule := range rules { switch rule.Strategy { case "string": 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 a62a09273c..86aae0b84a 100644 --- a/test/vendor/github.com/Microsoft/hcsshim/pkg/securitypolicy/securitypolicy.go +++ b/test/vendor/github.com/Microsoft/hcsshim/pkg/securitypolicy/securitypolicy.go @@ -1,6 +1,47 @@ package securitypolicy -// SecurityPolicy is the user supplied security policy to enforce. +import ( + "encoding/base64" + "encoding/json" + + "github.com/pkg/errors" +) + +// Internal version of SecurityPolicyContainer +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 + // as part of policy. + Layers []string `json:"layers"` +} + +// Internal versino of SecurityPolicyEnvironmentVariableRule +type securityPolicyEnvironmentVariableRule struct { + Strategy string `json:"type"` + Rule string `json:"rule"` +} + +// SecurityPolicyState is a structure that holds user supplied policy to enforce +// we keep both the encoded representation and the unmarshalled representation +// because different components need to have access to either of these +type SecurityPolicyState struct { + EncodedSecurityPolicy EncodedSecurityPolicy `json:"EncodedSecurityPolicy,omitempty"` + SecurityPolicy `json:"SecurityPolicy,omitempty"` +} + +// EncodedSecurityPolicy is a JSON representation of SecurityPolicy that has +// been base64 encoded for storage in an annotation embedded within another +// JSON configuration +type EncodedSecurityPolicy struct { + SecurityPolicy string `json:"SecurityPolicy,omitempty"` +} + +// JSON transport version type SecurityPolicy struct { // Flag that when set to true allows for all checks to pass. Currently used // to run with security policy enforcement "running dark"; checks can be in @@ -9,8 +50,10 @@ type SecurityPolicy struct { // standpoint. Policy enforcement isn't actually off as the policy is "allow // everything:. AllowAll bool `json:"allow_all"` + // Total number of containers in our map + NumContainers int `json:"num_containers"` // One or more containers that are allowed to run - Containers []SecurityPolicyContainer `json:"containers"` + Containers map[string]SecurityPolicyContainer `json:"containers"` } // SecurityPolicyContainer contains information about a container that should be @@ -21,15 +64,22 @@ type SecurityPolicy struct { // entries. Once that overlay creation is allowed, the command could not match // policy and running the command would be rejected. type SecurityPolicyContainer struct { + // Number of entries that should be in the "Command" map + NumCommands int `json:"num_commands"` // The command that we will allow the container to execute - Command []string `json:"command"` + Command map[string]string `json:"command"` + // Number of entries that should be in the "EnvRules" map + NumEnvRules int `json:"num_env_rules"` // 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 + EnvRules map[string]SecurityPolicyEnvironmentVariableRule `json:"env_rules"` + // Number of entries that should in the "Layers" map + NumLayers int `json:"num_layers"` + // 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 - // as part of policy. - Layers []string `json:"layers"` + // as part of policy. The map is interpreted as an ordered list by arranging + // the keys of the map as indexes like 0,1,2,3 to establish the order. + Layers map[string]string `json:"layers"` } type SecurityPolicyEnvironmentVariableRule struct { @@ -37,9 +87,36 @@ type SecurityPolicyEnvironmentVariableRule struct { 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 -type EncodedSecurityPolicy struct { - SecurityPolicy string `json:"SecurityPolicy,omitempty"` +// Constructs SecurityPolicyState from base64Policy string. It first decodes +// base64 policy and returns the structs security policy struct and encoded +// security policy for given policy. The security policy is transmitted as json +// in an annotation, so we first have to remove the base64 encoding that allows +// the JSON based policy to be passed as a string. From there, we decode the +// JSONand setup our security policy struct +func NewSecurityPolicyState(base64Policy string) (*SecurityPolicyState, error) { + // construct an encoded security policy that holds the base64 representation + encodedSecurityPolicy := EncodedSecurityPolicy{ + SecurityPolicy: base64Policy, + } + + // base64 decode the incoming policy string + // its base64 encoded because it is coming from an annotation + // annotations are a map of string to string + // we want to store a complex json object so.... base64 it is + jsonPolicy, err := base64.StdEncoding.DecodeString(base64Policy) + if err != nil { + return nil, errors.Wrap(err, "unable to decode policy from Base64 format") + } + + // json unmarshall the decoded to a SecurityPolicy + securityPolicy := SecurityPolicy{} + err = json.Unmarshal(jsonPolicy, &securityPolicy) + if err != nil { + return nil, errors.Wrap(err, "unable to unmarshal JSON policy") + } + + return &SecurityPolicyState{ + SecurityPolicy: securityPolicy, + EncodedSecurityPolicy: encodedSecurityPolicy, + }, nil } 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 97ffa980c9..8b5d8378c1 100644 --- a/test/vendor/github.com/Microsoft/hcsshim/pkg/securitypolicy/securitypolicyenforcer.go +++ b/test/vendor/github.com/Microsoft/hcsshim/pkg/securitypolicy/securitypolicyenforcer.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "regexp" + "strconv" "sync" "github.com/google/go-cmp/cmp" @@ -15,21 +16,23 @@ type SecurityPolicyEnforcer interface { EnforceStartContainerPolicy(containerID string, argList []string, envList []string) (err error) } -func NewSecurityPolicyEnforcer(policy *SecurityPolicy) (SecurityPolicyEnforcer, error) { - if policy == nil { - return nil, errors.New("security policy can't be nil") - } - - if policy.AllowAll { +func NewSecurityPolicyEnforcer(state SecurityPolicyState) (SecurityPolicyEnforcer, error) { + if state.SecurityPolicy.AllowAll { return &OpenDoorSecurityPolicyEnforcer{}, nil } else { - return NewStandardSecurityPolicyEnforcer(policy) + containers, err := toInternal(&state.SecurityPolicy) + if err != nil { + return nil, err + } + return NewStandardSecurityPolicyEnforcer(containers, state.EncodedSecurityPolicy.SecurityPolicy), nil } } type StandardSecurityPolicyEnforcer struct { - // The user supplied security policy. - SecurityPolicy SecurityPolicy + // EncodedSecurityPolicy state is needed for key release + EncodedSecurityPolicy string + // Containers from the user supplied security policy. + Containers []securityPolicyContainer // Devices and ContainerIndexToContainerIds are used to build up an // understanding of the containers running with a UVM as they come up and // map them back to a container definition from the user supplied @@ -95,36 +98,97 @@ type StandardSecurityPolicyEnforcer struct { var _ SecurityPolicyEnforcer = (*StandardSecurityPolicyEnforcer)(nil) -func NewStandardSecurityPolicyEnforcer(policy *SecurityPolicy) (*StandardSecurityPolicyEnforcer, error) { - if policy == nil { - return nil, errors.New("security policy can't be nil") - } - - // create new StandardSecurityPolicyEnforcer and add the new SecurityPolicy +func NewStandardSecurityPolicyEnforcer(containers []securityPolicyContainer, encoded string) *StandardSecurityPolicyEnforcer { + // create new StandardSecurityPolicyEnforcer and add the expected containers // to it // fill out corresponding devices structure by creating a "same shapped" // devices listing that corresponds to our container root hash lists // the devices list will get filled out as layers are mounted - devices := make([][]string, len(policy.Containers)) + devices := make([][]string, len(containers)) - for i, container := range policy.Containers { + for i, container := range containers { devices[i] = make([]string, len(container.Layers)) } return &StandardSecurityPolicyEnforcer{ - SecurityPolicy: *policy, + EncodedSecurityPolicy: encoded, + Containers: containers, Devices: devices, ContainerIndexToContainerIds: map[int][]string{}, startedContainers: map[string]struct{}{}, mutex: &sync.Mutex{}, - }, nil + } +} + +func toInternal(external *SecurityPolicy) ([]securityPolicyContainer, error) { + containerMapLength := len(external.Containers) + if external.NumContainers != containerMapLength { + errmsg := fmt.Sprintf("container numbers don't match in policy. expected: %d, actual: %d", external.NumContainers, containerMapLength) + return nil, errors.New(errmsg) + } + + internal := make([]securityPolicyContainer, containerMapLength) + + for i := 0; i < containerMapLength; i++ { + iContainer := securityPolicyContainer{} + + eContainer := external.Containers[strconv.Itoa(i)] + + // Command conversion + if eContainer.NumCommands != len(eContainer.Command) { + errmsg := fmt.Sprintf("command argument numbers don't match in policy. expected: %d, actual: %d", eContainer.NumCommands, len(eContainer.Command)) + return nil, errors.New(errmsg) + } + iContainer.Command = stringMapToStringArray(eContainer.Command) + + // Layers conversion + if eContainer.NumLayers != len(eContainer.Layers) { + errmsg := fmt.Sprintf("layer numbers don't match in policy. expected: %d, actual: %d", eContainer.NumLayers, len(eContainer.Layers)) + return nil, errors.New(errmsg) + } + iContainer.Layers = stringMapToStringArray(eContainer.Layers) + + // EnvRules conversion + envRulesMapLength := len(eContainer.EnvRules) + if eContainer.NumEnvRules != envRulesMapLength { + errmsg := fmt.Sprintf("env rule numbers don't match in policy. expected: %d, actual: %d", eContainer.NumEnvRules, envRulesMapLength) + return nil, errors.New(errmsg) + } + + envRules := make([]securityPolicyEnvironmentVariableRule, envRulesMapLength) + for i := 0; i < envRulesMapLength; i++ { + eIndex := strconv.Itoa(i) + rule := securityPolicyEnvironmentVariableRule{ + Strategy: eContainer.EnvRules[eIndex].Strategy, + Rule: eContainer.EnvRules[eIndex].Rule, + } + envRules[i] = rule + } + iContainer.EnvRules = envRules + + // save off new container + internal[i] = iContainer + } + + return internal, nil +} + +func stringMapToStringArray(in map[string]string) []string { + inLength := len(in) + out := make([]string, inLength) + + for i := 0; i < inLength; i++ { + out[i] = in[strconv.Itoa(i)] + } + + return out } func (policyState *StandardSecurityPolicyEnforcer) EnforcePmemMountPolicy(target string, deviceHash string) (err error) { policyState.mutex.Lock() defer policyState.mutex.Unlock() - if len(policyState.SecurityPolicy.Containers) < 1 { + if len(policyState.Containers) < 1 { return errors.New("policy doesn't allow mounting containers") } @@ -134,7 +198,7 @@ func (policyState *StandardSecurityPolicyEnforcer) EnforcePmemMountPolicy(target found := false - for i, container := range policyState.SecurityPolicy.Containers { + for i, container := range policyState.Containers { for ii, layer := range container.Layers { if deviceHash == layer { policyState.Devices[i][ii] = target @@ -154,7 +218,7 @@ func (policyState *StandardSecurityPolicyEnforcer) EnforceOverlayMountPolicy(con policyState.mutex.Lock() defer policyState.mutex.Unlock() - if len(policyState.SecurityPolicy.Containers) < 1 { + if len(policyState.Containers) < 1 { return errors.New("policy doesn't allow mounting containers") } @@ -194,7 +258,7 @@ func (policyState *StandardSecurityPolicyEnforcer) EnforceStartContainerPolicy(c policyState.mutex.Lock() defer policyState.mutex.Unlock() - if len(policyState.SecurityPolicy.Containers) < 1 { + if len(policyState.Containers) < 1 { return errors.New("policy doesn't allow mounting containers") } @@ -231,7 +295,7 @@ func (policyState *StandardSecurityPolicyEnforcer) enforceCommandPolicy(containe // security policy whose command line isn't a match. matchingCommandFound := false for _, possibleIndex := range possibleIndexes { - cmd := policyState.SecurityPolicy.Containers[possibleIndex].Command + cmd := policyState.Containers[possibleIndex].Command if cmp.Equal(cmd, argList) { matchingCommandFound = true } else { @@ -258,7 +322,7 @@ func (policyState *StandardSecurityPolicyEnforcer) enforceEnvironmentVariablePol for _, envVariable := range envList { matchingRuleFoundForSomeContainer := false for _, possibleIndex := range possibleIndexes { - envRules := policyState.SecurityPolicy.Containers[possibleIndex].EnvRules + envRules := policyState.Containers[possibleIndex].EnvRules ok := envIsMatchedByRule(envVariable, envRules) if ok { matchingRuleFoundForSomeContainer = true @@ -277,7 +341,7 @@ func (policyState *StandardSecurityPolicyEnforcer) enforceEnvironmentVariablePol return nil } -func envIsMatchedByRule(envVariable string, rules []SecurityPolicyEnvironmentVariableRule) bool { +func envIsMatchedByRule(envVariable string, rules []securityPolicyEnvironmentVariableRule) bool { for _, rule := range rules { switch rule.Strategy { case "string":