Add support for enforcing read only host paths in PSPs.

This commit is contained in:
Josh Horwitz
2018-05-10 15:54:09 -04:00
parent 86ae84b10e
commit c7fbcf35da
20 changed files with 593 additions and 342 deletions

View File

@@ -227,10 +227,33 @@ func (s *simpleProvider) ValidatePod(pod *api.Pod, fldPath *field.Path) field.Er
}
if fsType == policy.HostPath {
if !psputil.AllowsHostVolumePath(s.psp, v.HostPath.Path) {
allows, mustBeReadOnly := psputil.AllowsHostVolumePath(s.psp, v.HostPath.Path)
if !allows {
allErrs = append(allErrs, field.Invalid(
field.NewPath("spec", "volumes").Index(i).Child("hostPath", "pathPrefix"), v.HostPath.Path,
fmt.Sprintf("is not allowed to be used")))
} else if mustBeReadOnly {
// Ensure all the VolumeMounts that use this volume are read-only
for i, c := range pod.Spec.InitContainers {
for j, cv := range c.VolumeMounts {
if cv.Name == v.Name && !cv.ReadOnly {
allErrs = append(allErrs, field.Invalid(
field.NewPath("spec", "initContainers").Index(i).Child("volumeMounts").Index(j).Child("readOnly"),
cv.ReadOnly, "must be read-only"),
)
}
}
}
for i, c := range pod.Spec.Containers {
for j, cv := range c.VolumeMounts {
if cv.Name == v.Name && !cv.ReadOnly {
allErrs = append(allErrs, field.Invalid(
field.NewPath("spec", "containers").Index(i).Child("volumeMounts").Index(j).Child("readOnly"),
cv.ReadOnly, "must be read-only"),
)
}
}
}
}
}

View File

@@ -241,6 +241,32 @@ func TestValidatePodSecurityContextFailures(t *testing.T) {
{PathPrefix: "/foo/bar"},
}
failHostPathReadOnlyPod := defaultPod()
failHostPathReadOnlyPod.Spec.Containers[0].VolumeMounts = []api.VolumeMount{
{
Name: "bad volume",
ReadOnly: false,
},
}
failHostPathReadOnlyPod.Spec.Volumes = []api.Volume{
{
Name: "bad volume",
VolumeSource: api.VolumeSource{
HostPath: &api.HostPathVolumeSource{
Path: "/foo",
},
},
},
}
failHostPathReadOnlyPSP := defaultPSP()
failHostPathReadOnlyPSP.Spec.Volumes = []policy.FSType{policy.HostPath}
failHostPathReadOnlyPSP.Spec.AllowedHostPaths = []policy.AllowedHostPath{
{
PathPrefix: "/foo",
ReadOnly: true,
},
}
failOtherSysctlsAllowedPSP := defaultPSP()
failOtherSysctlsAllowedPSP.Annotations[policy.SysctlsPodSecurityPolicyAnnotationKey] = "bar,abc"
@@ -328,6 +354,11 @@ func TestValidatePodSecurityContextFailures(t *testing.T) {
psp: failHostPathDirPSP,
expectedError: "is not allowed to be used",
},
"failHostPathReadOnlyPSP": {
pod: failHostPathReadOnlyPod,
psp: failHostPathReadOnlyPSP,
expectedError: "must be read-only",
},
"failSafeSysctlFooPod with failNoSysctlAllowedSCC": {
pod: failSafeSysctlFooPod,
psp: failNoSysctlAllowedPSP,
@@ -598,28 +629,82 @@ func TestValidatePodSecurityContextSuccess(t *testing.T) {
Level: "level",
}
hostPathDirPodVolumeMounts := []api.VolumeMount{
{
Name: "writeable /foo/bar",
ReadOnly: false,
},
{
Name: "read only /foo/bar/baz",
ReadOnly: true,
},
{
Name: "parent read only volume",
ReadOnly: true,
},
{
Name: "read only child volume",
ReadOnly: true,
},
}
hostPathDirPod := defaultPod()
hostPathDirPod.Spec.InitContainers = []api.Container{
{
Name: defaultContainerName,
VolumeMounts: hostPathDirPodVolumeMounts,
},
}
hostPathDirPod.Spec.Containers[0].VolumeMounts = hostPathDirPodVolumeMounts
hostPathDirPod.Spec.Volumes = []api.Volume{
{
Name: "good volume",
Name: "writeable /foo/bar",
VolumeSource: api.VolumeSource{
HostPath: &api.HostPathVolumeSource{
Path: "/foo/bar",
},
},
},
{
Name: "read only /foo/bar/baz",
VolumeSource: api.VolumeSource{
HostPath: &api.HostPathVolumeSource{
Path: "/foo/bar/baz",
},
},
},
{
Name: "parent read only volume",
VolumeSource: api.VolumeSource{
HostPath: &api.HostPathVolumeSource{
Path: "/foo/",
},
},
},
{
Name: "read only child volume",
VolumeSource: api.VolumeSource{
HostPath: &api.HostPathVolumeSource{
Path: "/foo/readonly/child",
},
},
},
}
hostPathDirPSP := defaultPSP()
hostPathDirPSP.Spec.Volumes = []policy.FSType{policy.HostPath}
hostPathDirPSP.Spec.AllowedHostPaths = []policy.AllowedHostPath{
{PathPrefix: "/foo/bar"},
// overlapping test case where child is different than parent directory.
{PathPrefix: "/foo/bar/baz", ReadOnly: true},
{PathPrefix: "/foo", ReadOnly: true},
{PathPrefix: "/foo/bar", ReadOnly: false},
}
hostPathDirAsterisksPSP := defaultPSP()
hostPathDirAsterisksPSP.Spec.Volumes = []policy.FSType{policy.All}
hostPathDirAsterisksPSP.Spec.AllowedHostPaths = []policy.AllowedHostPath{
{PathPrefix: "/foo/bar"},
{PathPrefix: "/foo"},
}
sysctlAllowFooPSP := defaultPSP()

View File

@@ -175,23 +175,27 @@ func GroupFallsInRange(id int64, rng policy.IDRange) bool {
// AllowsHostVolumePath 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 AllowsHostVolumePath(psp *policy.PodSecurityPolicy, hostPath string) bool {
func AllowsHostVolumePath(psp *policy.PodSecurityPolicy, hostPath string) (pathIsAllowed, mustBeReadOnly bool) {
if psp == nil {
return false
return false, false
}
// If no allowed paths are specified then allow any path
if len(psp.Spec.AllowedHostPaths) == 0 {
return true
return true, false
}
for _, allowedPath := range psp.Spec.AllowedHostPaths {
if hasPathPrefix(hostPath, allowedPath.PathPrefix) {
return true
if !allowedPath.ReadOnly {
return true, allowedPath.ReadOnly
}
pathIsAllowed = true
mustBeReadOnly = true
}
}
return false
return pathIsAllowed, mustBeReadOnly
}
// hasPathPrefix returns true if the string matches pathPrefix exactly, or if is prefixed with pathPrefix at a path segment boundary

View File

@@ -17,10 +17,11 @@ limitations under the License.
package util
import (
api "k8s.io/kubernetes/pkg/apis/core"
"k8s.io/kubernetes/pkg/apis/policy"
"reflect"
"testing"
api "k8s.io/kubernetes/pkg/apis/core"
"k8s.io/kubernetes/pkg/apis/policy"
)
// TestVolumeSourceFSTypeDrift ensures that for every known type of volume source (by the fields on
@@ -105,41 +106,52 @@ func TestPSPAllowsFSType(t *testing.T) {
func TestAllowsHostVolumePath(t *testing.T) {
tests := map[string]struct {
psp *policy.PodSecurityPolicy
path string
allows bool
psp *policy.PodSecurityPolicy
path string
allows bool
mustBeReadOnly bool
}{
"nil psp": {
psp: nil,
path: "/test",
allows: false,
psp: nil,
path: "/test",
allows: false,
mustBeReadOnly: false,
},
"empty allowed paths": {
psp: &policy.PodSecurityPolicy{},
path: "/test",
allows: true,
psp: &policy.PodSecurityPolicy{},
path: "/test",
allows: true,
mustBeReadOnly: false,
},
"non-matching": {
psp: &policy.PodSecurityPolicy{
Spec: policy.PodSecurityPolicySpec{
AllowedHostPaths: []policy.AllowedHostPath{
{PathPrefix: "/foo"},
{
PathPrefix: "/foo",
ReadOnly: true,
},
},
},
},
path: "/foobar",
allows: false,
path: "/foobar",
allows: false,
mustBeReadOnly: false,
},
"match on direct match": {
psp: &policy.PodSecurityPolicy{
Spec: policy.PodSecurityPolicySpec{
AllowedHostPaths: []policy.AllowedHostPath{
{PathPrefix: "/foo"},
{
PathPrefix: "/foo",
ReadOnly: true,
},
},
},
},
path: "/foo",
allows: true,
path: "/foo",
allows: true,
mustBeReadOnly: true,
},
"match with trailing slash on host path": {
psp: &policy.PodSecurityPolicy{
@@ -149,8 +161,9 @@ func TestAllowsHostVolumePath(t *testing.T) {
},
},
},
path: "/foo/",
allows: true,
path: "/foo/",
allows: true,
mustBeReadOnly: false,
},
"match with trailing slash on allowed path": {
psp: &policy.PodSecurityPolicy{
@@ -160,19 +173,24 @@ func TestAllowsHostVolumePath(t *testing.T) {
},
},
},
path: "/foo",
allows: true,
path: "/foo",
allows: true,
mustBeReadOnly: false,
},
"match child directory": {
psp: &policy.PodSecurityPolicy{
Spec: policy.PodSecurityPolicySpec{
AllowedHostPaths: []policy.AllowedHostPath{
{PathPrefix: "/foo/"},
{
PathPrefix: "/foo/",
ReadOnly: true,
},
},
},
},
path: "/foo/bar",
allows: true,
path: "/foo/bar",
allows: true,
mustBeReadOnly: true,
},
"non-matching parent directory": {
psp: &policy.PodSecurityPolicy{
@@ -182,15 +200,19 @@ func TestAllowsHostVolumePath(t *testing.T) {
},
},
},
path: "/foo",
allows: false,
path: "/foo",
allows: false,
mustBeReadOnly: false,
},
}
for k, v := range tests {
allows := AllowsHostVolumePath(v.psp, v.path)
allows, mustBeReadOnly := AllowsHostVolumePath(v.psp, v.path)
if v.allows != allows {
t.Errorf("%s expected %t but got %t", k, v.allows, allows)
t.Errorf("allows: %s expected %t but got %t", k, v.allows, allows)
}
if v.mustBeReadOnly != mustBeReadOnly {
t.Errorf("mustBeReadOnly: %s expected %t but got %t", k, v.mustBeReadOnly, mustBeReadOnly)
}
}
}