mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-30 15:05:27 +00:00
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:
parent
5be1efe9bd
commit
e8b369ff3c
@ -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 {
|
||||
|
@ -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{
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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},
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user