mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-29 14:37:00 +00:00
Added host path whitelist to psp
This commit is contained in:
parent
52337d5db6
commit
f75b3f3d05
@ -884,6 +884,10 @@ type PodSecurityPolicySpec struct {
|
|||||||
// will not be forced to.
|
// will not be forced to.
|
||||||
// +optional
|
// +optional
|
||||||
ReadOnlyRootFilesystem bool
|
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
|
// HostPortRange defines a range of host ports that will be enabled by a policy
|
||||||
|
@ -909,6 +909,10 @@ type PodSecurityPolicySpec struct {
|
|||||||
// will not be forced to.
|
// will not be forced to.
|
||||||
// +optional
|
// +optional
|
||||||
ReadOnlyRootFilesystem bool `json:"readOnlyRootFilesystem,omitempty" protobuf:"varint,14,opt,name=readOnlyRootFilesystem"`
|
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.
|
// FS Type gives strong typing to different file systems that are used by volumes.
|
||||||
|
@ -241,6 +241,15 @@ func (s *simpleProvider) ValidatePodSecurityContext(pod *api.Pod, fldPath *field
|
|||||||
allErrs = append(allErrs, field.Invalid(
|
allErrs = append(allErrs, field.Invalid(
|
||||||
field.NewPath("spec", "volumes").Index(i), string(fsType),
|
field.NewPath("spec", "volumes").Index(i), string(fsType),
|
||||||
fmt.Sprintf("%s volumes are not allowed to be used", 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)))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 := defaultPSP()
|
||||||
failOtherSysctlsAllowedPSP.Annotations[extensions.SysctlsPodSecurityPolicyAnnotationKey] = "bar,abc"
|
failOtherSysctlsAllowedPSP.Annotations[extensions.SysctlsPodSecurityPolicyAnnotationKey] = "bar,abc"
|
||||||
|
|
||||||
@ -308,6 +323,11 @@ func TestValidatePodSecurityContextFailures(t *testing.T) {
|
|||||||
psp: defaultPSP(),
|
psp: defaultPSP(),
|
||||||
expectedError: "hostPath volumes are not allowed to be used",
|
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": {
|
"failSafeSysctlFooPod with failNoSysctlAllowedSCC": {
|
||||||
pod: failSafeSysctlFooPod,
|
pod: failSafeSysctlFooPod,
|
||||||
psp: failNoSysctlAllowedPSP,
|
psp: failNoSysctlAllowedPSP,
|
||||||
@ -706,13 +726,28 @@ func TestValidateContainerSecurityContextSuccess(t *testing.T) {
|
|||||||
hostDirPod := defaultPod()
|
hostDirPod := defaultPod()
|
||||||
hostDirPod.Spec.Volumes = []api.Volume{
|
hostDirPod.Spec.Volumes = []api.Volume{
|
||||||
{
|
{
|
||||||
Name: "bad volume",
|
Name: "good volume",
|
||||||
VolumeSource: api.VolumeSource{
|
VolumeSource: api.VolumeSource{
|
||||||
HostPath: &api.HostPathVolumeSource{},
|
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 := defaultPSP()
|
||||||
hostPortPSP.Spec.HostPorts = []extensions.HostPortRange{{Min: 1, Max: 1}}
|
hostPortPSP.Spec.HostPorts = []extensions.HostPortRange{{Min: 1, Max: 1}}
|
||||||
hostPortPod := defaultPod()
|
hostPortPod := defaultPod()
|
||||||
@ -773,6 +808,10 @@ func TestValidateContainerSecurityContextSuccess(t *testing.T) {
|
|||||||
pod: hostDirPod,
|
pod: hostDirPod,
|
||||||
psp: hostDirPSP,
|
psp: hostDirPSP,
|
||||||
},
|
},
|
||||||
|
"pass hostDir allowed directory validating PSP": {
|
||||||
|
pod: hostPathDirPod,
|
||||||
|
psp: hostPathDirPSP,
|
||||||
|
},
|
||||||
"pass hostPort validating PSP": {
|
"pass hostPort validating PSP": {
|
||||||
pod: hostPortPod,
|
pod: hostPortPod,
|
||||||
psp: hostPortPSP,
|
psp: hostPortPSP,
|
||||||
|
@ -18,6 +18,7 @@ package util
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"k8s.io/apimachinery/pkg/types"
|
"k8s.io/apimachinery/pkg/types"
|
||||||
"k8s.io/apimachinery/pkg/util/sets"
|
"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 {
|
func GroupFallsInRange(id types.UnixGroupID, rng extensions.GroupIDRange) bool {
|
||||||
return id >= rng.Min && id <= rng.Max
|
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
|
||||||
|
}
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user