Merge pull request #129816 from sambdavidson/master

Improve SA max token expiry with external signer logic, and plumb extended expiry duration.
This commit is contained in:
Kubernetes Prow Robot
2025-01-29 16:41:29 -08:00
committed by GitHub
13 changed files with 138 additions and 108 deletions

View File

@@ -223,7 +223,7 @@ func (p *legacyProvider) NewRESTStorage(apiResourceConfigSource serverstorage.AP
utilfeature.DefaultFeatureGate.Enabled(features.ServiceAccountTokenPodNodeInfo) {
nodeGetter = nodeStorage.Node.Store
}
serviceAccountStorage, err = serviceaccountstore.NewREST(restOptionsGetter, p.ServiceAccountIssuer, p.APIAudiences, p.ServiceAccountMaxExpiration, podStorage.Pod.Store, storage["secrets"].(rest.Getter), nodeGetter, p.ExtendExpiration, p.IsTokenSignerExternal)
serviceAccountStorage, err = serviceaccountstore.NewREST(restOptionsGetter, p.ServiceAccountIssuer, p.APIAudiences, p.ServiceAccountMaxExpiration, podStorage.Pod.Store, storage["secrets"].(rest.Getter), nodeGetter, p.ExtendExpiration, p.MaxExtendedExpiration)
if err != nil {
return genericapiserver.APIGroupInfo{}, err
}

View File

@@ -57,7 +57,7 @@ type GenericConfig struct {
ServiceAccountIssuer serviceaccount.TokenGenerator
ServiceAccountMaxExpiration time.Duration
ExtendExpiration bool
IsTokenSignerExternal bool
MaxExtendedExpiration time.Duration
APIAudiences authenticator.Audiences
@@ -103,9 +103,9 @@ func (c *GenericConfig) NewRESTStorage(apiResourceConfigSource serverstorage.API
var serviceAccountStorage *serviceaccountstore.REST
if c.ServiceAccountIssuer != nil {
serviceAccountStorage, err = serviceaccountstore.NewREST(restOptionsGetter, c.ServiceAccountIssuer, c.APIAudiences, c.ServiceAccountMaxExpiration, newNotFoundGetter(schema.GroupResource{Resource: "pods"}), secretStorage.Store, newNotFoundGetter(schema.GroupResource{Resource: "nodes"}), c.ExtendExpiration, c.IsTokenSignerExternal)
serviceAccountStorage, err = serviceaccountstore.NewREST(restOptionsGetter, c.ServiceAccountIssuer, c.APIAudiences, c.ServiceAccountMaxExpiration, newNotFoundGetter(schema.GroupResource{Resource: "pods"}), secretStorage.Store, newNotFoundGetter(schema.GroupResource{Resource: "nodes"}), c.ExtendExpiration, c.MaxExtendedExpiration)
} else {
serviceAccountStorage, err = serviceaccountstore.NewREST(restOptionsGetter, nil, nil, 0, newNotFoundGetter(schema.GroupResource{Resource: "pods"}), newNotFoundGetter(schema.GroupResource{Resource: "secrets"}), newNotFoundGetter(schema.GroupResource{Resource: "nodes"}), false, c.IsTokenSignerExternal)
serviceAccountStorage, err = serviceaccountstore.NewREST(restOptionsGetter, nil, nil, 0, newNotFoundGetter(schema.GroupResource{Resource: "pods"}), newNotFoundGetter(schema.GroupResource{Resource: "secrets"}), newNotFoundGetter(schema.GroupResource{Resource: "nodes"}), false, c.MaxExtendedExpiration)
}
if err != nil {
return genericapiserver.APIGroupInfo{}, err

View File

@@ -39,7 +39,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, nodeStorage rest.Getter, extendExpiration bool, isTokenSignerExternal bool) (*REST, error) {
func NewREST(optsGetter generic.RESTOptionsGetter, issuer token.TokenGenerator, auds authenticator.Audiences, max time.Duration, podStorage, secretStorage, nodeStorage rest.Getter, extendExpiration bool, maxExtendedExpiration time.Duration) (*REST, error) {
store := &genericregistry.Store{
NewFunc: func() runtime.Object { return &api.ServiceAccount{} },
NewListFunc: func() runtime.Object { return &api.ServiceAccountList{} },
@@ -61,16 +61,16 @@ func NewREST(optsGetter generic.RESTOptionsGetter, issuer token.TokenGenerator,
var trest *TokenREST
if issuer != nil && podStorage != nil && secretStorage != nil {
trest = &TokenREST{
svcaccts: store,
pods: podStorage,
secrets: secretStorage,
nodes: nodeStorage,
issuer: issuer,
auds: auds,
audsSet: sets.NewString(auds...),
maxExpirationSeconds: int64(max.Seconds()),
extendExpiration: extendExpiration,
isTokenSignerExternal: isTokenSignerExternal,
svcaccts: store,
pods: podStorage,
secrets: secretStorage,
nodes: nodeStorage,
issuer: issuer,
auds: auds,
audsSet: sets.NewString(auds...),
maxExpirationSeconds: int64(max.Seconds()),
maxExtendedExpirationSeconds: int64(maxExtendedExpiration.Seconds()),
extendExpiration: extendExpiration,
}
}

View File

@@ -19,6 +19,7 @@ package storage
import (
"context"
"testing"
"time"
"gopkg.in/go-jose/go-jose.v2/jwt"
@@ -55,7 +56,7 @@ func newTokenStorage(t *testing.T, issuer token.TokenGenerator, auds authenticat
ResourcePrefix: "serviceaccounts",
}
// set issuer, podStore and secretStore to allow the token endpoint to be initialised
rest, err := NewREST(restOptions, issuer, auds, 0, podStorage, secretStorage, nodeStorage, false, false)
rest, err := NewREST(restOptions, issuer, auds, 0, podStorage, secretStorage, nodeStorage, false, time.Hour*9999)
if err != nil {
t.Fatalf("unexpected error from REST storage: %v", err)
}

View File

@@ -56,16 +56,16 @@ func (r *TokenREST) Destroy() {
}
type TokenREST struct {
svcaccts rest.Getter
pods rest.Getter
secrets rest.Getter
nodes rest.Getter
issuer token.TokenGenerator
auds authenticator.Audiences
audsSet sets.String
maxExpirationSeconds int64
extendExpiration bool
isTokenSignerExternal bool
svcaccts rest.Getter
pods rest.Getter
secrets rest.Getter
nodes rest.Getter
issuer token.TokenGenerator
auds authenticator.Audiences
audsSet sets.String
maxExpirationSeconds int64
extendExpiration bool
maxExtendedExpirationSeconds int64
}
var _ = rest.NamedCreater(&TokenREST{})
@@ -218,13 +218,7 @@ func (r *TokenREST) Create(ctx context.Context, name string, obj runtime.Object,
exp := req.Spec.ExpirationSeconds
if r.extendExpiration && pod != nil && req.Spec.ExpirationSeconds == token.WarnOnlyBoundTokenExpirationSeconds && r.isKubeAudiences(req.Spec.Audiences) {
warnAfter = exp
// If token issuer is external-jwt-signer, then choose the smaller of
// ExpirationExtensionSeconds and max token lifetime supported by external signer.
if r.isTokenSignerExternal {
exp = min(r.maxExpirationSeconds, token.ExpirationExtensionSeconds)
} else {
exp = token.ExpirationExtensionSeconds
}
exp = r.maxExtendedExpirationSeconds
}
sc, pc, err := token.Claims(*svcacct, pod, secret, node, exp, warnAfter, req.Spec.Audiences)

View File

@@ -42,46 +42,53 @@ import (
func TestCreate_Token_WithExpiryCap(t *testing.T) {
testcases := []struct {
desc string
extendExpiration bool
maxExpirationSeconds int
expectedTokenAgeSec int
isExternal bool
desc string
extendExpiration bool
maxExpirationSeconds int
maxExtendedExpirationSeconds int
expectedTokenAgeSec int
}{
{
desc: "maxExpirationSeconds honoured",
extendExpiration: true,
maxExpirationSeconds: 5 * 60 * 60, // 5h
expectedTokenAgeSec: 5 * 60 * 60, // 5h
isExternal: true,
desc: "passed expiration respected if less than max",
extendExpiration: false,
maxExpirationSeconds: 5 * 60 * 60, // 5h
maxExtendedExpirationSeconds: token.ExpirationExtensionSeconds, // 1y
expectedTokenAgeSec: token.WarnOnlyBoundTokenExpirationSeconds, // 1h 7s
},
{
desc: "ExpirationExtensionSeconds used for exp",
extendExpiration: true,
maxExpirationSeconds: 2 * 365 * 24 * 60 * 60, // 2 years
expectedTokenAgeSec: token.ExpirationExtensionSeconds, // 1y
isExternal: true,
desc: "maxExtendedExpirationSeconds honoured",
extendExpiration: true,
maxExpirationSeconds: 2 * 60 * 60, // 2h
maxExtendedExpirationSeconds: 5 * 60 * 60, // 5h
expectedTokenAgeSec: 5 * 60 * 60, // 5h
},
{
desc: "ExpirationExtensionSeconds used for exp",
extendExpiration: true,
maxExpirationSeconds: 5 * 60 * 60, // 5h
expectedTokenAgeSec: token.ExpirationExtensionSeconds, // 1y
isExternal: false,
desc: "ExpirationExtensionSeconds used for exp",
extendExpiration: true,
maxExpirationSeconds: 2 * 365 * 24 * 60 * 60, // 2y
maxExtendedExpirationSeconds: token.ExpirationExtensionSeconds, // 1y
expectedTokenAgeSec: token.ExpirationExtensionSeconds, // 1y
},
{
desc: "requested time use with extension disabled",
extendExpiration: false,
maxExpirationSeconds: 5 * 60 * 60, // 5h
expectedTokenAgeSec: 3607, // 1h
isExternal: true,
desc: "ExpirationSeconds used for exp",
extendExpiration: true,
maxExpirationSeconds: 5 * 60 * 60, // 5h
maxExtendedExpirationSeconds: token.ExpirationExtensionSeconds, // 1y
expectedTokenAgeSec: token.ExpirationExtensionSeconds, // 1y
},
{
desc: "maxExpirationSeconds honoured with extension disabled",
extendExpiration: false,
maxExpirationSeconds: 30 * 60, // 30m
expectedTokenAgeSec: 30 * 60, // 30m
isExternal: true,
desc: "requested time use with extension disabled",
extendExpiration: false,
maxExpirationSeconds: 5 * 60 * 60, // 5h
expectedTokenAgeSec: 3607, // 1h
maxExtendedExpirationSeconds: token.ExpirationExtensionSeconds,
},
{
desc: "maxExpirationSeconds honoured with extension disabled",
extendExpiration: false,
maxExpirationSeconds: 30 * 60, // 30m
expectedTokenAgeSec: 30 * 60, // 30m
maxExtendedExpirationSeconds: token.ExpirationExtensionSeconds,
},
}
@@ -127,7 +134,7 @@ func TestCreate_Token_WithExpiryCap(t *testing.T) {
ctx = request.WithNamespace(ctx, serviceAccount.Namespace)
storage.Token.extendExpiration = tc.extendExpiration
storage.Token.maxExpirationSeconds = int64(tc.maxExpirationSeconds)
storage.Token.isTokenSignerExternal = tc.isExternal
storage.Token.maxExtendedExpirationSeconds = int64(tc.maxExtendedExpirationSeconds)
tokenReqTimeStamp := time.Now()
out, err := storage.Token.Create(ctx, serviceAccount.Name, &authenticationapi.TokenRequest{
@@ -161,13 +168,15 @@ func TestCreate_Token_WithExpiryCap(t *testing.T) {
t.Fatalf("Error unmarshalling Claims: %v", err)
}
structuredClaim.Expiry.Time()
upperBound := tokenReqTimeStamp.Add(time.Duration(tc.expectedTokenAgeSec+10) * time.Second)
lowerBound := tokenReqTimeStamp.Add(time.Duration(tc.expectedTokenAgeSec-10) * time.Second)
confidenceInterval := 10 // seconds
upperBound := tokenReqTimeStamp.Add(time.Duration(tc.expectedTokenAgeSec+confidenceInterval) * time.Second)
lowerBound := tokenReqTimeStamp.Add(time.Duration(tc.expectedTokenAgeSec-confidenceInterval) * time.Second)
// check for token expiration with a toleration of +/-10s after tokenReqTimeStamp to make for latencies.
if structuredClaim.Expiry.Time().After(upperBound) ||
structuredClaim.Expiry.Time().Before(lowerBound) {
t.Fatalf("expected token expiration to be between %v to %v\n was %v", upperBound, lowerBound, structuredClaim.Expiry.Time())
expiryDiff := structuredClaim.Expiry.Time().Sub(tokenReqTimeStamp)
t.Fatalf("expected token expiration to be %v (±%ds) in the future, was %v", time.Duration(tc.expectedTokenAgeSec)*time.Second, confidenceInterval, expiryDiff)
}
})