diff --git a/pkg/apis/extensions/types.go b/pkg/apis/extensions/types.go index b0b32c28b8c..7e343b237bf 100644 --- a/pkg/apis/extensions/types.go +++ b/pkg/apis/extensions/types.go @@ -884,6 +884,10 @@ type PodSecurityPolicySpec struct { // will not be forced to. // +optional ReadOnlyRootFilesystem bool + // AllowedHostPaths is a white list of allowed host path prefixes. Empty indicates that all + // host paths may be used. + // +optional + AllowedHostPaths []string } // HostPortRange defines a range of host ports that will be enabled by a policy diff --git a/pkg/apis/extensions/v1beta1/types.go b/pkg/apis/extensions/v1beta1/types.go index 47156eec52c..734d66b5eec 100644 --- a/pkg/apis/extensions/v1beta1/types.go +++ b/pkg/apis/extensions/v1beta1/types.go @@ -909,6 +909,10 @@ type PodSecurityPolicySpec struct { // will not be forced to. // +optional ReadOnlyRootFilesystem bool `json:"readOnlyRootFilesystem,omitempty" protobuf:"varint,14,opt,name=readOnlyRootFilesystem"` + // AllowedHostPaths is a white list of allowed host path prefixes. Empty indicates that all + // host paths may be used. + // +optional + AllowedHostPaths []string `json:"allowedHostPaths,omitempty" protobuf:"bytes,15,opt,name=allowedHostPaths"` } // FS Type gives strong typing to different file systems that are used by volumes. diff --git a/pkg/security/podsecuritypolicy/provider.go b/pkg/security/podsecuritypolicy/provider.go index 0f72f79c114..e92c6419fc9 100644 --- a/pkg/security/podsecuritypolicy/provider.go +++ b/pkg/security/podsecuritypolicy/provider.go @@ -241,6 +241,15 @@ func (s *simpleProvider) ValidatePodSecurityContext(pod *api.Pod, fldPath *field allErrs = append(allErrs, field.Invalid( field.NewPath("spec", "volumes").Index(i), string(fsType), fmt.Sprintf("%s volumes are not allowed to be used", string(fsType)))) + continue + } + + if fsType == extensions.HostPath { + if !psputil.PSPAllowsHostVolumePath(s.psp, v.HostPath.Path) { + allErrs = append(allErrs, field.Invalid( + field.NewPath("spec", "volumes").Index(i), string(fsType), + fmt.Sprintf("host path %s is not allowed to be used. allowed host paths: %v", v.HostPath.Path, s.psp.Spec.AllowedHostPaths))) + } } } } diff --git a/pkg/security/podsecuritypolicy/provider_test.go b/pkg/security/podsecuritypolicy/provider_test.go index c50ccd99b65..8cdfd65f07d 100644 --- a/pkg/security/podsecuritypolicy/provider_test.go +++ b/pkg/security/podsecuritypolicy/provider_test.go @@ -238,6 +238,21 @@ func TestValidatePodSecurityContextFailures(t *testing.T) { }, } + failHostPathDirPod := defaultPod() + failHostPathDirPod.Spec.Volumes = []api.Volume{ + { + Name: "bad volume", + VolumeSource: api.VolumeSource{ + HostPath: &api.HostPathVolumeSource{ + Path: "/fail", + }, + }, + }, + } + failHostPathDirPSP := defaultPSP() + failHostPathDirPSP.Spec.Volumes = []extensions.FSType{extensions.HostPath} + failHostPathDirPSP.Spec.AllowedHostPaths = []string{"/foo/bar"} + failOtherSysctlsAllowedPSP := defaultPSP() failOtherSysctlsAllowedPSP.Annotations[extensions.SysctlsPodSecurityPolicyAnnotationKey] = "bar,abc" @@ -308,6 +323,11 @@ func TestValidatePodSecurityContextFailures(t *testing.T) { psp: defaultPSP(), expectedError: "hostPath volumes are not allowed to be used", }, + "failHostPathDirPSP": { + pod: failHostPathDirPod, + psp: failHostPathDirPSP, + expectedError: "host path /fail is not allowed to be used. allowed host paths: [/foo/bar]", + }, "failSafeSysctlFooPod with failNoSysctlAllowedSCC": { pod: failSafeSysctlFooPod, psp: failNoSysctlAllowedPSP, @@ -706,13 +726,28 @@ func TestValidateContainerSecurityContextSuccess(t *testing.T) { hostDirPod := defaultPod() hostDirPod.Spec.Volumes = []api.Volume{ { - Name: "bad volume", + Name: "good volume", VolumeSource: api.VolumeSource{ HostPath: &api.HostPathVolumeSource{}, }, }, } + hostPathDirPod := defaultPod() + hostPathDirPod.Spec.Volumes = []api.Volume{ + { + Name: "good volume", + VolumeSource: api.VolumeSource{ + HostPath: &api.HostPathVolumeSource{ + Path: "/foo/bar/baz", + }, + }, + }, + } + hostPathDirPSP := defaultPSP() + hostPathDirPSP.Spec.Volumes = []extensions.FSType{extensions.HostPath} + hostPathDirPSP.Spec.AllowedHostPaths = []string{"/foo/bar"} + hostPortPSP := defaultPSP() hostPortPSP.Spec.HostPorts = []extensions.HostPortRange{{Min: 1, Max: 1}} hostPortPod := defaultPod() @@ -773,6 +808,10 @@ func TestValidateContainerSecurityContextSuccess(t *testing.T) { pod: hostDirPod, psp: hostDirPSP, }, + "pass hostDir allowed directory validating PSP": { + pod: hostPathDirPod, + psp: hostPathDirPSP, + }, "pass hostPort validating PSP": { pod: hostPortPod, psp: hostPortPSP, diff --git a/pkg/security/podsecuritypolicy/util/util.go b/pkg/security/podsecuritypolicy/util/util.go index b424846cf81..14e397349d7 100644 --- a/pkg/security/podsecuritypolicy/util/util.go +++ b/pkg/security/podsecuritypolicy/util/util.go @@ -18,6 +18,7 @@ package util import ( "fmt" + "strings" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/sets" @@ -168,3 +169,52 @@ func UserFallsInRange(id types.UnixUserID, rng extensions.UserIDRange) bool { func GroupFallsInRange(id types.UnixGroupID, rng extensions.GroupIDRange) bool { return id >= rng.Min && id <= rng.Max } + +// PSPAllowsHostVolumePath is a utility for checking if a PSP allows the host volume path. +// This only checks the path. You should still check to make sure the host volume fs type is allowed. +func PSPAllowsHostVolumePath(psp *extensions.PodSecurityPolicy, hostPath string) bool { + if psp == nil { + return false + } + + // If no allowed paths are specified then allow any path + if len(psp.Spec.AllowedHostPaths) == 0 { + return true + } + + for _, allowedPath := range psp.Spec.AllowedHostPaths { + if hasPathPrefix(hostPath, allowedPath) { + return true + } + } + + return false +} + +// hasPathPrefix returns true if the string matches pathPrefix exactly, or if is prefixed with pathPrefix at a path segment boundary +// the string and pathPrefix are both normalized to remove trailing slashes prior to checking. +func hasPathPrefix(s, pathPrefix string) bool { + + s = strings.TrimSuffix(s, "/") + pathPrefix = strings.TrimSuffix(pathPrefix, "/") + + // Short circuit if s doesn't contain the prefix at all + if !strings.HasPrefix(s, pathPrefix) { + return false + } + + pathPrefixLength := len(pathPrefix) + + if len(s) == pathPrefixLength { + // Exact match + return true + } + + if s[pathPrefixLength:pathPrefixLength+1] == "/" { + // The next character in s is a path segment boundary + // Check this instead of normalizing pathPrefix to avoid allocating on every call + return true + } + + return false +} diff --git a/pkg/security/podsecuritypolicy/util/util_test.go b/pkg/security/podsecuritypolicy/util/util_test.go index 63a2dd3de04..f230858ef35 100644 --- a/pkg/security/podsecuritypolicy/util/util_test.go +++ b/pkg/security/podsecuritypolicy/util/util_test.go @@ -103,3 +103,83 @@ func TestPSPAllowsFSType(t *testing.T) { } } } + +func TestPSPAllowsHostVolumePath(t *testing.T) { + tests := map[string]struct { + psp *extensions.PodSecurityPolicy + path string + allows bool + }{ + "nil psp": { + psp: nil, + path: "/test", + allows: false, + }, + "empty allowed paths": { + psp: &extensions.PodSecurityPolicy{}, + path: "/test", + allows: true, + }, + "non-matching": { + psp: &extensions.PodSecurityPolicy{ + Spec: extensions.PodSecurityPolicySpec{ + AllowedHostPaths: []string{"/foo"}, + }, + }, + path: "/foobar", + allows: false, + }, + "match on direct match": { + psp: &extensions.PodSecurityPolicy{ + Spec: extensions.PodSecurityPolicySpec{ + AllowedHostPaths: []string{"/foo"}, + }, + }, + path: "/foo", + allows: true, + }, + "match with trailing slash on host path": { + psp: &extensions.PodSecurityPolicy{ + Spec: extensions.PodSecurityPolicySpec{ + AllowedHostPaths: []string{"/foo"}, + }, + }, + path: "/foo/", + allows: true, + }, + "match with trailing slash on allowed path": { + psp: &extensions.PodSecurityPolicy{ + Spec: extensions.PodSecurityPolicySpec{ + AllowedHostPaths: []string{"/foo/"}, + }, + }, + path: "/foo", + allows: true, + }, + "match child directory": { + psp: &extensions.PodSecurityPolicy{ + Spec: extensions.PodSecurityPolicySpec{ + AllowedHostPaths: []string{"/foo/"}, + }, + }, + path: "/foo/bar", + allows: true, + }, + "non-matching parent directory": { + psp: &extensions.PodSecurityPolicy{ + Spec: extensions.PodSecurityPolicySpec{ + AllowedHostPaths: []string{"/foo/bar"}, + }, + }, + path: "/foo", + allows: false, + }, + } + + for k, v := range tests { + allows := PSPAllowsHostVolumePath(v.psp, v.path) + if v.allows != allows { + t.Errorf("%s expected PSPAllowsHostVolumePath to return %t but got %t", k, v.allows, allows) + } + } +}