...

Source file src/k8s.io/kubernetes/plugin/pkg/admission/serviceaccount/admission.go

Documentation: k8s.io/kubernetes/plugin/pkg/admission/serviceaccount

     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  }
   525  

View as plain text