mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-20 10:20:51 +00:00
Monitoring safe rollout of time-bound service account token.
This commit is contained in:
parent
57108f6c3e
commit
ae0e52d28c
@ -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)
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
|
63
pkg/serviceaccount/metrics.go
Normal file
63
pkg/serviceaccount/metrics.go
Normal 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)
|
||||
}
|
@ -530,7 +530,7 @@ func (s *Plugin) createVolume(tokenVolumeName, secretName string) api.Volume {
|
||||
{
|
||||
ServiceAccountToken: &api.ServiceAccountTokenProjection{
|
||||
Path: "token",
|
||||
ExpirationSeconds: 60 * 60,
|
||||
ExpirationSeconds: serviceaccount.WarnOnlyBoundTokenExpirationSeconds,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -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"}}}}},
|
||||
},
|
||||
|
@ -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{
|
||||
|
Loading…
Reference in New Issue
Block a user