Windows: Adds RunAsUserName field in WindowsOptions

Adds the field RunAsUserName in the WindowsSecurityContextOptions type,
which is used in PodSecurityContext and SecurityContext.

This field needs to allow for a valid set of usernames allowed for
Windows containers. It must have the format "U

This commit also validates the runAsUserName field, making sure that it valid,
having the format DOMAIN\USER (case insensitive), where DOMAIN\ is optional and
has to be a valid NetBios or DNS domain name.

For more information about the restrictions on the DOMAIN and USER parts, look here: [1] [2]

Adds the WindowsRunAsUserName alpha feature gate. By default, it is disabled.
If the feature gate is not enabled, the WindowsOptions.RunAsUserName field
will be dropped from both the PodSecurityContext and container
SecurityContext.

Co-Authored-By: Claudiu Belu <cbelu@cloudbasesolutions.com>

[1] https://support.microsoft.com/en-us/help/909264/naming-conventions-in-active-directory-for-computers-domains-sites-and
[2] https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.localaccounts/new-localuser?view=powershell-5.1
This commit is contained in:
James Sturtevant 2019-01-30 16:09:04 -08:00 committed by Claudiu Belu
parent 5be1efe9bd
commit e8b369ff3c
7 changed files with 485 additions and 4 deletions

View File

@ -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 {

View File

@ -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{

View File

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

View File

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

View File

@ -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 {

View File

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

View File

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