Skip to content

Commit

Permalink
Add tests for security policy enforcement (#1325)
Browse files Browse the repository at this point in the history
Add basic positive and negative tests for security policy enforcement.
Hide policy tests behind LCOWIntegrity feature flag.
Add ContainerConfigOpt and builder functions for creating security
policy configs.

Signed-off-by: Maksim An <[email protected]>
  • Loading branch information
anmaxvl authored Apr 8, 2022
1 parent 2957199 commit 70b87e3
Show file tree
Hide file tree
Showing 4 changed files with 212 additions and 21 deletions.
27 changes: 27 additions & 0 deletions pkg/securitypolicy/opts.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package securitypolicy

type ContainerConfigOpt func(*ContainerConfig) error

// WithEnvVarRules adds environment variable constraints to container policy config.
func WithEnvVarRules(envs []EnvRuleConfig) ContainerConfigOpt {
return func(c *ContainerConfig) error {
c.EnvRules = append(c.EnvRules, envs...)
return nil
}
}

// WithExpectedMounts adds expected mounts to container policy config.
func WithExpectedMounts(em []string) ContainerConfigOpt {
return func(c *ContainerConfig) error {
c.ExpectedMounts = append(c.ExpectedMounts, em...)
return nil
}
}

// WithWorkingDir sets working directory in container policy config.
func WithWorkingDir(wd string) ContainerConfigOpt {
return func(c *ContainerConfig) error {
c.WorkingDir = wd
return nil
}
}
4 changes: 2 additions & 2 deletions test/cri-containerd/layer_integrity_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import (
)

func Test_LCOW_Layer_Integrity(t *testing.T) {
requireFeatures(t, featureLCOWIntegrity, featureLCOW)
requireFeatures(t, featureLCOW, featureLCOWIntegrity)

client := newTestRuntimeClient(t)
ctx, cancel := context.WithCancel(context.Background())
Expand Down Expand Up @@ -85,7 +85,7 @@ func Test_LCOW_Layer_Integrity(t *testing.T) {

// Validate that verity target(s) present
output := shimDiagExecOutput(ctx, t, podID, []string{"ls", "-l", "/dev/mapper"})
filtered := filterStrings(strings.Split(output, "\n"), fmt.Sprintf("dm-verity-%s", scenario.layerType))
filtered := filterStrings(strings.Split(output, "\n"), fmt.Sprintf("verity-%s", scenario.layerType))
if len(filtered) == 0 {
t.Fatalf("expected verity targets for %s devices, none found.\n%s\n", scenario.layerType, output)
}
Expand Down
175 changes: 156 additions & 19 deletions test/cri-containerd/policy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,25 +9,17 @@ import (
"strings"
"testing"

runtime "k8s.io/cri-api/pkg/apis/runtime/v1alpha2"

"github.com/Microsoft/hcsshim/internal/tools/securitypolicy/helpers"
"github.com/Microsoft/hcsshim/pkg/annotations"
"github.com/Microsoft/hcsshim/pkg/securitypolicy"
"k8s.io/cri-api/pkg/apis/runtime/v1alpha2"
)

var (
validPolicyAlpineCommand = []string{"ash", "-c", "echo 'Hello'"}
)

type configOpt func(*securitypolicy.ContainerConfig) error

func withExpectedMounts(em []string) configOpt {
return func(conf *securitypolicy.ContainerConfig) error {
conf.ExpectedMounts = append(conf.ExpectedMounts, em...)
return nil
}
}

func securityPolicyFromContainers(containers []securitypolicy.ContainerConfig) (string, error) {
pc, err := helpers.PolicyContainersFromConfigs(containers)
if err != nil {
Expand All @@ -50,7 +42,7 @@ func sandboxSecurityPolicy(t *testing.T) string {
return policyString
}

func alpineSecurityPolicy(t *testing.T) string {
func alpineSecurityPolicy(t *testing.T, opts ...securitypolicy.ContainerConfigOpt) string {
defaultContainers := helpers.DefaultContainerConfigs()
alpineContainer := securitypolicy.NewContainerConfig(
"alpine:latest",
Expand All @@ -61,6 +53,12 @@ func alpineSecurityPolicy(t *testing.T) string {
[]string{},
)

for _, o := range opts {
if err := o(&alpineContainer); err != nil {
t.Fatalf("failed to apply config opt: %s", err)
}
}

containers := append(defaultContainers, alpineContainer)
policyString, err := securityPolicyFromContainers(containers)
if err != nil {
Expand All @@ -69,7 +67,7 @@ func alpineSecurityPolicy(t *testing.T) string {
return policyString
}

func sandboxRequestWithPolicy(t *testing.T, policy string) *v1alpha2.RunPodSandboxRequest {
func sandboxRequestWithPolicy(t *testing.T, policy string) *runtime.RunPodSandboxRequest {
return getRunPodSandboxRequest(
t,
lcowRuntimeHandler,
Expand All @@ -82,7 +80,7 @@ func sandboxRequestWithPolicy(t *testing.T, policy string) *v1alpha2.RunPodSandb
}

func Test_RunPodSandbox_WithPolicy_Allowed(t *testing.T) {
requireFeatures(t, featureLCOW)
requireFeatures(t, featureLCOW, featureLCOWIntegrity)
pullRequiredLCOWImages(t, []string{imageLcowK8sPause})

sandboxPolicy := sandboxSecurityPolicy(t)
Expand All @@ -99,7 +97,7 @@ func Test_RunPodSandbox_WithPolicy_Allowed(t *testing.T) {
}

func Test_RunSimpleAlpineContainer_WithPolicy_Allowed(t *testing.T) {
requireFeatures(t, featureLCOW)
requireFeatures(t, featureLCOW, featureLCOWIntegrity)
pullRequiredLCOWImages(t, []string{imageLcowK8sPause, imageLcowAlpine})

alpinePolicy := alpineSecurityPolicy(t)
Expand Down Expand Up @@ -148,16 +146,16 @@ func syncContainerConfigs(writePath, waitPath string) (writer, waiter *securityp
func syncContainerRequests(
writer, waiter *securitypolicy.ContainerConfig,
podID string,
podConfig *v1alpha2.PodSandboxConfig,
) (writerReq, waiterReq *v1alpha2.CreateContainerRequest) {
podConfig *runtime.PodSandboxConfig,
) (writerReq, waiterReq *runtime.CreateContainerRequest) {
writerReq = getCreateContainerRequest(
podID,
"alpine-writer",
"alpine:latest",
writer.Command,
podConfig,
)
writerReq.Config.Mounts = append(writerReq.Config.Mounts, &v1alpha2.Mount{
writerReq.Config.Mounts = append(writerReq.Config.Mounts, &runtime.Mount{
HostPath: "sandbox://host/path",
ContainerPath: "/mnt/shared/container-A",
})
Expand All @@ -169,7 +167,7 @@ func syncContainerRequests(
waiter.Command,
podConfig,
)
waiterReq.Config.Mounts = append(waiterReq.Config.Mounts, &v1alpha2.Mount{
waiterReq.Config.Mounts = append(waiterReq.Config.Mounts, &runtime.Mount{
// The HostPath must be the same as for the "writer" container
HostPath: "sandbox://host/path",
ContainerPath: "/mnt/shared/container-B",
Expand Down Expand Up @@ -246,7 +244,7 @@ func Test_RunContainers_WithSyncHooks_InvalidWaitPath(t *testing.T) {
defer removeContainer(t, client, ctx, cidWriter)
defer stopContainer(t, client, ctx, cidWriter)

_, err = client.StartContainer(ctx, &v1alpha2.StartContainerRequest{
_, err = client.StartContainer(ctx, &runtime.StartContainerRequest{
ContainerId: cidWaiter,
})
expectedErrString := "timeout while waiting for path"
Expand All @@ -260,3 +258,142 @@ func Test_RunContainers_WithSyncHooks_InvalidWaitPath(t *testing.T) {
}
}
}

func Test_RunContainer_ValidContainerConfigs_Allowed(t *testing.T) {
type sideEffect func(*runtime.CreateContainerRequest)
type config struct {
name string
sf sideEffect
opts []securitypolicy.ContainerConfigOpt
}

requireFeatures(t, featureLCOW, featureLCOWIntegrity)
pullRequiredLCOWImages(t, []string{imageLcowK8sPause, imageLcowAlpine})

client := newTestRuntimeClient(t)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

for _, testConfig := range []config{
{
name: "WorkingDir",
sf: func(req *runtime.CreateContainerRequest) {
req.Config.WorkingDir = "/root"
},
opts: []securitypolicy.ContainerConfigOpt{securitypolicy.WithWorkingDir("/root")},
},
{
name: "EnvironmentVariable",
sf: func(req *runtime.CreateContainerRequest) {
req.Config.Envs = append(req.Config.Envs, &runtime.KeyValue{
Key: "KEY",
Value: "VALUE",
})
},
opts: []securitypolicy.ContainerConfigOpt{
securitypolicy.WithEnvVarRules(
[]securitypolicy.EnvRuleConfig{
{
Strategy: securitypolicy.EnvVarRuleString,
Rule: "KEY=VALUE",
},
}),
},
},
} {
t.Run(testConfig.name, func(t *testing.T) {
alpinePolicy := alpineSecurityPolicy(t, testConfig.opts...)
sandboxRequest := sandboxRequestWithPolicy(t, alpinePolicy)

podID := runPodSandbox(t, client, ctx, sandboxRequest)
defer removePodSandbox(t, client, ctx, podID)
defer stopPodSandbox(t, client, ctx, podID)

containerRequest := getCreateContainerRequest(
podID,
"alpine-with-policy",
"alpine:latest",
validPolicyAlpineCommand,
sandboxRequest.Config,
)
testConfig.sf(containerRequest)

containerID := createContainer(t, client, ctx, containerRequest)
startContainer(t, client, ctx, containerID)
defer removeContainer(t, client, ctx, containerID)
defer stopContainer(t, client, ctx, containerID)
})
}
}

func Test_RunContainer_InvalidContainerConfigs_NotAllowed(t *testing.T) {
type sideEffect func(*runtime.CreateContainerRequest)
type config struct {
name string
sf sideEffect
expectedError string
}

requireFeatures(t, featureLCOW, featureLCOWIntegrity)
pullRequiredLCOWImages(t, []string{imageLcowK8sPause, imageLcowAlpine})

client := newTestRuntimeClient(t)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

alpinePolicy := alpineSecurityPolicy(t)
for _, testConfig := range []config{
{
name: "InvalidWorkingDir",
sf: func(req *runtime.CreateContainerRequest) {
req.Config.WorkingDir = "/non/existent"
},
expectedError: "working_dir \"/non/existent\" unmatched by policy rule",
},
{
name: "InvalidCommand",
sf: func(req *runtime.CreateContainerRequest) {
req.Config.Command = []string{"ash", "-c", "echo 'invalid command'"}
},
expectedError: "command [ash -c echo 'invalid command'] doesn't match policy",
},
{
name: "InvalidEnvironmentVariable",
sf: func(req *runtime.CreateContainerRequest) {
req.Config.Envs = append(req.Config.Envs, &runtime.KeyValue{
Key: "KEY",
Value: "VALUE",
})
},
expectedError: "env variable KEY=VALUE unmatched by policy rule",
},
} {
t.Run(testConfig.name, func(t *testing.T) {
sandboxRequest := sandboxRequestWithPolicy(t, alpinePolicy)

podID := runPodSandbox(t, client, ctx, sandboxRequest)
defer removePodSandbox(t, client, ctx, podID)
defer stopPodSandbox(t, client, ctx, podID)

containerRequest := getCreateContainerRequest(
podID,
"alpine-with-policy",
"alpine:latest",
validPolicyAlpineCommand,
sandboxRequest.Config,
)
testConfig.sf(containerRequest)

containerID := createContainer(t, client, ctx, containerRequest)
_, err := client.StartContainer(ctx, &runtime.StartContainerRequest{
ContainerId: containerID,
})
if err == nil {
t.Fatal("expected container start failure")
}
if !strings.Contains(err.Error(), testConfig.expectedError) {
t.Fatalf("expected %q in error message, got: %q", testConfig.expectedError, err)
}
})
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 70b87e3

Please sign in to comment.