Commit b5eadb5d authored by Kubernetes Submit Queue's avatar Kubernetes Submit Queue Committed by GitHub

Merge pull request #43946 from jhorwit2/jah/host-path-psp

Automatic merge from submit-queue (batch tested with PRs 46489, 46281, 46463, 46114, 43946) Allow PSP's to specify a whitelist of allowed paths for host volume **What this PR does / why we need it**: This PR adds the ability to whitelist paths for the host volume to ensure pods cannot access directories they aren't supposed to. E.g. `/var/lib/kubelet`, `/etc/kubernetes/*`, etc. **Which issue this PR fixes** *(optional, in `fixes #<issue number>(, fixes #<issue_number>, ...)` format, will close that issue when PR gets merged)*: fixes #29326 **Special notes for your reviewer**: **Release note**: ```release-note Allow PSP's to specify a whitelist of allowed paths for host volume based on path prefixes ```
parents 1889d654 e8ec654c
......@@ -53319,6 +53319,13 @@
"type": "string"
}
},
"allowedHostPaths": {
"description": "AllowedHostPaths is a white list of allowed host path prefixes. Empty indicates that all host paths may be used.",
"type": "array",
"items": {
"type": "string"
}
},
"defaultAddCapabilities": {
"description": "DefaultAddCapabilities is the default set of capabilities that will be added to the container unless the pod spec specifically drops the capability. You may not list a capabiility in both DefaultAddCapabilities and RequiredDropCapabilities.",
"type": "array",
......@@ -9984,6 +9984,13 @@
"readOnlyRootFilesystem": {
"type": "boolean",
"description": "ReadOnlyRootFilesystem when set to true will force containers to run with a read only root file system. If the container specifically requests to run with a non-read only root file system the PSP should deny the pod. If set to false the container may run with a read only root file system if it wishes but it will not be forced to."
},
"allowedHostPaths": {
"type": "array",
"items": {
"type": "string"
},
"description": "AllowedHostPaths is a white list of allowed host path prefixes. Empty indicates that all host paths may be used."
}
}
},
......
......@@ -7655,6 +7655,13 @@ Both these may change in the future. Incoming requests are matched against the h
<td class="tableblock halign-left valign-top"><p class="tableblock">boolean</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">false</p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock">allowedHostPaths</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">AllowedHostPaths is a white list of allowed host path prefixes. Empty indicates that all host paths may be used.</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">false</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">string array</p></td>
<td class="tableblock halign-left valign-top"></td>
</tr>
</tbody>
</table>
......@@ -8120,7 +8127,7 @@ Both these may change in the future. Incoming requests are matched against the h
</div>
<div id="footer">
<div id="footer-text">
Last updated 2017-05-27 18:54:49 UTC
Last updated 2017-05-29 17:05:24 UTC
</div>
</div>
</body>
......
......@@ -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
......
......@@ -710,6 +710,11 @@ message PodSecurityPolicySpec {
// will not be forced to.
// +optional
optional bool readOnlyRootFilesystem = 14;
// AllowedHostPaths is a white list of allowed host path prefixes. Empty indicates that all
// host paths may be used.
// +optional
repeated string allowedHostPaths = 15;
}
// ReplicaSet represents the configuration of a ReplicaSet.
......
......@@ -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.
......
......@@ -423,6 +423,7 @@ var map_PodSecurityPolicySpec = map[string]string{
"supplementalGroups": "SupplementalGroups is the strategy that will dictate what supplemental groups are used by the SecurityContext.",
"fsGroup": "FSGroup is the strategy that will dictate what fs group is used by the SecurityContext.",
"readOnlyRootFilesystem": "ReadOnlyRootFilesystem when set to true will force containers to run with a read only root file system. If the container specifically requests to run with a non-read only root file system the PSP should deny the pod. If set to false the container may run with a read only root file system if it wishes but it will not be forced to.",
"allowedHostPaths": "AllowedHostPaths is a white list of allowed host path prefixes. Empty indicates that all host paths may be used.",
}
func (PodSecurityPolicySpec) SwaggerDoc() map[string]string {
......
......@@ -1215,6 +1215,7 @@ func autoConvert_v1beta1_PodSecurityPolicySpec_To_extensions_PodSecurityPolicySp
return err
}
out.ReadOnlyRootFilesystem = in.ReadOnlyRootFilesystem
out.AllowedHostPaths = *(*[]string)(unsafe.Pointer(&in.AllowedHostPaths))
return nil
}
......@@ -1256,6 +1257,7 @@ func autoConvert_extensions_PodSecurityPolicySpec_To_v1beta1_PodSecurityPolicySp
return err
}
out.ReadOnlyRootFilesystem = in.ReadOnlyRootFilesystem
out.AllowedHostPaths = *(*[]string)(unsafe.Pointer(&in.AllowedHostPaths))
return nil
}
......
......@@ -826,6 +826,11 @@ func DeepCopy_v1beta1_PodSecurityPolicySpec(in interface{}, out interface{}, c *
if err := DeepCopy_v1beta1_FSGroupStrategyOptions(&in.FSGroup, &out.FSGroup, c); err != nil {
return err
}
if in.AllowedHostPaths != nil {
in, out := &in.AllowedHostPaths, &out.AllowedHostPaths
*out = make([]string, len(*in))
copy(*out, *in)
}
return nil
}
}
......
......@@ -818,6 +818,11 @@ func DeepCopy_extensions_PodSecurityPolicySpec(in interface{}, out interface{},
if err := DeepCopy_extensions_FSGroupStrategyOptions(&in.FSGroup, &out.FSGroup, c); err != nil {
return err
}
if in.AllowedHostPaths != nil {
in, out := &in.AllowedHostPaths, &out.AllowedHostPaths
*out = make([]string, len(*in))
copy(*out, *in)
}
return nil
}
}
......
......@@ -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)))
}
}
}
}
......
......@@ -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,
......
......@@ -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
}
......@@ -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)
}
}
}
......@@ -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
......
......@@ -710,6 +710,11 @@ message PodSecurityPolicySpec {
// will not be forced to.
// +optional
optional bool readOnlyRootFilesystem = 14;
// AllowedHostPaths is a white list of allowed host path prefixes. Empty indicates that all
// host paths may be used.
// +optional
repeated string allowedHostPaths = 15;
}
// ReplicaSet represents the configuration of a ReplicaSet.
......
......@@ -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.
......
......@@ -423,6 +423,7 @@ var map_PodSecurityPolicySpec = map[string]string{
"supplementalGroups": "SupplementalGroups is the strategy that will dictate what supplemental groups are used by the SecurityContext.",
"fsGroup": "FSGroup is the strategy that will dictate what fs group is used by the SecurityContext.",
"readOnlyRootFilesystem": "ReadOnlyRootFilesystem when set to true will force containers to run with a read only root file system. If the container specifically requests to run with a non-read only root file system the PSP should deny the pod. If set to false the container may run with a read only root file system if it wishes but it will not be forced to.",
"allowedHostPaths": "AllowedHostPaths is a white list of allowed host path prefixes. Empty indicates that all host paths may be used.",
}
func (PodSecurityPolicySpec) SwaggerDoc() map[string]string {
......
......@@ -1215,6 +1215,7 @@ func autoConvert_v1beta1_PodSecurityPolicySpec_To_extensions_PodSecurityPolicySp
return err
}
out.ReadOnlyRootFilesystem = in.ReadOnlyRootFilesystem
out.AllowedHostPaths = *(*[]string)(unsafe.Pointer(&in.AllowedHostPaths))
return nil
}
......@@ -1256,6 +1257,7 @@ func autoConvert_extensions_PodSecurityPolicySpec_To_v1beta1_PodSecurityPolicySp
return err
}
out.ReadOnlyRootFilesystem = in.ReadOnlyRootFilesystem
out.AllowedHostPaths = *(*[]string)(unsafe.Pointer(&in.AllowedHostPaths))
return nil
}
......
......@@ -826,6 +826,11 @@ func DeepCopy_v1beta1_PodSecurityPolicySpec(in interface{}, out interface{}, c *
if err := DeepCopy_v1beta1_FSGroupStrategyOptions(&in.FSGroup, &out.FSGroup, c); err != nil {
return err
}
if in.AllowedHostPaths != nil {
in, out := &in.AllowedHostPaths, &out.AllowedHostPaths
*out = make([]string, len(*in))
copy(*out, *in)
}
return nil
}
}
......
......@@ -818,6 +818,11 @@ func DeepCopy_extensions_PodSecurityPolicySpec(in interface{}, out interface{},
if err := DeepCopy_extensions_FSGroupStrategyOptions(&in.FSGroup, &out.FSGroup, c); err != nil {
return err
}
if in.AllowedHostPaths != nil {
in, out := &in.AllowedHostPaths, &out.AllowedHostPaths
*out = make([]string, len(*in))
copy(*out, *in)
}
return nil
}
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment