diff --git a/pkg/api/types.go b/pkg/api/types.go index af333de893d..e5ccc248ac6 100644 --- a/pkg/api/types.go +++ b/pkg/api/types.go @@ -1887,6 +1887,9 @@ type PodSpec struct { // ServiceAccountName is the name of the ServiceAccount to use to run this pod // The pod will be allowed to use secrets referenced by the ServiceAccount ServiceAccountName string + // AutomountServiceAccountToken indicates whether a service account token should be automatically mounted. + // +optional + AutomountServiceAccountToken *bool // NodeName is a request to schedule this pod onto a specific node. If it is non-empty, // the scheduler simply schedules this pod onto that node, assuming that it fits resource @@ -2425,6 +2428,11 @@ type ServiceAccount struct { // can be mounted in the pod, but ImagePullSecrets are only accessed by the kubelet. // +optional ImagePullSecrets []LocalObjectReference + + // AutomountServiceAccountToken indicates whether pods running as this service account should have an API token automatically mounted. + // Can be overridden at the pod level. + // +optional + AutomountServiceAccountToken *bool } // ServiceAccountList is a list of ServiceAccount objects diff --git a/pkg/api/v1/types.go b/pkg/api/v1/types.go index e56ade1c805..529b5de79c3 100644 --- a/pkg/api/v1/types.go +++ b/pkg/api/v1/types.go @@ -2160,6 +2160,9 @@ type PodSpec struct { // +k8s:conversion-gen=false // +optional DeprecatedServiceAccount string `json:"serviceAccount,omitempty" protobuf:"bytes,9,opt,name=serviceAccount"` + // AutomountServiceAccountToken indicates whether a service account token should be automatically mounted. + // +optional + AutomountServiceAccountToken *bool `json:"automountServiceAccountToken,omitempty" protobuf:"varint,21,opt,name=automountServiceAccountToken"` // NodeName is a request to schedule this pod onto a specific node. If it is non-empty, // the scheduler simply schedules this pod onto that node, assuming that it fits resource @@ -2801,6 +2804,11 @@ type ServiceAccount struct { // More info: http://kubernetes.io/docs/user-guide/secrets#manually-specifying-an-imagepullsecret // +optional ImagePullSecrets []LocalObjectReference `json:"imagePullSecrets,omitempty" protobuf:"bytes,3,rep,name=imagePullSecrets"` + + // AutomountServiceAccountToken indicates whether pods running as this service account should have an API token automatically mounted. + // Can be overridden at the pod level. + // +optional + AutomountServiceAccountToken *bool `json:"automountServiceAccountToken,omitempty" protobuf:"varint,4,opt,name=automountServiceAccountToken"` } // ServiceAccountList is a list of ServiceAccount objects diff --git a/plugin/pkg/admission/serviceaccount/admission.go b/plugin/pkg/admission/serviceaccount/admission.go index 679036f8f1e..efa89a356d4 100644 --- a/plugin/pkg/admission/serviceaccount/admission.go +++ b/plugin/pkg/admission/serviceaccount/admission.go @@ -222,7 +222,7 @@ func (s *serviceAccount) Admit(a admission.Attributes) (err error) { } } - if s.MountServiceAccountToken { + if s.MountServiceAccountToken && shouldAutomount(serviceAccount, pod) { if err := s.mountServiceAccountToken(serviceAccount, pod); err != nil { if _, ok := err.(errors.APIStatus); ok { return err @@ -239,6 +239,19 @@ func (s *serviceAccount) Admit(a admission.Attributes) (err error) { return nil } +func shouldAutomount(sa *api.ServiceAccount, pod *api.Pod) bool { + // Pod's preference wins + if pod.Spec.AutomountServiceAccountToken != nil { + return *pod.Spec.AutomountServiceAccountToken + } + // Then service account's + if sa.AutomountServiceAccountToken != nil { + return *sa.AutomountServiceAccountToken + } + // Default to true for backwards compatibility + return true +} + // enforceMountableSecrets indicates whether mountable secrets should be enforced for a particular service account // A global setting of true will override any flag set on the individual service account func (s *serviceAccount) enforceMountableSecrets(serviceAccount *api.ServiceAccount) bool { diff --git a/test/e2e/service_accounts.go b/test/e2e/service_accounts.go index 9c6d7c0dd79..bcedabc4806 100644 --- a/test/e2e/service_accounts.go +++ b/test/e2e/service_accounts.go @@ -35,6 +35,8 @@ import ( var serviceAccountTokenNamespaceVersion = utilversion.MustParseSemantic("v1.2.0") +var serviceAccountTokenAutomountVersion = utilversion.MustParseSemantic("v1.6.0-alpha.2") + var _ = framework.KubeDescribe("ServiceAccounts", func() { f := framework.NewDefaultFramework("svcaccounts") @@ -239,4 +241,145 @@ var _ = framework.KubeDescribe("ServiceAccounts", func() { }) } }) + + It("should allow opting out of API token automount [Conformance]", func() { + framework.SkipUnlessServerVersionGTE(serviceAccountTokenAutomountVersion, f.ClientSet.Discovery()) + + var err error + trueValue := true + falseValue := false + mountSA := &v1.ServiceAccount{ObjectMeta: metav1.ObjectMeta{Name: "mount"}, AutomountServiceAccountToken: &trueValue} + nomountSA := &v1.ServiceAccount{ObjectMeta: metav1.ObjectMeta{Name: "nomount"}, AutomountServiceAccountToken: &falseValue} + mountSA, err = f.ClientSet.Core().ServiceAccounts(f.Namespace.Name).Create(mountSA) + framework.ExpectNoError(err) + nomountSA, err = f.ClientSet.Core().ServiceAccounts(f.Namespace.Name).Create(nomountSA) + framework.ExpectNoError(err) + + // Standard get, update retry loop + framework.ExpectNoError(wait.Poll(time.Millisecond*500, framework.ServiceAccountProvisionTimeout, func() (bool, error) { + By("getting the auto-created API token") + sa, err := f.ClientSet.Core().ServiceAccounts(f.Namespace.Name).Get(mountSA.Name, metav1.GetOptions{}) + if apierrors.IsNotFound(err) { + framework.Logf("mount service account was not found") + return false, nil + } + if err != nil { + framework.Logf("error getting mount service account: %v", err) + return false, err + } + if len(sa.Secrets) == 0 { + framework.Logf("mount service account has no secret references") + return false, nil + } + for _, secretRef := range sa.Secrets { + secret, err := f.ClientSet.Core().Secrets(f.Namespace.Name).Get(secretRef.Name, metav1.GetOptions{}) + if err != nil { + framework.Logf("Error getting secret %s: %v", secretRef.Name, err) + continue + } + if secret.Type == v1.SecretTypeServiceAccountToken { + return true, nil + } + } + + framework.Logf("default service account has no secret references to valid service account tokens") + return false, nil + })) + + testcases := []struct { + PodName string + ServiceAccountName string + AutomountPodSpec *bool + ExpectTokenVolume bool + }{ + { + PodName: "pod-service-account-defaultsa", + ServiceAccountName: "default", + AutomountPodSpec: nil, + ExpectTokenVolume: true, // default is true + }, + { + PodName: "pod-service-account-mountsa", + ServiceAccountName: mountSA.Name, + AutomountPodSpec: nil, + ExpectTokenVolume: true, + }, + { + PodName: "pod-service-account-nomountsa", + ServiceAccountName: nomountSA.Name, + AutomountPodSpec: nil, + ExpectTokenVolume: false, + }, + + // Make sure pod spec trumps when opting in + { + PodName: "pod-service-account-defaultsa-mountspec", + ServiceAccountName: "default", + AutomountPodSpec: &trueValue, + ExpectTokenVolume: true, + }, + { + PodName: "pod-service-account-mountsa-mountspec", + ServiceAccountName: mountSA.Name, + AutomountPodSpec: &trueValue, + ExpectTokenVolume: true, + }, + { + PodName: "pod-service-account-nomountsa-mountspec", + ServiceAccountName: nomountSA.Name, + AutomountPodSpec: &trueValue, + ExpectTokenVolume: true, // pod spec trumps + }, + + // Make sure pod spec trumps when opting out + { + PodName: "pod-service-account-defaultsa-nomountspec", + ServiceAccountName: "default", + AutomountPodSpec: &falseValue, + ExpectTokenVolume: false, // pod spec trumps + }, + { + PodName: "pod-service-account-mountsa-nomountspec", + ServiceAccountName: mountSA.Name, + AutomountPodSpec: &falseValue, + ExpectTokenVolume: false, // pod spec trumps + }, + { + PodName: "pod-service-account-nomountsa-nomountspec", + ServiceAccountName: nomountSA.Name, + AutomountPodSpec: &falseValue, + ExpectTokenVolume: false, // pod spec trumps + }, + } + + for _, tc := range testcases { + pod := &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: tc.PodName}, + Spec: v1.PodSpec{ + Containers: []v1.Container{{Name: "token-test", Image: "gcr.io/google_containers/mounttest:0.7"}}, + RestartPolicy: v1.RestartPolicyNever, + ServiceAccountName: tc.ServiceAccountName, + AutomountServiceAccountToken: tc.AutomountPodSpec, + }, + } + createdPod, err := f.ClientSet.Core().Pods(f.Namespace.Name).Create(pod) + framework.ExpectNoError(err) + framework.Logf("created pod %s", tc.PodName) + + hasServiceAccountTokenVolume := false + for _, c := range createdPod.Spec.Containers { + for _, vm := range c.VolumeMounts { + if vm.MountPath == serviceaccount.DefaultAPITokenMountPath { + hasServiceAccountTokenVolume = true + } + } + } + + if hasServiceAccountTokenVolume != tc.ExpectTokenVolume { + framework.Failf("%s: expected volume=%v, got %v (%#v)", tc.PodName, tc.ExpectTokenVolume, hasServiceAccountTokenVolume, createdPod) + } else { + framework.Logf("pod %s service account token volume mount: %v", tc.PodName, hasServiceAccountTokenVolume) + } + } + }) })