diff --git a/pkg/api/pod/util.go b/pkg/api/pod/util.go index 67fb8fadbf5..35417fec68e 100644 --- a/pkg/api/pod/util.go +++ b/pkg/api/pod/util.go @@ -379,6 +379,8 @@ func dropDisabledFields( dropDisabledGMSAFields(podSpec, oldPodSpec) + dropDisabledRunAsUserNameFields(podSpec, oldPodSpec) + if !utilfeature.DefaultFeatureGate.Enabled(features.RuntimeClass) && !runtimeClassInUse(oldPodSpec) { // Set RuntimeClassName to nil only if feature is disabled and it is not used podSpec.RuntimeClassName = nil @@ -450,6 +452,38 @@ func dropDisabledGMSAFieldsFromContainers(containers []api.Container) { } } +// dropDisabledRunAsUserNameFields removes disabled fields related to WindowsOptions.RunAsUserName +// from the given PodSpec. +func dropDisabledRunAsUserNameFields(podSpec, oldPodSpec *api.PodSpec) { + if utilfeature.DefaultFeatureGate.Enabled(features.WindowsRunAsUserName) || + runAsUserNameFieldsInUse(oldPodSpec) { + return + } + + if podSpec.SecurityContext != nil { + dropDisabledRunAsUserNameFieldsFromWindowsSecurityOptions(podSpec.SecurityContext.WindowsOptions) + } + dropDisabledRunAsUserNameFieldsFromContainers(podSpec.Containers) + dropDisabledRunAsUserNameFieldsFromContainers(podSpec.InitContainers) +} + +// dropDisabledRunAsUserNameFieldsFromWindowsSecurityOptions removes disabled fields +// related to RunAsUserName from the given WindowsSecurityContextOptions. +func dropDisabledRunAsUserNameFieldsFromWindowsSecurityOptions(windowsOptions *api.WindowsSecurityContextOptions) { + if windowsOptions != nil { + windowsOptions.RunAsUserName = nil + } +} + +// dropDisabledRunAsUserNameFieldsFromContainers removes disabled fields +func dropDisabledRunAsUserNameFieldsFromContainers(containers []api.Container) { + for i := range containers { + if containers[i].SecurityContext != nil { + dropDisabledRunAsUserNameFieldsFromWindowsSecurityOptions(containers[i].SecurityContext.WindowsOptions) + } + } +} + // dropDisabledProcMountField removes disabled fields from PodSpec related // to ProcMount only if it is not already used by the old spec func dropDisabledProcMountField(podSpec, oldPodSpec *api.PodSpec) { @@ -703,6 +737,39 @@ func gMSAFieldsInUseInAnyContainer(containers []api.Container) bool { return false } +// runAsUserNameFieldsInUse returns true if the pod spec is non-nil and has the RunAsUserName +// field set in the PodSecurityContext or any container's SecurityContext. +func runAsUserNameFieldsInUse(podSpec *api.PodSpec) bool { + if podSpec == nil { + return false + } + + if podSpec.SecurityContext != nil && runAsUserNameFieldsInUseInWindowsSecurityOptions(podSpec.SecurityContext.WindowsOptions) { + return true + } + + return runAsUserNameFieldsInUseInAnyContainer(podSpec.Containers) || + runAsUserNameFieldsInUseInAnyContainer(podSpec.InitContainers) +} + +// runAsUserNameFieldsInUseInWindowsSecurityOptions returns true if the given WindowsSecurityContextOptions is +// non-nil and its RunAsUserName field is set. +func runAsUserNameFieldsInUseInWindowsSecurityOptions(windowsOptions *api.WindowsSecurityContextOptions) bool { + return windowsOptions != nil && windowsOptions.RunAsUserName != nil +} + +// runAsUserNameFieldsInUseInAnyContainer returns true if any of the given Containers has its +// SecurityContext's RunAsUserName field set. +func runAsUserNameFieldsInUseInAnyContainer(containers []api.Container) bool { + for _, container := range containers { + if container.SecurityContext != nil && runAsUserNameFieldsInUseInWindowsSecurityOptions(container.SecurityContext.WindowsOptions) { + return true + } + } + + return false +} + // subpathExprInUse returns true if the pod spec is non-nil and has a volume mount that makes use of the subPathExpr feature func subpathExprInUse(podSpec *api.PodSpec) bool { if podSpec == nil { diff --git a/pkg/api/pod/util_test.go b/pkg/api/pod/util_test.go index 574d7052c38..4c58838342c 100644 --- a/pkg/api/pod/util_test.go +++ b/pkg/api/pod/util_test.go @@ -1545,6 +1545,180 @@ func TestDropGMSAFields(t *testing.T) { } } +func TestDropWindowsRunAsUserNameFields(t *testing.T) { + defaultContainerSecurityContextFactory := func() *api.SecurityContext { + defaultProcMount := api.DefaultProcMount + return &api.SecurityContext{ProcMount: &defaultProcMount} + } + podWithoutWindowsOptionsFactory := func() *api.Pod { + return &api.Pod{ + Spec: api.PodSpec{ + RestartPolicy: api.RestartPolicyNever, + SecurityContext: &api.PodSecurityContext{}, + Containers: []api.Container{{Name: "container1", Image: "testimage", SecurityContext: defaultContainerSecurityContextFactory()}}, + InitContainers: []api.Container{{Name: "initContainer1", Image: "testimage", SecurityContext: defaultContainerSecurityContextFactory()}}, + }, + } + } + + type podFactoryInfo struct { + description string + hasRunAsUserNameField bool + // this factory should generate the input pod whose spec will be fed to dropDisabledFields + podFactory func() *api.Pod + // this factory should generate the expected pod after the RunAsUserName fields have been dropped + // we can't just use podWithoutWindowsOptionsFactory as is for this, since in some cases + // we'll be left with a WindowsSecurityContextOptions struct with no RunAsUserName field set, + // as oposed to a nil pointer in the pod generated by podWithoutWindowsOptionsFactory + // if this field is not set, it will default to the podFactory + strippedPodFactory func() *api.Pod + } + + toPtr := func(s string) *string { + return &s + } + + podFactoryInfos := []podFactoryInfo{ + { + description: "is nil", + hasRunAsUserNameField: false, + podFactory: func() *api.Pod { return nil }, + }, + { + description: "does not have any RunAsUserName field set", + hasRunAsUserNameField: false, + podFactory: podWithoutWindowsOptionsFactory, + }, + { + description: "has a pod-level WindowsSecurityContextOptions struct with no RunAsUserName field set", + hasRunAsUserNameField: false, + podFactory: func() *api.Pod { + pod := podWithoutWindowsOptionsFactory() + pod.Spec.SecurityContext.WindowsOptions = &api.WindowsSecurityContextOptions{} + return pod + }, + }, + { + description: "has a WindowsSecurityContextOptions struct with no RunAsUserName field set on a container", + hasRunAsUserNameField: false, + podFactory: func() *api.Pod { + pod := podWithoutWindowsOptionsFactory() + pod.Spec.Containers[0].SecurityContext.WindowsOptions = &api.WindowsSecurityContextOptions{} + return pod + }, + }, + { + description: "has a WindowsSecurityContextOptions struct with no RunAsUserName field set on an init container", + hasRunAsUserNameField: false, + podFactory: func() *api.Pod { + pod := podWithoutWindowsOptionsFactory() + pod.Spec.InitContainers[0].SecurityContext.WindowsOptions = &api.WindowsSecurityContextOptions{} + return pod + }, + }, + { + description: "has RunAsUserName field set in the PodSecurityContext", + hasRunAsUserNameField: true, + podFactory: func() *api.Pod { + pod := podWithoutWindowsOptionsFactory() + pod.Spec.SecurityContext.WindowsOptions = &api.WindowsSecurityContextOptions{RunAsUserName: toPtr("foo-lish")} + return pod + }, + strippedPodFactory: func() *api.Pod { + pod := podWithoutWindowsOptionsFactory() + pod.Spec.SecurityContext.WindowsOptions = &api.WindowsSecurityContextOptions{} + return pod + }, + }, + { + description: "has RunAsUserName field set in a container's SecurityContext", + hasRunAsUserNameField: true, + podFactory: func() *api.Pod { + pod := podWithoutWindowsOptionsFactory() + pod.Spec.Containers[0].SecurityContext.WindowsOptions = &api.WindowsSecurityContextOptions{RunAsUserName: toPtr("foo-lish")} + return pod + }, + strippedPodFactory: func() *api.Pod { + pod := podWithoutWindowsOptionsFactory() + pod.Spec.Containers[0].SecurityContext.WindowsOptions = &api.WindowsSecurityContextOptions{} + return pod + }, + }, + { + description: "has RunAsUserName field set in an init container's PodSecurityContext", + hasRunAsUserNameField: true, + podFactory: func() *api.Pod { + pod := podWithoutWindowsOptionsFactory() + pod.Spec.InitContainers[0].SecurityContext.WindowsOptions = &api.WindowsSecurityContextOptions{RunAsUserName: toPtr("foo-lish")} + return pod + }, + strippedPodFactory: func() *api.Pod { + pod := podWithoutWindowsOptionsFactory() + pod.Spec.InitContainers[0].SecurityContext.WindowsOptions = &api.WindowsSecurityContextOptions{} + return pod + }, + }, + } + + for _, enabled := range []bool{true, false} { + for _, oldPodFactoryInfo := range podFactoryInfos { + for _, newPodFactoryInfo := range podFactoryInfos { + newPodHasRunAsUserNameField, newPod := newPodFactoryInfo.hasRunAsUserNameField, newPodFactoryInfo.podFactory() + if newPod == nil { + continue + } + oldPodHasRunAsUserNameField, oldPod := oldPodFactoryInfo.hasRunAsUserNameField, oldPodFactoryInfo.podFactory() + + t.Run(fmt.Sprintf("feature enabled=%v, old pod %s, new pod %s", enabled, oldPodFactoryInfo.description, newPodFactoryInfo.description), func(t *testing.T) { + defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.WindowsRunAsUserName, enabled)() + + var oldPodSpec *api.PodSpec + if oldPod != nil { + oldPodSpec = &oldPod.Spec + } + dropDisabledFields(&newPod.Spec, nil, oldPodSpec, nil) + + // old pod should never be changed + if !reflect.DeepEqual(oldPod, oldPodFactoryInfo.podFactory()) { + t.Errorf("old pod changed: %v", diff.ObjectReflectDiff(oldPod, oldPodFactoryInfo.podFactory())) + } + + switch { + case enabled || oldPodHasRunAsUserNameField: + // new pod should not be changed if the feature is enabled, or if the old pod had the RunAsUserName field set + if !reflect.DeepEqual(newPod, newPodFactoryInfo.podFactory()) { + t.Errorf("new pod changed: %v", diff.ObjectReflectDiff(newPod, newPodFactoryInfo.podFactory())) + } + case newPodHasRunAsUserNameField: + // new pod should be changed + if reflect.DeepEqual(newPod, newPodFactoryInfo.podFactory()) { + t.Errorf("%v", oldPod) + t.Errorf("%v", newPod) + t.Errorf("new pod was not changed") + } + // new pod should not have the RunAsUserName field set + var expectedStrippedPod *api.Pod + if newPodFactoryInfo.strippedPodFactory == nil { + expectedStrippedPod = newPodFactoryInfo.podFactory() + } else { + expectedStrippedPod = newPodFactoryInfo.strippedPodFactory() + } + + if !reflect.DeepEqual(newPod, expectedStrippedPod) { + t.Errorf("new pod had some RunAsUserName field set: %v", diff.ObjectReflectDiff(newPod, expectedStrippedPod)) + } + default: + // new pod should not need to be changed + if !reflect.DeepEqual(newPod, newPodFactoryInfo.podFactory()) { + t.Errorf("new pod changed: %v", diff.ObjectReflectDiff(newPod, newPodFactoryInfo.podFactory())) + } + } + }) + } + } + } +} + func TestDropPodSysctls(t *testing.T) { podWithSysctls := func() *api.Pod { return &api.Pod{ diff --git a/pkg/apis/core/types.go b/pkg/apis/core/types.go index bbea532f19d..70e4e710842 100644 --- a/pkg/apis/core/types.go +++ b/pkg/apis/core/types.go @@ -2768,7 +2768,9 @@ type PodSecurityContext struct { // takes precedence for that container. // +optional SELinuxOptions *SELinuxOptions - // Windows security options. + // The Windows specific settings applied to all containers. + // If unspecified, the options within a container's SecurityContext will be used. + // If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. // +optional WindowsOptions *WindowsSecurityContextOptions // The UID to run the entrypoint of the container process. @@ -4704,7 +4706,9 @@ type SecurityContext struct { // PodSecurityContext, the value specified in SecurityContext takes precedence. // +optional SELinuxOptions *SELinuxOptions - // Windows security options. + // The Windows specific settings applied to all containers. + // If unspecified, the options from the PodSecurityContext will be used. + // If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. // +optional WindowsOptions *WindowsSecurityContextOptions // The UID to run the entrypoint of the container process. @@ -4786,6 +4790,14 @@ type WindowsSecurityContextOptions struct { // This field is alpha-level and is only honored by servers that enable the WindowsGMSA feature flag. // +optional GMSACredentialSpec *string + + // The UserName in Windows to run the entrypoint of the container process. + // Defaults to the user specified in image metadata if unspecified. + // May also be set in PodSecurityContext. If set in both SecurityContext and + // PodSecurityContext, the value specified in SecurityContext takes precedence. + // This field is alpha-level and it is only honored by servers that enable the WindowsRunAsUserName feature flag. + // +optional + RunAsUserName *string } // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object diff --git a/pkg/apis/core/validation/validation.go b/pkg/apis/core/validation/validation.go index b5420ac5738..afc92112093 100644 --- a/pkg/apis/core/validation/validation.go +++ b/pkg/apis/core/validation/validation.go @@ -5360,9 +5360,37 @@ func ValidateSecurityContext(sc *core.SecurityContext, fldPath *field.Path) fiel // maxGMSACredentialSpecLength is the max length, in bytes, for the actual contents // of a GMSA cred spec. In general, those shouldn't be more than a few hundred bytes, // so we want to give plenty of room here while still providing an upper bound. +// The runAsUserName field will be used to execute the given container's entrypoint, and +// it can be formatted as "DOMAIN/USER", where the DOMAIN is optional, maxRunAsUserNameDomainLength +// is the max character length for the user's DOMAIN, and maxRunAsUserNameUserLength +// is the max character length for the USER itself. Both the DOMAIN and USER have their +// own restrictions, and more information about them can be found here: +// https://support.microsoft.com/en-us/help/909264/naming-conventions-in-active-directory-for-computers-domains-sites-and +// https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.localaccounts/new-localuser?view=powershell-5.1 const ( maxGMSACredentialSpecLengthInKiB = 64 maxGMSACredentialSpecLength = maxGMSACredentialSpecLengthInKiB * 1024 + maxRunAsUserNameDomainLength = 256 + maxRunAsUserNameUserLength = 21 +) + +var ( + // control characters are not permitted in the runAsUserName field. + ctrlRegex = regexp.MustCompile(`[[:cntrl:]]+`) + + // a valid NetBios Domain name cannot start with a dot, has at least 1 character, + // at most 15 characters, and it cannot the characters: \ / : * ? " < > | + validNetBiosRegex = regexp.MustCompile(`^[^\\/:\*\?"<>|\.][^\\/:\*\?"<>|]{0,14}$`) + + // a valid DNS name contains only alphanumeric characters, dots, and dashes. + dnsLabelFormat = `[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?` + dnsSubdomainFormat = fmt.Sprintf(`^%s(?:\.%s)*$`, dnsLabelFormat, dnsLabelFormat) + validWindowsUserDomainDNSRegex = regexp.MustCompile(dnsSubdomainFormat) + + // a username is invalid if it contains the characters: " / \ [ ] : ; | = , + * ? < > @ + // or it contains only dots or spaces. + invalidUserNameCharsRegex = regexp.MustCompile(`["/\\:;|=,\+\*\?<>@\[\]]`) + invalidUserNameDotsSpacesRegex = regexp.MustCompile(`^[\. ]+$`) ) func validateWindowsSecurityContextOptions(windowsOptions *core.WindowsSecurityContextOptions, fieldPath *field.Path) field.ErrorList { @@ -5388,6 +5416,59 @@ func validateWindowsSecurityContextOptions(windowsOptions *core.WindowsSecurityC } } + if windowsOptions.RunAsUserName != nil { + if l := len(*windowsOptions.RunAsUserName); l == 0 { + allErrs = append(allErrs, field.Invalid(fieldPath.Child("runAsUserName"), windowsOptions.RunAsUserName, "runAsUserName cannot be an empty string")) + } else if ctrlRegex.MatchString(*windowsOptions.RunAsUserName) { + errMsg := fmt.Sprintf("runAsUserName cannot contain control characters") + allErrs = append(allErrs, field.Invalid(fieldPath.Child("runAsUserName"), windowsOptions.RunAsUserName, errMsg)) + } else if parts := strings.Split(*windowsOptions.RunAsUserName, "\\"); len(parts) > 2 { + errMsg := fmt.Sprintf("runAsUserName cannot contain more than one backslash") + allErrs = append(allErrs, field.Invalid(fieldPath.Child("runAsUserName"), windowsOptions.RunAsUserName, errMsg)) + } else { + var ( + hasDomain = false + domain = "" + user string + ) + if len(parts) == 1 { + user = parts[0] + } else { + hasDomain = true + domain = parts[0] + user = parts[1] + } + + if len(domain) >= maxRunAsUserNameDomainLength { + errMsg := fmt.Sprintf("runAsUserName's Domain length must be under %d characters", maxRunAsUserNameDomainLength) + allErrs = append(allErrs, field.Invalid(fieldPath.Child("runAsUserName"), windowsOptions.RunAsUserName, errMsg)) + } + + if hasDomain && !(validNetBiosRegex.MatchString(domain) || validWindowsUserDomainDNSRegex.MatchString(domain)) { + errMsg := "runAsUserName's Domain doesn't match the NetBios nor the DNS format" + allErrs = append(allErrs, field.Invalid(fieldPath.Child("runAsUserName"), windowsOptions.RunAsUserName, errMsg)) + } + + if l := len(user); l == 0 { + errMsg := fmt.Sprintf("runAsUserName's User cannot be empty") + allErrs = append(allErrs, field.Invalid(fieldPath.Child("runAsUserName"), windowsOptions.RunAsUserName, errMsg)) + } else if l >= maxRunAsUserNameUserLength { + errMsg := fmt.Sprintf("runAsUserName's User length must be under %d characters", maxRunAsUserNameUserLength) + allErrs = append(allErrs, field.Invalid(fieldPath.Child("runAsUserName"), windowsOptions.RunAsUserName, errMsg)) + } + + if invalidUserNameDotsSpacesRegex.MatchString(user) { + errMsg := `runAsUserName's User cannot contain only periods or spaces` + allErrs = append(allErrs, field.Invalid(fieldPath.Child("runAsUserName"), windowsOptions.RunAsUserName, errMsg)) + } + + if invalidUserNameCharsRegex.MatchString(user) { + errMsg := `runAsUserName's User cannot contain the following characters: "/\:;|=,+*?<>@[]` + allErrs = append(allErrs, field.Invalid(fieldPath.Child("runAsUserName"), windowsOptions.RunAsUserName, errMsg)) + } + } + } + return allErrs } diff --git a/pkg/apis/core/validation/validation_test.go b/pkg/apis/core/validation/validation_test.go index 7ec70eb87d2..b544631af59 100644 --- a/pkg/apis/core/validation/validation_test.go +++ b/pkg/apis/core/validation/validation_test.go @@ -13485,6 +13485,134 @@ func TestValidateWindowsSecurityContextOptions(t *testing.T) { }, expectedErrorSubstring: "gmsaCredentialSpec size must be under", }, + { + testName: "RunAsUserName is nil", + windowsOptions: &core.WindowsSecurityContextOptions{ + RunAsUserName: nil, + }, + }, + { + testName: "a valid RunAsUserName", + windowsOptions: &core.WindowsSecurityContextOptions{ + RunAsUserName: toPtr("Container. User"), + }, + }, + { + testName: "a valid RunAsUserName with NetBios Domain", + windowsOptions: &core.WindowsSecurityContextOptions{ + RunAsUserName: toPtr("Network Service\\Container. User"), + }, + }, + { + testName: "a valid RunAsUserName with DNS Domain", + windowsOptions: &core.WindowsSecurityContextOptions{ + RunAsUserName: toPtr(strings.Repeat("fOo", 20) + ".liSH\\Container. User"), + }, + }, + { + testName: "a valid RunAsUserName with DNS Domain with a single character segment", + windowsOptions: &core.WindowsSecurityContextOptions{ + RunAsUserName: toPtr(strings.Repeat("fOo", 20) + ".l\\Container. User"), + }, + }, + { + testName: "a valid RunAsUserName with a long single segment DNS Domain", + windowsOptions: &core.WindowsSecurityContextOptions{ + RunAsUserName: toPtr(strings.Repeat("a", 42) + "\\Container. User"), + }, + }, + { + testName: "an empty RunAsUserName", + windowsOptions: &core.WindowsSecurityContextOptions{ + RunAsUserName: toPtr(""), + }, + expectedErrorSubstring: "runAsUserName cannot be an empty string", + }, + { + testName: "RunAsUserName containing a control character", + windowsOptions: &core.WindowsSecurityContextOptions{ + RunAsUserName: toPtr("Container\tUser"), + }, + expectedErrorSubstring: "runAsUserName cannot contain control characters", + }, + { + testName: "RunAsUserName containing too many backslashes", + windowsOptions: &core.WindowsSecurityContextOptions{ + RunAsUserName: toPtr("Container\\Foo\\Lish"), + }, + expectedErrorSubstring: "runAsUserName cannot contain more than one backslash", + }, + { + testName: "RunAsUserName containing backslash but empty Domain", + windowsOptions: &core.WindowsSecurityContextOptions{ + RunAsUserName: toPtr("\\User"), + }, + expectedErrorSubstring: "runAsUserName's Domain doesn't match the NetBios nor the DNS format", + }, + { + testName: "RunAsUserName containing backslash but empty User", + windowsOptions: &core.WindowsSecurityContextOptions{ + RunAsUserName: toPtr("Container\\"), + }, + expectedErrorSubstring: "runAsUserName's User cannot be empty", + }, + { + testName: "RunAsUserName's NetBios Domain is too long", + windowsOptions: &core.WindowsSecurityContextOptions{ + RunAsUserName: toPtr("NetBios " + strings.Repeat("a", 8) + "\\user"), + }, + expectedErrorSubstring: "runAsUserName's Domain doesn't match the NetBios", + }, + { + testName: "RunAsUserName's DNS Domain is too long", + windowsOptions: &core.WindowsSecurityContextOptions{ + // even if this tests the max Domain length, the Domain should still be "valid". + RunAsUserName: toPtr(strings.Repeat(strings.Repeat("a", 63)+".", 4)[:253] + ".com\\user"), + }, + expectedErrorSubstring: "runAsUserName's Domain length must be under", + }, + { + testName: "RunAsUserName's User is too long", + windowsOptions: &core.WindowsSecurityContextOptions{ + RunAsUserName: toPtr(strings.Repeat("a", maxRunAsUserNameUserLength)), + }, + expectedErrorSubstring: "runAsUserName's User length must be under", + }, + { + testName: "RunAsUserName's User cannot contain only spaces or periods", + windowsOptions: &core.WindowsSecurityContextOptions{ + RunAsUserName: toPtr("... ..."), + }, + expectedErrorSubstring: "runAsUserName's User cannot contain only periods or spaces", + }, + { + testName: "RunAsUserName's NetBios Domain cannot start with a dot", + windowsOptions: &core.WindowsSecurityContextOptions{ + RunAsUserName: toPtr(".FooLish\\User"), + }, + expectedErrorSubstring: "runAsUserName's Domain doesn't match the NetBios", + }, + { + testName: "RunAsUserName's NetBios Domain cannot contain invalid characters", + windowsOptions: &core.WindowsSecurityContextOptions{ + RunAsUserName: toPtr("Foo? Lish?\\User"), + }, + expectedErrorSubstring: "runAsUserName's Domain doesn't match the NetBios", + }, + { + testName: "RunAsUserName's DNS Domain cannot contain invalid characters", + windowsOptions: &core.WindowsSecurityContextOptions{ + RunAsUserName: toPtr(strings.Repeat("a", 32) + ".com-\\user"), + }, + expectedErrorSubstring: "runAsUserName's Domain doesn't match the NetBios nor the DNS format", + }, + { + testName: "RunAsUserName's User cannot contain invalid characters", + windowsOptions: &core.WindowsSecurityContextOptions{ + RunAsUserName: toPtr("Container/User"), + }, + expectedErrorSubstring: "runAsUserName's User cannot contain the following characters", + }, } for _, testCase := range testCases { diff --git a/pkg/features/kube_features.go b/pkg/features/kube_features.go index 258d8445b76..6a693f13720 100644 --- a/pkg/features/kube_features.go +++ b/pkg/features/kube_features.go @@ -402,6 +402,12 @@ const ( // Enables GMSA support for Windows workloads. WindowsGMSA featuregate.Feature = "WindowsGMSA" + // owner: @bclau + // alpha: v1.16 + // + // Enables support for running container entrypoints as different usernames than their default ones. + WindowsRunAsUserName featuregate.Feature = "WindowsRunAsUserName" + // owner: @adisky // alpha: v1.14 // @@ -520,6 +526,7 @@ var defaultKubernetesFeatureGates = map[featuregate.Feature]featuregate.FeatureS TTLAfterFinished: {Default: false, PreRelease: featuregate.Alpha}, KubeletPodResources: {Default: true, PreRelease: featuregate.Beta}, WindowsGMSA: {Default: false, PreRelease: featuregate.Alpha}, + WindowsRunAsUserName: {Default: false, PreRelease: featuregate.Alpha}, ServiceLoadBalancerFinalizer: {Default: false, PreRelease: featuregate.Alpha}, LocalStorageCapacityIsolationFSQuotaMonitoring: {Default: false, PreRelease: featuregate.Alpha}, NonPreemptingPriority: {Default: false, PreRelease: featuregate.Alpha}, diff --git a/staging/src/k8s.io/api/core/v1/types.go b/staging/src/k8s.io/api/core/v1/types.go index 42573b99e2d..4a6510930ca 100644 --- a/staging/src/k8s.io/api/core/v1/types.go +++ b/staging/src/k8s.io/api/core/v1/types.go @@ -3038,7 +3038,9 @@ type PodSecurityContext struct { // takes precedence for that container. // +optional SELinuxOptions *SELinuxOptions `json:"seLinuxOptions,omitempty" protobuf:"bytes,1,opt,name=seLinuxOptions"` - // Windows security options. + // The Windows specific settings applied to all containers. + // If unspecified, the options within a container's SecurityContext will be used. + // If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. // +optional WindowsOptions *WindowsSecurityContextOptions `json:"windowsOptions,omitempty" protobuf:"bytes,8,opt,name=windowsOptions"` // The UID to run the entrypoint of the container process. @@ -5333,7 +5335,9 @@ type SecurityContext struct { // PodSecurityContext, the value specified in SecurityContext takes precedence. // +optional SELinuxOptions *SELinuxOptions `json:"seLinuxOptions,omitempty" protobuf:"bytes,3,opt,name=seLinuxOptions"` - // Windows security options. + // The Windows specific settings applied to all containers. + // If unspecified, the options from the PodSecurityContext will be used. + // If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. // +optional WindowsOptions *WindowsSecurityContextOptions `json:"windowsOptions,omitempty" protobuf:"bytes,10,opt,name=windowsOptions"` // The UID to run the entrypoint of the container process. @@ -5419,6 +5423,14 @@ type WindowsSecurityContextOptions struct { // This field is alpha-level and is only honored by servers that enable the WindowsGMSA feature flag. // +optional GMSACredentialSpec *string `json:"gmsaCredentialSpec,omitempty" protobuf:"bytes,2,opt,name=gmsaCredentialSpec"` + + // The UserName in Windows to run the entrypoint of the container process. + // Defaults to the user specified in image metadata if unspecified. + // May also be set in PodSecurityContext. If set in both SecurityContext and + // PodSecurityContext, the value specified in SecurityContext takes precedence. + // This field is alpha-level and it is only honored by servers that enable the WindowsRunAsUserName feature flag. + // +optional + RunAsUserName *string `json:"runAsUserName,omitempty" protobuf:"bytes,3,opt,name=runAsUserName"` } // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object