mirror of
https://github.com/k3s-io/kubernetes.git
synced 2026-01-29 21:29:24 +00:00
Add support for enforcing read only host paths in PSPs.
This commit is contained in:
@@ -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"),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user