k8s.io/kubernetes@v1.31.0-alpha.0.0.20240520171757-56147500dadc/pkg/controlplane/controller/clusterauthenticationtrust/cluster_authentication_trust_controller.go (about) 1 /* 2 Copyright 2019 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package clusterauthenticationtrust 18 19 import ( 20 "bytes" 21 "context" 22 "crypto/x509" 23 "encoding/json" 24 "encoding/pem" 25 "fmt" 26 "reflect" 27 "strings" 28 "time" 29 30 corev1 "k8s.io/api/core/v1" 31 "k8s.io/apimachinery/pkg/api/equality" 32 apierrors "k8s.io/apimachinery/pkg/api/errors" 33 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 34 utilruntime "k8s.io/apimachinery/pkg/util/runtime" 35 "k8s.io/apimachinery/pkg/util/sets" 36 "k8s.io/apimachinery/pkg/util/wait" 37 "k8s.io/apiserver/pkg/authentication/request/headerrequest" 38 "k8s.io/apiserver/pkg/server/dynamiccertificates" 39 corev1informers "k8s.io/client-go/informers/core/v1" 40 "k8s.io/client-go/kubernetes" 41 corev1client "k8s.io/client-go/kubernetes/typed/core/v1" 42 corev1listers "k8s.io/client-go/listers/core/v1" 43 "k8s.io/client-go/tools/cache" 44 "k8s.io/client-go/util/cert" 45 "k8s.io/client-go/util/workqueue" 46 "k8s.io/klog/v2" 47 ) 48 49 const ( 50 configMapNamespace = "kube-system" 51 configMapName = "extension-apiserver-authentication" 52 ) 53 54 // Controller holds the running state for the controller 55 type Controller struct { 56 requiredAuthenticationData ClusterAuthenticationInfo 57 58 configMapLister corev1listers.ConfigMapLister 59 configMapClient corev1client.ConfigMapsGetter 60 namespaceClient corev1client.NamespacesGetter 61 62 // queue is where incoming work is placed to de-dup and to allow "easy" rate limited requeues on errors. 63 // we only ever place one entry in here, but it is keyed as usual: namespace/name 64 queue workqueue.TypedRateLimitingInterface[string] 65 66 // kubeSystemConfigMapInformer is tracked so that we can start these on Run 67 kubeSystemConfigMapInformer cache.SharedIndexInformer 68 69 // preRunCaches are the caches to sync before starting the work of this control loop 70 preRunCaches []cache.InformerSynced 71 } 72 73 // ClusterAuthenticationInfo holds the information that will included in public configmap. 74 type ClusterAuthenticationInfo struct { 75 // ClientCA is the CA that can be used to verify the identity of normal clients 76 ClientCA dynamiccertificates.CAContentProvider 77 78 // RequestHeaderUsernameHeaders are the headers used by this kube-apiserver to determine username 79 RequestHeaderUsernameHeaders headerrequest.StringSliceProvider 80 // RequestHeaderGroupHeaders are the headers used by this kube-apiserver to determine groups 81 RequestHeaderGroupHeaders headerrequest.StringSliceProvider 82 // RequestHeaderExtraHeaderPrefixes are the headers used by this kube-apiserver to determine user.extra 83 RequestHeaderExtraHeaderPrefixes headerrequest.StringSliceProvider 84 // RequestHeaderAllowedNames are the sujbects allowed to act as a front proxy 85 RequestHeaderAllowedNames headerrequest.StringSliceProvider 86 // RequestHeaderCA is the CA that can be used to verify the front proxy 87 RequestHeaderCA dynamiccertificates.CAContentProvider 88 } 89 90 // NewClusterAuthenticationTrustController returns a controller that will maintain the kube-system configmap/extension-apiserver-authentication 91 // that holds information about how to aggregated apiservers are recommended (but not required) to configure themselves. 92 func NewClusterAuthenticationTrustController(requiredAuthenticationData ClusterAuthenticationInfo, kubeClient kubernetes.Interface) *Controller { 93 // we construct our own informer because we need such a small subset of the information available. Just one namespace. 94 kubeSystemConfigMapInformer := corev1informers.NewConfigMapInformer(kubeClient, configMapNamespace, 12*time.Hour, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}) 95 96 c := &Controller{ 97 requiredAuthenticationData: requiredAuthenticationData, 98 configMapLister: corev1listers.NewConfigMapLister(kubeSystemConfigMapInformer.GetIndexer()), 99 configMapClient: kubeClient.CoreV1(), 100 namespaceClient: kubeClient.CoreV1(), 101 queue: workqueue.NewTypedRateLimitingQueueWithConfig( 102 workqueue.DefaultTypedControllerRateLimiter[string](), 103 workqueue.TypedRateLimitingQueueConfig[string]{Name: "cluster_authentication_trust_controller"}, 104 ), 105 preRunCaches: []cache.InformerSynced{kubeSystemConfigMapInformer.HasSynced}, 106 kubeSystemConfigMapInformer: kubeSystemConfigMapInformer, 107 } 108 109 kubeSystemConfigMapInformer.AddEventHandler(cache.FilteringResourceEventHandler{ 110 FilterFunc: func(obj interface{}) bool { 111 if cast, ok := obj.(*corev1.ConfigMap); ok { 112 return cast.Namespace == configMapNamespace && cast.Name == configMapName 113 } 114 if tombstone, ok := obj.(cache.DeletedFinalStateUnknown); ok { 115 if cast, ok := tombstone.Obj.(*corev1.ConfigMap); ok { 116 return cast.Namespace == configMapNamespace && cast.Name == configMapName 117 } 118 } 119 return true // always return true just in case. The checks are fairly cheap 120 }, 121 Handler: cache.ResourceEventHandlerFuncs{ 122 // we have a filter, so any time we're called, we may as well queue. We only ever check one configmap 123 // so we don't have to be choosy about our key. 124 AddFunc: func(obj interface{}) { 125 c.queue.Add(keyFn()) 126 }, 127 UpdateFunc: func(oldObj, newObj interface{}) { 128 c.queue.Add(keyFn()) 129 }, 130 DeleteFunc: func(obj interface{}) { 131 c.queue.Add(keyFn()) 132 }, 133 }, 134 }) 135 136 return c 137 } 138 139 func (c *Controller) syncConfigMap() error { 140 originalAuthConfigMap, err := c.configMapLister.ConfigMaps(configMapNamespace).Get(configMapName) 141 if apierrors.IsNotFound(err) { 142 originalAuthConfigMap = &corev1.ConfigMap{ 143 ObjectMeta: metav1.ObjectMeta{Namespace: configMapNamespace, Name: configMapName}, 144 } 145 } else if err != nil { 146 return err 147 } 148 // keep the original to diff against later before updating 149 authConfigMap := originalAuthConfigMap.DeepCopy() 150 151 existingAuthenticationInfo, err := getClusterAuthenticationInfoFor(originalAuthConfigMap.Data) 152 if err != nil { 153 return err 154 } 155 combinedInfo, err := combinedClusterAuthenticationInfo(existingAuthenticationInfo, c.requiredAuthenticationData) 156 if err != nil { 157 return err 158 } 159 authConfigMap.Data, err = getConfigMapDataFor(combinedInfo) 160 if err != nil { 161 return err 162 } 163 164 if equality.Semantic.DeepEqual(authConfigMap, originalAuthConfigMap) { 165 klog.V(5).Info("no changes to configmap") 166 return nil 167 } 168 klog.V(2).Infof("writing updated authentication info to %s configmaps/%s", configMapNamespace, configMapName) 169 170 if err := createNamespaceIfNeeded(c.namespaceClient, authConfigMap.Namespace); err != nil { 171 return err 172 } 173 if err := writeConfigMap(c.configMapClient, authConfigMap); err != nil { 174 return err 175 } 176 177 return nil 178 } 179 180 func createNamespaceIfNeeded(nsClient corev1client.NamespacesGetter, ns string) error { 181 if _, err := nsClient.Namespaces().Get(context.TODO(), ns, metav1.GetOptions{}); err == nil { 182 // the namespace already exists 183 return nil 184 } 185 newNs := &corev1.Namespace{ 186 ObjectMeta: metav1.ObjectMeta{ 187 Name: ns, 188 Namespace: "", 189 }, 190 } 191 _, err := nsClient.Namespaces().Create(context.TODO(), newNs, metav1.CreateOptions{}) 192 if err != nil && apierrors.IsAlreadyExists(err) { 193 err = nil 194 } 195 return err 196 } 197 198 func writeConfigMap(configMapClient corev1client.ConfigMapsGetter, required *corev1.ConfigMap) error { 199 _, err := configMapClient.ConfigMaps(required.Namespace).Update(context.TODO(), required, metav1.UpdateOptions{}) 200 if apierrors.IsNotFound(err) { 201 _, err := configMapClient.ConfigMaps(required.Namespace).Create(context.TODO(), required, metav1.CreateOptions{}) 202 return err 203 } 204 205 // If the configmap is too big, clear the entire thing and count on this controller (or another one) to add the correct data back. 206 // We return the original error which causes the controller to re-queue. 207 // Too big means 208 // 1. request is so big the generic request catcher finds it 209 // 2. the content is so large that that the server sends a validation error "Too long: must have at most 1048576 characters" 210 if apierrors.IsRequestEntityTooLargeError(err) || (apierrors.IsInvalid(err) && strings.Contains(err.Error(), "Too long")) { 211 if deleteErr := configMapClient.ConfigMaps(required.Namespace).Delete(context.TODO(), required.Name, metav1.DeleteOptions{}); deleteErr != nil { 212 return deleteErr 213 } 214 return err 215 } 216 217 return err 218 } 219 220 // combinedClusterAuthenticationInfo combines two sets of authentication information into a new one 221 func combinedClusterAuthenticationInfo(lhs, rhs ClusterAuthenticationInfo) (ClusterAuthenticationInfo, error) { 222 ret := ClusterAuthenticationInfo{ 223 RequestHeaderAllowedNames: combineUniqueStringSlices(lhs.RequestHeaderAllowedNames, rhs.RequestHeaderAllowedNames), 224 RequestHeaderExtraHeaderPrefixes: combineUniqueStringSlices(lhs.RequestHeaderExtraHeaderPrefixes, rhs.RequestHeaderExtraHeaderPrefixes), 225 RequestHeaderGroupHeaders: combineUniqueStringSlices(lhs.RequestHeaderGroupHeaders, rhs.RequestHeaderGroupHeaders), 226 RequestHeaderUsernameHeaders: combineUniqueStringSlices(lhs.RequestHeaderUsernameHeaders, rhs.RequestHeaderUsernameHeaders), 227 } 228 229 var err error 230 ret.ClientCA, err = combineCertLists(lhs.ClientCA, rhs.ClientCA) 231 if err != nil { 232 return ClusterAuthenticationInfo{}, err 233 } 234 ret.RequestHeaderCA, err = combineCertLists(lhs.RequestHeaderCA, rhs.RequestHeaderCA) 235 if err != nil { 236 return ClusterAuthenticationInfo{}, err 237 } 238 239 return ret, nil 240 } 241 242 func getConfigMapDataFor(authenticationInfo ClusterAuthenticationInfo) (map[string]string, error) { 243 data := map[string]string{} 244 if authenticationInfo.ClientCA != nil { 245 if caBytes := authenticationInfo.ClientCA.CurrentCABundleContent(); len(caBytes) > 0 { 246 data["client-ca-file"] = string(caBytes) 247 } 248 } 249 250 if authenticationInfo.RequestHeaderCA == nil { 251 return data, nil 252 } 253 254 if caBytes := authenticationInfo.RequestHeaderCA.CurrentCABundleContent(); len(caBytes) > 0 { 255 var err error 256 257 // encoding errors aren't going to get better, so just fail on them. 258 data["requestheader-username-headers"], err = jsonSerializeStringSlice(authenticationInfo.RequestHeaderUsernameHeaders.Value()) 259 if err != nil { 260 return nil, err 261 } 262 data["requestheader-group-headers"], err = jsonSerializeStringSlice(authenticationInfo.RequestHeaderGroupHeaders.Value()) 263 if err != nil { 264 return nil, err 265 } 266 data["requestheader-extra-headers-prefix"], err = jsonSerializeStringSlice(authenticationInfo.RequestHeaderExtraHeaderPrefixes.Value()) 267 if err != nil { 268 return nil, err 269 } 270 271 data["requestheader-client-ca-file"] = string(caBytes) 272 data["requestheader-allowed-names"], err = jsonSerializeStringSlice(authenticationInfo.RequestHeaderAllowedNames.Value()) 273 if err != nil { 274 return nil, err 275 } 276 } 277 278 return data, nil 279 } 280 281 func getClusterAuthenticationInfoFor(data map[string]string) (ClusterAuthenticationInfo, error) { 282 ret := ClusterAuthenticationInfo{} 283 284 var err error 285 ret.RequestHeaderGroupHeaders, err = jsonDeserializeStringSlice(data["requestheader-group-headers"]) 286 if err != nil { 287 return ClusterAuthenticationInfo{}, err 288 } 289 ret.RequestHeaderExtraHeaderPrefixes, err = jsonDeserializeStringSlice(data["requestheader-extra-headers-prefix"]) 290 if err != nil { 291 return ClusterAuthenticationInfo{}, err 292 } 293 ret.RequestHeaderAllowedNames, err = jsonDeserializeStringSlice(data["requestheader-allowed-names"]) 294 if err != nil { 295 return ClusterAuthenticationInfo{}, err 296 } 297 ret.RequestHeaderUsernameHeaders, err = jsonDeserializeStringSlice(data["requestheader-username-headers"]) 298 if err != nil { 299 return ClusterAuthenticationInfo{}, err 300 } 301 302 if caBundle := data["requestheader-client-ca-file"]; len(caBundle) > 0 { 303 ret.RequestHeaderCA, err = dynamiccertificates.NewStaticCAContent("existing", []byte(caBundle)) 304 if err != nil { 305 return ClusterAuthenticationInfo{}, err 306 } 307 } 308 309 if caBundle := data["client-ca-file"]; len(caBundle) > 0 { 310 ret.ClientCA, err = dynamiccertificates.NewStaticCAContent("existing", []byte(caBundle)) 311 if err != nil { 312 return ClusterAuthenticationInfo{}, err 313 } 314 } 315 316 return ret, nil 317 } 318 319 func jsonSerializeStringSlice(in []string) (string, error) { 320 out, err := json.Marshal(in) 321 if err != nil { 322 return "", err 323 } 324 return string(out), err 325 } 326 327 func jsonDeserializeStringSlice(in string) (headerrequest.StringSliceProvider, error) { 328 if len(in) == 0 { 329 return nil, nil 330 } 331 332 out := []string{} 333 if err := json.Unmarshal([]byte(in), &out); err != nil { 334 return nil, err 335 } 336 return headerrequest.StaticStringSlice(out), nil 337 } 338 339 func combineUniqueStringSlices(lhs, rhs headerrequest.StringSliceProvider) headerrequest.StringSliceProvider { 340 ret := []string{} 341 present := sets.String{} 342 343 if lhs != nil { 344 for _, curr := range lhs.Value() { 345 if present.Has(curr) { 346 continue 347 } 348 ret = append(ret, curr) 349 present.Insert(curr) 350 } 351 } 352 353 if rhs != nil { 354 for _, curr := range rhs.Value() { 355 if present.Has(curr) { 356 continue 357 } 358 ret = append(ret, curr) 359 present.Insert(curr) 360 } 361 } 362 363 return headerrequest.StaticStringSlice(ret) 364 } 365 366 func combineCertLists(lhs, rhs dynamiccertificates.CAContentProvider) (dynamiccertificates.CAContentProvider, error) { 367 certificates := []*x509.Certificate{} 368 369 if lhs != nil { 370 lhsCABytes := lhs.CurrentCABundleContent() 371 lhsCAs, err := cert.ParseCertsPEM(lhsCABytes) 372 if err != nil { 373 return nil, err 374 } 375 certificates = append(certificates, lhsCAs...) 376 } 377 if rhs != nil { 378 rhsCABytes := rhs.CurrentCABundleContent() 379 rhsCAs, err := cert.ParseCertsPEM(rhsCABytes) 380 if err != nil { 381 return nil, err 382 } 383 certificates = append(certificates, rhsCAs...) 384 } 385 386 certificates = filterExpiredCerts(certificates...) 387 388 finalCertificates := []*x509.Certificate{} 389 // now check for duplicates. n^2, but super simple 390 for i := range certificates { 391 found := false 392 for j := range finalCertificates { 393 if reflect.DeepEqual(certificates[i].Raw, finalCertificates[j].Raw) { 394 found = true 395 break 396 } 397 } 398 if !found { 399 finalCertificates = append(finalCertificates, certificates[i]) 400 } 401 } 402 403 finalCABytes, err := encodeCertificates(finalCertificates...) 404 if err != nil { 405 return nil, err 406 } 407 408 if len(finalCABytes) == 0 { 409 return nil, nil 410 } 411 // it makes sense for this list to be static because the combination of sources is only used just before writing and 412 // is recalculated 413 return dynamiccertificates.NewStaticCAContent("combined", finalCABytes) 414 } 415 416 // filterExpiredCerts checks are all certificates in the bundle valid, i.e. they have not expired. 417 // The function returns new bundle with only valid certificates or error if no valid certificate is found. 418 // We allow five minutes of slack for NotAfter comparisons 419 func filterExpiredCerts(certs ...*x509.Certificate) []*x509.Certificate { 420 fiveMinutesAgo := time.Now().Add(-5 * time.Minute) 421 422 var validCerts []*x509.Certificate 423 for _, c := range certs { 424 if c.NotAfter.After(fiveMinutesAgo) { 425 validCerts = append(validCerts, c) 426 } 427 } 428 429 return validCerts 430 } 431 432 // Enqueue a method to allow separate control loops to cause the controller to trigger and reconcile content. 433 func (c *Controller) Enqueue() { 434 c.queue.Add(keyFn()) 435 } 436 437 // Run the controller until stopped. 438 func (c *Controller) Run(ctx context.Context, workers int) { 439 defer utilruntime.HandleCrash() 440 // make sure the work queue is shutdown which will trigger workers to end 441 defer c.queue.ShutDown() 442 443 klog.Infof("Starting cluster_authentication_trust_controller controller") 444 defer klog.Infof("Shutting down cluster_authentication_trust_controller controller") 445 446 // we have a personal informer that is narrowly scoped, start it. 447 go c.kubeSystemConfigMapInformer.Run(ctx.Done()) 448 449 // wait for your secondary caches to fill before starting your work 450 if !cache.WaitForNamedCacheSync("cluster_authentication_trust_controller", ctx.Done(), c.preRunCaches...) { 451 return 452 } 453 454 // only run one worker 455 go wait.Until(c.runWorker, time.Second, ctx.Done()) 456 457 // checks are cheap. run once a minute just to be sure we stay in sync in case fsnotify fails again 458 // start timer that rechecks every minute, just in case. this also serves to prime the controller quickly. 459 _ = wait.PollImmediateUntil(1*time.Minute, func() (bool, error) { 460 c.queue.Add(keyFn()) 461 return false, nil 462 }, ctx.Done()) 463 464 // wait until we're told to stop 465 <-ctx.Done() 466 } 467 468 func (c *Controller) runWorker() { 469 // hot loop until we're told to stop. processNextWorkItem will automatically wait until there's work 470 // available, so we don't worry about secondary waits 471 for c.processNextWorkItem() { 472 } 473 } 474 475 // processNextWorkItem deals with one key off the queue. It returns false when it's time to quit. 476 func (c *Controller) processNextWorkItem() bool { 477 // pull the next work item from queue. It should be a key we use to lookup something in a cache 478 key, quit := c.queue.Get() 479 if quit { 480 return false 481 } 482 // you always have to indicate to the queue that you've completed a piece of work 483 defer c.queue.Done(key) 484 485 // do your work on the key. This method will contains your "do stuff" logic 486 err := c.syncConfigMap() 487 if err == nil { 488 // if you had no error, tell the queue to stop tracking history for your key. This will 489 // reset things like failure counts for per-item rate limiting 490 c.queue.Forget(key) 491 return true 492 } 493 494 // there was a failure so be sure to report it. This method allows for pluggable error handling 495 // which can be used for things like cluster-monitoring 496 utilruntime.HandleError(fmt.Errorf("%v failed with : %v", key, err)) 497 // since we failed, we should requeue the item to work on later. This method will add a backoff 498 // to avoid hotlooping on particular items (they're probably still not going to work right away) 499 // and overall controller protection (everything I've done is broken, this controller needs to 500 // calm down or it can starve other useful work) cases. 501 c.queue.AddRateLimited(key) 502 503 return true 504 } 505 506 func keyFn() string { 507 // this format matches DeletionHandlingMetaNamespaceKeyFunc for our single key 508 return configMapNamespace + "/" + configMapName 509 } 510 511 func encodeCertificates(certs ...*x509.Certificate) ([]byte, error) { 512 b := bytes.Buffer{} 513 for _, cert := range certs { 514 if err := pem.Encode(&b, &pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw}); err != nil { 515 return []byte{}, err 516 } 517 } 518 return b.Bytes(), nil 519 }