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
commit 76506f1d87
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 138 additions and 108 deletions

View File

@ -44,6 +44,7 @@ import (
"k8s.io/kubernetes/pkg/controlplane/reconcilers" "k8s.io/kubernetes/pkg/controlplane/reconcilers"
kubeoptions "k8s.io/kubernetes/pkg/kubeapiserver/options" kubeoptions "k8s.io/kubernetes/pkg/kubeapiserver/options"
kubeletclient "k8s.io/kubernetes/pkg/kubelet/client" kubeletclient "k8s.io/kubernetes/pkg/kubelet/client"
"k8s.io/kubernetes/pkg/serviceaccount"
netutils "k8s.io/utils/net" netutils "k8s.io/utils/net"
) )
@ -272,8 +273,9 @@ func TestAddFlags(t *testing.T) {
OIDC: s.Authentication.OIDC, OIDC: s.Authentication.OIDC,
RequestHeader: &apiserveroptions.RequestHeaderAuthenticationOptions{}, RequestHeader: &apiserveroptions.RequestHeaderAuthenticationOptions{},
ServiceAccounts: &kubeoptions.ServiceAccountAuthenticationOptions{ ServiceAccounts: &kubeoptions.ServiceAccountAuthenticationOptions{
Lookup: true, Lookup: true,
ExtendExpiration: true, ExtendExpiration: true,
MaxExtendedExpiration: serviceaccount.ExpirationExtensionSeconds * time.Second,
}, },
TokenFile: &kubeoptions.TokenFileAuthenticationOptions{}, TokenFile: &kubeoptions.TokenFileAuthenticationOptions{},
TokenSuccessCacheTTL: 10 * time.Second, TokenSuccessCacheTTL: 10 * time.Second,
@ -348,7 +350,7 @@ func TestAddFlags(t *testing.T) {
s.Authorization.AreLegacyFlagsSet = nil s.Authorization.AreLegacyFlagsSet = nil
if !reflect.DeepEqual(expected, s) { if !reflect.DeepEqual(expected, s) {
t.Errorf("Got different run options than expected.\nDifference detected on:\n%s", cmp.Diff(expected, s, cmpopts.IgnoreUnexported(admission.Plugins{}, kubeoptions.OIDCAuthenticationOptions{}, kubeoptions.AnonymousAuthenticationOptions{}))) t.Errorf("Got different run options than expected.\nDifference detected on:\n%s", cmp.Diff(expected, s, cmpopts.IgnoreFields(apiserveroptions.ServerRunOptions{}, "ComponentGlobalsRegistry"), cmpopts.IgnoreUnexported(admission.Plugins{}, kubeoptions.OIDCAuthenticationOptions{}, kubeoptions.AnonymousAuthenticationOptions{})))
} }
testEffectiveVersion := s.GenericServerRunOptions.ComponentGlobalsRegistry.EffectiveVersionFor("test") testEffectiveVersion := s.GenericServerRunOptions.ComponentGlobalsRegistry.EffectiveVersionFor("test")
if testEffectiveVersion.EmulationVersion().String() != "1.31" { if testEffectiveVersion.EmulationVersion().String() != "1.31" {

View File

@ -52,8 +52,8 @@ func (c *CompletedConfig) NewCoreGenericConfig() *corerest.GenericConfig {
LoopbackClientConfig: c.Generic.LoopbackClientConfig, LoopbackClientConfig: c.Generic.LoopbackClientConfig,
ServiceAccountIssuer: c.Extra.ServiceAccountIssuer, ServiceAccountIssuer: c.Extra.ServiceAccountIssuer,
ExtendExpiration: c.Extra.ExtendExpiration, ExtendExpiration: c.Extra.ExtendExpiration,
IsTokenSignerExternal: c.Extra.IsTokenSignerExternal,
ServiceAccountMaxExpiration: c.Extra.ServiceAccountMaxExpiration, ServiceAccountMaxExpiration: c.Extra.ServiceAccountMaxExpiration,
MaxExtendedExpiration: c.Extra.ServiceAccountExtendedMaxExpiration,
APIAudiences: c.Generic.Authentication.APIAudiences, APIAudiences: c.Generic.Authentication.APIAudiences,
Informers: c.Extra.VersionedInformers, Informers: c.Extra.VersionedInformers,
} }

View File

@ -89,10 +89,10 @@ type Extra struct {
// version skew. If unset, AdvertiseAddress/BindAddress will be used. // version skew. If unset, AdvertiseAddress/BindAddress will be used.
PeerAdvertiseAddress peerreconcilers.PeerAdvertiseAddress PeerAdvertiseAddress peerreconcilers.PeerAdvertiseAddress
ServiceAccountIssuer serviceaccount.TokenGenerator ServiceAccountIssuer serviceaccount.TokenGenerator
ServiceAccountMaxExpiration time.Duration ServiceAccountMaxExpiration time.Duration
ExtendExpiration bool ServiceAccountExtendedMaxExpiration time.Duration
IsTokenSignerExternal bool ExtendExpiration bool
// ServiceAccountIssuerDiscovery // ServiceAccountIssuerDiscovery
ServiceAccountIssuerURL string ServiceAccountIssuerURL string
@ -299,10 +299,10 @@ func CreateConfig(
ProxyTransport: proxyTransport, ProxyTransport: proxyTransport,
SystemNamespaces: opts.SystemNamespaces, SystemNamespaces: opts.SystemNamespaces,
ServiceAccountIssuer: opts.ServiceAccountIssuer, ServiceAccountIssuer: opts.ServiceAccountIssuer,
ServiceAccountMaxExpiration: opts.ServiceAccountTokenMaxExpiration, ServiceAccountMaxExpiration: opts.ServiceAccountTokenMaxExpiration,
ExtendExpiration: opts.Authentication.ServiceAccounts.ExtendExpiration, ServiceAccountExtendedMaxExpiration: opts.Authentication.ServiceAccounts.MaxExtendedExpiration,
IsTokenSignerExternal: opts.Authentication.ServiceAccounts.IsTokenSignerExternal, ExtendExpiration: opts.Authentication.ServiceAccounts.ExtendExpiration,
VersionedInformers: versionedInformers, VersionedInformers: versionedInformers,
}, },

View File

@ -316,15 +316,19 @@ func (o *Options) completeServiceAccountOptions(ctx context.Context, completed *
if metadata.MaxTokenExpirationSeconds < validation.MinTokenAgeSec { if metadata.MaxTokenExpirationSeconds < validation.MinTokenAgeSec {
return fmt.Errorf("max token life supported by external-jwt-signer (%ds) is less than acceptable (min %ds)", metadata.MaxTokenExpirationSeconds, validation.MinTokenAgeSec) return fmt.Errorf("max token life supported by external-jwt-signer (%ds) is less than acceptable (min %ds)", metadata.MaxTokenExpirationSeconds, validation.MinTokenAgeSec)
} }
if completed.Authentication.ServiceAccounts.MaxExpiration != 0 { maxExternalExpiration := time.Duration(metadata.MaxTokenExpirationSeconds) * time.Second
return fmt.Errorf("service-account-max-token-expiration and service-account-signing-endpoint are mutually exclusive and cannot be set at the same time") switch {
case completed.Authentication.ServiceAccounts.MaxExpiration == 0:
completed.Authentication.ServiceAccounts.MaxExpiration = maxExternalExpiration
case completed.Authentication.ServiceAccounts.MaxExpiration > maxExternalExpiration:
return fmt.Errorf("service-account-max-token-expiration cannot be set longer than the token expiration supported by service-account-signing-endpoint: %s > %s", completed.Authentication.ServiceAccounts.MaxExpiration, maxExternalExpiration)
} }
transitionWarningFmt = "service-account-extend-token-expiration is true, in order to correctly trigger safe transition logic, token lifetime supported by external-jwt-signer must be longer than %d seconds (currently %s)" transitionWarningFmt = "service-account-extend-token-expiration is true, in order to correctly trigger safe transition logic, token lifetime supported by external-jwt-signer must be longer than %d seconds (currently %s)"
expExtensionWarningFmt = "service-account-extend-token-expiration is true, tokens validity will be caped at the smaller of %d seconds and maximum token lifetime supported by external-jwt-signer (%s)" expExtensionWarningFmt = "service-account-extend-token-expiration is true, tokens validity will be caped at the smaller of %d seconds and maximum token lifetime supported by external-jwt-signer (%s)"
completed.ServiceAccountIssuer = plugin completed.ServiceAccountIssuer = plugin
completed.Authentication.ServiceAccounts.ExternalPublicKeysGetter = cache completed.Authentication.ServiceAccounts.ExternalPublicKeysGetter = cache
completed.Authentication.ServiceAccounts.MaxExpiration = time.Duration(metadata.MaxTokenExpirationSeconds) * time.Second // shorten ExtendedExpiration, if needed, to fit within the external signer's max expiration
completed.Authentication.ServiceAccounts.IsTokenSignerExternal = true completed.Authentication.ServiceAccounts.MaxExtendedExpiration = min(maxExternalExpiration, completed.Authentication.ServiceAccounts.MaxExtendedExpiration)
} }
} }

