From 7671a84561e9c30d5e0963554d8e32edf1d81cb4 Mon Sep 17 00:00:00 2001 From: "Sean T. Allen" Date: Thu, 9 Sep 2021 09:25:17 -0400 Subject: [PATCH] Switch JSON policy schema from using arrays to maps The existing array based policy doesn't work with managed HSMs query language, so, we have switched it up to using maps instead of arrays. Map keys correspond to the array index that an entry would have. This allows us to keep ordering. Signed-off-by: Sean T. Allen --- internal/guest/runtime/hcsv2/uvm.go | 18 +- internal/tools/securitypolicy/README.md | 97 ++--- internal/tools/securitypolicy/main.go | 67 +++- pkg/securitypolicy/securitypolicy.go | 101 ++++- pkg/securitypolicy/securitypolicy_test.go | 345 +++++++++++------- pkg/securitypolicy/securitypolicyenforcer.go | 116 ++++-- .../pkg/securitypolicy/securitypolicy.go | 101 ++++- .../securitypolicy/securitypolicyenforcer.go | 116 ++++-- 8 files changed, 683 insertions(+), 278 deletions(-) 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":