From a1b52fb17a6f7f34f676bf399e82979b8ff9b072 Mon Sep 17 00:00:00 2001 From: Jyoti Mahapatra <49211422+jyotimahapatra@users.noreply.github.com> Date: Mon, 31 Jan 2022 16:01:52 -0800 Subject: [PATCH] extend sa token if audience is apiserver (#105954) Signed-off-by: Jyoti Mahapatra --- .../core/serviceaccount/storage/storage.go | 2 + .../core/serviceaccount/storage/token.go | 10 +++- test/integration/auth/svcaccttoken_test.go | 60 ++++++++++++++++++- 3 files changed, 68 insertions(+), 4 deletions(-) diff --git a/pkg/registry/core/serviceaccount/storage/storage.go b/pkg/registry/core/serviceaccount/storage/storage.go index ef2b1f4f988..60f83b74133 100644 --- a/pkg/registry/core/serviceaccount/storage/storage.go +++ b/pkg/registry/core/serviceaccount/storage/storage.go @@ -20,6 +20,7 @@ import ( "time" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/sets" "k8s.io/apiserver/pkg/authentication/authenticator" "k8s.io/apiserver/pkg/registry/generic" genericregistry "k8s.io/apiserver/pkg/registry/generic/registry" @@ -64,6 +65,7 @@ func NewREST(optsGetter generic.RESTOptionsGetter, issuer token.TokenGenerator, secrets: secretStorage, issuer: issuer, auds: auds, + audsSet: sets.NewString(auds...), maxExpirationSeconds: int64(max.Seconds()), extendExpiration: extendExpiration, } diff --git a/pkg/registry/core/serviceaccount/storage/token.go b/pkg/registry/core/serviceaccount/storage/token.go index 4732099fcd7..19d6c55a2cc 100644 --- a/pkg/registry/core/serviceaccount/storage/token.go +++ b/pkg/registry/core/serviceaccount/storage/token.go @@ -27,6 +27,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/sets" "k8s.io/apiserver/pkg/authentication/authenticator" genericapirequest "k8s.io/apiserver/pkg/endpoints/request" "k8s.io/apiserver/pkg/registry/rest" @@ -46,6 +47,7 @@ type TokenREST struct { secrets getter issuer token.TokenGenerator auds authenticator.Audiences + audsSet sets.String maxExpirationSeconds int64 extendExpiration bool } @@ -129,7 +131,7 @@ func (r *TokenREST) Create(ctx context.Context, name string, obj runtime.Object, // 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 { + if r.extendExpiration && pod != nil && out.Spec.ExpirationSeconds == token.WarnOnlyBoundTokenExpirationSeconds && r.isKubeAudiences(out.Spec.Audiences) { warnAfter = exp exp = token.ExpirationExtensionSeconds } @@ -176,3 +178,9 @@ func newContext(ctx context.Context, resource, name string, gvk schema.GroupVers } return genericapirequest.WithRequestInfo(ctx, &newInfo) } + +// isKubeAudiences returns true if the tokenaudiences is a strict subset of apiserver audiences. +func (r *TokenREST) isKubeAudiences(tokenAudience []string) bool { + // tokenAudiences must be a strict subset of apiserver audiences + return r.audsSet.HasAll(tokenAudience...) +} diff --git a/test/integration/auth/svcaccttoken_test.go b/test/integration/auth/svcaccttoken_test.go index da50bf4736e..ac06695420f 100644 --- a/test/integration/auth/svcaccttoken_test.go +++ b/test/integration/auth/svcaccttoken_test.go @@ -54,13 +54,17 @@ import ( "k8s.io/kubernetes/test/integration/framework" ) -// This key is for testing purposes only and is not considered secure. -const ecdsaPrivateKey = `-----BEGIN EC PRIVATE KEY----- +const ( + // This key is for testing purposes only and is not considered secure. + ecdsaPrivateKey = `-----BEGIN EC PRIVATE KEY----- MHcCAQEEIEZmTmUhuanLjPA2CLquXivuwBDHTt5XYwgIr/kA1LtRoAoGCCqGSM49 AwEHoUQDQgAEH6cuzP8XuD5wal6wf9M6xDljTOPLX2i8uIp/C/ASqiIGUeeKQtX0 /IR3qCXyThP/dbCiHrF3v1cuhBOHY8CLVg== -----END EC PRIVATE KEY-----` + tokenExpirationSeconds = 60*60 + 7 +) + func TestServiceAccountTokenCreate(t *testing.T) { // Build client config, clientset, and informers @@ -382,7 +386,7 @@ func TestServiceAccountTokenCreate(t *testing.T) { }) t.Run("expiration extended token", func(t *testing.T) { - var requestExp int64 = 60*60 + 7 + var requestExp int64 = tokenExpirationSeconds treq := &authenticationv1.TokenRequest{ Spec: authenticationv1.TokenRequestSpec{ Audiences: []string{"api"}, @@ -435,6 +439,56 @@ func TestServiceAccountTokenCreate(t *testing.T) { } }) + t.Run("extended expiration extended does not apply for other audiences", func(t *testing.T) { + var requestExp int64 = tokenExpirationSeconds + treq := &authenticationv1.TokenRequest{ + Spec: authenticationv1.TokenRequestSpec{ + Audiences: []string{"not-the-api", "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) + } + + // Give some tolerance to avoid flakiness since we are using real time. + var leeway int64 = 10 + actualExpiry := jwt.NewNumericDate(time.Now().Add(time.Duration(60*60) * time.Second)) + assumedExpiry := jwt.NewNumericDate(time.Now().Add(time.Duration(requestExp) * time.Second)) + + warnAfter := getSubObject(t, getPayload(t, treq.Status.Token), "kubernetes.io", "warnafter") + if warnAfter != "null" { + t.Errorf("warn after should be empty.Found: %s", warnAfter) + } + + exp, err := strconv.ParseInt(getSubObject(t, getPayload(t, treq.Status.Token), "exp"), 10, 64) + if err != nil { + t.Fatalf("error parsing exp: %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) + } + + checkExpiration(t, treq, requestExp) + expStatus := treq.Status.ExpirationTimestamp.Time.Unix() + if expStatus < 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{