View File

@ -32,7 +32,6 @@ import (
"github.com/google/go-cmp/cmp/cmpopts" "github.com/google/go-cmp/cmp/cmpopts"
"github.com/spf13/pflag" "github.com/spf13/pflag"
noopoteltrace "go.opentelemetry.io/otel/trace/noop" noopoteltrace "go.opentelemetry.io/otel/trace/noop"
utilruntime "k8s.io/apimachinery/pkg/util/runtime" utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apiserver/pkg/admission" "k8s.io/apiserver/pkg/admission"
apiserveroptions "k8s.io/apiserver/pkg/server/options" apiserveroptions "k8s.io/apiserver/pkg/server/options"
@ -46,6 +45,7 @@ import (
"k8s.io/component-base/metrics" "k8s.io/component-base/metrics"
utilversion "k8s.io/component-base/version" utilversion "k8s.io/component-base/version"
kubeoptions "k8s.io/kubernetes/pkg/kubeapiserver/options" kubeoptions "k8s.io/kubernetes/pkg/kubeapiserver/options"
"k8s.io/kubernetes/pkg/serviceaccount"
v1alpha1testing "k8s.io/kubernetes/pkg/serviceaccount/externaljwt/plugin/testing/v1alpha1" v1alpha1testing "k8s.io/kubernetes/pkg/serviceaccount/externaljwt/plugin/testing/v1alpha1"
netutils "k8s.io/utils/net" netutils "k8s.io/utils/net"
) )
@ -264,8 +264,9 @@ func TestAddFlags(t *testing.T) {
OIDC: s.Authentication.OIDC, OIDC: s.Authentication.OIDC,
RequestHeader: &apiserveroptions.RequestHeaderAuthenticationOptions{}, RequestHeader: &apiserveroptions.RequestHeaderAuthenticationOptions{},
ServiceAccounts: &kubeoptions.ServiceAccountAuthenticationOptions{ ServiceAccounts: &kubeoptions.ServiceAccountAuthenticationOptions{
Lookup: true, Lookup: true,
ExtendExpiration: true, ExtendExpiration: true,
MaxExtendedExpiration: serviceaccount.ExpirationExtensionSeconds * time.Second,
}, },
TokenFile: &kubeoptions.TokenFileAuthenticationOptions{}, TokenFile: &kubeoptions.TokenFileAuthenticationOptions{},
TokenSuccessCacheTTL: 10 * time.Second, TokenSuccessCacheTTL: 10 * time.Second,
@ -309,7 +310,7 @@ func TestAddFlags(t *testing.T) {
s.Authorization.AreLegacyFlagsSet = nil s.Authorization.AreLegacyFlagsSet = nil
if !reflect.DeepEqual(expected, s) { if !reflect.DeepEqual(expected, s) {
t.Errorf("Got different run options than expected.\nDifference detected on:\n%s", cmp.Diff(expected, s, cmpopts.IgnoreUnexported(admission.Plugins{}, kubeoptions.OIDCAuthenticationOptions{}, kubeoptions.AnonymousAuthenticationOptions{}))) t.Errorf("Got different run options than expected.\nDifference detected on:\n%s", cmp.Diff(expected, s, cmpopts.IgnoreFields(apiserveroptions.ServerRunOptions{}, "ComponentGlobalsRegistry"), cmpopts.IgnoreUnexported(admission.Plugins{}, kubeoptions.OIDCAuthenticationOptions{}, kubeoptions.AnonymousAuthenticationOptions{})))
} }
testEffectiveVersion := s.GenericServerRunOptions.ComponentGlobalsRegistry.EffectiveVersionFor("test") testEffectiveVersion := s.GenericServerRunOptions.ComponentGlobalsRegistry.EffectiveVersionFor("test")
@ -352,13 +353,14 @@ func TestCompleteForServiceAccount(t *testing.T) {
externalSigner bool externalSigner bool
signingKeyFiles string signingKeyFiles string
maxExpiration time.Duration maxExpiration time.Duration
maxExtendedExpiration time.Duration
externalMaxExpirationSec int64 externalMaxExpirationSec int64
fetchError error fetchError error
metadataError error metadataError error
wantError error wantError error
expectedMaxtokenExp time.Duration expectedMaxtokenExp time.Duration
expectedIsExternalSigner bool expectedExtendedMaxTokenExp time.Duration
externalPublicKeyGetterPresent bool externalPublicKeyGetterPresent bool
}{ }{
{ {
@ -373,7 +375,7 @@ func TestCompleteForServiceAccount(t *testing.T) {
wantError: fmt.Errorf("service-account-signing-key-file and service-account-signing-endpoint are mutually exclusive and cannot be set at the same time"), wantError: fmt.Errorf("service-account-signing-key-file and service-account-signing-endpoint are mutually exclusive and cannot be set at the same time"),
}, },
{ {
desc: "max token expiration breaching accepteable values", desc: "max token expiration breaching acceptable values",
issuers: []string{ issuers: []string{
"iss", "iss",
}, },
@ -392,35 +394,50 @@ func TestCompleteForServiceAccount(t *testing.T) {
signingKeyFiles: "private_key.pem", signingKeyFiles: "private_key.pem",
maxExpiration: time.Second * 3600, maxExpiration: time.Second * 3600,
expectedIsExternalSigner: false,
externalPublicKeyGetterPresent: false, externalPublicKeyGetterPresent: false,
expectedMaxtokenExp: time.Second * 3600, expectedMaxtokenExp: time.Second * 3600,
}, },
{ {
desc: "signing endpoint provided", desc: "signing endpoint provided, use endpoint expiration",
issuers: []string{ issuers: []string{
"iss", "iss",
}, },
externalSigner: true, externalSigner: true,
signingKeyFiles: "", signingKeyFiles: "",
maxExpiration: 0, maxExpiration: 0,
maxExtendedExpiration: 365 * 24 * time.Hour,
externalMaxExpirationSec: 600, // 10m externalMaxExpirationSec: 600, // 10m
expectedIsExternalSigner: true, expectedMaxtokenExp: 10 * time.Minute,
expectedExtendedMaxTokenExp: 10 * time.Minute,
externalPublicKeyGetterPresent: true, externalPublicKeyGetterPresent: true,
expectedMaxtokenExp: time.Second * 600, // 10m
}, },
{ {
desc: "signing endpoint provided and max token expiration set", desc: "signing endpoint provided, use local smaller expirations",
issuers: []string{ issuers: []string{
"iss", "iss",
}, },
externalSigner: true, externalSigner: true,
signingKeyFiles: "", signingKeyFiles: "",
maxExpiration: time.Second * 3600, maxExpiration: 1 * time.Hour,
externalMaxExpirationSec: 600, // 10m maxExtendedExpiration: 24 * time.Hour,
externalMaxExpirationSec: 31556952, // 1 year
wantError: fmt.Errorf("service-account-max-token-expiration and service-account-signing-endpoint are mutually exclusive and cannot be set at the same time"), expectedMaxtokenExp: 1 * time.Hour,
expectedExtendedMaxTokenExp: 24 * time.Hour,
externalPublicKeyGetterPresent: true,
},
{
desc: "signing endpoint provided and want larger than signer can provide",
issuers: []string{
"iss",
},
externalSigner: true,
signingKeyFiles: "",
maxExpiration: 1 * time.Hour, // want 1hr
externalMaxExpirationSec: 600, // signer can only sign 10m
wantError: fmt.Errorf("service-account-max-token-expiration cannot be set longer than the token expiration supported by service-account-signing-endpoint: 1h0m0s > 10m0s"),
}, },
{ {
desc: "signing endpoint provided but return smaller than accaptable max token exp", desc: "signing endpoint provided but return smaller than accaptable max token exp",
@ -481,8 +498,9 @@ func TestCompleteForServiceAccount(t *testing.T) {
options.ServiceAccountSigningKeyFile = tc.signingKeyFiles options.ServiceAccountSigningKeyFile = tc.signingKeyFiles
options.Authentication = &kubeoptions.BuiltInAuthenticationOptions{ options.Authentication = &kubeoptions.BuiltInAuthenticationOptions{
ServiceAccounts: &kubeoptions.ServiceAccountAuthenticationOptions{ ServiceAccounts: &kubeoptions.ServiceAccountAuthenticationOptions{
Issuers: tc.issuers, Issuers: tc.issuers,
MaxExpiration: tc.maxExpiration, MaxExpiration: tc.maxExpiration,
MaxExtendedExpiration: tc.maxExtendedExpiration,
}, },
} }
@ -507,8 +525,8 @@ func TestCompleteForServiceAccount(t *testing.T) {
if tc.externalPublicKeyGetterPresent != (co.Authentication.ServiceAccounts.ExternalPublicKeysGetter != nil) { if tc.externalPublicKeyGetterPresent != (co.Authentication.ServiceAccounts.ExternalPublicKeysGetter != nil) {
t.Errorf("Unexpected value of ExternalPublicKeysGetter: %v", co.Authentication.ServiceAccounts.ExternalPublicKeysGetter) t.Errorf("Unexpected value of ExternalPublicKeysGetter: %v", co.Authentication.ServiceAccounts.ExternalPublicKeysGetter)
} }
if tc.expectedIsExternalSigner != co.Authentication.ServiceAccounts.IsTokenSignerExternal { if tc.expectedExtendedMaxTokenExp != co.Authentication.ServiceAccounts.MaxExtendedExpiration {
t.Errorf("Expected IsTokenSignerExternal %v, found %v", tc.expectedIsExternalSigner, co.Authentication.ServiceAccounts.IsTokenSignerExternal) t.Errorf("Expected MaxExtendedExpiration %v, found %v", tc.expectedExtendedMaxTokenExp, co.Authentication.ServiceAccounts.MaxExtendedExpiration)
} }
if tc.expectedMaxtokenExp.Seconds() != co.Authentication.ServiceAccounts.MaxExpiration.Seconds() { if tc.expectedMaxtokenExp.Seconds() != co.Authentication.ServiceAccounts.MaxExpiration.Seconds() {
t.Errorf("Expected MaxExpiration to be %v, found %v", tc.expectedMaxtokenExp, co.Authentication.ServiceAccounts.MaxExpiration) t.Errorf("Expected MaxExpiration to be %v, found %v", tc.expectedMaxtokenExp, co.Authentication.ServiceAccounts.MaxExpiration)

View File

@ -131,9 +131,9 @@ type ServiceAccountAuthenticationOptions struct {
Lookup bool Lookup bool
Issuers []string Issuers []string
JWKSURI string JWKSURI string
MaxExpiration time.Duration
ExtendExpiration bool ExtendExpiration bool
IsTokenSignerExternal bool MaxExpiration time.Duration
MaxExtendedExpiration time.Duration
// OptionalTokenGetter is a function that returns a service account token getter. // OptionalTokenGetter is a function that returns a service account token getter.
// If not set, the default token getter will be used. // If not set, the default token getter will be used.
OptionalTokenGetter func(factory informers.SharedInformerFactory) serviceaccount.ServiceAccountTokenGetter OptionalTokenGetter func(factory informers.SharedInformerFactory) serviceaccount.ServiceAccountTokenGetter
@ -224,6 +224,7 @@ func (o *BuiltInAuthenticationOptions) WithServiceAccounts() *BuiltInAuthenticat
} }
o.ServiceAccounts.Lookup = true o.ServiceAccounts.Lookup = true
o.ServiceAccounts.ExtendExpiration = true o.ServiceAccounts.ExtendExpiration = true
o.ServiceAccounts.MaxExtendedExpiration = serviceaccount.ExpirationExtensionSeconds * time.Second
return o return o
} }

View File

@ -437,11 +437,12 @@ func TestBuiltInAuthenticationOptionsAddFlags(t *testing.T) {
AllowedNames: []string{"kube-aggregator"}, AllowedNames: []string{"kube-aggregator"},
}, },
ServiceAccounts: &ServiceAccountAuthenticationOptions{ ServiceAccounts: &ServiceAccountAuthenticationOptions{
KeyFiles: []string{"cert", "key"}, KeyFiles: []string{"cert", "key"},
Lookup: true, Lookup: true,
Issuers: []string{"http://foo.bar.com"}, Issuers: []string{"http://foo.bar.com"},
JWKSURI: "https://qux.com", JWKSURI: "https://qux.com",
ExtendExpiration: true, ExtendExpiration: true,
MaxExtendedExpiration: serviceaccount.ExpirationExtensionSeconds * time.Second,
}, },
TokenFile: &TokenFileAuthenticationOptions{ TokenFile: &TokenFileAuthenticationOptions{
TokenFile: "tokenfile", TokenFile: "tokenfile",

View File

@ -223,7 +223,7 @@ func (p *legacyProvider) NewRESTStorage(apiResourceConfigSource serverstorage.AP
utilfeature.DefaultFeatureGate.Enabled(features.ServiceAccountTokenPodNodeInfo) { utilfeature.DefaultFeatureGate.Enabled(features.ServiceAccountTokenPodNodeInfo) {
nodeGetter = nodeStorage.Node.Store 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 { if err != nil {
return genericapiserver.APIGroupInfo{}, err return genericapiserver.APIGroupInfo{}, err
} }

View File

@ -57,7 +57,7 @@ type GenericConfig struct {
ServiceAccountIssuer serviceaccount.TokenGenerator ServiceAccountIssuer serviceaccount.TokenGenerator
ServiceAccountMaxExpiration time.Duration ServiceAccountMaxExpiration time.Duration
ExtendExpiration bool ExtendExpiration bool
IsTokenSignerExternal bool MaxExtendedExpiration time.Duration
APIAudiences authenticator.Audiences APIAudiences authenticator.Audiences
@ -103,9 +103,9 @@ func (c *GenericConfig) NewRESTStorage(apiResourceConfigSource serverstorage.API
var serviceAccountStorage *serviceaccountstore.REST var serviceAccountStorage *serviceaccountstore.REST
if c.ServiceAccountIssuer != nil { 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 { } 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 { if err != nil {
return genericapiserver.APIGroupInfo{}, err return genericapiserver.APIGroupInfo{}, err

View File

@ -39,7 +39,7 @@ type REST struct {
} }
// NewREST returns a RESTStorage object that will work against service accounts. // 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{ store := &genericregistry.Store{
NewFunc: func() runtime.Object { return &api.ServiceAccount{} }, NewFunc: func() runtime.Object { return &api.ServiceAccount{} },
NewListFunc: func() runtime.Object { return &api.ServiceAccountList{} }, NewListFunc: func() runtime.Object { return &api.ServiceAccountList{} },
@ -61,16 +61,16 @@ func NewREST(optsGetter generic.RESTOptionsGetter, issuer token.TokenGenerator,
var trest *TokenREST var trest *TokenREST
if issuer != nil && podStorage != nil && secretStorage != nil { if issuer != nil && podStorage != nil && secretStorage != nil {
trest = &TokenREST{ trest = &TokenREST{
svcaccts: store, svcaccts: store,
pods: podStorage, pods: podStorage,
secrets: secretStorage, secrets: secretStorage,
nodes: nodeStorage, nodes: nodeStorage,
issuer: issuer, issuer: issuer,
auds: auds, auds: auds,
audsSet: sets.NewString(auds...), audsSet: sets.NewString(auds...),
maxExpirationSeconds: int64(max.Seconds()), maxExpirationSeconds: int64(max.Seconds()),
extendExpiration: extendExpiration, maxExtendedExpirationSeconds: int64(maxExtendedExpiration.Seconds()),
isTokenSignerExternal: isTokenSignerExternal, extendExpiration: extendExpiration,
} }
} }

View File

@ -19,6 +19,7 @@ package storage
import ( import (
"context" "context"
"testing" "testing"
"time"
"gopkg.in/go-jose/go-jose.v2/jwt" "gopkg.in/go-jose/go-jose.v2/jwt"
@ -55,7 +56,7 @@ func newTokenStorage(t *testing.T, issuer token.TokenGenerator, auds authenticat
ResourcePrefix: "serviceaccounts", ResourcePrefix: "serviceaccounts",
} }
// set issuer, podStore and secretStore to allow the token endpoint to be initialised // 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 { if err != nil {
t.Fatalf("unexpected error from REST storage: %v", err) t.Fatalf("unexpected error from REST storage: %v", err)
} }

View File

@ -56,16 +56,16 @@ func (r *TokenREST) Destroy() {
} }
type TokenREST struct { type TokenREST struct {
svcaccts rest.Getter svcaccts rest.Getter
pods rest.Getter pods rest.Getter
secrets rest.Getter secrets rest.Getter
nodes rest.Getter nodes rest.Getter
issuer token.TokenGenerator issuer token.TokenGenerator
auds authenticator.Audiences auds authenticator.Audiences
audsSet sets.String audsSet sets.String
maxExpirationSeconds int64 maxExpirationSeconds int64
extendExpiration bool extendExpiration bool
isTokenSignerExternal bool maxExtendedExpirationSeconds int64
} }
var _ = rest.NamedCreater(&TokenREST{}) var _ = rest.NamedCreater(&TokenREST{})
@ -218,13 +218,7 @@ func (r *TokenREST) Create(ctx context.Context, name string, obj runtime.Object,
exp := req.Spec.ExpirationSeconds exp := req.Spec.ExpirationSeconds
if r.extendExpiration && pod != nil && req.Spec.ExpirationSeconds == token.WarnOnlyBoundTokenExpirationSeconds && r.isKubeAudiences(req.Spec.Audiences) { if r.extendExpiration && pod != nil && req.Spec.ExpirationSeconds == token.WarnOnlyBoundTokenExpirationSeconds && r.isKubeAudiences(req.Spec.Audiences) {
warnAfter = exp warnAfter = exp
// If token issuer is external-jwt-signer, then choose the smaller of exp = r.maxExtendedExpirationSeconds
// ExpirationExtensionSeconds and max token lifetime supported by external signer.
if r.isTokenSignerExternal {
exp = min(r.maxExpirationSeconds, token.ExpirationExtensionSeconds)
} else {
exp = token.ExpirationExtensionSeconds
}
} }
sc, pc, err := token.Claims(*svcacct, pod, secret, node, exp, warnAfter, req.Spec.Audiences) 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) { func TestCreate_Token_WithExpiryCap(t *testing.T) {
testcases := []struct { testcases := []struct {
desc string desc string
extendExpiration bool extendExpiration bool
maxExpirationSeconds int maxExpirationSeconds int
expectedTokenAgeSec int maxExtendedExpirationSeconds int
isExternal bool expectedTokenAgeSec int
}{ }{
{ {
desc: "maxExpirationSeconds honoured", desc: "passed expiration respected if less than max",
extendExpiration: true, extendExpiration: false,
maxExpirationSeconds: 5 * 60 * 60, // 5h maxExpirationSeconds: 5 * 60 * 60, // 5h
expectedTokenAgeSec: 5 * 60 * 60, // 5h maxExtendedExpirationSeconds: token.ExpirationExtensionSeconds, // 1y
isExternal: true, expectedTokenAgeSec: token.WarnOnlyBoundTokenExpirationSeconds, // 1h 7s
}, },
{ {
desc: "ExpirationExtensionSeconds used for exp", desc: "maxExtendedExpirationSeconds honoured",
extendExpiration: true, extendExpiration: true,
maxExpirationSeconds: 2 * 365 * 24 * 60 * 60, // 2 years maxExpirationSeconds: 2 * 60 * 60, // 2h
expectedTokenAgeSec: token.ExpirationExtensionSeconds, // 1y maxExtendedExpirationSeconds: 5 * 60 * 60, // 5h
isExternal: true, expectedTokenAgeSec: 5 * 60 * 60, // 5h
}, },
{ {
desc: "ExpirationExtensionSeconds used for exp", desc: "ExpirationExtensionSeconds used for exp",
extendExpiration: true, extendExpiration: true,
maxExpirationSeconds: 5 * 60 * 60, // 5h maxExpirationSeconds: 2 * 365 * 24 * 60 * 60, // 2y
expectedTokenAgeSec: token.ExpirationExtensionSeconds, // 1y maxExtendedExpirationSeconds: token.ExpirationExtensionSeconds, // 1y
isExternal: false, expectedTokenAgeSec: token.ExpirationExtensionSeconds, // 1y
}, },
{ {
desc: "requested time use with extension disabled", desc: "ExpirationSeconds used for exp",
extendExpiration: false, extendExpiration: true,
maxExpirationSeconds: 5 * 60 * 60, // 5h maxExpirationSeconds: 5 * 60 * 60, // 5h
expectedTokenAgeSec: 3607, // 1h maxExtendedExpirationSeconds: token.ExpirationExtensionSeconds, // 1y
isExternal: true, expectedTokenAgeSec: token.ExpirationExtensionSeconds, // 1y
}, },
{ {
desc: "maxExpirationSeconds honoured with extension disabled", desc: "requested time use with extension disabled",
extendExpiration: false, extendExpiration: false,
maxExpirationSeconds: 30 * 60, // 30m maxExpirationSeconds: 5 * 60 * 60, // 5h
expectedTokenAgeSec: 30 * 60, // 30m expectedTokenAgeSec: 3607, // 1h
isExternal: true, 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) ctx = request.WithNamespace(ctx, serviceAccount.Namespace)
storage.Token.extendExpiration = tc.extendExpiration storage.Token.extendExpiration = tc.extendExpiration
storage.Token.maxExpirationSeconds = int64(tc.maxExpirationSeconds) storage.Token.maxExpirationSeconds = int64(tc.maxExpirationSeconds)
storage.Token.isTokenSignerExternal = tc.isExternal storage.Token.maxExtendedExpirationSeconds = int64(tc.maxExtendedExpirationSeconds)
tokenReqTimeStamp := time.Now() tokenReqTimeStamp := time.Now()
out, err := storage.Token.Create(ctx, serviceAccount.Name, &authenticationapi.TokenRequest{ 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) t.Fatalf("Error unmarshalling Claims: %v", err)
} }
structuredClaim.Expiry.Time() structuredClaim.Expiry.Time()
upperBound := tokenReqTimeStamp.Add(time.Duration(tc.expectedTokenAgeSec+10) * time.Second) confidenceInterval := 10 // seconds
lowerBound := tokenReqTimeStamp.Add(time.Duration(tc.expectedTokenAgeSec-10) * time.Second) 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. // check for token expiration with a toleration of +/-10s after tokenReqTimeStamp to make for latencies.
if structuredClaim.Expiry.Time().After(upperBound) || if structuredClaim.Expiry.Time().After(upperBound) ||
structuredClaim.Expiry.Time().Before(lowerBound) { 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)
} }
}) })