Skip to content

Commit

Permalink
apis: add validation for HostUsers
Browse files Browse the repository at this point in the history
This commit just adds a validation according to KEP-127. We check that
only the supported volumes for phase 1 of the KEP are accepted.

Signed-off-by: Rodrigo Campos <rodrigoca@microsoft.com>
  • Loading branch information
rata committed Aug 3, 2022
1 parent 482e76d commit cf8164b
Show file tree
Hide file tree
Showing 4 changed files with 329 additions and 0 deletions.
18 changes: 18 additions & 0 deletions pkg/api/pod/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -539,6 +539,15 @@ func dropDisabledFields(
})
}

// If the feature is disabled and not in use, drop the hostUsers field.
if !utilfeature.DefaultFeatureGate.Enabled(features.UserNamespacesStatelessPodsSupport) && !hostUsersInUse(oldPodSpec) {
// Drop the field in podSpec only if SecurityContext is not nil.
// If it is nil, there is no need to set hostUsers=nil (it will be nil too).
if podSpec.SecurityContext != nil {
podSpec.SecurityContext.HostUsers = nil
}
}

dropDisabledProcMountField(podSpec, oldPodSpec)

dropDisabledCSIVolumeSourceAlphaFields(podSpec, oldPodSpec)
Expand Down Expand Up @@ -672,6 +681,15 @@ func nodeTaintsPolicyInUse(podSpec *api.PodSpec) bool {
return false
}

// hostUsersInUse returns true if the pod spec has spec.hostUsers field set.
func hostUsersInUse(podSpec *api.PodSpec) bool {
if podSpec != nil && podSpec.SecurityContext != nil && podSpec.SecurityContext.HostUsers != nil {
return true
}

return false
}

// procMountInUse returns true if the pod spec is non-nil and has a SecurityContext's ProcMount field set to a non-default value
func procMountInUse(podSpec *api.PodSpec) bool {
if podSpec == nil {
Expand Down
97 changes: 97 additions & 0 deletions pkg/api/pod/util_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1949,3 +1949,100 @@ func TestDropDisabledMatchLabelKeysField(t *testing.T) {
})
}
}

func TestDropHostUsers(t *testing.T) {
falseVar := false
trueVar := true

podWithoutHostUsers := func() *api.Pod {
return &api.Pod{
Spec: api.PodSpec{
SecurityContext: &api.PodSecurityContext{}},
}
}
podWithHostUsersFalse := func() *api.Pod {
return &api.Pod{
Spec: api.PodSpec{
SecurityContext: &api.PodSecurityContext{
HostUsers: &falseVar,
},
},
}
}
podWithHostUsersTrue := func() *api.Pod {
return &api.Pod{
Spec: api.PodSpec{
SecurityContext: &api.PodSecurityContext{
HostUsers: &trueVar,
},
},
}
}

podInfo := []struct {
description string
hasHostUsers bool
pod func() *api.Pod
}{
{
description: "with hostUsers=true",
hasHostUsers: true,
pod: podWithHostUsersTrue,
},
{
description: "with hostUsers=false",
hasHostUsers: true,
pod: podWithHostUsersFalse,
},
{
description: "with hostUsers=nil",
pod: func() *api.Pod { return nil },
},
}

for _, enabled := range []bool{true, false} {
for _, oldPodInfo := range podInfo {
for _, newPodInfo := range podInfo {
oldPodHasHostUsers, oldPod := oldPodInfo.hasHostUsers, oldPodInfo.pod()
newPodHasHostUsers, newPod := newPodInfo.hasHostUsers, newPodInfo.pod()
if newPod == nil {
continue
}

t.Run(fmt.Sprintf("feature enabled=%v, old pod %v, new pod %v", enabled, oldPodInfo.description, newPodInfo.description), func(t *testing.T) {
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.UserNamespacesStatelessPodsSupport, enabled)()

DropDisabledPodFields(newPod, oldPod)

// old pod should never be changed
if !reflect.DeepEqual(oldPod, oldPodInfo.pod()) {
t.Errorf("old pod changed: %v", cmp.Diff(oldPod, oldPodInfo.pod()))
}

switch {
case enabled || oldPodHasHostUsers:
// new pod should not be changed if the feature is enabled, or if the old pod had hostUsers
if !reflect.DeepEqual(newPod, newPodInfo.pod()) {
t.Errorf("new pod changed: %v", cmp.Diff(newPod, newPodInfo.pod()))
}
case newPodHasHostUsers:
// new pod should be changed
if reflect.DeepEqual(newPod, newPodInfo.pod()) {
t.Errorf("new pod was not changed")
}
// new pod should not have hostUsers
if exp := podWithoutHostUsers(); !reflect.DeepEqual(newPod, exp) {
t.Errorf("new pod had hostUsers: %v", cmp.Diff(newPod, exp))
}
default:
// new pod should not need to be changed
if !reflect.DeepEqual(newPod, newPodInfo.pod()) {
t.Errorf("new pod changed: %v", cmp.Diff(newPod, newPodInfo.pod()))
}
}
})
}
}
}

}
47 changes: 47 additions & 0 deletions pkg/apis/core/validation/validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -3075,6 +3075,52 @@ func validateContainerCommon(ctr *core.Container, volumes map[string]core.Volume
allErrs = append(allErrs, validatePullPolicy(ctr.ImagePullPolicy, path.Child("imagePullPolicy"))...)
allErrs = append(allErrs, ValidateResourceRequirements(&ctr.Resources, path.Child("resources"), opts)...)
allErrs = append(allErrs, ValidateSecurityContext(ctr.SecurityContext, path.Child("securityContext"))...)
return allErrs
}

