/* Copyright 2022 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package ctbattest import ( "context" "fmt" "io" "k8s.io/apiserver/pkg/admission" genericadmissioninit "k8s.io/apiserver/pkg/admission/initializer" "k8s.io/apiserver/pkg/authorization/authorizer" "k8s.io/component-base/featuregate" "k8s.io/klog/v2" api "k8s.io/kubernetes/pkg/apis/certificates" kapihelper "k8s.io/kubernetes/pkg/apis/core/helper" "k8s.io/kubernetes/pkg/features" "k8s.io/kubernetes/pkg/registry/rbac" "k8s.io/kubernetes/plugin/pkg/admission/certificates" ) const PluginName = "ClusterTrustBundleAttest" func Register(plugins *admission.Plugins) { plugins.Register(PluginName, func(config io.Reader) (admission.Interface, error) { return NewPlugin(), nil }) } // Plugin is the ClusterTrustBundle attest plugin. // // In order to create or update a ClusterTrustBundle that sets signerName, // you must have the following permission: group=certificates.k8s.io // resource=signers resourceName= verb=attest. type Plugin struct { *admission.Handler authz authorizer.Authorizer inspectedFeatureGates bool enabled bool } var _ admission.ValidationInterface = &Plugin{} var _ admission.InitializationValidator = &Plugin{} var _ genericadmissioninit.WantsAuthorizer = &Plugin{} var _ genericadmissioninit.WantsFeatures = &Plugin{} func NewPlugin() *Plugin { return &Plugin{ Handler: admission.NewHandler(admission.Create, admission.Update), } } // SetAuthorizer sets the plugin's authorizer. func (p *Plugin) SetAuthorizer(authz authorizer.Authorizer) { p.authz = authz } // InspectFeatureGates implements WantsFeatures. func (p *Plugin) InspectFeatureGates(featureGates featuregate.FeatureGate) { p.enabled = featureGates.Enabled(features.ClusterTrustBundle) p.inspectedFeatureGates = true } // ValidateInitialization checks that the plugin was initialized correctly. func (p *Plugin) ValidateInitialization() error { if p.authz == nil { return fmt.Errorf("%s requires an authorizer", PluginName) } if !p.inspectedFeatureGates { return fmt.Errorf("%s did not see feature gates", PluginName) } return nil } var clusterTrustBundleGroupResource = api.Resource("clustertrustbundles") func (p *Plugin) Validate(ctx context.Context, a admission.Attributes, _ admission.ObjectInterfaces) error { if !p.enabled { return nil } if a.GetResource().GroupResource() != clusterTrustBundleGroupResource { return nil } newBundle, ok := a.GetObject().(*api.ClusterTrustBundle) if !ok { return admission.NewForbidden(a, fmt.Errorf("expected type ClusterTrustBundle, got: %T", a.GetOldObject())) } // Unlike CSRs, it's OK to validate against the *new* object, because // updates to signer name will be rejected during validation. // If signer name isn't specified, we don't need to perform the // attest check. if newBundle.Spec.SignerName == "" { return nil } // Skip the attest check when the semantics of the bundle are unchanged to support storage migration and GC workflows if a.GetOperation() == admission.Update && rbac.IsOnlyMutatingGCFields(a.GetObject(), a.GetOldObject(), kapihelper.Semantic) { return nil } if !certificates.IsAuthorizedForSignerName(ctx, p.authz, a.GetUserInfo(), "attest", newBundle.Spec.SignerName) { klog.V(4).Infof("user not permitted to attest ClusterTrustBundle %q with signerName %q", newBundle.Name, newBundle.Spec.SignerName) return admission.NewForbidden(a, fmt.Errorf("user not permitted to attest for signerName %q", newBundle.Spec.SignerName)) } return nil }