k8s.io/kubernetes@v1.31.0-alpha.0.0.20240520171757-56147500dadc/plugin/pkg/admission/serviceaccount/admission.go (about) 1 /* 2 Copyright 2014 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 serviceaccount 18 19 import ( 20 "context" 21 "fmt" 22 "io" 23 "math/rand" 24 "strconv" 25 "strings" 26 "time" 27 28 corev1 "k8s.io/api/core/v1" 29 "k8s.io/apimachinery/pkg/api/errors" 30 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 31 "k8s.io/apimachinery/pkg/util/sets" 32 "k8s.io/apiserver/pkg/admission" 33 genericadmissioninitializer "k8s.io/apiserver/pkg/admission/initializer" 34 "k8s.io/apiserver/pkg/storage/names" 35 "k8s.io/client-go/informers" 36 "k8s.io/client-go/kubernetes" 37 corev1listers "k8s.io/client-go/listers/core/v1" 38 podutil "k8s.io/kubernetes/pkg/api/pod" 39 api "k8s.io/kubernetes/pkg/apis/core" 40 "k8s.io/kubernetes/pkg/serviceaccount" 41 "k8s.io/utils/pointer" 42 ) 43 44 const ( 45 // DefaultServiceAccountName is the name of the default service account to set on pods which do not specify a service account 46 DefaultServiceAccountName = "default" 47 48 // EnforceMountableSecretsAnnotation is a default annotation that indicates that a service account should enforce mountable secrets. 49 // The value must be true to have this annotation take effect 50 EnforceMountableSecretsAnnotation = "kubernetes.io/enforce-mountable-secrets" 51 52 // ServiceAccountVolumeName is the prefix name that will be added to volumes that mount ServiceAccount secrets 53 ServiceAccountVolumeName = "kube-api-access" 54 55 // DefaultAPITokenMountPath is the path that ServiceAccountToken secrets are automounted to. 56 // The token file would then be accessible at /var/run/secrets/kubernetes.io/serviceaccount 57 DefaultAPITokenMountPath = "/var/run/secrets/kubernetes.io/serviceaccount" 58 59 // PluginName is the name of this admission plugin 60 PluginName = "ServiceAccount" 61 ) 62 63 // Register registers a plugin 64 func Register(plugins *admission.Plugins) { 65 plugins.Register(PluginName, func(config io.Reader) (admission.Interface, error) { 66 serviceAccountAdmission := NewServiceAccount() 67 return serviceAccountAdmission, nil 68 }) 69 } 70 71 var _ = admission.Interface(&Plugin{}) 72 73 // Plugin contains the client used by the admission controller 74 type Plugin struct { 75 *admission.Handler 76 77 // LimitSecretReferences rejects pods that reference secrets their service accounts do not reference 78 LimitSecretReferences bool 79 // MountServiceAccountToken creates Volume and VolumeMounts for the first referenced ServiceAccountToken for the pod's service account 80 MountServiceAccountToken bool 81 82 client kubernetes.Interface 83 84 serviceAccountLister corev1listers.ServiceAccountLister 85 86 generateName func(string) string 87 } 88 89 var _ admission.MutationInterface = &Plugin{} 90 var _ admission.ValidationInterface = &Plugin{} 91 var _ = genericadmissioninitializer.WantsExternalKubeClientSet(&Plugin{}) 92 var _ = genericadmissioninitializer.WantsExternalKubeInformerFactory(&Plugin{}) 93 94 // NewServiceAccount returns an admission.Interface implementation which limits admission of Pod CREATE requests based on the pod's ServiceAccount: 95 // 1. If the pod does not specify a ServiceAccount, it sets the pod's ServiceAccount to "default" 96 // 2. It ensures the ServiceAccount referenced by the pod exists 97 // 3. If LimitSecretReferences is true, it rejects the pod if the pod references Secret objects which the pod's ServiceAccount does not reference 98 // 4. If the pod does not contain any ImagePullSecrets, the ImagePullSecrets of the service account are added. 99 // 5. If MountServiceAccountToken is true, it adds a VolumeMount with the pod's ServiceAccount's api token secret to containers 100 func NewServiceAccount() *Plugin { 101 return &Plugin{ 102 Handler: admission.NewHandler(admission.Create, admission.Update), 103 // TODO: enable this once we've swept secret usage to account for adding secret references to service accounts 104 LimitSecretReferences: false, 105 // Auto mount service account API token secrets 106 MountServiceAccountToken: true, 107 108 generateName: names.SimpleNameGenerator.GenerateName, 109 } 110 } 111 112 // SetExternalKubeClientSet sets the client for the plugin 113 func (s *Plugin) SetExternalKubeClientSet(cl kubernetes.Interface) { 114 s.client = cl 115 } 116 117 // SetExternalKubeInformerFactory registers informers with the plugin 118 func (s *Plugin) SetExternalKubeInformerFactory(f informers.SharedInformerFactory) { 119 serviceAccountInformer := f.Core().V1().ServiceAccounts() 120 s.serviceAccountLister = serviceAccountInformer.Lister() 121 s.SetReadyFunc(func() bool { 122 return serviceAccountInformer.Informer().HasSynced() 123 }) 124 } 125 126 // ValidateInitialization ensures an authorizer is set. 127 func (s *Plugin) ValidateInitialization() error { 128 if s.client == nil { 129 return fmt.Errorf("missing client") 130 } 131 if s.serviceAccountLister == nil { 132 return fmt.Errorf("missing serviceAccountLister") 133 } 134 return nil 135 } 136 137 // Admit verifies if the pod should be admitted 138 func (s *Plugin) Admit(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces) (err error) { 139 if shouldIgnore(a) { 140 return nil 141 } 142 if a.GetOperation() != admission.Create { 143 // we only mutate pods during create requests 144 return nil 145 } 146 pod := a.GetObject().(*api.Pod) 147 148 // Don't modify the spec of mirror pods. 149 // That makes the kubelet very angry and confused, and it immediately deletes the pod (because the spec doesn't match) 150 // That said, don't allow mirror pods to reference ServiceAccounts or SecretVolumeSources either 151 if _, isMirrorPod := pod.Annotations[api.MirrorPodAnnotationKey]; isMirrorPod { 152 return s.Validate(ctx, a, o) 153 } 154 155 // Set the default service account if needed 156 if len(pod.Spec.ServiceAccountName) == 0 { 157 pod.Spec.ServiceAccountName = DefaultServiceAccountName 158 } 159 160 serviceAccount, err := s.getServiceAccount(a.GetNamespace(), pod.Spec.ServiceAccountName) 161 if err != nil { 162 return admission.NewForbidden(a, fmt.Errorf("error looking up service account %s/%s: %w", a.GetNamespace(), pod.Spec.ServiceAccountName, err)) 163 } 164 if s.MountServiceAccountToken && shouldAutomount(serviceAccount, pod) { 165 s.mountServiceAccountToken(serviceAccount, pod) 166 } 167 if len(pod.Spec.ImagePullSecrets) == 0 { 168 pod.Spec.ImagePullSecrets = make([]api.LocalObjectReference, len(serviceAccount.ImagePullSecrets)) 169 for i := 0; i < len(serviceAccount.ImagePullSecrets); i++ { 170 pod.Spec.ImagePullSecrets[i].Name = serviceAccount.ImagePullSecrets[i].Name 171 } 172 } 173 174 return s.Validate(ctx, a, o) 175 } 176 177 // Validate the data we obtained 178 func (s *Plugin) Validate(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces) (err error) { 179 if shouldIgnore(a) { 180 return nil 181 } 182 183 pod := a.GetObject().(*api.Pod) 184 185 if a.GetOperation() == admission.Update && a.GetSubresource() == "ephemeralcontainers" { 186 return s.limitEphemeralContainerSecretReferences(pod, a) 187 } 188 189 if a.GetOperation() != admission.Create { 190 // we only validate pod specs during create requests 191 return nil 192 } 193 194 // Mirror pods have restrictions on what they can reference 195 if _, isMirrorPod := pod.Annotations[api.MirrorPodAnnotationKey]; isMirrorPod { 196 if len(pod.Spec.ServiceAccountName) != 0 { 197 return admission.NewForbidden(a, fmt.Errorf("a mirror pod may not reference service accounts")) 198 } 199 hasSecrets := false 200 podutil.VisitPodSecretNames(pod, func(name string) bool { 201 hasSecrets = true 202 return false 203 }, podutil.AllContainers) 204 if hasSecrets { 205 return admission.NewForbidden(a, fmt.Errorf("a mirror pod may not reference secrets")) 206 } 207 for _, v := range pod.Spec.Volumes { 208 if proj := v.Projected; proj != nil { 209 for _, projSource := range proj.Sources { 210 if projSource.ServiceAccountToken != nil { 211 return admission.NewForbidden(a, fmt.Errorf("a mirror pod may not use ServiceAccountToken volume projections")) 212 } 213 } 214 } 215 } 216 return nil 217 } 218 219 // Require container pods to have service accounts 220 if len(pod.Spec.ServiceAccountName) == 0 { 221 return admission.NewForbidden(a, fmt.Errorf("no service account specified for pod %s/%s", a.GetNamespace(), pod.Name)) 222 } 223 // Ensure the referenced service account exists 224 serviceAccount, err := s.getServiceAccount(a.GetNamespace(), pod.Spec.ServiceAccountName) 225 if err != nil { 226 return admission.NewForbidden(a, fmt.Errorf("error looking up service account %s/%s: %v", a.GetNamespace(), pod.Spec.ServiceAccountName, err)) 227 } 228 229 if s.enforceMountableSecrets(serviceAccount) { 230 if err := s.limitSecretReferences(serviceAccount, pod); err != nil { 231 return admission.NewForbidden(a, err) 232 } 233 } 234 235 return nil 236 } 237 238 func shouldIgnore(a admission.Attributes) bool { 239 if a.GetResource().GroupResource() != api.Resource("pods") || (a.GetSubresource() != "" && a.GetSubresource() != "ephemeralcontainers") { 240 return true 241 } 242 obj := a.GetObject() 243 if obj == nil { 244 return true 245 } 246 _, ok := obj.(*api.Pod) 247 if !ok { 248 return true 249 } 250 251 return false 252 } 253 254 func shouldAutomount(sa *corev1.ServiceAccount, pod *api.Pod) bool { 255 // Pod's preference wins 256 if pod.Spec.AutomountServiceAccountToken != nil { 257 return *pod.Spec.AutomountServiceAccountToken 258 } 259 // Then service account's 260 if sa.AutomountServiceAccountToken != nil { 261 return *sa.AutomountServiceAccountToken 262 } 263 // Default to true for backwards compatibility 264 return true 265 } 266 267 // enforceMountableSecrets indicates whether mountable secrets should be enforced for a particular service account 268 // A global setting of true will override any flag set on the individual service account 269 func (s *Plugin) enforceMountableSecrets(serviceAccount *corev1.ServiceAccount) bool { 270 if s.LimitSecretReferences { 271 return true 272 } 273 274 if value, ok := serviceAccount.Annotations[EnforceMountableSecretsAnnotation]; ok { 275 enforceMountableSecretCheck, _ := strconv.ParseBool(value) 276 return enforceMountableSecretCheck 277 } 278 279 return false 280 } 281 282 // getServiceAccount returns the ServiceAccount for the given namespace and name if it exists 283 func (s *Plugin) getServiceAccount(namespace string, name string) (*corev1.ServiceAccount, error) { 284 serviceAccount, err := s.serviceAccountLister.ServiceAccounts(namespace).Get(name) 285 if err == nil { 286 return serviceAccount, nil 287 } 288 if !errors.IsNotFound(err) { 289 return nil, err 290 } 291 292 // Could not find in cache, attempt to look up directly 293 numAttempts := 1 294 if name == DefaultServiceAccountName { 295 // If this is the default serviceaccount, attempt more times, since it should be auto-created by the controller 296 numAttempts = 10 297 } 298 retryInterval := time.Duration(rand.Int63n(100)+int64(100)) * time.Millisecond 299 for i := 0; i < numAttempts; i++ { 300 if i != 0 { 301 time.Sleep(retryInterval) 302 } 303 serviceAccount, err := s.client.CoreV1().ServiceAccounts(namespace).Get(context.TODO(), name, metav1.GetOptions{}) 304 if err == nil { 305 return serviceAccount, nil 306 } 307 if !errors.IsNotFound(err) { 308 return nil, err 309 } 310 } 311 312 return nil, errors.NewNotFound(api.Resource("serviceaccount"), name) 313 } 314 315 func (s *Plugin) limitSecretReferences(serviceAccount *corev1.ServiceAccount, pod *api.Pod) error { 316 // Ensure all secrets the pod references are allowed by the service account 317 mountableSecrets := sets.NewString() 318 for _, s := range serviceAccount.Secrets { 319 mountableSecrets.Insert(s.Name) 320 } 321 for _, volume := range pod.Spec.Volumes { 322 source := volume.VolumeSource 323 if source.Secret == nil { 324 continue 325 } 326 secretName := source.Secret.SecretName 327 if !mountableSecrets.Has(secretName) { 328 return fmt.Errorf("volume with secret.secretName=\"%s\" is not allowed because service account %s does not reference that secret", secretName, serviceAccount.Name) 329 } 330 } 331 332 for _, container := range pod.Spec.InitContainers { 333 for _, env := range container.Env { 334 if env.ValueFrom != nil && env.ValueFrom.SecretKeyRef != nil { 335 if !mountableSecrets.Has(env.ValueFrom.SecretKeyRef.Name) { 336 return fmt.Errorf("init container %s with envVar %s referencing secret.secretName=\"%s\" is not allowed because service account %s does not reference that secret", container.Name, env.Name, env.ValueFrom.SecretKeyRef.Name, serviceAccount.Name) 337 } 338 } 339 } 340 for _, envFrom := range container.EnvFrom { 341 if envFrom.SecretRef != nil { 342 if !mountableSecrets.Has(envFrom.SecretRef.Name) { 343 return fmt.Errorf("init container %s with envFrom referencing secret.secretName=\"%s\" is not allowed because service account %s does not reference that secret", container.Name, envFrom.SecretRef.Name, serviceAccount.Name) 344 } 345 } 346 } 347 } 348 349 for _, container := range pod.Spec.Containers { 350 for _, env := range container.Env { 351 if env.ValueFrom != nil && env.ValueFrom.SecretKeyRef != nil { 352 if !mountableSecrets.Has(env.ValueFrom.SecretKeyRef.Name) { 353 return fmt.Errorf("container %s with envVar %s referencing secret.secretName=\"%s\" is not allowed because service account %s does not reference that secret", container.Name, env.Name, env.ValueFrom.SecretKeyRef.Name, serviceAccount.Name) 354 } 355 } 356 } 357 for _, envFrom := range container.EnvFrom { 358 if envFrom.SecretRef != nil { 359 if !mountableSecrets.Has(envFrom.SecretRef.Name) { 360 return fmt.Errorf("container %s with envFrom referencing secret.secretName=\"%s\" is not allowed because service account %s does not reference that secret", container.Name, envFrom.SecretRef.Name, serviceAccount.Name) 361 } 362 } 363 } 364 } 365 366 // limit pull secret references as well 367 pullSecrets := sets.NewString() 368 for _, s := range serviceAccount.ImagePullSecrets { 369 pullSecrets.Insert(s.Name) 370 } 371 for i, pullSecretRef := range pod.Spec.ImagePullSecrets { 372 if !pullSecrets.Has(pullSecretRef.Name) { 373 return fmt.Errorf(`imagePullSecrets[%d].name="%s" is not allowed because service account %s does not reference that imagePullSecret`, i, pullSecretRef.Name, serviceAccount.Name) 374 } 375 } 376 return nil 377 } 378 379 func (s *Plugin) limitEphemeralContainerSecretReferences(pod *api.Pod, a admission.Attributes) error { 380 // Require ephemeral container pods to have service accounts 381 if len(pod.Spec.ServiceAccountName) == 0 { 382 return admission.NewForbidden(a, fmt.Errorf("no service account specified for pod %s/%s", a.GetNamespace(), pod.Name)) 383 } 384 // Ensure the referenced service account exists 385 serviceAccount, err := s.getServiceAccount(a.GetNamespace(), pod.Spec.ServiceAccountName) 386 if err != nil { 387 return admission.NewForbidden(a, fmt.Errorf("error looking up service account %s/%s: %w", a.GetNamespace(), pod.Spec.ServiceAccountName, err)) 388 } 389 if !s.enforceMountableSecrets(serviceAccount) { 390 return nil 391 } 392 // Ensure all secrets the ephemeral containers reference are allowed by the service account 393 mountableSecrets := sets.NewString() 394 for _, s := range serviceAccount.Secrets { 395 mountableSecrets.Insert(s.Name) 396 } 397 for _, container := range pod.Spec.EphemeralContainers { 398 for _, env := range container.Env { 399 if env.ValueFrom != nil && env.ValueFrom.SecretKeyRef != nil { 400 if !mountableSecrets.Has(env.ValueFrom.SecretKeyRef.Name) { 401 return fmt.Errorf("ephemeral container %s with envVar %s referencing secret.secretName=\"%s\" is not allowed because service account %s does not reference that secret", container.Name, env.Name, env.ValueFrom.SecretKeyRef.Name, serviceAccount.Name) 402 } 403 } 404 } 405 for _, envFrom := range container.EnvFrom { 406 if envFrom.SecretRef != nil { 407 if !mountableSecrets.Has(envFrom.SecretRef.Name) { 408 return fmt.Errorf("ephemeral container %s with envFrom referencing secret.secretName=\"%s\" is not allowed because service account %s does not reference that secret", container.Name, envFrom.SecretRef.Name, serviceAccount.Name) 409 } 410 } 411 } 412 } 413 return nil 414 } 415 416 func (s *Plugin) mountServiceAccountToken(serviceAccount *corev1.ServiceAccount, pod *api.Pod) { 417 // Find the volume and volume name for the ServiceAccountTokenSecret if it already exists 418 tokenVolumeName := "" 419 hasTokenVolume := false 420 allVolumeNames := sets.NewString() 421 for _, volume := range pod.Spec.Volumes { 422 allVolumeNames.Insert(volume.Name) 423 if strings.HasPrefix(volume.Name, ServiceAccountVolumeName+"-") { 424 tokenVolumeName = volume.Name 425 hasTokenVolume = true 426 break 427 } 428 } 429 430 // Determine a volume name for the ServiceAccountTokenSecret in case we need it 431 if len(tokenVolumeName) == 0 { 432 tokenVolumeName = s.generateName(ServiceAccountVolumeName + "-") 433 } 434 435 // Create the prototypical VolumeMount 436 volumeMount := api.VolumeMount{ 437 Name: tokenVolumeName, 438 ReadOnly: true, 439 MountPath: DefaultAPITokenMountPath, 440 } 441 442 // Ensure every container mounts the APISecret volume 443 needsTokenVolume := false 444 for i, container := range pod.Spec.InitContainers { 445 existingContainerMount := false 446 for _, volumeMount := range container.VolumeMounts { 447 // Existing mounts at the default mount path prevent mounting of the API token 448 if volumeMount.MountPath == DefaultAPITokenMountPath { 449 existingContainerMount = true 450 break 451 } 452 } 453 if !existingContainerMount { 454 pod.Spec.InitContainers[i].VolumeMounts = append(pod.Spec.InitContainers[i].VolumeMounts, volumeMount) 455 needsTokenVolume = true 456 } 457 } 458 for i, container := range pod.Spec.Containers { 459 existingContainerMount := false 460 for _, volumeMount := range container.VolumeMounts { 461 // Existing mounts at the default mount path prevent mounting of the API token 462 if volumeMount.MountPath == DefaultAPITokenMountPath { 463 existingContainerMount = true 464 break 465 } 466 } 467 if !existingContainerMount { 468 pod.Spec.Containers[i].VolumeMounts = append(pod.Spec.Containers[i].VolumeMounts, volumeMount) 469 needsTokenVolume = true 470 } 471 } 472 473 // Add the volume if a container needs it 474 if !hasTokenVolume && needsTokenVolume { 475 pod.Spec.Volumes = append(pod.Spec.Volumes, api.Volume{ 476 Name: tokenVolumeName, 477 VolumeSource: api.VolumeSource{ 478 Projected: TokenVolumeSource(), 479 }, 480 }) 481 } 482 } 483 484 // TokenVolumeSource returns the projected volume source for service account token. 485 func TokenVolumeSource() *api.ProjectedVolumeSource { 486 return &api.ProjectedVolumeSource{ 487 // explicitly set default value, see #104464 488 DefaultMode: pointer.Int32(corev1.ProjectedVolumeSourceDefaultMode), 489 Sources: []api.VolumeProjection{ 490 { 491 ServiceAccountToken: &api.ServiceAccountTokenProjection{ 492 Path: "token", 493 ExpirationSeconds: serviceaccount.WarnOnlyBoundTokenExpirationSeconds, 494 }, 495 }, 496 { 497 ConfigMap: &api.ConfigMapProjection{ 498 LocalObjectReference: api.LocalObjectReference{ 499 Name: "kube-root-ca.crt", 500 }, 501 Items: []api.KeyToPath{ 502 { 503 Key: "ca.crt", 504 Path: "ca.crt", 505 }, 506 }, 507 }, 508 }, 509 { 510 DownwardAPI: &api.DownwardAPIProjection{ 511 Items: []api.DownwardAPIVolumeFile{ 512 { 513 Path: "namespace", 514 FieldRef: &api.ObjectFieldSelector{ 515 APIVersion: "v1", 516 FieldPath: "metadata.namespace", 517 }, 518 }, 519 }, 520 }, 521 }, 522 }, 523 } 524 }