diff --git a/cmd/kube-controller-manager/app/options/options.go b/cmd/kube-controller-manager/app/options/options.go index 4ff4dbb34d0d8..8009ada9ab040 100644 --- a/cmd/kube-controller-manager/app/options/options.go +++ b/cmd/kube-controller-manager/app/options/options.go @@ -185,7 +185,7 @@ func (s *CMServer) AddFlags(fs *pflag.FlagSet, allControllers []string, disabled fs.StringVar(&s.ServiceAccountKeyFile, "service-account-private-key-file", s.ServiceAccountKeyFile, "Filename containing a PEM-encoded private RSA or ECDSA key used to sign service account tokens.") fs.StringVar(&s.ClusterSigningCertFile, "cluster-signing-cert-file", s.ClusterSigningCertFile, "Filename containing a PEM-encoded X509 CA certificate used to issue cluster-scoped certificates") fs.StringVar(&s.ClusterSigningKeyFile, "cluster-signing-key-file", s.ClusterSigningKeyFile, "Filename containing a PEM-encoded RSA or ECDSA private key used to sign cluster-scoped certificates") - fs.StringVar(&s.ApproveAllKubeletCSRsForGroup, "insecure-experimental-approve-all-kubelet-csrs-for-group", s.ApproveAllKubeletCSRsForGroup, "The group for which the controller-manager will auto approve all CSRs for kubelet client certificates.") + fs.BoolVar(&s.EnableKubeletCSRApprover, "enable-kubelet-csr-approver", s.EnableKubeletCSRApprover, "Enable the kubelet CSR approver which approves kubelet CSRs based on subject access reviews") fs.BoolVar(&s.EnableProfiling, "profiling", true, "Enable profiling via web interface host:port/debug/pprof/") fs.BoolVar(&s.EnableContentionProfiling, "contention-profiling", false, "Enable lock contention profiling, if profiling is enabled") fs.StringVar(&s.ClusterName, "cluster-name", s.ClusterName, "The instance prefix for the cluster") diff --git a/pkg/apis/componentconfig/types.go b/pkg/apis/componentconfig/types.go index 1b619003ccd75..4c7a81cba69cc 100644 --- a/pkg/apis/componentconfig/types.go +++ b/pkg/apis/componentconfig/types.go @@ -828,12 +828,9 @@ type KubeControllerManagerConfiguration struct { // clusterSigningCertFile is the filename containing a PEM-encoded // RSA or ECDSA private key used to issue cluster-scoped certificates ClusterSigningKeyFile string - // approveAllKubeletCSRs tells the CSR controller to approve all CSRs originating - // from the kubelet bootstrapping group automatically. - // WARNING: this grants all users with access to the certificates API group - // the ability to create credentials for any user that has access to the boostrapping - // user's credentials. - ApproveAllKubeletCSRsForGroup string + // enableKubeletCSRApprover tells the CSR controller to approve + // kubelet CSRs based on SubjectAccessReviews. + EnableKubeletCSRApprover bool // enableProfiling enables profiling via web interface host:port/debug/pprof/ EnableProfiling bool // enableContentionProfiling enables lock contention profiling, if enableProfiling is true. diff --git a/pkg/controller/certificates/approver/BUILD b/pkg/controller/certificates/approver/BUILD index fa8cdacd04b4a..e5749d0115205 100644 --- a/pkg/controller/certificates/approver/BUILD +++ b/pkg/controller/certificates/approver/BUILD @@ -21,6 +21,7 @@ go_library( srcs = ["groupapprove.go"], tags = ["automanaged"], deps = [ + "//pkg/apis/authorization/v1beta1:go_default_library", "//pkg/apis/certificates/v1beta1:go_default_library", "//pkg/client/clientset_generated/clientset:go_default_library", "//pkg/client/informers/informers_generated/externalversions/certificates/v1beta1:go_default_library", diff --git a/pkg/controller/certificates/approver/groupapprove.go b/pkg/controller/certificates/approver/groupapprove.go index 0a9a56de33670..aee97800a560c 100644 --- a/pkg/controller/certificates/approver/groupapprove.go +++ b/pkg/controller/certificates/approver/groupapprove.go @@ -22,19 +22,21 @@ import ( "strings" utilruntime "k8s.io/apimachinery/pkg/util/runtime" + authorizationapi "k8s.io/kubernetes/pkg/apis/authorization/v1beta1" capi "k8s.io/kubernetes/pkg/apis/certificates/v1beta1" "k8s.io/kubernetes/pkg/client/clientset_generated/clientset" certificatesinformers "k8s.io/kubernetes/pkg/client/informers/informers_generated/externalversions/certificates/v1beta1" "k8s.io/kubernetes/pkg/controller/certificates" ) -func NewCSRApprovingController( - client clientset.Interface, - csrInformer certificatesinformers.CertificateSigningRequestInformer, - approveAllKubeletCSRsForGroup string, -) (*certificates.CertificateController, error) { +const ( + selfNodeClientCertAccess = "certificatessigningrequests/selfnodeclientcert" + nodeClientCertAccess = "certificatessigningrequests/nodeclientcert" + nodeServerCertAccess = "certificatessigningrequests/nodeservercert" +) + +func NewCSRApprovingController(client clientset.Interface, csrInformer certificatesinformers.CertificateSigningRequestInformer) (*certificates.CertificateController, error) { approver := &groupApprover{ - approveAllKubeletCSRsForGroup: approveAllKubeletCSRsForGroup, client: client, } return certificates.NewCertificateController( @@ -46,8 +48,7 @@ func NewCSRApprovingController( // groupApprover implements AutoApprover for signing Kubelet certificates. type groupApprover struct { - approveAllKubeletCSRsForGroup string - client clientset.Interface + client clientset.Interface } func (ga *groupApprover) handle(csr *capi.CertificateSigningRequest) error { @@ -67,17 +68,6 @@ func (ga *groupApprover) handle(csr *capi.CertificateSigningRequest) error { } func (cc *groupApprover) autoApprove(csr *capi.CertificateSigningRequest) (*capi.CertificateSigningRequest, error) { - isKubeletBootstrapGroup := false - for _, g := range csr.Spec.Groups { - if g == cc.approveAllKubeletCSRsForGroup { - isKubeletBootstrapGroup = true - break - } - } - if !isKubeletBootstrapGroup { - return csr, nil - } - x509cr, err := capi.ParseCSR(csr) if err != nil { utilruntime.HandleError(fmt.Errorf("unable to parse csr %q: %v", csr.Name, err)) @@ -86,22 +76,77 @@ func (cc *groupApprover) autoApprove(csr *capi.CertificateSigningRequest) (*capi if !reflect.DeepEqual([]string{"system:nodes"}, x509cr.Subject.Organization) { return csr, nil } - if !strings.HasPrefix(x509cr.Subject.CommonName, "system:node:") { - return csr, nil - } if len(x509cr.DNSNames)+len(x509cr.EmailAddresses)+len(x509cr.IPAddresses) != 0 { return csr, nil } - if !hasExactUsages(csr, kubeletClientUsages) { - return csr, nil + if hasExactUsages(csr, kubeletClientUsages) { + if !strings.HasPrefix(x509cr.Subject.CommonName, "system:node:") { + return csr, nil + } + // check kubelet up for client cert renewal + if csr.Spec.Username == x509cr.Subject.CommonName { + approve, err := cc.doAccessReview(csr, selfNodeClientCertAccess) + if err != nil { + return nil, err + } + if approve { + approveWithMessage(csr, "Auto approving of all kubelet client certificate rotation after SAR.") + return csr, nil + } + } + // check kubelet client cret bootstrapper + approve, err := cc.doAccessReview(csr, nodeClientCertAccess) + if err != nil { + return nil, err + } + if approve { + approveWithMessage(csr, "Auto approving of all kubelet client certificate rotation after SAR.") + return csr, nil + } + } + if hasExactUsages(csr, kubeletServerUsages) { + if "system:node:"+x509cr.Subject.CommonName != csr.Spec.Username { + return csr, nil + } + // check kubelet creating server cert + approve, err := cc.doAccessReview(csr, "certificatessigningrequests/nodeservercert") + if err != nil { + return nil, err + } + if approve { + approveWithMessage(csr, "Auto approving of all kubelet client certificate rotation after SAR.") + return csr, nil + } + } + return csr, nil +} + +func (cc *groupApprover) doAccessReview(csr *capi.CertificateSigningRequest, verb string) (bool, error) { + extra := make(map[string]authorizationapi.ExtraValue) + for k, v := range csr.Spec.Extra { + extra[k] = authorizationapi.ExtraValue(v) + } + + sar := &authorizationapi.SubjectAccessReview{ + Spec: authorizationapi.SubjectAccessReviewSpec{ + User: csr.Spec.Username, + Groups: csr.Spec.Groups, + Extra: extra, + }, } + sar, err := cc.client.AuthorizationV1beta1().SubjectAccessReviews().Create(sar) + if err != nil { + return false, err + } + return sar.Status.Allowed, nil +} +func approveWithMessage(csr *capi.CertificateSigningRequest, message string) { csr.Status.Conditions = append(csr.Status.Conditions, capi.CertificateSigningRequestCondition{ Type: capi.CertificateApproved, Reason: "AutoApproved", Message: "Auto approving of all kubelet CSRs is enabled on the controller manager", }) - return csr, nil } var kubeletClientUsages = []capi.KeyUsage{ @@ -110,6 +155,12 @@ var kubeletClientUsages = []capi.KeyUsage{ capi.UsageClientAuth, } +var kubeletServerUsages = []capi.KeyUsage{ + capi.UsageKeyEncipherment, + capi.UsageDigitalSignature, + capi.UsageServerAuth, +} + func hasExactUsages(csr *capi.CertificateSigningRequest, usages []capi.KeyUsage) bool { if len(usages) != len(csr.Spec.Usages) { return false