extend sa token if audience is apiserver (#105954)

Signed-off-by: Jyoti Mahapatra <jyotima@amazon.com>
This commit is contained in:
Jyoti Mahapatra 2022-01-31 16:01:52 -08:00 committed by GitHub
parent 7c2e612569
commit a1b52fb17a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 68 additions and 4 deletions

View File

@ -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,
}

View File

@ -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...)
}

View File

@ -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{