diff --git a/go.mod b/go.mod index ef71d0060328e..961479aa76d63 100644 --- a/go.mod +++ b/go.mod @@ -234,6 +234,7 @@ require ( replace ( github.com/onsi/ginkgo/v2 => github.com/openshift/onsi-ginkgo/v2 v2.6.1-0.20241205171354-8006f302fd12 + github.com/openshift/apiserver-library-go => github.com/openshift/apiserver-library-go v0.0.0-20250502092109-8d341e94467d k8s.io/api => ./staging/src/k8s.io/api k8s.io/apiextensions-apiserver => ./staging/src/k8s.io/apiextensions-apiserver k8s.io/apimachinery => ./staging/src/k8s.io/apimachinery diff --git a/go.sum b/go.sum index 3c56b908dc7fd..5f819f7a35b74 100644 --- a/go.sum +++ b/go.sum @@ -429,8 +429,8 @@ github.com/openshift-eng/openshift-tests-extension v0.0.0-20250220212757-b9c4d98 github.com/openshift-eng/openshift-tests-extension v0.0.0-20250220212757-b9c4d98a0c45/go.mod h1:6gkP5f2HL0meusT0Aim8icAspcD1cG055xxBZ9yC68M= github.com/openshift/api v0.0.0-20250129162653-107848b719c5 h1:PzJJmofv/P9R1JUxO8X6tAMxKACVS6Quxo/xBzMkSmI= github.com/openshift/api v0.0.0-20250129162653-107848b719c5/go.mod h1:yk60tHAmHhtVpJQo3TwVYq2zpuP70iJIFDCmeKMIzPw= -github.com/openshift/apiserver-library-go v0.0.0-20250127121756-dc9a973f14ce h1:w0Up6YV1APcn20v/1h5IfuToz96o2pVqZyjzbw0yotU= -github.com/openshift/apiserver-library-go v0.0.0-20250127121756-dc9a973f14ce/go.mod h1:kkSwH4osgejnRIyHfsfkv0V0xfmgH4Yk/VDObaJukHU= +github.com/openshift/apiserver-library-go v0.0.0-20250502092109-8d341e94467d h1:M/oS6u+K6AYb2pkQJWsnwfts8R85sPJDbB0wgv7KKKo= +github.com/openshift/apiserver-library-go v0.0.0-20250502092109-8d341e94467d/go.mod h1:kkSwH4osgejnRIyHfsfkv0V0xfmgH4Yk/VDObaJukHU= github.com/openshift/build-machinery-go v0.0.0-20250102153059-e85a1a7ecb5c/go.mod h1:8jcm8UPtg2mCAsxfqKil1xrmRMI3a+XU2TZ9fF8A7TE= github.com/openshift/client-go v0.0.0-20250125113824-8e1f0b8fa9a7 h1:4iliLcvr1P9EUMZgIaSNEKNQQzBn+L6PSequlFOuB6Q= github.com/openshift/client-go v0.0.0-20250125113824-8e1f0b8fa9a7/go.mod h1:2tcufBE4Cu6RNgDCxcUJepa530kGo5GFVfR9BSnndhI= diff --git a/vendor/github.com/openshift/apiserver-library-go/pkg/securitycontextconstraints/sccadmission/admission.go b/vendor/github.com/openshift/apiserver-library-go/pkg/securitycontextconstraints/sccadmission/admission.go index 701d978f7ca45..02ceaacd4026e 100644 --- a/vendor/github.com/openshift/apiserver-library-go/pkg/securitycontextconstraints/sccadmission/admission.go +++ b/vendor/github.com/openshift/apiserver-library-go/pkg/securitycontextconstraints/sccadmission/admission.go @@ -17,8 +17,6 @@ import ( "k8s.io/apimachinery/pkg/util/wait" "k8s.io/apiserver/pkg/admission" "k8s.io/apiserver/pkg/admission/initializer" - "k8s.io/apiserver/pkg/authentication/serviceaccount" - "k8s.io/apiserver/pkg/authentication/user" "k8s.io/apiserver/pkg/authorization/authorizer" "k8s.io/client-go/informers" corev1listers "k8s.io/client-go/listers/core/v1" @@ -103,7 +101,7 @@ func (c *constraint) Admit(ctx context.Context, a admission.Attributes, _ admiss specMutationAllowed := a.GetOperation() == admission.Create ephemeralContainersMutationAllowed := specMutationAllowed || (a.GetOperation() == admission.Update && a.GetSubresource() == "ephemeralcontainers") - allowedPod, sccName, validationErrs, err := c.computeSecurityContext(ctx, a, pod, specMutationAllowed, ephemeralContainersMutationAllowed, pod.ObjectMeta.Annotations[securityv1.RequiredSCCAnnotation], "") + allowedPod, sccAnnotations, validationErrs, err := c.computeSecurityContext(ctx, a, pod, specMutationAllowed, ephemeralContainersMutationAllowed, pod.ObjectMeta.Annotations[securityv1.RequiredSCCAnnotation], "") if err != nil { return admission.NewForbidden(a, err) } @@ -111,11 +109,19 @@ func (c *constraint) Admit(ctx context.Context, a admission.Attributes, _ admiss if allowedPod != nil { *pod = *allowedPod // annotate and accept the pod - klog.V(4).Infof("pod %s (generate: %s) validated against provider %s", pod.Name, pod.GenerateName, sccName) + klog.V(4).Infof( + "pod %s (generate: %s) validated against provider %s", + pod.Name, pod.GenerateName, sccAnnotations[securityv1.ValidatedSCCAnnotation], + ) + if pod.ObjectMeta.Annotations == nil { pod.ObjectMeta.Annotations = map[string]string{} } - pod.ObjectMeta.Annotations[securityv1.ValidatedSCCAnnotation] = sccName + + for key, value := range sccAnnotations { + pod.ObjectMeta.Annotations[key] = value + } + return nil } @@ -188,115 +194,44 @@ func requireStandardSCCs(sccs []*securityv1.SecurityContextConstraints, err erro return fmt.Errorf("securitycontextconstraints.security.openshift.io cache is missing %v", strings.Join(missingSCCs.List(), ", ")) } -func (c *constraint) areListersSynced() bool { - for _, syncFunc := range c.listersSynced { - if !syncFunc() { - return false - } - } - return true -} - func (c *constraint) computeSecurityContext( ctx context.Context, a admission.Attributes, pod *coreapi.Pod, specMutationAllowed, ephemeralContainersMutationAllowed bool, requiredSCCName, validatedSCCHint string, -) (*coreapi.Pod, string, field.ErrorList, error) { +) (*coreapi.Pod, map[string]string, field.ErrorList, error) { // get all constraints that are usable by the user klog.V(4).Infof("getting security context constraints for pod %s (generate: %s) in namespace %s with user info %v", pod.Name, pod.GenerateName, a.GetNamespace(), a.GetUserInfo()) - err := wait.PollImmediateWithContext(ctx, 1*time.Second, 10*time.Second, func(context.Context) (bool, error) { - return c.areListersSynced(), nil - }) - if err != nil { - return nil, "", nil, admission.NewForbidden(a, fmt.Errorf("securitycontextconstraints.security.openshift.io cache is not synchronized")) + if err := c.waitForReadyState(ctx); err != nil { + return nil, nil, nil, admission.NewForbidden(a, err) } - // wait a few seconds until the synchronized list returns all the required SCCs created by the kas-o. - // If this doesn't happen, then indicate which ones are missing. This seems odd, but our CI system suggests that this happens occasionally. - // If the SCCs were all deleted, then no pod will pass SCC admission until the SCCs are recreated, but the kas-o (which recreates them) - // bypasses SCC admission, so this does not create a cycle. - var requiredSCCErr error - err = wait.PollImmediateWithContext(ctx, 1*time.Second, 10*time.Second, func(context.Context) (bool, error) { - if requiredSCCErr = requireStandardSCCs(c.sccLister.List(labels.Everything())); requiredSCCErr != nil { - return false, nil - } - return true, nil - }) + constraints, err := c.listSortedSCCs(requiredSCCName, validatedSCCHint, specMutationAllowed) if err != nil { - if requiredSCCErr != nil { - return nil, "", nil, admission.NewForbidden(a, requiredSCCErr) - } - return nil, "", nil, admission.NewForbidden(a, fmt.Errorf("securitycontextconstraints.security.openshift.io required check failed oddly")) + return nil, nil, nil, admission.NewForbidden(a, err) } - var constraints []*securityv1.SecurityContextConstraints - if len(requiredSCCName) > 0 { - requiredSCC, err := c.sccLister.Get(requiredSCCName) - if err != nil { - return nil, "", nil, admission.NewForbidden(a, fmt.Errorf("failed to retrieve the required SCC %q: %w", requiredSCCName, err)) - } - constraints = []*securityv1.SecurityContextConstraints{requiredSCC} - } else { - constraints, err = c.sccLister.List(labels.Everything()) - if err != nil { - return nil, "", nil, admission.NewForbidden(a, err) - } - } - - if len(constraints) == 0 { - return nil, "", nil, admission.NewForbidden(a, fmt.Errorf("no SecurityContextConstraints found in cluster")) - } - sort.Sort(sccsort.ByPriority(constraints)) - - // If mutation is not allowed and validatedSCCHint is provided, check the validated policy first. - // Keep the order the same for everything else - sort.SliceStable(constraints, func(i, j int) bool { - // disregard the ephemeral containers here, the rest of the pod should still - // not get mutated and so we are primarily interested in the SCC that matched previously - if !specMutationAllowed { - if constraints[i].Name == validatedSCCHint { - return true - } - if constraints[j].Name == validatedSCCHint { - return false - } - } - return i < j - }) - providers, errs := sccmatching.CreateProvidersFromConstraints(ctx, a.GetNamespace(), constraints, c.namespaceLister) logProviders(pod, providers, errs) if len(errs) > 0 { - return nil, "", nil, kutilerrors.NewAggregate(errs) + return nil, nil, nil, kutilerrors.NewAggregate(errs) } if len(providers) == 0 { - return nil, "", nil, admission.NewForbidden(a, fmt.Errorf("no SecurityContextConstraintsProvider available to validate pod request")) + return nil, nil, nil, admission.NewForbidden(a, fmt.Errorf("no SecurityContextConstraintsProvider available to validate pod request")) } // all containers in a single pod must validate under a single provider or we will reject the request var ( allowedPod *coreapi.Pod allowingProvider sccmatching.SecurityContextConstraintsProvider + allowedForType string validationErrs field.ErrorList - saUserInfo user.Info ) - userInfo := a.GetUserInfo() - if len(pod.Spec.ServiceAccountName) > 0 { - saUserInfo = serviceaccount.UserInfo(a.GetNamespace(), pod.Spec.ServiceAccountName, "") - } - - allowedForUserOrSA := func(provider sccmatching.SecurityContextConstraintsProvider) bool { - sccName := provider.GetSCCName() - sccUsers := provider.GetSCCUsers() - sccGroups := provider.GetSCCGroups() - return sccmatching.ConstraintAppliesTo(ctx, sccName, sccUsers, sccGroups, userInfo, a.GetNamespace(), c.authorizer) || - (saUserInfo != nil && sccmatching.ConstraintAppliesTo(ctx, sccName, sccUsers, sccGroups, saUserInfo, a.GetNamespace(), c.authorizer)) - } + sccChecker := newSCCAuthorizerChecker(c.authorizer, a, pod.Spec.ServiceAccountName) appliesToPod := func(provider sccmatching.SecurityContextConstraintsProvider, pod *coreapi.Pod) (podCopy *coreapi.Pod, errs field.ErrorList) { podCopy = pod.DeepCopy() @@ -323,7 +258,8 @@ loop: restrictedV2SCCProvider = providers[i] } - if !allowedForUserOrSA(provider) { + currentType := sccChecker.allowedForType(ctx, provider) + if currentType == allowedForNone { denied = append(denied, provider.GetSCCName()) // this will cause every security context constraint attempted, in order, to the failure validationErrs = append(validationErrs, @@ -351,6 +287,7 @@ loop: // even on creating. We prefer most restrictive SCC in this case even if it mutates a pod. allowedPod = podCopy allowingProvider = provider + allowedForType = currentType klog.V(5).Infof("pod %s (generate: %s) validated against provider %s with mutation", pod.Name, pod.GenerateName, provider.GetSCCName()) break loop case ephemeralContainersMutationAllowed: @@ -360,6 +297,7 @@ loop: if apiequality.Semantic.DeepEqual(pod, podCopyCopy) { allowedPod = podCopy allowingProvider = provider + allowedForType = currentType klog.V(5).Infof("pod %s (generate: %s) validated against provider %s with ephemeralContainers mutation", pod.Name, pod.GenerateName, provider.GetSCCName()) break loop } @@ -369,6 +307,7 @@ loop: // if we don't allow mutation, only use the validated pod if it didn't require any spec changes allowedPod = podCopy allowingProvider = provider + allowedForType = currentType klog.V(5).Infof("pod %s (generate: %s) validated against provider %s without mutation", pod.Name, pod.GenerateName, provider.GetSCCName()) break loop default: @@ -402,7 +341,7 @@ loop: // find next provider that was not chosen var nextNotChosenProvider sccmatching.SecurityContextConstraintsProvider for _, provider := range providers[i+1:] { - if !allowedForUserOrSA(provider) { + if sccChecker.allowedForType(ctx, provider) == allowedForNone { continue } if _, errs := appliesToPod(provider, pod); len(errs) == 0 { @@ -450,7 +389,7 @@ loop: } if allowedPod == nil || allowingProvider == nil { - return nil, "", validationErrs, nil + return nil, nil, validationErrs, nil } if !specMutationAllowed { @@ -458,7 +397,12 @@ loop: a.AddAnnotation("securitycontextconstraints.admission.openshift.io/chosen", allowingProvider.GetSCCName()) } - return allowedPod, allowingProvider.GetSCCName(), validationErrs, nil + podAnnotations := map[string]string{ + securityv1.ValidatedSCCAnnotation: allowingProvider.GetSCCName(), + "security.openshift.io/validated-scc-subject-type": allowedForType, + } + + return allowedPod, podAnnotations, validationErrs, nil } var ignoredSubresources = sets.NewString( @@ -589,6 +533,96 @@ func (c *constraint) ValidateInitialization() error { return nil } +func (c *constraint) listSortedSCCs( + requiredSCCName, validatedSCCHint string, + specMutationAllowed bool, +) ([]*securityv1.SecurityContextConstraints, error) { + var err error + var constraints []*securityv1.SecurityContextConstraints + + if len(requiredSCCName) > 0 { + requiredSCC, err := c.sccLister.Get(requiredSCCName) + if err != nil { + return nil, fmt.Errorf("failed to retrieve the required SCC %q: %w", requiredSCCName, err) + } + constraints = []*securityv1.SecurityContextConstraints{requiredSCC} + } else { + constraints, err = c.sccLister.List(labels.Everything()) + if err != nil { + return nil, err + } + } + + if len(constraints) == 0 { + return nil, fmt.Errorf("no SecurityContextConstraints found in cluster") + } + + sort.Sort(sccsort.ByPriority(constraints)) + + if specMutationAllowed { + return constraints, nil + } + + // If mutation is not allowed and validatedSCCHint is provided, check the validated policy first. + // Keep the order the same for everything else + sort.SliceStable(constraints, func(i, j int) bool { + // disregard the ephemeral containers here, the rest of the pod should still + // not get mutated and so we are primarily interested in the SCC that matched previously + if constraints[i].Name == validatedSCCHint { + return true + } + if constraints[j].Name == validatedSCCHint { + return false + } + return i < j + }) + + return constraints, nil +} + +// waitForReadyState ensures the admission controller has a complete and +// consistent view of SCCs before making admission decisions. It first waits for +// the internal cache to sync, then verifies all standard SCCs are present. +func (c *constraint) waitForReadyState(ctx context.Context) error { + const ( + interval = 1 * time.Second + timeout = 10 * time.Second + immediate = true + ) + + err := wait.PollUntilContextTimeout(ctx, interval, timeout, immediate, func(ctx context.Context) (bool, error) { + for _, syncFunc := range c.listersSynced { + if !syncFunc() { + return false, nil + } + } + return true, nil + }) + if err != nil { + return fmt.Errorf("securitycontextconstraints.security.openshift.io cache is not synchronized") + } + + // wait a few seconds until the synchronized list returns all the required SCCs created by the kas-o. + // If this doesn't happen, then indicate which ones are missing. This seems odd, but our CI system suggests that this happens occasionally. + // If the SCCs were all deleted, then no pod will pass SCC admission until the SCCs are recreated, but the kas-o (which recreates them) + // bypasses SCC admission, so this does not create a cycle. + var requiredSCCErr error + err = wait.PollUntilContextTimeout(ctx, interval, timeout, immediate, func(context.Context) (bool, error) { + if requiredSCCErr = requireStandardSCCs(c.sccLister.List(labels.Everything())); requiredSCCErr != nil { + return false, nil + } + return true, nil + }) + if err != nil { + if requiredSCCErr != nil { + return requiredSCCErr + } + return fmt.Errorf("securitycontextconstraints.security.openshift.io required check failed oddly") + } + + return nil +} + // logProviders logs what providers were found for the pod as well as any errors that were encountered // while creating providers. func logProviders(pod *coreapi.Pod, providers []sccmatching.SecurityContextConstraintsProvider, providerCreationErrs []error) { diff --git a/vendor/github.com/openshift/apiserver-library-go/pkg/securitycontextconstraints/sccadmission/scc_authz_check.go b/vendor/github.com/openshift/apiserver-library-go/pkg/securitycontextconstraints/sccadmission/scc_authz_check.go new file mode 100644 index 0000000000000..623b568a3af49 --- /dev/null +++ b/vendor/github.com/openshift/apiserver-library-go/pkg/securitycontextconstraints/sccadmission/scc_authz_check.go @@ -0,0 +1,62 @@ +package sccadmission + +import ( + "context" + + "k8s.io/apiserver/pkg/admission" + "k8s.io/apiserver/pkg/authentication/serviceaccount" + "k8s.io/apiserver/pkg/authentication/user" + "k8s.io/apiserver/pkg/authorization/authorizer" + + "github.com/openshift/apiserver-library-go/pkg/securitycontextconstraints/sccmatching" +) + +const ( + allowedForUser = "user" + allowedForSA = "serviceaccount" + allowedForNone = "none" +) + +type sccAuthorizationChecker struct { + authz authorizer.Authorizer + userInfo user.Info + namespace string + serviceAccountName string +} + +func newSCCAuthorizerChecker(authz authorizer.Authorizer, attr admission.Attributes, serviceAccountName string) *sccAuthorizationChecker { + return &sccAuthorizationChecker{ + authz: authz, + userInfo: attr.GetUserInfo(), + namespace: attr.GetNamespace(), + serviceAccountName: serviceAccountName, + } +} + +func (c *sccAuthorizationChecker) allowedForType(ctx context.Context, provider sccmatching.SecurityContextConstraintsProvider) string { + sccName := provider.GetSCCName() + sccUsers := provider.GetSCCUsers() + sccGroups := provider.GetSCCGroups() + + if len(c.serviceAccountName) != 0 { + saUserInfo := serviceaccount.UserInfo(c.namespace, c.serviceAccountName, "") + + if sccmatching.ConstraintAppliesTo( + ctx, + sccName, sccUsers, sccGroups, + saUserInfo, c.namespace, c.authz, + ) { + return allowedForSA + } + } + + if sccmatching.ConstraintAppliesTo( + ctx, + sccName, sccUsers, sccGroups, + c.userInfo, c.namespace, c.authz, + ) { + return allowedForUser + } + + return allowedForNone +} diff --git a/vendor/modules.txt b/vendor/modules.txt index c0da386bec691..4b286a9c84572 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -530,7 +530,7 @@ github.com/openshift/api/security github.com/openshift/api/security/v1 github.com/openshift/api/template/v1 github.com/openshift/api/user/v1 -# github.com/openshift/apiserver-library-go v0.0.0-20250127121756-dc9a973f14ce +# github.com/openshift/apiserver-library-go v0.0.0-20250127121756-dc9a973f14ce => github.com/openshift/apiserver-library-go v0.0.0-20250502092109-8d341e94467d ## explicit; go 1.23.0 github.com/openshift/apiserver-library-go/pkg/admission/imagepolicy github.com/openshift/apiserver-library-go/pkg/admission/imagepolicy/apis/imagepolicy/v1 @@ -1524,3 +1524,4 @@ sigs.k8s.io/yaml sigs.k8s.io/yaml/goyaml.v2 sigs.k8s.io/yaml/goyaml.v3 # github.com/onsi/ginkgo/v2 => github.com/openshift/onsi-ginkgo/v2 v2.6.1-0.20241205171354-8006f302fd12 +# github.com/openshift/apiserver-library-go => github.com/openshift/apiserver-library-go v0.0.0-20250502092109-8d341e94467d