apis: add validation for HostUsers

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>
This commit is contained in:
Rodrigo Campos
2022-07-07 16:49:39 +02:00
parent 482e76dc2c
commit cf8164bccf
4 changed files with 329 additions and 0 deletions

View File

@@ -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
}
@@ -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))

View File

@@ -18277,6 +18277,7 @@ func TestValidateOSFields(t *testing.T) {
"SecurityContext.HostIPC",
"SecurityContext.HostNetwork",
"SecurityContext.HostPID",
"SecurityContext.HostUsers",
"SecurityContext.RunAsGroup",
"SecurityContext.RunAsUser",
"SecurityContext.SELinuxOptions",
@@ -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