diff --git a/pkg/securitypolicy/rego_utils_test.go b/pkg/securitypolicy/rego_utils_test.go index 341741eef2..dbe016098d 100644 --- a/pkg/securitypolicy/rego_utils_test.go +++ b/pkg/securitypolicy/rego_utils_test.go @@ -2218,40 +2218,6 @@ func (*generatedConstraints) Generate(r *rand.Rand, _ int) reflect.Value { 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 diff --git a/pkg/securitypolicy/regopolicy_linux_test.go b/pkg/securitypolicy/regopolicy_linux_test.go index 0e782724da..94cfd8d031 100644 --- a/pkg/securitypolicy/regopolicy_linux_test.go +++ b/pkg/securitypolicy/regopolicy_linux_test.go @@ -17,6 +17,8 @@ import ( "testing" "testing/quick" + specInternal "github.com/Microsoft/hcsshim/internal/guest/spec" + "github.com/Microsoft/hcsshim/internal/guestpath" rpi "github.com/Microsoft/hcsshim/internal/regopolicyinterpreter" oci "github.com/opencontainers/runtime-spec/specs-go" ) @@ -6324,3 +6326,15 @@ func testGetUserInfo(t *testing.T, tc getUserInfoTestCase, userStr string, regoE } }) } + +// substituteUVMPath substitutes mount prefix to an appropriate path inside +// UVM. At policy generation time, it's impossible to tell what the sandboxID +// will be, so the prefix substitution needs to happen during runtime. +func substituteUVMPath(sandboxID string, m mountInternal) mountInternal { + if strings.HasPrefix(m.Source, guestpath.SandboxMountPrefix) { + m.Source = specInternal.SandboxMountSource(sandboxID, m.Source) + } else if strings.HasPrefix(m.Source, guestpath.HugePagesMountPrefix) { + m.Source = specInternal.HugePagesMountSource(sandboxID, m.Source) + } + return m +} diff --git a/pkg/securitypolicy/regopolicy_windows_test.go b/pkg/securitypolicy/regopolicy_windows_test.go index 2fc8158a14..1675410d33 100644 --- a/pkg/securitypolicy/regopolicy_windows_test.go +++ b/pkg/securitypolicy/regopolicy_windows_test.go @@ -1329,3 +1329,13 @@ func Test_Rego_DumpStacksPolicy_Off(t *testing.T) { t.Errorf("Test_Rego_DumpStacksPolicy_Off: %v", err) } } + +// This is a no-op for windows. +// substituteUVMPath substitutes mount prefix to an appropriate path inside +// UVM. At policy generation time, it's impossible to tell what the sandboxID +// will be, so the prefix substitution needs to happen during runtime. +func substituteUVMPath(sandboxID string, m mountInternal) mountInternal { + //no-op for windows + _ = sandboxID + return m +} diff --git a/pkg/securitypolicy/securitypolicy.go b/pkg/securitypolicy/securitypolicy.go index 2bb8b0e1d9..c294da2c11 100644 --- a/pkg/securitypolicy/securitypolicy.go +++ b/pkg/securitypolicy/securitypolicy.go @@ -26,7 +26,7 @@ var apiCodeTemplate string var APICode = strings.Replace(apiCodeTemplate, "@@API_VERSION@@", apiVersion, 1) var FrameworkCode = strings.Replace(frameworkCodeTemplate, "@@FRAMEWORK_VERSION@@", frameworkVersion, 1) -var ErrInvalidOpenDoorPolicy = errors.New("allow_all cannot be set to 'true' when Containers are non-empty") +var ErrInvalidOpenDoorPolicy = errors.New("Invalid policy for open-door enforcer") type EnvVarRule string diff --git a/pkg/securitypolicy/securitypolicy_linux.go b/pkg/securitypolicy/securitypolicy_linux.go index 6296c33a17..cb04e03d92 100644 --- a/pkg/securitypolicy/securitypolicy_linux.go +++ b/pkg/securitypolicy/securitypolicy_linux.go @@ -8,10 +8,8 @@ import ( "os" "path/filepath" "strconv" - "strings" specInternal "github.com/Microsoft/hcsshim/internal/guest/spec" - "github.com/Microsoft/hcsshim/internal/guestpath" "github.com/moby/sys/user" oci "github.com/opencontainers/runtime-spec/specs-go" "github.com/pkg/errors" @@ -20,19 +18,6 @@ import ( //nolint:unused const osType = "linux" -// This is being used by StandEnforcer. -// substituteUVMPath substitutes mount prefix to an appropriate path inside -// UVM. At policy generation time, it's impossible to tell what the sandboxID -// will be, so the prefix substitution needs to happen during runtime. -func substituteUVMPath(sandboxID string, m mountInternal) mountInternal { - if strings.HasPrefix(m.Source, guestpath.SandboxMountPrefix) { - m.Source = specInternal.SandboxMountSource(sandboxID, m.Source) - } else if strings.HasPrefix(m.Source, guestpath.HugePagesMountPrefix) { - m.Source = specInternal.HugePagesMountSource(sandboxID, m.Source) - } - return m -} - // SandboxMountsDir returns sandbox mounts directory inside UVM/host. func SandboxMountsDir(sandboxID string) string { return specInternal.SandboxMountsDir((sandboxID)) diff --git a/pkg/securitypolicy/securitypolicy_windows.go b/pkg/securitypolicy/securitypolicy_windows.go index 26b3515a33..6f873fef26 100644 --- a/pkg/securitypolicy/securitypolicy_windows.go +++ b/pkg/securitypolicy/securitypolicy_windows.go @@ -8,16 +8,6 @@ import oci "github.com/opencontainers/runtime-spec/specs-go" //nolint:unused const osType = "windows" -// This is being used by StandEnforcer and is a no-op for windows. -// substituteUVMPath substitutes mount prefix to an appropriate path inside -// UVM. At policy generation time, it's impossible to tell what the sandboxID -// will be, so the prefix substitution needs to happen during runtime. -func substituteUVMPath(sandboxID string, m mountInternal) mountInternal { - //no-op for windows - _ = sandboxID - return m -} - // SandboxMountsDir returns sandbox mounts directory inside UVM/host. func SandboxMountsDir(sandboxID string) string { return "" diff --git a/pkg/securitypolicy/securitypolicyenforcer.go b/pkg/securitypolicy/securitypolicyenforcer.go index b951a607d4..760f7a4996 100644 --- a/pkg/securitypolicy/securitypolicyenforcer.go +++ b/pkg/securitypolicy/securitypolicyenforcer.go @@ -2,12 +2,7 @@ package securitypolicy import ( "context" - "encoding/base64" - "encoding/json" "fmt" - "regexp" - "strconv" - "sync" "syscall" "github.com/Microsoft/hcsshim/internal/protocol/guestrequest" @@ -47,18 +42,20 @@ type SignalContainerOptions struct { } const ( - openDoorEnforcer = "open_door" - standardEnforcer = "standard" + openDoorEnforcerName = "open_door" ) var ( registeredEnforcers = map[string]createEnforcerFunc{} - defaultEnforcer = standardEnforcer + + // defaultConfidentialEnforcer is set to rego in + // securitypolicyenforcer_rego.go if the relevant build tag is set. + // Otherwise we do not support any confidential enforcers. + defaultConfidentialEnforcer = "" ) func init() { - registeredEnforcers[openDoorEnforcer] = createOpenDoorEnforcer - registeredEnforcers[standardEnforcer] = createStandardEnforcer + registeredEnforcers[openDoorEnforcerName] = createOpenDoorEnforcer } type SecurityPolicyEnforcer interface { @@ -145,109 +142,19 @@ func (s stringSet) contains(item string) bool { return contains } -func newSecurityPolicyFromBase64JSON(base64EncodedPolicy string) (*SecurityPolicy, error) { - // 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(base64EncodedPolicy) - if err != nil { - return nil, errors.Wrap(err, "unable to decode policy from Base64 format") - } - - // json unmarshall the decoded to a SecurityPolicy - securityPolicy := new(SecurityPolicy) - err = json.Unmarshal(jsonPolicy, securityPolicy) - if err != nil { - return nil, errors.Wrap(err, "unable to unmarshal JSON policy") - } - - return securityPolicy, nil -} - -// createAllowAllEnforcer creates and returns OpenDoorSecurityPolicyEnforcer instance. -// Both AllowAll and Containers cannot be set at the same time. -func createOpenDoorEnforcer(base64EncodedPolicy string, _, _ []oci.Mount, _ int) (SecurityPolicyEnforcer, error) { - // This covers the case when an "open_door" enforcer was requested, but no - // actual security policy was passed. This can happen e.g. when a container - // scratch is created for the first time. - if base64EncodedPolicy == "" { - return &OpenDoorSecurityPolicyEnforcer{}, nil - } - - securityPolicy, err := newSecurityPolicyFromBase64JSON(base64EncodedPolicy) - if err != nil { - return nil, err - } - - policyContainers := securityPolicy.Containers - if !securityPolicy.AllowAll || policyContainers.Length > 0 || len(policyContainers.Elements) > 0 { - return nil, ErrInvalidOpenDoorPolicy - } - return &OpenDoorSecurityPolicyEnforcer{ - encodedSecurityPolicy: base64EncodedPolicy, - }, nil -} - -func (c Containers) toInternal() ([]*securityPolicyContainer, error) { - containerMapLength := len(c.Elements) - internal := make([]*securityPolicyContainer, containerMapLength) - - for i := 0; i < containerMapLength; i++ { - index := strconv.Itoa(i) - cConf, ok := c.Elements[index] - if !ok { - return nil, fmt.Errorf("container constraint with index %q not found", index) - } - cInternal, err := cConf.toInternal() - if err != nil { - return nil, err - } - internal[i] = cInternal - } - - return internal, nil -} - -// createStandardEnforcer creates and returns StandardSecurityPolicyEnforcer instance. -// Make sure that the input JSON policy can be converted to internal representation -// and that `criMounts` and `criPrivilegedMounts` can be injected before successful return. -func createStandardEnforcer( - base64EncodedPolicy string, - criMounts, - criPrivilegedMounts []oci.Mount, - maxErrorMessageLength int, -) (SecurityPolicyEnforcer, error) { - securityPolicy, err := newSecurityPolicyFromBase64JSON(base64EncodedPolicy) - if err != nil { - return nil, err - } - - if securityPolicy.AllowAll { - return createOpenDoorEnforcer(base64EncodedPolicy, criMounts, criPrivilegedMounts, maxErrorMessageLength) - } - - containers, err := securityPolicy.Containers.toInternal() - if err != nil { - return nil, err - } - - enforcer := NewStandardSecurityPolicyEnforcer(containers, base64EncodedPolicy) - - if err := enforcer.ExtendDefaultMounts(criMounts); err != nil { - return nil, err - } - - addPrivilegedMountsWrapper := WithPrivilegedMounts(criPrivilegedMounts) - if err := addPrivilegedMountsWrapper(enforcer); err != nil { - return nil, err - } - return enforcer, nil -} - -// CreateSecurityPolicyEnforcer returns an appropriate enforcer for input parameters. -// When `enforcer` isn't return either an AllowAll or default enforcer. -// Returns an error if the requested `enforcer` implementation isn't registered. +// CreateSecurityPolicyEnforcer returns an appropriate enforcer for input +// parameters. Returns an error if the requested `enforcer` implementation +// isn't registered. +// +// This function can be called both on confidential and non-confidential +// containers, but in the non-confidential case the policy would be empty. +// Normally enforcer is not specified, in which case we use either the default +// for confidential (Rego), or the open door enforcer, depending on whether +// policy is not empty. However, the host may override this. This override is +// not measured in the SNP hostData, and so the enforcer must make sure the +// policy provided is a valid policy for that enforcer. (For example, for +// open_door, it must either be empty or contain only the "allow_all" field set +// to true.) func CreateSecurityPolicyEnforcer( enforcer string, base64EncodedPolicy string, @@ -256,9 +163,13 @@ func CreateSecurityPolicyEnforcer( maxErrorMessageLength int, ) (SecurityPolicyEnforcer, error) { if enforcer == "" { - enforcer = defaultEnforcer if base64EncodedPolicy == "" { - enforcer = openDoorEnforcer + enforcer = openDoorEnforcerName + } else { + if defaultConfidentialEnforcer == "" { + return nil, fmt.Errorf("GCS not built with Rego support") + } + enforcer = defaultConfidentialEnforcer } } @@ -269,691 +180,19 @@ func CreateSecurityPolicyEnforcer( } } -// newMountConstraint creates an internal mount constraint object from given -// source, destination, type and options. -func newMountConstraint(src, dst string, mType string, mOpts []string) mountInternal { - return mountInternal{ - Source: src, - Destination: dst, - Type: mType, - Options: mOpts, - } -} - -type standardEnforcerOpt func(e *StandardSecurityPolicyEnforcer) error - -// WithPrivilegedMounts converts the input mounts to internal mount constraints -// and extends existing internal mount constraints if the container is allowed -// to be executed in elevated mode. -func WithPrivilegedMounts(mounts []oci.Mount) standardEnforcerOpt { - return func(e *StandardSecurityPolicyEnforcer) error { - for _, c := range e.Containers { - if c.AllowElevated { - for _, m := range mounts { - mi := mountInternal{ - Source: m.Source, - Destination: m.Destination, - Type: m.Type, - Options: m.Options, - } - c.Mounts = append(c.Mounts, mi) - } - } - } - return nil - } -} - -// StandardSecurityPolicyEnforcer implements SecurityPolicyEnforcer interface -// and is responsible for enforcing various SecurityPolicy constraints. -// -// Most of the work that this security policy enforcer does it around managing -// state needed to map from a container definition in the SecurityPolicy to -// a specific container ID as we bring up each container. For example, see -// EnforceCreateContainerPolicy where most of the functionality is handling the -// case were policy containers share an overlay and have to try to distinguish -// them based on the command line arguments, environment variables or working -// directory. -// -// Containers that share the same base image, and perhaps further information, -// will have an entry per container instance in the SecurityPolicy. For example, -// a policy that has two containers that use Ubuntu 18.04 will have an entry for -// each even if they share the same command line. -type StandardSecurityPolicyEnforcer struct { - // encodedSecurityPolicy state is needed for key release +type OpenDoorSecurityPolicyEnforcer struct { encodedSecurityPolicy string - // Containers is the internal representation of users' container policies. - Containers []*securityPolicyContainer - // Devices is a mapping between target and a corresponding root hash. Target - // is a path to a particular block device or its mount point inside UVM and - // root hash is the dm-verity root hash of that device. Mainly the stored - // devices represent read-only container layers, but this may change. - // As the UVM goes through its process of bringing up containers, we have to - // piece together information about what is going on. - Devices map[string]string - // ContainerIndexToContainerIds is a mapping between containers in the - // SecurityPolicy and possible container IDs that have been created by runc, - // but have not yet been run. - // - // As containers can have exactly the same base image and be "the same" at - // the time we are doing overlay, the ContainerIndexToContainerIds is a set - // of possible containers for a given container id. Go doesn't have a set - // type, so we are doing the idiomatic go thing of using a map[string]struct{} - // to represent the set. - ContainerIndexToContainerIds map[int]map[string]struct{} - // startedContainers is a set of container IDs that were allowed to start. - // Because Go doesn't have sets as a built-in data structure, we are using a map. - startedContainers map[string]struct{} - // mutex guards against concurrent access to fields. - mutex *sync.Mutex - // DefaultMounts are mount constraints for container mounts added by CRI and - // GCS. Since default mounts will be allowed for all containers in the UVM - // they are not added to each individual policy container and kept as global - // policy rules. - DefaultMounts []mountInternal - // DefaultEnvs are environment variable constraints for variables added - // by CRI and GCS. Since default envs will be allowed for all containers - // in the UVM they are not added to each individual policy container and - // kept as global policy rules. - DefaultEnvs []EnvRuleConfig -} - -var _ SecurityPolicyEnforcer = (*StandardSecurityPolicyEnforcer)(nil) - -func NewStandardSecurityPolicyEnforcer( - containers []*securityPolicyContainer, - encoded string, -) *StandardSecurityPolicyEnforcer { - return &StandardSecurityPolicyEnforcer{ - encodedSecurityPolicy: encoded, - Containers: containers, - Devices: map[string]string{}, - ContainerIndexToContainerIds: map[int]map[string]struct{}{}, - startedContainers: map[string]struct{}{}, - mutex: &sync.Mutex{}, - } -} - -// EnforceDeviceMountPolicy for StandardSecurityPolicyEnforcer validates that -// the target block device's root hash matches any container in SecurityPolicy. -// Block device targets with invalid root hashes are rejected. -// -// At the time that devices are being mounted, we do not know a container -// that they will be used for; only that there is a device with a given root -// hash that being mounted. We check to make sure that the root hash for the -// devices is a root hash that exists for 1 or more layers in any container -// in the supplied SecurityPolicy. Each "seen" layer is recorded in devices -// as it is mounted. -func (pe *StandardSecurityPolicyEnforcer) EnforceDeviceMountPolicy(ctx context.Context, target string, deviceHash string) (err error) { - pe.mutex.Lock() - defer pe.mutex.Unlock() - - if len(pe.Containers) < 1 { - return errors.New("policy doesn't allow mounting containers") - } - - if deviceHash == "" { - return errors.New("device is missing verity root hash") - } - - for _, container := range pe.Containers { - for _, layer := range container.Layers { - if deviceHash == layer { - if existingHash := pe.Devices[target]; existingHash != "" { - return fmt.Errorf( - "conflicting device hashes for target %s: old=%s, new=%s", - target, - existingHash, - deviceHash, - ) - } - pe.Devices[target] = deviceHash - return nil - } - } - } - - return fmt.Errorf("roothash %s for mount %s doesn't match policy", deviceHash, target) -} - -// EnforceDeviceUnmountPolicy for StandardSecurityPolicyEnforcer first validate -// that the target mount was one of the allowed devices and then removes it from -// the mapping. -// -// When proper protocol enforcement is in place, this will also make sure that -// the device isn't currently used by any running container in an overlay. -func (pe *StandardSecurityPolicyEnforcer) EnforceDeviceUnmountPolicy(ctx context.Context, unmountTarget string) (err error) { - pe.mutex.Lock() - defer pe.mutex.Unlock() - - if _, ok := pe.Devices[unmountTarget]; !ok { - return fmt.Errorf("device doesn't exist: %s", unmountTarget) - } - delete(pe.Devices, unmountTarget) - - return nil -} - -// EnforceOverlayMountPolicy for StandardSecurityPolicyEnforcer validates that -// layerPaths represent a valid overlay file system and is allowed by the -// SecurityPolicy. -// -// When overlay filesystems created, look up the root hash chain for an incoming -// overlay and verify it against containers in the policy. -// Overlay filesystem creation is the first time we have a "container ID" -// available to us. The container id identifies the container in question going -// forward. We record the mapping of container index in the policy to a set of -// possible container IDs so that when we have future operations like -// "run command" which come with a container ID, we can find the corresponding -// container index and use that to look up the command in the appropriate -// security policy container instance. -func (pe *StandardSecurityPolicyEnforcer) EnforceOverlayMountPolicy(ctx context.Context, containerID string, layerPaths []string, target string) (err error) { - pe.mutex.Lock() - defer pe.mutex.Unlock() - - if len(pe.Containers) < 1 { - return errors.New("policy doesn't allow mounting containers") - } - - if _, e := pe.startedContainers[containerID]; e { - return errors.New("container has already been started") - } - - var incomingOverlay []string - for _, layer := range layerPaths { - if hash, ok := pe.Devices[layer]; !ok { - return fmt.Errorf("overlay layer isn't mounted: %s", layer) - } else { - incomingOverlay = append(incomingOverlay, hash) - } - } - - // check if any of the containers allow the incoming overlay. - var matchedContainers []int - for i, container := range pe.Containers { - if equalForOverlay(incomingOverlay, container.Layers) { - matchedContainers = append(matchedContainers, i) - } - } - - if len(matchedContainers) == 0 { - errmsg := fmt.Sprintf("layerPaths '%v' doesn't match any valid overlay", layerPaths) - return errors.New(errmsg) - } - - for _, i := range matchedContainers { - existing := pe.ContainerIndexToContainerIds[i] - if len(existing) >= len(matchedContainers) { - errmsg := fmt.Sprintf("layerPaths '%v' already used in maximum number of container overlays. This is likely because the security policy allows the container to be run only once.", layerPaths) - return errors.New(errmsg) - } - pe.expandMatchesForContainerIndex(i, containerID) - } - - return nil -} - -// EnforceCreateContainerPolicy for StandardSecurityPolicyEnforcer validates -// the input container command, env and working directory against containers in -// the SecurityPolicy. The enforcement also narrows down the containers that -// have the same overlays by comparing their command, env and working directory -// rules. -// -// 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 SecurityPolicy. -func (pe *StandardSecurityPolicyEnforcer) EnforceCreateContainerPolicy( - ctx context.Context, - sandboxID string, - containerID string, - argList []string, - envList []string, - workingDir string, - mounts []oci.Mount, - privileged bool, - noNewPrivileges bool, - user IDName, - groups []IDName, - umask string, - caps *oci.LinuxCapabilities, - seccomp string, -) (allowedEnvs EnvList, - allowedCapabilities *oci.LinuxCapabilities, - stdioAccessAllowed bool, - err error) { - pe.mutex.Lock() - defer pe.mutex.Unlock() - - if len(pe.Containers) < 1 { - return nil, nil, true, errors.New("policy doesn't allow mounting containers") - } - - if _, e := pe.startedContainers[containerID]; e { - return nil, nil, true, errors.New("container has already been started") - } - - if err = pe.enforceCommandPolicy(containerID, argList); err != nil { - return nil, nil, true, err - } - - if err = pe.enforceEnvironmentVariablePolicy(containerID, envList); err != nil { - return nil, nil, true, err - } - - if err = pe.enforceWorkingDirPolicy(containerID, workingDir); err != nil { - return nil, nil, true, err - } - - if err = pe.enforcePrivilegedPolicy(containerID, privileged); err != nil { - return nil, nil, true, err - } - - if err = pe.enforceMountPolicy(sandboxID, containerID, mounts); err != nil { - return nil, nil, true, err - } - - // record that we've allowed this container to start - pe.startedContainers[containerID] = struct{}{} - - return envList, caps, true, nil -} - -func (*StandardSecurityPolicyEnforcer) EnforceCreateContainerPolicyV2( - ctx context.Context, - containerID string, - argList []string, - envList []string, - workingDir string, - mounts []oci.Mount, - user IDName, - opts *CreateContainerOptions, -) (EnvList, *oci.LinuxCapabilities, bool, error) { - return envList, opts.Capabilities, true, nil -} - -// Stub. We are deprecating the standard enforcer. Newly added enforcement -// points are simply allowed. -func (*StandardSecurityPolicyEnforcer) EnforceExecInContainerPolicy(_ context.Context, _ string, _ []string, envList []string, _ string, _ bool, _ IDName, _ []IDName, _ string, caps *oci.LinuxCapabilities) (EnvList, *oci.LinuxCapabilities, bool, error) { - return envList, caps, true, nil -} - -func (*StandardSecurityPolicyEnforcer) EnforceExecInContainerPolicyV2( - ctx context.Context, - containerID string, - argList []string, - envList []string, - workingDir string, - user IDName, - opts *ExecOptions, -) (EnvList, *oci.LinuxCapabilities, bool, error) { - return envList, opts.Capabilities, true, nil } -// Stub. We are deprecating the standard enforcer. Newly added enforcement -// points are simply allowed. -func (*StandardSecurityPolicyEnforcer) EnforceExecExternalProcessPolicy(_ context.Context, _ []string, envList []string, _ string) (EnvList, bool, error) { - return envList, true, nil -} - -// Stub. We are deprecating the standard enforcer. Newly added enforcement -// points are simply allowed. -func (*StandardSecurityPolicyEnforcer) EnforceShutdownContainerPolicy(context.Context, string) error { - return nil -} - -// Stub. We are deprecating the standard enforcer. Newly added enforcement -// points are simply allowed. -func (*StandardSecurityPolicyEnforcer) EnforceSignalContainerProcessPolicy(context.Context, string, syscall.Signal, bool, []string) error { - return nil -} - -func (*StandardSecurityPolicyEnforcer) EnforceSignalContainerProcessPolicyV2(ctx context.Context, containerID string, opts *SignalContainerOptions) error { - return nil -} - -// Stub. We are deprecating the standard enforcer. Newly added enforcement -// points are simply allowed. -func (*StandardSecurityPolicyEnforcer) EnforcePlan9MountPolicy(context.Context, string) error { - return nil -} - -// Stub. We are deprecating the standard enforcer. Newly added enforcement -// points are simply allowed. -func (*StandardSecurityPolicyEnforcer) EnforcePlan9UnmountPolicy(context.Context, string) error { - return nil -} - -// Stub. We are deprecating the standard enforcer. Newly added enforcement -// points are simply allowed. -func (*StandardSecurityPolicyEnforcer) EnforceOverlayUnmountPolicy(context.Context, string) error { - return nil -} - -// Stub. We are deprecating the standard enforcer. Newly added enforcement -// points are simply allowed. -func (*StandardSecurityPolicyEnforcer) EnforceGetPropertiesPolicy(context.Context) error { - return nil -} - -// Stub. We are deprecating the standard enforcer. Newly added enforcement -// points are simply allowed. -func (*StandardSecurityPolicyEnforcer) EnforceDumpStacksPolicy(context.Context) error { - return nil -} - -// Stub. We are deprecating the standard enforcer. Newly added enforcement -// points are simply allowed. -func (*StandardSecurityPolicyEnforcer) EnforceRuntimeLoggingPolicy(context.Context) error { - return nil -} - -// Stub. We are deprecating the standard enforcer. Newly added enforcement -// points are simply allowed. -func (*StandardSecurityPolicyEnforcer) LoadFragment(context.Context, string, string, string) error { - return nil -} - -// Stub. We are deprecating the standard enforcer. Newly added enforcement -// points are simply allowed. -func (StandardSecurityPolicyEnforcer) EnforceScratchMountPolicy(context.Context, string, bool) error { - return nil -} - -// Stub. We are deprecating the standard enforcer. Newly added enforcement -// points are simply allowed. -func (StandardSecurityPolicyEnforcer) EnforceScratchUnmountPolicy(context.Context, string) error { - return nil -} - -func (StandardSecurityPolicyEnforcer) EnforceVerifiedCIMsPolicy(ctx context.Context, containerID string, layerHashes []string) error { - return nil -} - -// Stub. We are deprecating the standard enforcer. -func (StandardSecurityPolicyEnforcer) GetUserInfo(spec *oci.Process, rootPath string) (IDName, []IDName, string, error) { - return IDName{}, nil, "", nil -} - -func (pe *StandardSecurityPolicyEnforcer) enforceCommandPolicy(containerID string, argList []string) (err error) { - // Get a list of all the indexes into our security policy's list of - // containers that are possible matches for this containerID based - // on the image overlay layout - possibleIndices := pe.possibleIndicesForID(containerID) - - // Loop through every possible match and do two things: - // 1- see if any command matches. we need at least one match or - // we don't allow the container to start - // 2- remove this containerID as a possible match for any container from the - // security policy whose command line isn't a match. - matchingCommandFound := false - for _, possibleIndex := range possibleIndices { - cmd := pe.Containers[possibleIndex].Command - if stringSlicesEqual(cmd, argList) { - matchingCommandFound = true - } else { - // a possible matching index turned out not to match, so we - // need to update that list and remove it - pe.narrowMatchesForContainerIndex(possibleIndex, containerID) - } - } - - if !matchingCommandFound { - errmsg := fmt.Sprintf("command %v doesn't match policy", argList) - return errors.New(errmsg) - } - - return nil -} - -func (pe *StandardSecurityPolicyEnforcer) enforceEnvironmentVariablePolicy(containerID string, envList []string) (err error) { - // Get a list of all the indexes into our security policy's list of - // containers that are possible matches for this containerID based - // on the image overlay layout and command line - possibleIndices := pe.possibleIndicesForID(containerID) - - for _, envVariable := range envList { - matchingRuleFoundForSomeContainer := false - for _, possibleIndex := range possibleIndices { - envRules := pe.Containers[possibleIndex].EnvRules - ok := envIsMatchedByRule(envVariable, envRules) - if ok { - matchingRuleFoundForSomeContainer = true - } else { - // a possible matching index turned out not to match, so we - // need to update that list and remove it - pe.narrowMatchesForContainerIndex(possibleIndex, containerID) - } - } - - if !matchingRuleFoundForSomeContainer { - return fmt.Errorf("env variable %s unmatched by policy rule", envVariable) - } - } - - return nil -} - -func (pe *StandardSecurityPolicyEnforcer) enforceWorkingDirPolicy(containerID string, workingDir string) error { - possibleIndices := pe.possibleIndicesForID(containerID) - - matched := false - for _, pIndex := range possibleIndices { - pWorkingDir := pe.Containers[pIndex].WorkingDir - if pWorkingDir == workingDir { - matched = true - } else { - pe.narrowMatchesForContainerIndex(pIndex, containerID) - } - } - if !matched { - return fmt.Errorf("working_dir %q unmatched by policy rule", workingDir) - } - return nil -} - -func (pe *StandardSecurityPolicyEnforcer) enforcePrivilegedPolicy(containerID string, privileged bool) error { - // We only need to check for privilege escalation - if !privileged { - return nil - } - - possibleIndices := pe.possibleIndicesForID(containerID) - - matched := false - for _, pIndex := range possibleIndices { - pAllowElevated := pe.Containers[pIndex].AllowElevated - if pAllowElevated { - matched = true - } else { - pe.narrowMatchesForContainerIndex(pIndex, containerID) - } - } - if !matched { - return errors.New("privileged escalation unmatched by policy rule") - } - return nil -} - -func envIsMatchedByRule(envVariable string, rules []EnvRuleConfig) bool { - for _, rule := range rules { - switch rule.Strategy { - case "string": - if rule.Rule == envVariable { - return true - } - case "re2": - // if the match errors out, we don't care. it's not a match - matched, _ := regexp.MatchString(rule.Rule, envVariable) - if matched { - return true - } - } - } - - return false -} - -// StandardSecurityPolicyEnforcer.mutex lock must be held prior to calling this function. -func (pe *StandardSecurityPolicyEnforcer) expandMatchesForContainerIndex(index int, idToAdd string) { - _, keyExists := pe.ContainerIndexToContainerIds[index] - if !keyExists { - pe.ContainerIndexToContainerIds[index] = map[string]struct{}{} - } - - pe.ContainerIndexToContainerIds[index][idToAdd] = struct{}{} -} - -// StandardSecurityPolicyEnforcer.mutex lock must be held prior to calling this function. -func (pe *StandardSecurityPolicyEnforcer) narrowMatchesForContainerIndex(index int, idToRemove string) { - delete(pe.ContainerIndexToContainerIds[index], idToRemove) -} - -func equalForOverlay(a1 []string, a2 []string) bool { - // We've stored the layers from bottom to top they are in layerPaths as - // top to bottom (the order a string gets concatenated for the unix mount - // command). W do our check with that in mind. - if len(a1) != len(a2) { - return false - } - topIndex := len(a2) - 1 - for i, v := range a1 { - if v != a2[topIndex-i] { - return false - } - } - return true -} - -// StandardSecurityPolicyEnforcer.mutex lock must be held prior to calling this function. -func (pe *StandardSecurityPolicyEnforcer) possibleIndicesForID(containerID string) []int { - var possibleIndices []int - for index, ids := range pe.ContainerIndexToContainerIds { - for id := range ids { - if containerID == id { - possibleIndices = append(possibleIndices, index) - } - } - } - return possibleIndices -} - -func (pe *StandardSecurityPolicyEnforcer) enforceDefaultMounts(specMount oci.Mount) error { - for _, mountConstraint := range pe.DefaultMounts { - if err := mountConstraint.validate(specMount); err == nil { - return nil - } - } - return fmt.Errorf("mount not allowed by default mount constraints: %+v", specMount) -} - -// ExtendDefaultMounts for StandardSecurityPolicyEnforcer adds default mounts -// added by CRI and GCS to the list of DefaultMounts, which are always allowed. -func (pe *StandardSecurityPolicyEnforcer) ExtendDefaultMounts(defaultMounts []oci.Mount) error { - pe.mutex.Lock() - defer pe.mutex.Unlock() - - for _, mnt := range defaultMounts { - pe.DefaultMounts = append(pe.DefaultMounts, newMountConstraint( - mnt.Source, - mnt.Destination, - mnt.Type, - mnt.Options, - )) - } - return nil -} - -// enforceMountPolicy for StandardSecurityPolicyEnforcer validates various -// default mounts injected into container spec by GCS or containerD. As part of -// the enforcement, the method also narrows down possible container IDs with -// the same overlay. -func (pe *StandardSecurityPolicyEnforcer) enforceMountPolicy(sandboxID, containerID string, mounts []oci.Mount) (err error) { - possibleIndices := pe.possibleIndicesForID(containerID) - - for _, mount := range mounts { - // first check against default mounts - if err := pe.enforceDefaultMounts(mount); err == nil { - continue - } - - mountOk := false - // check against user provided mount constraints, which helps to figure - // out which container this mount spec corresponds to. - for _, pIndex := range possibleIndices { - cont := pe.Containers[pIndex] - if err = cont.matchMount(sandboxID, mount); err == nil { - mountOk = true - } else { - pe.narrowMatchesForContainerIndex(pIndex, containerID) - } - } - - if !mountOk { - retErr := fmt.Errorf("mount %+v is not allowed by mount constraints", mount) - return retErr - } - } - - return nil -} - -// validate checks given OCI mount against mount policy. Destination is checked -// by direct string comparisons and Source is checked via a regular expression. -// This is done this way, because container path (Destination) is always fixed, -// however, the host/UVM path (Source) can include IDs generated at runtime and -// impossible to know in advance. -// -// NOTE: Different matching strategies can be added by introducing a separate -// path matching config, which isn't needed at the moment. -func (m *mountInternal) validate(mSpec oci.Mount) error { - if m.Type != mSpec.Type { - return fmt.Errorf("mount type not allowed by policy: expected=%q, actual=%q", m.Type, mSpec.Type) - } - if ok, _ := regexp.MatchString(m.Source, mSpec.Source); !ok { - return fmt.Errorf("mount source not allowed by policy: expected=%q, actual=%q", m.Source, mSpec.Source) - } - if m.Destination != mSpec.Destination && m.Destination != "" { - return fmt.Errorf("mount destination not allowed by policy: expected=%q, actual=%q", m.Destination, mSpec.Destination) - } - if !stringSlicesEqual(m.Options, mSpec.Options) { - return fmt.Errorf("mount options not allowed by policy: expected=%q, actual=%q", m.Options, mSpec.Options) - } - return nil -} - -// matchMount matches given OCI mount against mount constraints. If no match -// found, the mount is not allowed. -func (c *securityPolicyContainer) matchMount(sandboxID string, m oci.Mount) (err error) { - for _, constraint := range c.Mounts { - // now that we know the sandboxID we can get the actual path for - // various destination path types by adding a UVM mount prefix - constraint = substituteUVMPath(sandboxID, constraint) - if err = constraint.validate(m); err == nil { - return nil - } - } - return fmt.Errorf("mount is not allowed by policy: %+v", m) -} - -func stringSlicesEqual(slice1, slice2 []string) bool { - if len(slice1) != len(slice2) { - return false - } - - for i := 0; i < len(slice1); i++ { - if slice1[i] != slice2[i] { - return false - } +// Creates and returns OpenDoorSecurityPolicyEnforcer instance (used for +// non-confidential containers). The provided base64EncodedPolicy must be +// empty. +func createOpenDoorEnforcer(base64EncodedPolicy string, _, _ []oci.Mount, _ int) (SecurityPolicyEnforcer, error) { + if base64EncodedPolicy == "" { + return &OpenDoorSecurityPolicyEnforcer{}, nil } - return true -} -func (pe *StandardSecurityPolicyEnforcer) EncodedSecurityPolicy() string { - return pe.encodedSecurityPolicy -} - -type OpenDoorSecurityPolicyEnforcer struct { - encodedSecurityPolicy string + return nil, ErrInvalidOpenDoorPolicy } var _ SecurityPolicyEnforcer = (*OpenDoorSecurityPolicyEnforcer)(nil) @@ -1071,9 +310,7 @@ func (OpenDoorSecurityPolicyEnforcer) EnforceVerifiedCIMsPolicy(ctx context.Cont return nil } -type ClosedDoorSecurityPolicyEnforcer struct { - encodedSecurityPolicy string //nolint:unused -} +type ClosedDoorSecurityPolicyEnforcer struct{} var _ SecurityPolicyEnforcer = (*ClosedDoorSecurityPolicyEnforcer)(nil) diff --git a/pkg/securitypolicy/securitypolicyenforcer_rego.go b/pkg/securitypolicy/securitypolicyenforcer_rego.go index 7bafd5e91a..f12d8d1fb1 100644 --- a/pkg/securitypolicy/securitypolicyenforcer_rego.go +++ b/pkg/securitypolicy/securitypolicyenforcer_rego.go @@ -9,7 +9,6 @@ import ( "encoding/base64" "encoding/json" "fmt" - "strconv" "strings" "syscall" @@ -29,7 +28,7 @@ func init() { // are no race conditions. When multiple init functions are defined in a // single package, the order of their execution is determined by the // filename. - defaultEnforcer = regoEnforcerName + defaultConfidentialEnforcer = regoEnforcerName defaultMarshaller = regoMarshaller } @@ -102,6 +101,14 @@ func (a stringSet) intersect(b stringSet) stringSet { type inputData map[string]interface{} +func isValidJsonObject(input string) bool { + type emptyStruct = struct{} + + var body emptyStruct + err := json.Unmarshal([]byte(input), &body) + return err == nil +} + func createRegoEnforcer(base64EncodedPolicy string, defaultMounts []oci.Mount, privilegedMounts []oci.Mount, @@ -114,87 +121,7 @@ func createRegoEnforcer(base64EncodedPolicy string, return nil, fmt.Errorf("unable to decode policy from Base64 format: %w", err) } - // Try to unmarshal the JSON - - var code string - securityPolicy := new(SecurityPolicy) - err = json.Unmarshal(rawPolicy, securityPolicy) - if err == nil { - if securityPolicy.AllowAll { - return createOpenDoorEnforcer(base64EncodedPolicy, defaultMounts, privilegedMounts, maxErrorMessageLength) - } - - if osType == "linux" { - containers := make([]*Container, securityPolicy.Containers.Length) - for i := 0; i < securityPolicy.Containers.Length; i++ { - index := strconv.Itoa(i) - cConf, ok := securityPolicy.Containers.Elements[index] - if !ok { - return nil, fmt.Errorf("container constraint with index %q not found", index) - } - cConf.AllowStdioAccess = true - cConf.NoNewPrivileges = false - cConf.User = UserConfig{ - UserIDName: IDNameConfig{Strategy: IDNameStrategyAny}, - GroupIDNames: []IDNameConfig{{Strategy: IDNameStrategyAny}}, - Umask: "0022", - } - cConf.SeccompProfileSHA256 = "" - containers[i] = &cConf - } - - code, err = osAwareMarshalRego( - securityPolicy.AllowAll, - containers, - nil, - osType, - []ExternalProcessConfig{}, - []FragmentConfig{}, - true, - true, - true, - false, - true, - false, - ) - if err != nil { - return nil, fmt.Errorf("error marshaling the policy to Rego: %w", err) - } - } else if osType == "windows" { - windows_containers := make([]*WindowsContainer, securityPolicy.Containers.Length) - for i := 0; i < securityPolicy.Containers.Length; i++ { - index := strconv.Itoa(i) - cConf, ok := securityPolicy.WindowsContainers.Elements[index] - if !ok { - return nil, fmt.Errorf("container constraint with index %q not found", index) - } - cConf.AllowStdioAccess = true - windows_containers[i] = &cConf - } - - code, err = osAwareMarshalRego( - securityPolicy.AllowAll, - nil, - windows_containers, - osType, - []ExternalProcessConfig{}, - []FragmentConfig{}, - true, - true, - true, - false, - true, - false, - ) - if err != nil { - return nil, fmt.Errorf("error marshaling the policy to Rego: %w", err) - } - } - } else { - // this is either a Rego policy or malformed JSON - code = string(rawPolicy) - } - + code := string(rawPolicy) regoPolicy, err := newRegoPolicy(code, defaultMounts, privilegedMounts, osType) if err != nil { return nil, fmt.Errorf("error creating Rego policy: %w", err) diff --git a/pkg/securitypolicy/standardpolicy_test.go b/pkg/securitypolicy/standardpolicy_test.go deleted file mode 100644 index 9a88b90831..0000000000 --- a/pkg/securitypolicy/standardpolicy_test.go +++ /dev/null @@ -1,839 +0,0 @@ -//go:build linux && rego -// +build linux,rego - -package securitypolicy - -import ( - "context" - - "strconv" - - "testing" - "testing/quick" - - "github.com/google/go-cmp/cmp" -) - -// 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) - } -}