...

Source file src/k8s.io/kubernetes/pkg/serviceaccount/claims.go

Documentation: k8s.io/kubernetes/pkg/serviceaccount

     1  /*
     2  Copyright 2018 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  	"errors"
    22  	"fmt"
    23  	"time"
    24  
    25  	"github.com/google/uuid"
    26  	"gopkg.in/square/go-jose.v2/jwt"
    27  	"k8s.io/klog/v2"
    28  
    29  	"k8s.io/apiserver/pkg/audit"
    30  	apiserverserviceaccount "k8s.io/apiserver/pkg/authentication/serviceaccount"
    31  	utilfeature "k8s.io/apiserver/pkg/util/feature"
    32  	"k8s.io/kubernetes/pkg/apis/core"
    33  	"k8s.io/kubernetes/pkg/features"
    34  )
    35  
    36  const (
    37  	// Injected bound service account token expiration which triggers monitoring of its time-bound feature.
    38  	WarnOnlyBoundTokenExpirationSeconds = 60*60 + 7
    39  
    40  	// Extended expiration for those modified tokens involved in safe rollout if time-bound feature.
    41  	ExpirationExtensionSeconds = 24 * 365 * 60 * 60
    42  )
    43  
    44  var (
    45  	// time.Now stubbed out to allow testing
    46  	now = time.Now
    47  	// uuid.New stubbed out to allow testing
    48  	newUUID = uuid.NewString
    49  )
    50  
    51  type privateClaims struct {
    52  	Kubernetes kubernetes `json:"kubernetes.io,omitempty"`
    53  }
    54  
    55  type kubernetes struct {
    56  	Namespace string           `json:"namespace,omitempty"`
    57  	Svcacct   ref              `json:"serviceaccount,omitempty"`
    58  	Pod       *ref             `json:"pod,omitempty"`
    59  	Secret    *ref             `json:"secret,omitempty"`
    60  	Node      *ref             `json:"node,omitempty"`
    61  	WarnAfter *jwt.NumericDate `json:"warnafter,omitempty"`
    62  }
    63  
    64  type ref struct {
    65  	Name string `json:"name,omitempty"`
    66  	UID  string `json:"uid,omitempty"`
    67  }
    68  
    69  func Claims(sa core.ServiceAccount, pod *core.Pod, secret *core.Secret, node *core.Node, expirationSeconds, warnafter int64, audience []string) (*jwt.Claims, interface{}, error) {
    70  	now := now()
    71  	sc := &jwt.Claims{
    72  		Subject:   apiserverserviceaccount.MakeUsername(sa.Namespace, sa.Name),
    73  		Audience:  jwt.Audience(audience),
    74  		IssuedAt:  jwt.NewNumericDate(now),
    75  		NotBefore: jwt.NewNumericDate(now),
    76  		Expiry:    jwt.NewNumericDate(now.Add(time.Duration(expirationSeconds) * time.Second)),
    77  	}
    78  	if utilfeature.DefaultFeatureGate.Enabled(features.ServiceAccountTokenJTI) {
    79  		sc.ID = newUUID()
    80  	}
    81  	pc := &privateClaims{
    82  		Kubernetes: kubernetes{
    83  			Namespace: sa.Namespace,
    84  			Svcacct: ref{
    85  				Name: sa.Name,
    86  				UID:  string(sa.UID),
    87  			},
    88  		},
    89  	}
    90  
    91  	if secret != nil && (node != nil || pod != nil) {
    92  		return nil, nil, fmt.Errorf("internal error, token can only be bound to one object type")
    93  	}
    94  	switch {
    95  	case pod != nil:
    96  		pc.Kubernetes.Pod = &ref{
    97  			Name: pod.Name,
    98  			UID:  string(pod.UID),
    99  		}
   100  		if utilfeature.DefaultFeatureGate.Enabled(features.ServiceAccountTokenPodNodeInfo) {
   101  			// if this is bound to a pod and the node information is available, persist that too
   102  			if node != nil {
   103  				pc.Kubernetes.Node = &ref{
   104  					Name: node.Name,
   105  					UID:  string(node.UID),
   106  				}
   107  			}
   108  		}
   109  	case secret != nil:
   110  		pc.Kubernetes.Secret = &ref{
   111  			Name: secret.Name,
   112  			UID:  string(secret.UID),
   113  		}
   114  	case node != nil:
   115  		if !utilfeature.DefaultFeatureGate.Enabled(features.ServiceAccountTokenNodeBinding) {
   116  			return nil, nil, fmt.Errorf("token bound to Node object requested, but %q feature gate is disabled", features.ServiceAccountTokenNodeBinding)
   117  		}
   118  		pc.Kubernetes.Node = &ref{
   119  			Name: node.Name,
   120  			UID:  string(node.UID),
   121  		}
   122  	}
   123  
   124  	if warnafter != 0 {
   125  		pc.Kubernetes.WarnAfter = jwt.NewNumericDate(now.Add(time.Duration(warnafter) * time.Second))
   126  	}
   127  
   128  	return sc, pc, nil
   129  }
   130  
   131  func NewValidator(getter ServiceAccountTokenGetter) Validator {
   132  	return &validator{
   133  		getter: getter,
   134  	}
   135  }
   136  
   137  type validator struct {
   138  	getter ServiceAccountTokenGetter
   139  }
   140  
   141  var _ = Validator(&validator{})
   142  
   143  func (v *validator) Validate(ctx context.Context, _ string, public *jwt.Claims, privateObj interface{}) (*apiserverserviceaccount.ServiceAccountInfo, error) {
   144  	private, ok := privateObj.(*privateClaims)
   145  	if !ok {
   146  		klog.Errorf("service account jwt validator expected private claim of type *privateClaims but got: %T", privateObj)
   147  		return nil, errors.New("service account token claims could not be validated due to unexpected private claim")
   148  	}
   149  	nowTime := now()
   150  	err := public.Validate(jwt.Expected{
   151  		Time: nowTime,
   152  	})
   153  	switch err {
   154  	case nil:
   155  		// successful validation
   156  
   157  	case jwt.ErrExpired:
   158  		return nil, errors.New("service account token has expired")
   159  
   160  	case jwt.ErrNotValidYet:
   161  		return nil, errors.New("service account token is not valid yet")
   162  
   163  	case jwt.ErrIssuedInTheFuture:
   164  		return nil, errors.New("service account token is issued in the future")
   165  
   166  	// our current use of jwt.Expected above should make these cases impossible to hit
   167  	case jwt.ErrInvalidAudience, jwt.ErrInvalidID, jwt.ErrInvalidIssuer, jwt.ErrInvalidSubject:
   168  		klog.Errorf("service account token claim validation got unexpected validation failure: %v", err)
   169  		return nil, fmt.Errorf("service account token claims could not be validated: %w", err) // safe to pass these errors back to the user
   170  
   171  	default:
   172  		klog.Errorf("service account token claim validation got unexpected error type: %T", err)                         // avoid leaking unexpected information into the logs
   173  		return nil, errors.New("service account token claims could not be validated due to unexpected validation error") // return an opaque error
   174  	}
   175  
   176  	// consider things deleted prior to now()-leeway to be invalid
   177  	invalidIfDeletedBefore := nowTime.Add(-jwt.DefaultLeeway)
   178  	namespace := private.Kubernetes.Namespace
   179  	saref := private.Kubernetes.Svcacct
   180  	podref := private.Kubernetes.Pod
   181  	noderef := private.Kubernetes.Node
   182  	secref := private.Kubernetes.Secret
   183  	// Make sure service account still exists (name and UID)
   184  	serviceAccount, err := v.getter.GetServiceAccount(namespace, saref.Name)
   185  	if err != nil {
   186  		klog.V(4).Infof("Could not retrieve service account %s/%s: %v", namespace, saref.Name, err)
   187  		return nil, err
   188  	}
   189  
   190  	if string(serviceAccount.UID) != saref.UID {
   191  		klog.V(4).Infof("Service account UID no longer matches %s/%s: %q != %q", namespace, saref.Name, string(serviceAccount.UID), saref.UID)
   192  		return nil, fmt.Errorf("service account UID (%s) does not match claim (%s)", serviceAccount.UID, saref.UID)
   193  	}
   194  	if serviceAccount.DeletionTimestamp != nil && serviceAccount.DeletionTimestamp.Time.Before(invalidIfDeletedBefore) {
   195  		klog.V(4).Infof("Service account has been deleted %s/%s", namespace, saref.Name)
   196  		return nil, fmt.Errorf("service account %s/%s has been deleted", namespace, saref.Name)
   197  	}
   198  
   199  	if secref != nil {
   200  		// Make sure token hasn't been invalidated by deletion of the secret
   201  		secret, err := v.getter.GetSecret(namespace, secref.Name)
   202  		if err != nil {
   203  			klog.V(4).Infof("Could not retrieve bound secret %s/%s for service account %s/%s: %v", namespace, secref.Name, namespace, saref.Name, err)
   204  			return nil, errors.New("service account token has been invalidated")
   205  		}
   206  		if secref.UID != string(secret.UID) {
   207  			klog.V(4).Infof("Secret UID no longer matches %s/%s: %q != %q", namespace, secref.Name, string(secret.UID), secref.UID)
   208  			return nil, fmt.Errorf("secret UID (%s) does not match service account secret ref claim (%s)", secret.UID, secref.UID)
   209  		}
   210  		if secret.DeletionTimestamp != nil && secret.DeletionTimestamp.Time.Before(invalidIfDeletedBefore) {
   211  			klog.V(4).Infof("Bound secret is deleted and awaiting removal: %s/%s for service account %s/%s", namespace, secref.Name, namespace, saref.Name)
   212  			return nil, errors.New("service account token has been invalidated")
   213  		}
   214  	}
   215  
   216  	var podName, podUID string
   217  	if podref != nil {
   218  		// Make sure token hasn't been invalidated by deletion of the pod
   219  		pod, err := v.getter.GetPod(namespace, podref.Name)
   220  		if err != nil {
   221  			klog.V(4).Infof("Could not retrieve bound pod %s/%s for service account %s/%s: %v", namespace, podref.Name, namespace, saref.Name, err)
   222  			return nil, errors.New("service account token has been invalidated")
   223  		}
   224  		if podref.UID != string(pod.UID) {
   225  			klog.V(4).Infof("Pod UID no longer matches %s/%s: %q != %q", namespace, podref.Name, string(pod.UID), podref.UID)
   226  			return nil, fmt.Errorf("pod UID (%s) does not match service account pod ref claim (%s)", pod.UID, podref.UID)
   227  		}
   228  		if pod.DeletionTimestamp != nil && pod.DeletionTimestamp.Time.Before(invalidIfDeletedBefore) {
   229  			klog.V(4).Infof("Bound pod is deleted and awaiting removal: %s/%s for service account %s/%s", namespace, podref.Name, namespace, saref.Name)
   230  			return nil, errors.New("service account token has been invalidated")
   231  		}
   232  		podName = podref.Name
   233  		podUID = podref.UID
   234  	}
   235  
   236  	var nodeName, nodeUID string
   237  	if noderef != nil {
   238  		switch {
   239  		case podref != nil:
   240  			if utilfeature.DefaultFeatureGate.Enabled(features.ServiceAccountTokenPodNodeInfo) {
   241  				// for pod-bound tokens, just extract the node claims
   242  				nodeName = noderef.Name
   243  				nodeUID = noderef.UID
   244  			}
   245  		case podref == nil:
   246  			if !utilfeature.DefaultFeatureGate.Enabled(features.ServiceAccountTokenNodeBindingValidation) {
   247  				klog.V(4).Infof("ServiceAccount token is bound to a Node object, but the node bound token validation feature is disabled")
   248  				return nil, fmt.Errorf("token is bound to a Node object but the %s feature gate is disabled", features.ServiceAccountTokenNodeBindingValidation)
   249  			}
   250  
   251  			node, err := v.getter.GetNode(noderef.Name)
   252  			if err != nil {
   253  				klog.V(4).Infof("Could not retrieve node object %q for service account %s/%s: %v", noderef.Name, namespace, saref.Name, err)
   254  				return nil, errors.New("service account token has been invalidated")
   255  			}
   256  			if noderef.UID != string(node.UID) {
   257  				klog.V(4).Infof("Node UID no longer matches %s: %q != %q", noderef.Name, string(node.UID), noderef.UID)
   258  				return nil, fmt.Errorf("node UID (%s) does not match service account node ref claim (%s)", node.UID, noderef.UID)
   259  			}
   260  			if node.DeletionTimestamp != nil && node.DeletionTimestamp.Time.Before(invalidIfDeletedBefore) {
   261  				klog.V(4).Infof("Node %q is deleted and awaiting removal for service account %s/%s", node.Name, namespace, saref.Name)
   262  				return nil, errors.New("service account token has been invalidated")
   263  			}
   264  			nodeName = noderef.Name
   265  			nodeUID = noderef.UID
   266  		}
   267  	}
   268  
   269  	// Check special 'warnafter' field for projected service account token transition.
   270  	warnafter := private.Kubernetes.WarnAfter
   271  	if warnafter != nil && *warnafter != 0 {
   272  		if nowTime.After(warnafter.Time()) {
   273  			secondsAfterWarn := nowTime.Unix() - warnafter.Time().Unix()
   274  			auditInfo := fmt.Sprintf("subject: %s, seconds after warning threshold: %d", public.Subject, secondsAfterWarn)
   275  			audit.AddAuditAnnotation(ctx, "authentication.k8s.io/stale-token", auditInfo)
   276  			staleTokensTotal.WithContext(ctx).Inc()
   277  		} else {
   278  			validTokensTotal.WithContext(ctx).Inc()
   279  		}
   280  	}
   281  
   282  	var jti string
   283  	if utilfeature.DefaultFeatureGate.Enabled(features.ServiceAccountTokenJTI) {
   284  		jti = public.ID
   285  	}
   286  	return &apiserverserviceaccount.ServiceAccountInfo{
   287  		Namespace:    private.Kubernetes.Namespace,
   288  		Name:         private.Kubernetes.Svcacct.Name,
   289  		UID:          private.Kubernetes.Svcacct.UID,
   290  		PodName:      podName,
   291  		PodUID:       podUID,
   292  		NodeName:     nodeName,
   293  		NodeUID:      nodeUID,
   294  		CredentialID: apiserverserviceaccount.CredentialIDForJTI(jti),
   295  	}, nil
   296  }
   297  
   298  func (v *validator) NewPrivateClaims() interface{} {
   299  	return &privateClaims{}
   300  }
   301  

View as plain text