func validateHostUsers(spec *core.PodSpec, fldPath *field.Path) field.ErrorList {
allErrs := field.ErrorList{}

// Only make the following checks if hostUsers is false (otherwise, the container uses the
// same userns as the host, and so there isn't anything to check).
if spec.SecurityContext == nil || spec.SecurityContext.HostUsers == nil || *spec.SecurityContext.HostUsers == true {
return allErrs
}

// For now only these volumes are supported:
// - configmap
// - secret
// - downwardAPI
// - emptyDir
// - projected
// So reject anything else.
for i, vol := range spec.Volumes {
switch {
case vol.EmptyDir != nil:
case vol.Secret != nil:
case vol.DownwardAPI != nil:
case vol.ConfigMap != nil:
case vol.Projected != nil:
default:
allErrs = append(allErrs, field.Forbidden(fldPath.Child("volumes").Index(i), "volume type not supported when `pod.Spec.HostUsers` is false"))
}
}

// We decided to restrict the usage of userns with other host namespaces:
// https://github.com/kubernetes/kubernetes/pull/111090#discussion_r935994282
// The tl;dr is: you can easily run into permission issues that seem unexpected, we don't
// know of any good use case and we can always enable them later.

// Note we already validated above spec.SecurityContext is not nil.
if spec.SecurityContext.HostNetwork {
allErrs = append(allErrs, field.Forbidden(fldPath.Child("hostNetwork"), "when `pod.Spec.HostUsers` is false"))
}
if spec.SecurityContext.HostPID {
allErrs = append(allErrs, field.Forbidden(fldPath.Child("HostPID"), "when `pod.Spec.HostUsers` is false"))
}
if spec.SecurityContext.HostIPC {
allErrs = append(allErrs, field.Forbidden(fldPath.Child("HostIPC"), "when `pod.Spec.HostUsers` is false"))
}

return allErrs
}
Expand Down Expand Up @@ -3545,6 +3591,7 @@ func ValidatePodSpec(spec *core.PodSpec, podMeta *metav1.ObjectMeta, fldPath *fi
allErrs = append(allErrs, validateReadinessGates(spec.ReadinessGates, fldPath.Child("readinessGates"))...)
allErrs = append(allErrs, validateTopologySpreadConstraints(spec.TopologySpreadConstraints, fldPath.Child("topologySpreadConstraints"))...)
allErrs = append(allErrs, validateWindowsHostProcessPod(spec, fldPath, opts)...)
allErrs = append(allErrs, validateHostUsers(spec, fldPath)...)
if len(spec.ServiceAccountName) > 0 {
for _, msg := range ValidateServiceAccountName(spec.ServiceAccountName, false) {
allErrs = append(allErrs, field.Invalid(fldPath.Child("serviceAccountName"), spec.ServiceAccountName, msg))
Expand Down
167 changes: 167 additions & 0 deletions pkg/apis/core/validation/validation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18277,6 +18277,7 @@ func TestValidateOSFields(t *testing.T) {
"SecurityContext.HostIPC",
"SecurityContext.HostNetwork",
"SecurityContext.HostPID",
"SecurityContext.HostUsers",
"SecurityContext.RunAsGroup",
"SecurityContext.RunAsUser",
"SecurityContext.SELinuxOptions",
Expand Down Expand Up @@ -20572,6 +20573,172 @@ func TestValidateNonSpecialIP(t *testing.T) {
}
}

func TestValidateHostUsers(t *testing.T) {
falseVar := false
trueVar := true

cases := []struct {
name string
success bool
spec *core.PodSpec
}{
{
name: "empty",
success: true,
spec: &core.PodSpec{},
},
{
name: "hostUsers unset",
success: true,
spec: &core.PodSpec{
SecurityContext: &core.PodSecurityContext{},
},
},
{
name: "hostUsers=false",
success: true,
spec: &core.PodSpec{
SecurityContext: &core.PodSecurityContext{
HostUsers: &falseVar,
},
},
},
{
name: "hostUsers=true",
success: true,
spec: &core.PodSpec{
SecurityContext: &core.PodSecurityContext{
HostUsers: &trueVar,
},
},
},
{
name: "hostUsers=false & volumes",
success: true,
spec: &core.PodSpec{
SecurityContext: &core.PodSecurityContext{
HostUsers: &falseVar,
},
Volumes: []core.Volume{
{
Name: "configmap",
VolumeSource: core.VolumeSource{
ConfigMap: &core.ConfigMapVolumeSource{
LocalObjectReference: core.LocalObjectReference{Name: "configmap"},
},
},
},
{
Name: "secret",
VolumeSource: core.VolumeSource{
Secret: &core.SecretVolumeSource{
SecretName: "secret",
},
},
},
{
Name: "downward-api",
VolumeSource: core.VolumeSource{
DownwardAPI: &core.DownwardAPIVolumeSource{},
},
},
{
Name: "proj",
VolumeSource: core.VolumeSource{
Projected: &core.ProjectedVolumeSource{},
},
},
{
Name: "empty-dir",
VolumeSource: core.VolumeSource{
EmptyDir: &core.EmptyDirVolumeSource{},
},
},
},
},
},
{
name: "hostUsers=false - unsupported volume",
success: false,
spec: &core.PodSpec{
SecurityContext: &core.PodSecurityContext{
HostUsers: &falseVar,
},
Volumes: []core.Volume{
{
Name: "host-path",
VolumeSource: core.VolumeSource{
HostPath: &core.HostPathVolumeSource{},
},
},
},
},
},
{
// It should ignore unsupported volumes with hostUsers=true.
name: "hostUsers=true - unsupported volume",
success: true,
spec: &core.PodSpec{
SecurityContext: &core.PodSecurityContext{
HostUsers: &trueVar,
},
Volumes: []core.Volume{
{
Name: "host-path",
VolumeSource: core.VolumeSource{
HostPath: &core.HostPathVolumeSource{},
},
},
},
},
},
{
name: "hostUsers=false & HostNetwork",
success: false,
spec: &core.PodSpec{
SecurityContext: &core.PodSecurityContext{
HostUsers: &falseVar,
HostNetwork: true,
},
},
},
{
name: "hostUsers=false & HostPID",
success: false,
spec: &core.PodSpec{
SecurityContext: &core.PodSecurityContext{
HostUsers: &falseVar,
HostPID: true,
},
},
},
{
name: "hostUsers=false & HostIPC",
success: false,
spec: &core.PodSpec{
SecurityContext: &core.PodSecurityContext{
HostUsers: &falseVar,
HostIPC: true,
},
},
},
}

for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
fPath := field.NewPath("spec")

allErrs := validateHostUsers(tc.spec, fPath)
if !tc.success && len(allErrs) == 0 {
t.Errorf("Unexpected success")
}
if tc.success && len(allErrs) != 0 {
t.Errorf("Unexpected error(s): %v", allErrs)
}
})
}
}

func TestValidateWindowsHostProcessPod(t *testing.T) {
const containerName = "container"
falseVar := false
Expand Down

0 comments on commit cf8164b

Please sign in to comment.