//go:build linux // +build linux package securitypolicy import ( "context" "fmt" "math/rand" "os" "path/filepath" "reflect" "strconv" "strings" "syscall" "testing" "testing/quick" "time" "github.com/Microsoft/hcsshim/internal/guestpath" "github.com/google/go-cmp/cmp" ) const ( // variables that influence generated test fixtures minStringLength = 10 maxContainersInGeneratedConstraints = 32 maxLayersInGeneratedContainer = 32 maxGeneratedContainerID = 1000000 maxGeneratedCommandLength = 128 maxGeneratedCommandArgs = 12 maxGeneratedEnvironmentVariables = 16 maxGeneratedEnvironmentVariableRuleLength = 64 maxGeneratedEnvironmentVariableRules = 8 maxGeneratedFragmentNamespaceLength = 32 maxGeneratedMountTargetLength = 256 maxGeneratedVersion = 10 rootHashLength = 64 maxGeneratedMounts = 4 maxGeneratedMountSourceLength = 32 maxGeneratedMountDestinationLength = 32 maxGeneratedMountOptions = 5 maxGeneratedMountOptionLength = 32 maxGeneratedExecProcesses = 4 maxGeneratedWorkingDirLength = 128 maxSignalNumber = 64 maxGeneratedNameLength = 8 maxGeneratedGroupNames = 4 maxGeneratedCapabilities = 12 maxGeneratedCapabilitesLength = 24 // additional consts // the standard enforcer tests don't do anything with the encoded policy // string. this const exists to make that explicit ignoredEncodedPolicyString = "" ) var testRand *rand.Rand var testDataGenerator *dataGenerator func init() { seed := time.Now().Unix() if seedStr, ok := os.LookupEnv("SEED"); ok { if parsedSeed, err := strconv.ParseInt(seedStr, 10, 64); err != nil { fmt.Fprintf(os.Stderr, "failed to parse seed: %d\n", seed) } else { seed = parsedSeed } } testRand = rand.New(rand.NewSource(seed)) fmt.Fprintf(os.Stdout, "securitypolicy_test seed: %d\n", seed) testDataGenerator = newDataGenerator(testRand) } // 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 { containers, err := p.Containers.toInternal() 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.Containers.Length { t.Errorf("number of containers doesn't match. internal: %d, external: %d", len(containers), p.Containers.Length) return false } // do by index comparison of containers for i := 0; i < len(containers); i++ { internal := containers[i] external := p.Containers.Elements[strconv.Itoa(i)] // verify sanity with size if len(internal.Command) != external.Command.Length { t.Errorf("number of command args doesn't match for container %d. internal: %d, external: %d", i, len(internal.Command), external.Command.Length) } if len(internal.EnvRules) != external.EnvRules.Length { t.Errorf("number of env rules doesn't match for container %d. internal: %d, external: %d", i, len(internal.EnvRules), external.EnvRules.Length) } if len(internal.Layers) != external.Layers.Length { t.Errorf("number of layers doesn't match for container %d. internal: %d, external: %d", i, len(internal.Layers), external.Layers.Length) } // do by index comparison of sub-items for j := 0; j < len(internal.Command); j++ { if internal.Command[j] != external.Command.Elements[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.Elements[strconv.Itoa(j)]) } } for j := 0; j < len(internal.EnvRules); j++ { irule := internal.EnvRules[j] erule := external.EnvRules.Elements[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.Elements[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.Elements[strconv.Itoa(j)]) } } } return !t.Failed() } if err := quick.Check(f, &quick.Config{MaxCount: 1000, Rand: testRand}); err != nil { t.Errorf("Test_StandardSecurityPolicyEnforcer_From_Security_Policy_Conversion failed: %v", err) } } // Verify that StandardSecurityPolicyEnforcer.EnforceDeviceMountPolicy will // return an error when there's no matching root hash in the policy func Test_EnforceDeviceMountPolicy_No_Matches(t *testing.T) { f := func(p *generatedConstraints) bool { policy := NewStandardSecurityPolicyEnforcer(p.containers, ignoredEncodedPolicyString) target := generateMountTarget(testRand) rootHash := generateInvalidRootHash(testRand) err := policy.EnforceDeviceMountPolicy(p.ctx, target, rootHash) // we expect an error, not getting one means something is broken return err != nil } if err := quick.Check(f, &quick.Config{MaxCount: 1000, Rand: testRand}); err != nil { t.Errorf("Test_EnforceDeviceMountPolicy_No_Matches failed: %v", err) } } // Verify that StandardSecurityPolicyEnforcer.EnforceDeviceMountPolicy doesn't // return an error when there's a matching root hash in the policy func Test_EnforceDeviceMountPolicy_Matches(t *testing.T) { f := func(p *generatedConstraints) bool { policy := NewStandardSecurityPolicyEnforcer(p.containers, ignoredEncodedPolicyString) target := generateMountTarget(testRand) rootHash := selectRootHashFromConstraints(p, testRand) err := policy.EnforceDeviceMountPolicy(p.ctx, target, rootHash) // getting an error means something is broken return err == nil } if err := quick.Check(f, &quick.Config{MaxCount: 1000, Rand: testRand}); err != nil { t.Errorf("Test_EnforceDeviceMountPolicy_No_Matches failed: %v", err) } } func Test_EnforceDeviceUmountPolicy_Removes_Device_Entries(t *testing.T) { f := func(p *generatedConstraints) bool { policy := NewStandardSecurityPolicyEnforcer(p.containers, ignoredEncodedPolicyString) target := generateMountTarget(testRand) rootHash := selectRootHashFromConstraints(p, testRand) err := policy.EnforceDeviceMountPolicy(p.ctx, target, rootHash) if err != nil { t.Error(err) return false } if v, ok := policy.Devices[target]; !ok || v != rootHash { t.Errorf("root hash is missing or doesn't match: actual=%q expected=%q", v, rootHash) return false } err = policy.EnforceDeviceUnmountPolicy(p.ctx, target) if err != nil { t.Error(err) return false } return cmp.Equal(policy.Devices, map[string]string{}) } if err := quick.Check(f, &quick.Config{MaxCount: 1000, Rand: testRand}); err != nil { t.Errorf("Test_EnforceDeviceUmountPolicy_Removes_Device_Entries failed: %v", err) } } // 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 *generatedConstraints) bool { tc, err := setupContainerWithOverlay(p, false) if err != nil { t.Error(err) return false } err = tc.policy.EnforceOverlayMountPolicy(p.ctx, tc.containerID, tc.layers, generateMountTarget(testRand)) // not getting an error means something is broken return err != nil } if err := quick.Check(f, &quick.Config{MaxCount: 1000, Rand: testRand}); err != nil { t.Errorf("Test_EnforceOverlayMountPolicy_No_Matches failed: %v", err) } } // 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 *generatedConstraints) bool { tc, err := setupContainerWithOverlay(p, true) if err != nil { t.Error(err) return false } err = tc.policy.EnforceOverlayMountPolicy(p.ctx, tc.containerID, tc.layers, generateMountTarget(testRand)) // getting an error means something is broken return err == nil } if err := quick.Check(f, &quick.Config{MaxCount: 1000, Rand: testRand}); err != nil { t.Errorf("Test_EnforceOverlayMountPolicy_Matches: %v", err) } } // 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) { gc := generateConstraints(testRand, 1) tc, err := setupContainerWithOverlay(gc, true) if err != nil { t.Fatalf("expected nil error got: %v", err) } if err := tc.policy.EnforceOverlayMountPolicy(gc.ctx, tc.containerID, tc.layers, generateMountTarget(testRand)); err != nil { t.Fatalf("expected nil error got: %v", err) } if err := tc.policy.EnforceOverlayMountPolicy(gc.ctx, tc.containerID, tc.layers, generateMountTarget(testRand)); err == nil { t.Fatal("able to create overlay for the same container twice") } } // Test that if more than 1 instance of the same image is started, that we can // create all the overlays that are required. So for example, if there are // 13 instances of image X that all share the same overlay of root hashes, // all 13 should be allowed. func Test_EnforceOverlayMountPolicy_Multiple_Instances_Same_Container(t *testing.T) { ctx := context.Background() for containersToCreate := 2; containersToCreate <= maxContainersInGeneratedConstraints; containersToCreate++ { var containers []*securityPolicyContainer for i := 1; i <= containersToCreate; i++ { arg := "command " + strconv.Itoa(i) c := &securityPolicyContainer{ Command: []string{arg}, Layers: []string{"1", "2"}, } containers = append(containers, c) } sp := NewStandardSecurityPolicyEnforcer(containers, "") for i := 0; i < len(containers); i++ { layerPaths, err := testDataGenerator.createValidOverlayForContainer(sp, containers[i]) if err != nil { t.Fatal("unexpected error on test setup") } id := testDataGenerator.uniqueContainerID() err = sp.EnforceOverlayMountPolicy(ctx, id, layerPaths, generateMountTarget(testRand)) if err != nil { t.Fatalf("failed with %d containers", containersToCreate) } } } } // Verify that can't create more containers using an overlay than exists in the // policy. For example, if there is a single instance of image Foo in the // policy, we should be able to create a single container for that overlay // but no more than that one. func Test_EnforceOverlayMountPolicy_Overlay_Single_Container_Twice_With_Different_IDs(t *testing.T) { p := generateConstraints(testRand, 1) sp := NewStandardSecurityPolicyEnforcer(p.containers, ignoredEncodedPolicyString) var containerIDOne, containerIDTwo string for containerIDOne == containerIDTwo { containerIDOne = generateContainerID(testRand) containerIDTwo = generateContainerID(testRand) } container := selectContainerFromContainerList(p.containers, testRand) layerPaths, err := testDataGenerator.createValidOverlayForContainer(sp, container) if err != nil { t.Fatalf("expected nil error got: %v", err) } err = sp.EnforceOverlayMountPolicy(p.ctx, containerIDOne, layerPaths, generateMountTarget(testRand)) if err != nil { t.Fatalf("expected nil error got: %v", err) } err = sp.EnforceOverlayMountPolicy(p.ctx, containerIDTwo, layerPaths, generateMountTarget(testRand)) if err == nil { t.Fatal("able to reuse an overlay across containers") } } func Test_EnforceCommandPolicy_Matches(t *testing.T) { f := func(p *generatedConstraints) bool { tc, err := setupContainerWithOverlay(p, true) if err != nil { t.Error(err) return false } if err := tc.policy.EnforceOverlayMountPolicy(p.ctx, tc.containerID, tc.layers, generateMountTarget(testRand)); err != nil { t.Errorf("failed to enforce overlay mount policy: %s", err) return false } err = tc.policy.enforceCommandPolicy(tc.containerID, tc.container.Command) // getting an error means something is broken return err == nil } if err := quick.Check(f, &quick.Config{MaxCount: 1000, Rand: testRand}); err != nil { t.Errorf("Test_EnforceCommandPolicy_Matches: %v", err) } } func Test_EnforceCommandPolicy_NoMatches(t *testing.T) { f := func(p *generatedConstraints) bool { tc, err := setupContainerWithOverlay(p, true) if err != nil { t.Error(err) return false } if err := tc.policy.EnforceOverlayMountPolicy(p.ctx, tc.containerID, tc.layers, generateMountTarget(testRand)); err != nil { t.Errorf("failed to enforce overlay mount policy: %s", err) return false } err = tc.policy.enforceCommandPolicy(tc.containerID, generateCommand(testRand)) // not getting an error means something is broken return err != nil } if err := quick.Check(f, &quick.Config{MaxCount: 1000, Rand: testRand}); err != nil { t.Errorf("Test_EnforceCommandPolicy_NoMatches: %v", err) } } // This is a tricky test. // The key to understanding it is, that when we have multiple containers // with the same base aka same mounts and overlay, then we don't know at the // time of overlay which container from policy is a given container id refers // to. Instead we have a list of possible container ids for the so far matching // containers in policy. We can narrow down the list of possible containers // at the time that we enforce commands. // // 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 *generatedConstraints) bool { // create two additional containers that "share everything" // except that they have different commands testContainerOne := generateConstraintsContainer(testRand, 1, 5) testContainerTwo := *testContainerOne testContainerTwo.Command = generateCommand(testRand) // add new containers to policy before creating enforcer p.containers = append(p.containers, testContainerOne, &testContainerTwo) policy := NewStandardSecurityPolicyEnforcer(p.containers, ignoredEncodedPolicyString) testContainerOneID := "" testContainerTwoID := "" indexForContainerOne := -1 indexForContainerTwo := -1 // mount and overlay all our containers for index, container := range p.containers { containerID := generateContainerID(testRand) layerPaths, err := testDataGenerator.createValidOverlayForContainer(policy, container) if err != nil { return false } err = policy.EnforceOverlayMountPolicy(p.ctx, containerID, layerPaths, generateMountTarget(testRand)) if err != nil { return false } if cmp.Equal(container, testContainerOne) { testContainerOneID = containerID indexForContainerOne = index } if cmp.Equal(container, &testContainerTwo) { testContainerTwoID = containerID indexForContainerTwo = index } } // validate our expectations prior to enforcing command policy containerOneMapping := policy.ContainerIndexToContainerIds[indexForContainerOne] if len(containerOneMapping) != 2 { return false } for id := range containerOneMapping { if (id != testContainerOneID) && (id != testContainerTwoID) { return false } } containerTwoMapping := policy.ContainerIndexToContainerIds[indexForContainerTwo] if len(containerTwoMapping) != 2 { return false } for id := range containerTwoMapping { if (id != testContainerOneID) && (id != testContainerTwoID) { return false } } // enforce command policy for containerOne // this will narrow our list of possible ids down err := policy.enforceCommandPolicy(testContainerOneID, testContainerOne.Command) if err != nil { return false } // Ok, we have full setup and we can now verify that when we enforced // command policy above that it correctly narrowed down containerTwo updatedMapping := policy.ContainerIndexToContainerIds[indexForContainerTwo] if len(updatedMapping) != 1 { return false } for id := range updatedMapping { if id != testContainerTwoID { return false } } return true } // This is a more expensive test to run than others, so we run fewer times // for each run, if err := quick.Check(f, &quick.Config{MaxCount: 100}); err != nil { t.Errorf("Test_EnforceCommandPolicy_NarrowingMatches: %v", err) } } func Test_EnforceEnvironmentVariablePolicy_Matches(t *testing.T) { f := func(p *generatedConstraints) bool { tc, err := setupContainerWithOverlay(p, true) if err != nil { t.Error(err) return false } if err = tc.policy.EnforceOverlayMountPolicy(p.ctx, tc.containerID, tc.layers, generateMountTarget(testRand)); err != nil { t.Errorf("failed to enforce overlay mount policy: %s", err) return false } envVars := buildEnvironmentVariablesFromEnvRules(tc.container.EnvRules, testRand) err = tc.policy.enforceEnvironmentVariablePolicy(tc.containerID, envVars) // getting an error means something is broken return err == nil } if err := quick.Check(f, &quick.Config{MaxCount: 1000, Rand: testRand}); err != nil { t.Errorf("Test_EnforceEnvironmentVariablePolicy_Matches: %v", err) } } func Test_EnforceEnvironmentVariablePolicy_Re2Match(t *testing.T) { p := generateConstraints(testRand, 1) container := generateConstraintsContainer(testRand, 1, 1) // add a rule to re2 match re2MatchRule := EnvRuleConfig{ Strategy: EnvVarRuleRegex, Rule: "PREFIX_.+=.+", } container.EnvRules = append(container.EnvRules, re2MatchRule) p.containers = append(p.containers, container) policy := NewStandardSecurityPolicyEnforcer(p.containers, ignoredEncodedPolicyString) containerID := generateContainerID(testRand) layerPaths, err := testDataGenerator.createValidOverlayForContainer(policy, container) if err != nil { t.Fatalf("expected nil error got: %v", err) } err = policy.EnforceOverlayMountPolicy(p.ctx, containerID, layerPaths, generateMountTarget(testRand)) if err != nil { t.Fatalf("expected nil error got: %v", err) } envVars := []string{"PREFIX_FOO=BAR"} err = policy.enforceEnvironmentVariablePolicy(containerID, envVars) // getting an error means something is broken if err != nil { t.Fatalf("expected nil error got: %v", err) } } func Test_EnforceEnvironmentVariablePolicy_NotAllMatches(t *testing.T) { f := func(p *generatedConstraints) bool { tc, err := setupContainerWithOverlay(p, true) if err != nil { t.Error(err) return false } if err = tc.policy.EnforceOverlayMountPolicy(p.ctx, tc.containerID, tc.layers, generateMountTarget(testRand)); err != nil { t.Errorf("failed to enforce overlay mount policy: %s", err) return false } envVars := generateEnvironmentVariables(testRand) envVars = append(envVars, generateNeverMatchingEnvironmentVariable(testRand)) err = tc.policy.enforceEnvironmentVariablePolicy(tc.containerID, envVars) // not getting an error means something is broken return err != nil } if err := quick.Check(f, &quick.Config{MaxCount: 1000, Rand: testRand}); err != nil { t.Errorf("Test_EnforceEnvironmentVariablePolicy_NotAllMatches: %v", err) } } // This is a tricky test. // The key to understanding it is, that when we have multiple containers // with the same base aka same mounts and overlay, then we don't know at the // time of overlay which container from policy is a given container id refers // to. Instead we have a list of possible container ids for the so far matching // containers in policy. We can narrow down the list of possible containers // at the time that we enforce environment variables, the same as we do with // commands. // // This test verifies the "narrowing possible container ids that could be // the container in our policy" functionality works correctly. func Test_EnforceEnvironmentVariablePolicy_NarrowingMatches(t *testing.T) { f := func(p *generatedConstraints) bool { // create two additional containers that "share everything" // except that they have different environment variables testContainerOne := generateConstraintsContainer(testRand, 1, 5) testContainerTwo := *testContainerOne testContainerTwo.EnvRules = generateEnvironmentVariableRules(testRand) // add new containers to policy before creating enforcer p.containers = append(p.containers, testContainerOne, &testContainerTwo) policy := NewStandardSecurityPolicyEnforcer(p.containers, ignoredEncodedPolicyString) testContainerOneID := "" testContainerTwoID := "" indexForContainerOne := -1 indexForContainerTwo := -1 // mount and overlay all our containers for index, container := range p.containers { containerID := generateContainerID(testRand) layerPaths, err := testDataGenerator.createValidOverlayForContainer(policy, container) if err != nil { t.Error(err) return false } err = policy.EnforceOverlayMountPolicy(p.ctx, containerID, layerPaths, generateMountTarget(testRand)) if err != nil { t.Error(err) return false } if cmp.Equal(container, testContainerOne) { testContainerOneID = containerID indexForContainerOne = index } if cmp.Equal(container, &testContainerTwo) { testContainerTwoID = containerID indexForContainerTwo = index } } // validate our expectations prior to enforcing command policy containerOneMapping := policy.ContainerIndexToContainerIds[indexForContainerOne] if len(containerOneMapping) != 2 { return false } for id := range containerOneMapping { if (id != testContainerOneID) && (id != testContainerTwoID) { return false } } containerTwoMapping := policy.ContainerIndexToContainerIds[indexForContainerTwo] if len(containerTwoMapping) != 2 { return false } for id := range containerTwoMapping { if (id != testContainerOneID) && (id != testContainerTwoID) { return false } } // enforce command policy for containerOne // this will narrow our list of possible ids down envVars := buildEnvironmentVariablesFromEnvRules(testContainerOne.EnvRules, testRand) err := policy.enforceEnvironmentVariablePolicy(testContainerOneID, envVars) if err != nil { t.Error(err) return false } // Ok, we have full setup and we can now verify that when we enforced // command policy above that it correctly narrowed down containerTwo updatedMapping := policy.ContainerIndexToContainerIds[indexForContainerTwo] if len(updatedMapping) != 1 { return false } for id := range updatedMapping { if id != testContainerTwoID { return false } } return true } // This is a more expensive test to run than others, so we run fewer times // for each run, if err := quick.Check(f, &quick.Config{MaxCount: 100}); err != nil { t.Errorf("Test_EnforceEnvironmentVariablePolicy_NarrowingMatches: %v", err) } } func Test_WorkingDirectoryPolicy_Matches(t *testing.T) { testFunc := func(gc *generatedConstraints) bool { tc, err := setupContainerWithOverlay(gc, true) if err != nil { t.Error(err) return false } if err := tc.policy.EnforceOverlayMountPolicy(gc.ctx, tc.containerID, tc.layers, generateMountTarget(testRand)); err != nil { t.Errorf("failed to enforce overlay mount policy: %s", err) return false } return tc.policy.enforceWorkingDirPolicy(tc.containerID, tc.container.WorkingDir) == nil } if err := quick.Check(testFunc, &quick.Config{MaxCount: 1000, Rand: testRand}); err != nil { t.Errorf("Test_WorkingDirectoryPolicy_Matches: %v", err) } } func Test_WorkingDirectoryPolicy_NoMatches(t *testing.T) { testFunc := func(gc *generatedConstraints) bool { tc, err := setupContainerWithOverlay(gc, true) if err != nil { t.Error(err) return false } if err := tc.policy.EnforceOverlayMountPolicy(gc.ctx, tc.containerID, tc.layers, generateMountTarget(testRand)); err != nil { t.Errorf("failed to enforce overlay mount policy: %s", err) return false } return tc.policy.enforceWorkingDirPolicy(tc.containerID, randString(testRand, 20)) != nil } if err := quick.Check(testFunc, &quick.Config{MaxCount: 1000, Rand: testRand}); err != nil { t.Errorf("Test_WorkingDirectoryPolicy_NoMatches: %v", err) } } // Consequent layers. func Test_Overlay_Duplicate_Layers(t *testing.T) { f := func(p *generatedConstraints) bool { c1 := generateConstraintsContainer(testRand, 5, 5) numLayers := len(c1.Layers) // make sure first container has two identical layers c1.Layers[numLayers-3] = c1.Layers[numLayers-2] policy := NewStandardSecurityPolicyEnforcer([]*securityPolicyContainer{c1}, ignoredEncodedPolicyString) // generate mount targets mountTargets := make([]string, numLayers) for i := 0; i < numLayers; i++ { mountTargets[i] = randString(testRand, maxGeneratedMountTargetLength) } // call into mount enforcement for i := 0; i < numLayers; i++ { if err := policy.EnforceDeviceMountPolicy(p.ctx, mountTargets[i], c1.Layers[i]); err != nil { t.Errorf("failed to enforce device mount policy: %s", err) return false } } if len(policy.Devices) != numLayers { t.Errorf("the number of mounted devices %v don't match the expectation: targets=%v layers=%v", policy.Devices, mountTargets, c1.Layers) return false } overlay := make([]string, numLayers) for i := 0; i < numLayers; i++ { overlay[i] = mountTargets[numLayers-i-1] } containerID := randString(testRand, 32) if err := policy.EnforceOverlayMountPolicy(p.ctx, containerID, overlay, generateMountTarget(testRand)); err != nil { t.Errorf("failed to enforce overlay mount policy: %s", err) return false } // validate the state of the ContainerIndexToContainerIds mapping if containerIDs, ok := policy.ContainerIndexToContainerIds[0]; !ok { t.Errorf("container index to containerIDs mapping was not set: %v", containerIDs) return false } else { if _, ok := containerIDs[containerID]; !ok { t.Errorf("containerID is missing from possible containerIDs set: %v", containerIDs) return false } } for _, mountTarget := range mountTargets { if err := policy.EnforceDeviceUnmountPolicy(p.ctx, mountTarget); err != nil { t.Errorf("failed to enforce unmount policy: %s", err) return false } } return true } if err := quick.Check(f, &quick.Config{MaxCount: 1, Rand: testRand}); err != nil { t.Errorf("failed to run stuff: %s", err) } } func Test_EnforceDeviceMountPolicy_DifferentTargetsWithTheSameHash(t *testing.T) { ctx := context.Background() c := generateConstraintsContainer(testRand, 2, 2) policy := NewStandardSecurityPolicyEnforcer([]*securityPolicyContainer{c}, ignoredEncodedPolicyString) mountTarget := randString(testRand, 10) if err := policy.EnforceDeviceMountPolicy(ctx, mountTarget, c.Layers[0]); err != nil { t.Fatalf("unexpected error: %s", err) } // Mounting the second layer at the same mount target should fail if err := policy.EnforceDeviceMountPolicy(ctx, mountTarget, c.Layers[1]); err == nil { t.Fatal("expected conflicting device hashes error") } } func Test_EnforcePrivileged_AllowElevatedAllowsPrivilegedContainer(t *testing.T) { c := generateConstraints(testRand, 1) c.containers[0].AllowElevated = true tc, err := setupContainerWithOverlay(c, true) if err != nil { t.Fatalf("unexpected error during test setup: %s", err) } if err := tc.policy.EnforceOverlayMountPolicy(c.ctx, tc.containerID, tc.layers, generateMountTarget(testRand)); err != nil { t.Fatalf("failed to enforce overlay mount policy: %s", err) } err = tc.policy.enforcePrivilegedPolicy(tc.containerID, true) if err != nil { t.Fatalf("expected privilege escalation to be allowed: %s", err) } } func Test_EnforcePrivileged_AllowElevatedAllowsUnprivilegedContainer(t *testing.T) { c := generateConstraints(testRand, 1) c.containers[0].AllowElevated = true tc, err := setupContainerWithOverlay(c, true) if err != nil { t.Fatalf("unexpected error during test setup: %s", err) } if err := tc.policy.EnforceOverlayMountPolicy(c.ctx, tc.containerID, tc.layers, generateMountTarget(testRand)); err != nil { t.Fatalf("failed to enforce overlay mount policy: %s", err) } err = tc.policy.enforcePrivilegedPolicy(tc.containerID, true) if err != nil { t.Fatalf("expected lack of escalation to be fine: %s", err) } } func Test_EnforcePrivileged_NoAllowElevatedDenysPrivilegedContainer(t *testing.T) { c := generateConstraints(testRand, 1) c.containers[0].AllowElevated = false tc, err := setupContainerWithOverlay(c, true) if err != nil { t.Fatalf("unexpected error during test setup: %s", err) } if err := tc.policy.EnforceOverlayMountPolicy(c.ctx, tc.containerID, tc.layers, generateMountTarget(testRand)); err != nil { t.Fatalf("failed to enforce overlay mount policy: %s", err) } err = tc.policy.enforcePrivilegedPolicy(tc.containerID, true) if err == nil { t.Fatal("expected escalation to be denied") } } func Test_EnforcePrivileged_NoAllowElevatedAllowsUnprivilegedContainer(t *testing.T) { c := generateConstraints(testRand, 1) c.containers[0].AllowElevated = false tc, err := setupContainerWithOverlay(c, true) if err != nil { t.Fatalf("unexpected error during test setup: %s", err) } if err := tc.policy.EnforceOverlayMountPolicy(c.ctx, tc.containerID, tc.layers, generateMountTarget(testRand)); err != nil { t.Fatalf("failed to enforce overlay mount policy: %s", err) } err = tc.policy.enforcePrivilegedPolicy(tc.containerID, false) if err != nil { t.Fatalf("expected lack of escalation to be fine: %s", err) } } // // Setup and "fixtures" follow... // func (*SecurityPolicy) Generate(r *rand.Rand, _ int) reflect.Value { // 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: Containers{ Elements: map[string]Container{}, }, } p.AllowAll = false numContainers := int(atLeastOneAtMost(r, maxContainersInGeneratedConstraints)) for i := 0; i < numContainers; i++ { c := Container{ Command: CommandArgs{ Elements: map[string]string{}, }, EnvRules: EnvRules{ Elements: map[string]EnvRuleConfig{}, }, Layers: Layers{ Elements: map[string]string{}, }, } // command numArgs := int(atLeastOneAtMost(r, maxGeneratedCommandArgs)) for j := 0; j < numArgs; j++ { c.Command.Elements[strconv.Itoa(j)] = randVariableString(r, maxGeneratedCommandLength) } c.Command.Length = numArgs // layers numLayers := int(atLeastOneAtMost(r, maxLayersInGeneratedContainer)) for j := 0; j < numLayers; j++ { c.Layers.Elements[strconv.Itoa(j)] = generateRootHash(r) } c.Layers.Length = numLayers // env variable rules numEnvRules := int(atMost(r, maxGeneratedEnvironmentVariableRules)) for j := 0; j < numEnvRules; j++ { rule := EnvRuleConfig{ Strategy: "string", Rule: randVariableString(r, maxGeneratedEnvironmentVariableRuleLength), Required: false, } c.EnvRules.Elements[strconv.Itoa(j)] = rule } c.EnvRules.Length = numEnvRules p.Containers.Elements[strconv.Itoa(i)] = c } p.Containers.Length = numContainers return reflect.ValueOf(p) } func (*generatedConstraints) Generate(r *rand.Rand, _ int) reflect.Value { c := generateConstraints(r, maxContainersInGeneratedConstraints) return reflect.ValueOf(c) } type testConfig struct { container *securityPolicyContainer layers []string containerID string policy *StandardSecurityPolicyEnforcer } func setupContainerWithOverlay(gc *generatedConstraints, valid bool) (tc *testConfig, err error) { sp := NewStandardSecurityPolicyEnforcer(gc.containers, ignoredEncodedPolicyString) containerID := testDataGenerator.uniqueContainerID() c := selectContainerFromContainerList(gc.containers, testRand) var layerPaths []string if valid { layerPaths, err = testDataGenerator.createValidOverlayForContainer(sp, c) if err != nil { return nil, fmt.Errorf("error creating valid overlay: %w", err) } } else { layerPaths, err = testDataGenerator.createInvalidOverlayForContainer(sp, c) if err != nil { return nil, fmt.Errorf("error creating invalid overlay: %w", err) } } return &testConfig{ container: c, layers: layerPaths, containerID: containerID, policy: sp, }, nil } func generateConstraints(r *rand.Rand, maxContainers int32) *generatedConstraints { var containers []*securityPolicyContainer numContainers := (int)(atLeastOneAtMost(r, maxContainers)) for i := 0; i < numContainers; i++ { containers = append(containers, generateConstraintsContainer(r, 1, maxLayersInGeneratedContainer)) } return &generatedConstraints{ containers: containers, externalProcesses: make([]*externalProcess, 0), fragments: make([]*fragment, 0), allowGetProperties: randBool(r), allowDumpStacks: randBool(r), allowRuntimeLogging: false, allowEnvironmentVariableDropping: false, allowUnencryptedScratch: randBool(r), namespace: generateFragmentNamespace(testRand), svn: generateSVN(testRand), allowCapabilityDropping: false, ctx: context.Background(), } } func generateConstraintsContainer(r *rand.Rand, minNumberOfLayers, maxNumberOfLayers int32) *securityPolicyContainer { c := securityPolicyContainer{} p := generateContainerInitProcess(r) c.Command = p.Command c.EnvRules = p.EnvRules c.WorkingDir = p.WorkingDir c.Mounts = generateMounts(r) numLayers := int(atLeastNAtMostM(r, minNumberOfLayers, maxNumberOfLayers)) for i := 0; i < numLayers; i++ { c.Layers = append(c.Layers, generateRootHash(r)) } c.ExecProcesses = generateExecProcesses(r) c.Signals = generateListOfSignals(r, 0, maxSignalNumber) c.AllowStdioAccess = randBool(r) c.NoNewPrivileges = randBool(r) c.User = generateUser(r) c.Capabilities = generateInternalCapabilities(r) c.SeccompProfileSHA256 = generateSeccomp(r) return &c } func generateSeccomp(r *rand.Rand) string { if randBool(r) { // 50% chance of no seccomp profile return "" } return generateRootHash(r) } func generateInternalCapabilities(r *rand.Rand) *capabilitiesInternal { return &capabilitiesInternal{ Bounding: generateCapabilitiesSet(r, 0), Effective: generateCapabilitiesSet(r, 0), Inheritable: generateCapabilitiesSet(r, 0), Permitted: generateCapabilitiesSet(r, 0), Ambient: generateCapabilitiesSet(r, 0), } } func generateCapabilitiesSet(r *rand.Rand, minSize int32) []string { capabilities := make([]string, 0) numArgs := atLeastNAtMostM(r, minSize, maxGeneratedCapabilities) for i := 0; i < int(numArgs); i++ { capabilities = append(capabilities, generateCapability(r)) } return capabilities } func generateCapability(r *rand.Rand) string { return randVariableString(r, maxGeneratedCapabilitesLength) } func generateContainerInitProcess(r *rand.Rand) containerInitProcess { return containerInitProcess{ Command: generateCommand(r), EnvRules: generateEnvironmentVariableRules(r), WorkingDir: generateWorkingDir(r), AllowStdioAccess: randBool(r), } } func generateContainerExecProcess(r *rand.Rand) containerExecProcess { return containerExecProcess{ Command: generateCommand(r), Signals: generateListOfSignals(r, 0, maxSignalNumber), } } func generateRootHash(r *rand.Rand) string { return randString(r, rootHashLength) } func generateWorkingDir(r *rand.Rand) string { return randVariableString(r, maxGeneratedWorkingDirLength) } func generateCommand(r *rand.Rand) []string { var args []string numArgs := atLeastOneAtMost(r, maxGeneratedCommandArgs) for i := 0; i < int(numArgs); i++ { args = append(args, randVariableString(r, maxGeneratedCommandLength)) } return args } func generateEnvironmentVariableRules(r *rand.Rand) []EnvRuleConfig { var rules []EnvRuleConfig numArgs := atLeastOneAtMost(r, maxGeneratedEnvironmentVariableRules) for i := 0; i < int(numArgs); i++ { rule := EnvRuleConfig{ Strategy: "string", Rule: randVariableString(r, maxGeneratedEnvironmentVariableRuleLength), } rules = append(rules, rule) } return rules } func generateExecProcesses(r *rand.Rand) []containerExecProcess { var processes []containerExecProcess numProcesses := atLeastOneAtMost(r, maxGeneratedExecProcesses) for i := 0; i < int(numProcesses); i++ { processes = append(processes, generateContainerExecProcess(r)) } return processes } func generateUmask(r *rand.Rand) string { // we are generating values from 000 to 777 as decimal values // to ensure we cover the full range of umask values // and so the resulting string will be a 4 digit octal representation // even though we are using decimal values return fmt.Sprintf("%04d", randMinMax(r, 0, 777)) } func generateIDNameConfig(r *rand.Rand) IDNameConfig { strategies := []IDNameStrategy{IDNameStrategyName, IDNameStrategyID} strategy := strategies[randMinMax(r, 0, int32(len(strategies)-1))] switch strategy { case IDNameStrategyName: return IDNameConfig{ Strategy: IDNameStrategyName, Rule: randVariableString(r, maxGeneratedNameLength), } case IDNameStrategyID: return IDNameConfig{ Strategy: IDNameStrategyID, Rule: fmt.Sprintf("%d", r.Uint32()), } } panic("unreachable") } func generateUser(r *rand.Rand) UserConfig { numGroups := int(atLeastOneAtMost(r, maxGeneratedGroupNames)) groupIDs := make([]IDNameConfig, numGroups) for i := 0; i < numGroups; i++ { groupIDs[i] = generateIDNameConfig(r) } return UserConfig{ UserIDName: generateIDNameConfig(r), GroupIDNames: groupIDs, Umask: generateUmask(r), } } func generateEnvironmentVariables(r *rand.Rand) []string { var envVars []string numVars := atLeastOneAtMost(r, maxGeneratedEnvironmentVariables) for i := 0; i < int(numVars); i++ { variable := randVariableString(r, maxGeneratedEnvironmentVariableRuleLength) envVars = append(envVars, variable) } return envVars } func generateNeverMatchingEnvironmentVariable(r *rand.Rand) string { return randString(r, maxGeneratedEnvironmentVariableRuleLength+1) } func buildEnvironmentVariablesFromEnvRules(rules []EnvRuleConfig, r *rand.Rand) []string { vars := make([]string, 0) // Select some number of the valid, matching rules to be environment // variable numberOfRules := int32(len(rules)) numberOfMatches := randMinMax(r, 1, numberOfRules) // Build in all required rules, this isn't a setup method of "missing item" // tests for _, rule := range rules { if rule.Required { vars = append(vars, rule.Rule) numberOfMatches-- } } usedIndexes := map[int]struct{}{} for numberOfMatches > 0 { anIndex := -1 if (numberOfMatches * 2) > numberOfRules { // if we have a lot of matches, randomly select exists := true for exists { anIndex = int(randMinMax(r, 0, numberOfRules-1)) _, exists = usedIndexes[anIndex] } } else { // we have a "smaller set of rules. we'll just iterate and select from // available exists := true for exists { anIndex++ _, exists = usedIndexes[anIndex] } } vars = append(vars, rules[anIndex].Rule) usedIndexes[anIndex] = struct{}{} numberOfMatches-- } return vars } func generateMountTarget(r *rand.Rand) string { return randVariableString(r, maxGeneratedMountTargetLength) } func generateInvalidRootHash(r *rand.Rand) string { // Guaranteed to be an incorrect size as it maxes out in size at one less // than the correct length. If this ever creates a hash that passes, we // have a seriously weird bug return randVariableString(r, rootHashLength-1) } func generateFragmentNamespace(r *rand.Rand) string { return randChar(r) + randVariableString(r, maxGeneratedFragmentNamespaceLength) } func generateSVN(r *rand.Rand) string { return strconv.FormatInt(int64(randMinMax(r, 0, maxGeneratedVersion)), 10) } func selectRootHashFromConstraints(constraints *generatedConstraints, r *rand.Rand) string { numberOfContainersInConstraints := len(constraints.containers) container := constraints.containers[r.Intn(numberOfContainersInConstraints)] numberOfLayersInContainer := len(container.Layers) return container.Layers[r.Intn(numberOfLayersInContainer)] } func generateContainerID(r *rand.Rand) string { id := atLeastOneAtMost(r, maxGeneratedContainerID) return strconv.FormatInt(int64(id), 10) } func generateMounts(r *rand.Rand) []mountInternal { numberOfMounts := atLeastOneAtMost(r, maxGeneratedMounts) mounts := make([]mountInternal, numberOfMounts) for i := 0; i < int(numberOfMounts); i++ { numberOfOptions := atLeastOneAtMost(r, maxGeneratedMountOptions) options := make([]string, numberOfOptions) for j := 0; j < int(numberOfOptions); j++ { options[j] = randVariableString(r, maxGeneratedMountOptionLength) } sourcePrefix := "" // select a "source type". our default is "no special prefix" ie a // "standard source". prefixType := randMinMax(r, 1, 3) if prefixType == 2 { // sandbox mount, gets special handling sourcePrefix = guestpath.SandboxMountPrefix } else if prefixType == 3 { // huge page mount, gets special handling sourcePrefix = guestpath.HugePagesMountPrefix } source := filepath.Join(sourcePrefix, randVariableString(r, maxGeneratedMountSourceLength)) destination := randVariableString(r, maxGeneratedMountDestinationLength) mounts[i] = mountInternal{ Source: source, Destination: destination, Options: options, Type: "bind", } } return mounts } func generateListOfSignals(r *rand.Rand, atLeast int32, atMost int32) []syscall.Signal { numSignals := int(atLeastNAtMostM(r, atLeast, atMost)) signalSet := make(map[syscall.Signal]struct{}) for i := 0; i < numSignals; i++ { signal := generateSignal(r) signalSet[signal] = struct{}{} } var signals []syscall.Signal for k := range signalSet { signals = append(signals, k) } return signals } func generateSignal(r *rand.Rand) syscall.Signal { return syscall.Signal(atLeastOneAtMost(r, maxSignalNumber)) } func selectContainerFromContainerList(containers []*securityPolicyContainer, r *rand.Rand) *securityPolicyContainer { return containers[r.Intn(len(containers))] } type dataGenerator struct { rng *rand.Rand mountTargets stringSet containerIDs stringSet sandboxIDs stringSet enforcementPoints stringSet fragmentIssuers stringSet fragmentFeeds stringSet fragmentNamespaces stringSet } func newDataGenerator(rng *rand.Rand) *dataGenerator { return &dataGenerator{ rng: rng, mountTargets: make(stringSet), containerIDs: make(stringSet), sandboxIDs: make(stringSet), enforcementPoints: make(stringSet), fragmentIssuers: make(stringSet), fragmentFeeds: make(stringSet), fragmentNamespaces: make(stringSet), } } func (s *stringSet) randUnique(r *rand.Rand, generator func(*rand.Rand) string) string { for { item := generator(r) if !s.contains(item) { s.add(item) return item } } } func (gen *dataGenerator) uniqueMountTarget() string { return gen.mountTargets.randUnique(gen.rng, generateMountTarget) } func (gen *dataGenerator) uniqueContainerID() string { return gen.containerIDs.randUnique(gen.rng, generateContainerID) } func (gen *dataGenerator) createValidOverlayForContainer(enforcer SecurityPolicyEnforcer, container *securityPolicyContainer) ([]string, error) { ctx := context.Background() // storage for our mount paths overlay := make([]string, len(container.Layers)) for i := 0; i < len(container.Layers); i++ { mount := gen.uniqueMountTarget() err := enforcer.EnforceDeviceMountPolicy(ctx, mount, container.Layers[i]) if err != nil { return overlay, err } overlay[len(overlay)-i-1] = mount } return overlay, nil } func (gen *dataGenerator) createInvalidOverlayForContainer(enforcer SecurityPolicyEnforcer, container *securityPolicyContainer) ([]string, error) { method := gen.rng.Intn(3) if method == 0 { return gen.invalidOverlaySameSizeWrongMounts(enforcer, container) } else if method == 1 { return gen.invalidOverlayCorrectDevicesWrongOrderSomeMissing(enforcer, container) } else { return gen.invalidOverlayRandomJunk(enforcer, container) } } func (gen *dataGenerator) invalidOverlaySameSizeWrongMounts(enforcer SecurityPolicyEnforcer, container *securityPolicyContainer) ([]string, error) { ctx := context.Background() // storage for our mount paths overlay := make([]string, len(container.Layers)) for i := 0; i < len(container.Layers); i++ { mount := gen.uniqueMountTarget() err := enforcer.EnforceDeviceMountPolicy(ctx, mount, container.Layers[i]) if err != nil { return overlay, err } // generate a random new mount point to cause an error overlay[len(overlay)-i-1] = gen.uniqueMountTarget() } return overlay, nil } func (gen *dataGenerator) invalidOverlayCorrectDevicesWrongOrderSomeMissing(enforcer SecurityPolicyEnforcer, container *securityPolicyContainer) ([]string, error) { ctx := context.Background() if len(container.Layers) == 1 { // won't work with only 1, we need to bail out to another method return gen.invalidOverlayRandomJunk(enforcer, container) } // storage for our mount paths var overlay []string for i := 0; i < len(container.Layers); i++ { mount := gen.uniqueMountTarget() err := enforcer.EnforceDeviceMountPolicy(ctx, mount, container.Layers[i]) if err != nil { return overlay, err } if gen.rng.Intn(10) != 0 { overlay = append(overlay, mount) } } return overlay, nil } func (gen *dataGenerator) invalidOverlayRandomJunk(enforcer SecurityPolicyEnforcer, container *securityPolicyContainer) ([]string, error) { ctx := context.Background() // create "junk" for entry layersToCreate := gen.rng.Int31n(maxLayersInGeneratedContainer) overlay := make([]string, layersToCreate) for i := 0; i < int(layersToCreate); i++ { overlay[i] = gen.uniqueMountTarget() } // setup entirely different and "correct" expected mounting for i := 0; i < len(container.Layers); i++ { mount := gen.uniqueMountTarget() err := enforcer.EnforceDeviceMountPolicy(ctx, mount, container.Layers[i]) if err != nil { return overlay, err } } return overlay, nil } func randVariableString(r *rand.Rand, maxLen int32) string { return randString(r, atLeastOneAtMost(r, maxLen)) } func randString(r *rand.Rand, length int32) string { if length < minStringLength { length = minStringLength } charset := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" sb := strings.Builder{} sb.Grow(int(length)) for i := 0; i < (int)(length); i++ { sb.WriteByte(charset[r.Intn(len(charset))]) } return sb.String() } func randChar(r *rand.Rand) string { charset := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" return string(charset[r.Intn(len(charset))]) } func randBool(r *rand.Rand) bool { return randMinMax(r, 0, 1) == 0 } func randMinMax(r *rand.Rand, min int32, max int32) int32 { return r.Int31n(max-min+1) + min } func atLeastNAtMostM(r *rand.Rand, min, max int32) int32 { return randMinMax(r, min, max) } func atLeastOneAtMost(r *rand.Rand, most int32) int32 { return atLeastNAtMostM(r, 1, most) } func atMost(r *rand.Rand, most int32) int32 { return randMinMax(r, 0, most) } type generatedConstraints struct { containers []*securityPolicyContainer externalProcesses []*externalProcess fragments []*fragment allowGetProperties bool allowDumpStacks bool allowRuntimeLogging bool allowEnvironmentVariableDropping bool allowUnencryptedScratch bool namespace string svn string allowCapabilityDropping bool ctx context.Context } type containerInitProcess struct { Command []string EnvRules []EnvRuleConfig WorkingDir string AllowStdioAccess bool }