Monitoring safe rollout of time-bound service account token.

This commit is contained in:
Jiajie Yang 2020-03-13 14:49:47 -07:00
parent 57108f6c3e
commit ae0e52d28c
16 changed files with 251 additions and 33 deletions

View File

@ -351,6 +351,7 @@ func CreateKubeAPIServerConfig(
ServiceAccountIssuer: s.ServiceAccountIssuer,
ServiceAccountMaxExpiration: s.ServiceAccountTokenMaxExpiration,
ExtendExpiration: s.Authentication.ServiceAccounts.ExtendExpiration,
VersionedInformers: versionedInformers,
},
@ -689,6 +690,14 @@ func Complete(s *options.ServerRunOptions) (completedServerRunOptions, error) {
s.Authentication.ServiceAccounts.MaxExpiration > upBound {
return options, fmt.Errorf("the serviceaccount max expiration must be between 1 hour to 2^32 seconds")
}
if s.Authentication.ServiceAccounts.ExtendExpiration {
if s.Authentication.ServiceAccounts.MaxExpiration < serviceaccount.WarnOnlyBoundTokenExpirationSeconds*time.Second {
klog.Warningf("service-account-extend-token-expiration is true, in order to correctly trigger safe transition logic, service-account-max-token-expiration must be set longer than 3607 seconds (currently %s)", s.Authentication.ServiceAccounts.MaxExpiration)
}
if s.Authentication.ServiceAccounts.MaxExpiration < serviceaccount.ExpirationExtensionSeconds*time.Second {
klog.Warningf("service-account-extend-token-expiration is true, enabling tokens valid up to 1 year, which is longer than service-account-max-token-expiration set to %s", s.Authentication.ServiceAccounts.MaxExpiration)
}
}
}
s.ServiceAccountIssuer, err = serviceaccount.JWTTokenGenerator(s.Authentication.ServiceAccounts.Issuer, sk)

View File

@ -78,6 +78,7 @@ type ServiceAccountAuthenticationOptions struct {
Issuer string
JWKSURI string
MaxExpiration time.Duration
ExtendExpiration bool
}
type TokenFileAuthenticationOptions struct {
@ -304,6 +305,12 @@ func (s *BuiltInAuthenticationOptions) AddFlags(fs *pflag.FlagSet) {
fs.DurationVar(&s.ServiceAccounts.MaxExpiration, "service-account-max-token-expiration", s.ServiceAccounts.MaxExpiration, ""+
"The maximum validity duration of a token created by the service account token issuer. If an otherwise valid "+
"TokenRequest with a validity duration larger than this value is requested, a token will be issued with a validity duration of this value.")
fs.BoolVar(&s.ServiceAccounts.ExtendExpiration, "service-account-extend-token-expiration", s.ServiceAccounts.ExtendExpiration, ""+
"Turns on projected service account expiration extension during token generation, "+
"which helps safe transition from legacy token to bound service account token feature. "+
"If this flag is enabled, admission injected tokens would be extended up to 1 year to "+
"prevent unexpected failure during transition, ignoring value of service-account-max-token-expiration.")
}
if s.TokenFile != nil {

View File

@ -190,6 +190,7 @@ type ExtraConfig struct {
ServiceAccountIssuer serviceaccount.TokenGenerator
ServiceAccountMaxExpiration time.Duration
ExtendExpiration bool
// ServiceAccountIssuerDiscovery
ServiceAccountIssuerURL string
@ -397,6 +398,7 @@ func (c completedConfig) New(delegationTarget genericapiserver.DelegationTarget)
ServiceNodePortRange: c.ExtraConfig.ServiceNodePortRange,
LoopbackClientConfig: c.GenericConfig.LoopbackClientConfig,
ServiceAccountIssuer: c.ExtraConfig.ServiceAccountIssuer,
ExtendExpiration: c.ExtraConfig.ExtendExpiration,
ServiceAccountMaxExpiration: c.ExtraConfig.ServiceAccountMaxExpiration,
APIAudiences: c.GenericConfig.Authentication.APIAudiences,
}

View File

@ -85,6 +85,7 @@ type LegacyRESTStorageProvider struct {
ServiceAccountIssuer serviceaccount.TokenGenerator
ServiceAccountMaxExpiration time.Duration
ExtendExpiration bool
APIAudiences authenticator.Audiences
@ -181,9 +182,9 @@ func (c LegacyRESTStorageProvider) NewLegacyRESTStorage(restOptionsGetter generi
var serviceAccountStorage *serviceaccountstore.REST
if c.ServiceAccountIssuer != nil && utilfeature.DefaultFeatureGate.Enabled(features.TokenRequest) {
serviceAccountStorage, err = serviceaccountstore.NewREST(restOptionsGetter, c.ServiceAccountIssuer, c.APIAudiences, c.ServiceAccountMaxExpiration, podStorage.Pod.Store, secretStorage.Store)
serviceAccountStorage, err = serviceaccountstore.NewREST(restOptionsGetter, c.ServiceAccountIssuer, c.APIAudiences, c.ServiceAccountMaxExpiration, podStorage.Pod.Store, secretStorage.Store, c.ExtendExpiration)
} else {
serviceAccountStorage, err = serviceaccountstore.NewREST(restOptionsGetter, nil, nil, 0, nil, nil)
serviceAccountStorage, err = serviceaccountstore.NewREST(restOptionsGetter, nil, nil, 0, nil, nil, false)
}
if err != nil {
return LegacyRESTStorage{}, genericapiserver.APIGroupInfo{}, err

View File

@ -38,7 +38,7 @@ type REST struct {
}
// NewREST returns a RESTStorage object that will work against service accounts.
func NewREST(optsGetter generic.RESTOptionsGetter, issuer token.TokenGenerator, auds authenticator.Audiences, max time.Duration, podStorage, secretStorage *genericregistry.Store) (*REST, error) {
func NewREST(optsGetter generic.RESTOptionsGetter, issuer token.TokenGenerator, auds authenticator.Audiences, max time.Duration, podStorage, secretStorage *genericregistry.Store, extendExpiration bool) (*REST, error) {
store := &genericregistry.Store{
NewFunc: func() runtime.Object { return &api.ServiceAccount{} },
NewListFunc: func() runtime.Object { return &api.ServiceAccountList{} },
@ -65,6 +65,7 @@ func NewREST(optsGetter generic.RESTOptionsGetter, issuer token.TokenGenerator,
issuer: issuer,
auds: auds,
maxExpirationSeconds: int64(max.Seconds()),
extendExpiration: extendExpiration,
}
}

View File

@ -38,7 +38,7 @@ func newStorage(t *testing.T) (*REST, *etcd3testing.EtcdTestServer) {
DeleteCollectionWorkers: 1,
ResourcePrefix: "serviceaccounts",
}
rest, err := NewREST(restOptions, nil, nil, 0, nil, nil)
rest, err := NewREST(restOptions, nil, nil, 0, nil, nil, false)
if err != nil {
t.Fatalf("unexpected error from REST storage: %v", err)
}

View File

@ -19,6 +19,7 @@ package storage
import (
"context"
"fmt"
"time"
authenticationapiv1 "k8s.io/api/authentication/v1"
"k8s.io/apimachinery/pkg/api/errors"
@ -46,6 +47,7 @@ type TokenREST struct {
issuer token.TokenGenerator
auds authenticator.Audiences
maxExpirationSeconds int64
extendExpiration bool
}
var _ = rest.NamedCreater(&TokenREST{})
@ -121,7 +123,21 @@ func (r *TokenREST) Create(ctx context.Context, name string, obj runtime.Object,
out.Spec.ExpirationSeconds = r.maxExpirationSeconds
}
sc, pc := token.Claims(*svcacct, pod, secret, out.Spec.ExpirationSeconds, out.Spec.Audiences)
// Tweak expiration for safe transition of projected service account token.
// Warn (instead of fail) after requested expiration time.
// Fail after hard-coded extended expiration time.
// Only perform the extension when token is pod-bound.
var warnAfter int64
exp := out.Spec.ExpirationSeconds
if r.extendExpiration && pod != nil && out.Spec.ExpirationSeconds == token.WarnOnlyBoundTokenExpirationSeconds {
warnAfter = exp
exp = token.ExpirationExtensionSeconds
}
// Save current time before building the token, to make sure the expiration
// returned in TokenRequestStatus would be earlier than exp field in token.
nowTime := time.Now()
sc, pc := token.Claims(*svcacct, pod, secret, exp, warnAfter, out.Spec.Audiences)
tokdata, err := r.issuer.GenerateToken(sc, pc)
if err != nil {
return nil, fmt.Errorf("failed to generate token: %v", err)
@ -129,7 +145,7 @@ func (r *TokenREST) Create(ctx context.Context, name string, obj runtime.Object,
out.Status = authenticationapi.TokenRequestStatus{
Token: tokdata,
ExpirationTimestamp: metav1.Time{Time: sc.Expiry.Time()},
ExpirationTimestamp: metav1.Time{Time: nowTime.Add(time.Duration(out.Spec.ExpirationSeconds) * time.Second)},
}
return out, nil
}

View File

@ -12,6 +12,7 @@ go_library(
"claims.go",
"jwt.go",
"legacy.go",
"metrics.go",
"openidmetadata.go",
"util.go",
],
@ -21,9 +22,12 @@ go_library(
"//staging/src/k8s.io/api/core/v1:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/util/errors:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/util/sets:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/audit:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/authentication/authenticator:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/authentication/serviceaccount:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/authentication/user:go_default_library",
"//staging/src/k8s.io/component-base/metrics:go_default_library",
"//staging/src/k8s.io/component-base/metrics/legacyregistry:go_default_library",
"//vendor/gopkg.in/square/go-jose.v2:go_default_library",
"//vendor/gopkg.in/square/go-jose.v2/jwt:go_default_library",
"//vendor/k8s.io/klog:go_default_library",

View File

@ -17,17 +17,26 @@ limitations under the License.
package serviceaccount
import (
"context"
"errors"
"fmt"
"time"
"gopkg.in/square/go-jose.v2/jwt"
"k8s.io/klog"
"k8s.io/apiserver/pkg/audit"
apiserverserviceaccount "k8s.io/apiserver/pkg/authentication/serviceaccount"
"k8s.io/klog"
"k8s.io/kubernetes/pkg/apis/core"
)
const (
// Injected bound service account token expiration which triggers monitoring of its time-bound feature.
WarnOnlyBoundTokenExpirationSeconds = 60*60 + 7
// Extended expiration for those modifed tokens involved in safe rollout if time-bound feature.
ExpirationExtensionSeconds = 24 * 365 * 60 * 60
)
// time.Now stubbed out to allow testing
var now = time.Now
@ -40,6 +49,7 @@ type kubernetes struct {
Svcacct ref `json:"serviceaccount,omitempty"`
Pod *ref `json:"pod,omitempty"`
Secret *ref `json:"secret,omitempty"`
WarnAfter jwt.NumericDate `json:"warnafter,omitempty"`
}
type ref struct {
@ -47,7 +57,7 @@ type ref struct {
UID string `json:"uid,omitempty"`
}
func Claims(sa core.ServiceAccount, pod *core.Pod, secret *core.Secret, expirationSeconds int64, audience []string) (*jwt.Claims, interface{}) {
func Claims(sa core.ServiceAccount, pod *core.Pod, secret *core.Secret, expirationSeconds, warnafter int64, audience []string) (*jwt.Claims, interface{}) {
now := now()
sc := &jwt.Claims{
Subject: apiserverserviceaccount.MakeUsername(sa.Namespace, sa.Name),
@ -77,6 +87,11 @@ func Claims(sa core.ServiceAccount, pod *core.Pod, secret *core.Secret, expirati
UID: string(secret.UID),
}
}
if warnafter != 0 {
pc.Kubernetes.WarnAfter = jwt.NewNumericDate(now.Add(time.Duration(warnafter) * time.Second))
}
return sc, pc
}
@ -92,7 +107,7 @@ type validator struct {
var _ = Validator(&validator{})
func (v *validator) Validate(_ string, public *jwt.Claims, privateObj interface{}) (*ServiceAccountInfo, error) {
func (v *validator) Validate(ctx context.Context, _ string, public *jwt.Claims, privateObj interface{}) (*ServiceAccountInfo, error) {
private, ok := privateObj.(*privateClaims)
if !ok {
klog.Errorf("jwt validator expected private claim of type *privateClaims but got: %T", privateObj)
@ -169,6 +184,19 @@ func (v *validator) Validate(_ string, public *jwt.Claims, privateObj interface{
podUID = podref.UID
}
// Check special 'warnafter' field for projected service account token transition.
warnafter := private.Kubernetes.WarnAfter
if warnafter != 0 {
if nowTime.After(warnafter.Time()) {
secondsAfterWarn := nowTime.Unix() - warnafter.Time().Unix()
auditInfo := fmt.Sprintf("subject: %s, seconds after warning threshold: %d", public.Subject, secondsAfterWarn)
audit.AddAuditAnnotation(ctx, "authentication.k8s.io/stale-token", auditInfo)
staleTokensTotal.Inc()
} else {
validTokensTotal.Inc()
}
}
return &ServiceAccountInfo{
Namespace: private.Kubernetes.Namespace,
Name: private.Kubernetes.Svcacct.Name,

View File

@ -17,6 +17,7 @@ limitations under the License.
package serviceaccount
import (
"context"
"encoding/json"
"fmt"
"testing"
@ -66,6 +67,7 @@ func TestClaims(t *testing.T) {
pod *core.Pod
sec *core.Secret
exp int64
warnafter int64
aud []string
// desired
sc *jwt.Claims
@ -161,6 +163,31 @@ func TestClaims(t *testing.T) {
},
},
},
{
// warn after provided
sa: sa,
pod: pod,
sec: sec,
exp: 60 * 60 * 24,
warnafter: 60 * 60,
// nil audience
aud: nil,
sc: &jwt.Claims{
Subject: "system:serviceaccount:myns:mysvcacct",
IssuedAt: jwt.NumericDate(1514764800),
NotBefore: jwt.NumericDate(1514764800),
Expiry: jwt.NumericDate(1514764800 + 60*60*24),
},
pc: &privateClaims{
Kubernetes: kubernetes{
Namespace: "myns",
Svcacct: ref{Name: "mysvcacct", UID: "mysvcacct-uid"},
Pod: &ref{Name: "mypod", UID: "mypod-uid"},
WarnAfter: jwt.NumericDate(1514764800 + 60*60),
},
},
},
}
for i, c := range cs {
t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) {
@ -175,7 +202,7 @@ func TestClaims(t *testing.T) {
return string(b)
}
sc, pc := Claims(c.sa, c.pod, c.sec, c.exp, c.aud)
sc, pc := Claims(c.sa, c.pod, c.sec, c.exp, c.warnafter, c.aud)
if spew(sc) != spew(c.sc) {
t.Errorf("standard claims differed\n\tsaw:\t%s\n\twant:\t%s", spew(sc), spew(c.sc))
}
@ -310,7 +337,7 @@ func TestValidatePrivateClaims(t *testing.T) {
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
v := &validator{tc.getter}
_, err := v.Validate("", &jwt.Claims{Expiry: jwt.NumericDate(nowUnix)}, tc.private)
_, err := v.Validate(context.Background(), "", &jwt.Claims{Expiry: jwt.NumericDate(nowUnix)}, tc.private)
if err != nil && !tc.expectErr {
t.Fatal(err)
}

View File

@ -33,6 +33,7 @@ import (
v1 "k8s.io/api/core/v1"
utilerrors "k8s.io/apimachinery/pkg/util/errors"
"k8s.io/apiserver/pkg/audit"
"k8s.io/apiserver/pkg/authentication/authenticator"
)
@ -244,7 +245,7 @@ type Validator interface {
// Validate validates a token and returns user information or an error.
// Validator can assume that the issuer and signature of a token are already
// verified when this function is called.
Validate(tokenData string, public *jwt.Claims, private interface{}) (*ServiceAccountInfo, error)
Validate(ctx context.Context, tokenData string, public *jwt.Claims, private interface{}) (*ServiceAccountInfo, error)
// NewPrivateClaims returns a struct that the authenticator should
// deserialize the JWT payload into. The authenticator may then pass this
// struct back to the Validator as the 'private' argument to a Validate()
@ -287,6 +288,8 @@ func (j *jwtTokenAuthenticator) AuthenticateToken(ctx context.Context, tokenData
tokenAudiences := authenticator.Audiences(public.Audience)
if len(tokenAudiences) == 0 {
// only apiserver audiences are allowed for legacy tokens
audit.AddAuditAnnotation(ctx, "authentication.k8s.io/legacy-token", public.Subject)
legacyTokensTotal.Inc()
tokenAudiences = j.implicitAuds
}
@ -303,7 +306,7 @@ func (j *jwtTokenAuthenticator) AuthenticateToken(ctx context.Context, tokenData
// If we get here, we have a token with a recognized signature and
// issuer string.
sa, err := j.validator.Validate(tokenData, public, private)
sa, err := j.validator.Validate(ctx, tokenData, public, private)
if err != nil {
return nil, false, err
}

View File

@ -18,6 +18,7 @@ package serviceaccount
import (
"bytes"
"context"
"errors"
"fmt"
@ -62,7 +63,7 @@ type legacyValidator struct {
var _ = Validator(&legacyValidator{})
func (v *legacyValidator) Validate(tokenData string, public *jwt.Claims, privateObj interface{}) (*ServiceAccountInfo, error) {
func (v *legacyValidator) Validate(ctx context.Context, tokenData string, public *jwt.Claims, privateObj interface{}) (*ServiceAccountInfo, error) {
private, ok := privateObj.(*legacyPrivateClaims)
if !ok {
klog.Errorf("jwt validator expected private claim of type *legacyPrivateClaims but got: %T", privateObj)

View File

@ -0,0 +1,63 @@
/*
Copyright 2020 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package serviceaccount
import (
"k8s.io/component-base/metrics"
"k8s.io/component-base/metrics/legacyregistry"
)
const kubeServiceAccountSubsystem = "serviceaccount"
var (
// LegacyTokensTotal is the number of legacy tokens used against apiserver.
legacyTokensTotal = metrics.NewCounter(
&metrics.CounterOpts{
Subsystem: kubeServiceAccountSubsystem,
Name: "legacy_tokens_total",
Help: "Cumulative legacy service account tokens used",
StabilityLevel: metrics.ALPHA,
},
)
// StaleTokensTotal is the number of stale projected tokens not refreshed on
// client side.
staleTokensTotal = metrics.NewCounter(
&metrics.CounterOpts{
Subsystem: kubeServiceAccountSubsystem,
Name: "stale_tokens_total",
Help: "Cumulative stale projected service account tokens used",
StabilityLevel: metrics.ALPHA,
},
)
// ValidTokensTotal is the number of valid projected tokens used.
validTokensTotal = metrics.NewCounter(
&metrics.CounterOpts{
Subsystem: kubeServiceAccountSubsystem,
Name: "valid_tokens_total",
Help: "Cumulative valid projected service account tokens used",
StabilityLevel: metrics.ALPHA,
},
)
)
func init() {
legacyregistry.MustRegister(legacyTokensTotal)
legacyregistry.MustRegister(staleTokensTotal)
legacyregistry.MustRegister(validTokensTotal)
}

View File

@ -530,7 +530,7 @@ func (s *Plugin) createVolume(tokenVolumeName, secretName string) api.Volume {
{
ServiceAccountToken: &api.ServiceAccountTokenProjection{
Path: "token",
ExpirationSeconds: 60 * 60,
ExpirationSeconds: serviceaccount.WarnOnlyBoundTokenExpirationSeconds,
},
},
{

View File

@ -252,7 +252,7 @@ func TestAssignsDefaultServiceAccountAndBoundTokenWithNoSecretTokens(t *testing.
VolumeSource: api.VolumeSource{
Projected: &api.ProjectedVolumeSource{
Sources: []api.VolumeProjection{
{ServiceAccountToken: &api.ServiceAccountTokenProjection{ExpirationSeconds: 3600, Path: "token"}},
{ServiceAccountToken: &api.ServiceAccountTokenProjection{ExpirationSeconds: 3607, Path: "token"}},
{ConfigMap: &api.ConfigMapProjection{LocalObjectReference: api.LocalObjectReference{Name: "kube-root-ca.crt"}, Items: []api.KeyToPath{{Key: "ca.crt", Path: "ca.crt"}}}},
{DownwardAPI: &api.DownwardAPIProjection{Items: []api.DownwardAPIVolumeFile{{Path: "namespace", FieldRef: &api.ObjectFieldSelector{APIVersion: "v1", FieldPath: "metadata.namespace"}}}}},
},

View File

@ -27,6 +27,7 @@ import (
"net/http"
"net/url"
"reflect"
"strconv"
"strings"
"testing"
"time"
@ -77,7 +78,7 @@ func TestServiceAccountTokenCreate(t *testing.T) {
const iss = "https://foo.bar.example.com"
aud := authenticator.Audiences{"api"}
maxExpirationSeconds := int64(60 * 60)
maxExpirationSeconds := int64(60 * 60 * 2)
maxExpirationDuration, err := time.ParseDuration(fmt.Sprintf("%ds", maxExpirationSeconds))
if err != nil {
t.Fatalf("err: %v", err)
@ -116,6 +117,7 @@ func TestServiceAccountTokenCreate(t *testing.T) {
masterConfig.ExtraConfig.ServiceAccountIssuer = tokenGenerator
masterConfig.ExtraConfig.ServiceAccountMaxExpiration = maxExpirationDuration
masterConfig.GenericConfig.Authentication.APIAudiences = aud
masterConfig.ExtraConfig.ExtendExpiration = true
masterConfig.ExtraConfig.ServiceAccountIssuerURL = iss
masterConfig.ExtraConfig.ServiceAccountJWKSURI = ""
@ -373,7 +375,7 @@ func TestServiceAccountTokenCreate(t *testing.T) {
coresa := core.ServiceAccount{
ObjectMeta: sa.ObjectMeta,
}
_, pc := serviceaccount.Claims(coresa, nil, nil, 0, nil)
_, pc := serviceaccount.Claims(coresa, nil, nil, 0, 0, nil)
tok, err := masterConfig.ExtraConfig.ServiceAccountIssuer.GenerateToken(sc, pc)
if err != nil {
t.Fatalf("err signing expired token: %v", err)
@ -383,6 +385,60 @@ func TestServiceAccountTokenCreate(t *testing.T) {
doTokenReview(t, cs, treq, true)
})
t.Run("expiration extended token", func(t *testing.T) {
var requestExp int64 = 60*60 + 7
treq := &authenticationv1.TokenRequest{
Spec: authenticationv1.TokenRequestSpec{
Audiences: []string{"api"},
ExpirationSeconds: &requestExp,
BoundObjectRef: &authenticationv1.BoundObjectReference{
Kind: "Pod",
APIVersion: "v1",
Name: pod.Name,
},
},
}
sa, del := createDeleteSvcAcct(t, cs, sa)
defer del()
pod, delPod := createDeletePod(t, cs, pod)
defer delPod()
treq.Spec.BoundObjectRef.UID = pod.UID
treq, err = cs.CoreV1().ServiceAccounts(sa.Namespace).CreateToken(context.TODO(), sa.Name, treq, metav1.CreateOptions{})
if err != nil {
t.Fatalf("err: %v", err)
}
doTokenReview(t, cs, treq, false)
// Give some tolerance to avoid flakiness since we are using real time.
var leeway int64 = 2
actualExpiry := jwt.NewNumericDate(time.Now().Add(time.Duration(24*365) * time.Hour))
assumedExpiry := jwt.NewNumericDate(time.Now().Add(time.Duration(requestExp) * time.Second))
exp, err := strconv.ParseInt(getSubObject(t, getPayload(t, treq.Status.Token), "exp"), 10, 64)
if err != nil {
t.Fatalf("error parsing exp: %v", err)
}
warnafter, err := strconv.ParseInt(getSubObject(t, getPayload(t, treq.Status.Token), "kubernetes.io", "warnafter"), 10, 64)
if err != nil {
t.Fatalf("error parsing warnafter: %v", err)
}
if exp < int64(actualExpiry)-leeway || exp > int64(actualExpiry)+leeway {
t.Errorf("unexpected token exp %d, should within range of %d +- %d seconds", exp, actualExpiry, leeway)
}
if warnafter < int64(assumedExpiry)-leeway || warnafter > int64(assumedExpiry)+leeway {
t.Errorf("unexpected token warnafter %d, should within range of %d +- %d seconds", warnafter, assumedExpiry, leeway)
}
checkExpiration(t, treq, requestExp)
expStatus := treq.Status.ExpirationTimestamp.Time.Unix()
if expStatus < int64(assumedExpiry)-leeway || warnafter > int64(assumedExpiry)+leeway {
t.Errorf("unexpected expiration returned in tokenrequest status %d, should within range of %d +- %d seconds", expStatus, assumedExpiry, leeway)
}
})
t.Run("a token without an api audience is invalid", func(t *testing.T) {
treq := &authenticationv1.TokenRequest{
Spec: authenticationv1.TokenRequestSpec{