Merge pull request #63653 from WanLinghao/token_expiry_limit

Automatic merge from submit-queue. If you want to cherry-pick this change to another branch, please follow the instructions <a href="https://github.com/kubernetes/community/blob/master/contributors/devel/cherry-picks.md">here</a>.

Add limit to the TokenRequest expiration time

**What this PR does / why we need it**:
A new API TokenRequest has been implemented.It improves current serviceaccount model from many ways.
This patch adds limit to TokenRequest expiration time.


**Which issue(s) this PR fixes** *(optional, in `fixes #<issue number>(, fixes #<issue_number>, ...)` format, will close the issue(s) when PR gets merged)*:
Fixes #63575

**Special notes for your reviewer**:

**Release note**:

```release-note
NONE
```
This commit is contained in:
Kubernetes Submit Queue 2018-06-27 00:31:08 -07:00 committed by GitHub
commit 2da49321e6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 175 additions and 35 deletions

View File

@ -326,8 +326,12 @@ func CreateKubeAPIServerConfig(
return return
} }
var issuer serviceaccount.TokenGenerator var (
var apiAudiences []string issuer serviceaccount.TokenGenerator
apiAudiences []string
maxExpiration time.Duration
)
if s.ServiceAccountSigningKeyFile != "" || if s.ServiceAccountSigningKeyFile != "" ||
s.Authentication.ServiceAccounts.Issuer != "" || s.Authentication.ServiceAccounts.Issuer != "" ||
len(s.Authentication.ServiceAccounts.APIAudiences) > 0 { len(s.Authentication.ServiceAccounts.APIAudiences) > 0 {
@ -347,8 +351,19 @@ func CreateKubeAPIServerConfig(
lastErr = fmt.Errorf("failed to parse service-account-issuer-key-file: %v", err) lastErr = fmt.Errorf("failed to parse service-account-issuer-key-file: %v", err)
return return
} }
if s.Authentication.ServiceAccounts.MaxExpiration != 0 {
lowBound := time.Hour
upBound := time.Duration(1<<32) * time.Second
if s.Authentication.ServiceAccounts.MaxExpiration < lowBound ||
s.Authentication.ServiceAccounts.MaxExpiration > upBound {
lastErr = fmt.Errorf("the serviceaccount max expiration is out of range, must be between 1 hour to 2^32 seconds")
return
}
}
issuer = serviceaccount.JWTTokenGenerator(s.Authentication.ServiceAccounts.Issuer, sk) issuer = serviceaccount.JWTTokenGenerator(s.Authentication.ServiceAccounts.Issuer, sk)
apiAudiences = s.Authentication.ServiceAccounts.APIAudiences apiAudiences = s.Authentication.ServiceAccounts.APIAudiences
maxExpiration = s.Authentication.ServiceAccounts.MaxExpiration
} }
config = &master.Config{ config = &master.Config{
@ -382,8 +397,9 @@ func CreateKubeAPIServerConfig(
EndpointReconcilerType: reconcilers.Type(s.EndpointReconcilerType), EndpointReconcilerType: reconcilers.Type(s.EndpointReconcilerType),
MasterCount: s.MasterCount, MasterCount: s.MasterCount,
ServiceAccountIssuer: issuer, ServiceAccountIssuer: issuer,
ServiceAccountAPIAudiences: apiAudiences, ServiceAccountAPIAudiences: apiAudiences,
ServiceAccountMaxExpiration: maxExpiration,
}, },
} }

View File

@ -73,10 +73,11 @@ type PasswordFileAuthenticationOptions struct {
} }
type ServiceAccountAuthenticationOptions struct { type ServiceAccountAuthenticationOptions struct {
KeyFiles []string KeyFiles []string
Lookup bool Lookup bool
Issuer string Issuer string
APIAudiences []string APIAudiences []string
MaxExpiration time.Duration
} }
type TokenFileAuthenticationOptions struct { type TokenFileAuthenticationOptions struct {
@ -260,6 +261,10 @@ func (s *BuiltInAuthenticationOptions) AddFlags(fs *pflag.FlagSet) {
fs.StringSliceVar(&s.ServiceAccounts.APIAudiences, "service-account-api-audiences", s.ServiceAccounts.APIAudiences, ""+ fs.StringSliceVar(&s.ServiceAccounts.APIAudiences, "service-account-api-audiences", s.ServiceAccounts.APIAudiences, ""+
"Identifiers of the API. The service account token authenticator will validate that "+ "Identifiers of the API. The service account token authenticator will validate that "+
"tokens used against the API are bound to at least one of these audiences.") "tokens used against the API are bound to at least one of these audiences.")
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.")
} }
if s.TokenFile != nil { if s.TokenFile != nil {

View File

@ -163,8 +163,9 @@ type ExtraConfig struct {
// Selects which reconciler to use // Selects which reconciler to use
EndpointReconcilerType reconcilers.Type EndpointReconcilerType reconcilers.Type
ServiceAccountIssuer serviceaccount.TokenGenerator ServiceAccountIssuer serviceaccount.TokenGenerator
ServiceAccountAPIAudiences []string ServiceAccountAPIAudiences []string
ServiceAccountMaxExpiration time.Duration
} }
type Config struct { type Config struct {
@ -318,15 +319,16 @@ func (c completedConfig) New(delegationTarget genericapiserver.DelegationTarget)
// install legacy rest storage // install legacy rest storage
if c.ExtraConfig.APIResourceConfigSource.VersionEnabled(apiv1.SchemeGroupVersion) { if c.ExtraConfig.APIResourceConfigSource.VersionEnabled(apiv1.SchemeGroupVersion) {
legacyRESTStorageProvider := corerest.LegacyRESTStorageProvider{ legacyRESTStorageProvider := corerest.LegacyRESTStorageProvider{
StorageFactory: c.ExtraConfig.StorageFactory, StorageFactory: c.ExtraConfig.StorageFactory,
ProxyTransport: c.ExtraConfig.ProxyTransport, ProxyTransport: c.ExtraConfig.ProxyTransport,
KubeletClientConfig: c.ExtraConfig.KubeletClientConfig, KubeletClientConfig: c.ExtraConfig.KubeletClientConfig,
EventTTL: c.ExtraConfig.EventTTL, EventTTL: c.ExtraConfig.EventTTL,
ServiceIPRange: c.ExtraConfig.ServiceIPRange, ServiceIPRange: c.ExtraConfig.ServiceIPRange,
ServiceNodePortRange: c.ExtraConfig.ServiceNodePortRange, ServiceNodePortRange: c.ExtraConfig.ServiceNodePortRange,
LoopbackClientConfig: c.GenericConfig.LoopbackClientConfig, LoopbackClientConfig: c.GenericConfig.LoopbackClientConfig,
ServiceAccountIssuer: c.ExtraConfig.ServiceAccountIssuer, ServiceAccountIssuer: c.ExtraConfig.ServiceAccountIssuer,
ServiceAccountAPIAudiences: c.ExtraConfig.ServiceAccountAPIAudiences, ServiceAccountAPIAudiences: c.ExtraConfig.ServiceAccountAPIAudiences,
ServiceAccountMaxExpiration: c.ExtraConfig.ServiceAccountMaxExpiration,
} }
m.InstallLegacyAPI(&c, c.GenericConfig.RESTOptionsGetter, legacyRESTStorageProvider) m.InstallLegacyAPI(&c, c.GenericConfig.RESTOptionsGetter, legacyRESTStorageProvider)
} }

View File

@ -79,8 +79,9 @@ type LegacyRESTStorageProvider struct {
ServiceIPRange net.IPNet ServiceIPRange net.IPNet
ServiceNodePortRange utilnet.PortRange ServiceNodePortRange utilnet.PortRange
ServiceAccountIssuer serviceaccount.TokenGenerator ServiceAccountIssuer serviceaccount.TokenGenerator
ServiceAccountAPIAudiences []string ServiceAccountAPIAudiences []string
ServiceAccountMaxExpiration time.Duration
LoopbackClientConfig *restclient.Config LoopbackClientConfig *restclient.Config
} }
@ -141,9 +142,9 @@ func (c LegacyRESTStorageProvider) NewLegacyRESTStorage(restOptionsGetter generi
var serviceAccountStorage *serviceaccountstore.REST var serviceAccountStorage *serviceaccountstore.REST
if c.ServiceAccountIssuer != nil && utilfeature.DefaultFeatureGate.Enabled(features.TokenRequest) { if c.ServiceAccountIssuer != nil && utilfeature.DefaultFeatureGate.Enabled(features.TokenRequest) {
serviceAccountStorage = serviceaccountstore.NewREST(restOptionsGetter, c.ServiceAccountIssuer, c.ServiceAccountAPIAudiences, podStorage.Pod.Store, secretStorage.Store) serviceAccountStorage = serviceaccountstore.NewREST(restOptionsGetter, c.ServiceAccountIssuer, c.ServiceAccountAPIAudiences, c.ServiceAccountMaxExpiration, podStorage.Pod.Store, secretStorage.Store)
} else { } else {
serviceAccountStorage = serviceaccountstore.NewREST(restOptionsGetter, nil, nil, nil, nil) serviceAccountStorage = serviceaccountstore.NewREST(restOptionsGetter, nil, nil, 0, nil, nil)
} }
serviceRESTStorage, serviceStatusStorage := servicestore.NewGenericREST(restOptionsGetter) serviceRESTStorage, serviceStatusStorage := servicestore.NewGenericREST(restOptionsGetter)

View File

@ -17,6 +17,8 @@ limitations under the License.
package storage package storage
import ( import (
"time"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apiserver/pkg/registry/generic" "k8s.io/apiserver/pkg/registry/generic"
genericregistry "k8s.io/apiserver/pkg/registry/generic/registry" genericregistry "k8s.io/apiserver/pkg/registry/generic/registry"
@ -35,7 +37,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 []string, podStorage, secretStorage *genericregistry.Store) *REST { func NewREST(optsGetter generic.RESTOptionsGetter, issuer token.TokenGenerator, auds []string, max time.Duration, podStorage, secretStorage *genericregistry.Store) *REST {
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{} },
@ -56,11 +58,12 @@ 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,
issuer: issuer, issuer: issuer,
auds: auds, auds: auds,
maxExpirationSeconds: int64(max.Seconds()),
} }
} }

View File

@ -38,7 +38,7 @@ func newStorage(t *testing.T) (*REST, *etcdtesting.EtcdTestServer) {
DeleteCollectionWorkers: 1, DeleteCollectionWorkers: 1,
ResourcePrefix: "serviceaccounts", ResourcePrefix: "serviceaccounts",
} }
return NewREST(restOptions, nil, nil, nil, nil), server return NewREST(restOptions, nil, nil, 0, nil, nil), server
} }
func validNewServiceAccount(name string) *api.ServiceAccount { func validNewServiceAccount(name string) *api.ServiceAccount {

View File

@ -39,11 +39,12 @@ func (r *TokenREST) New() runtime.Object {
} }
type TokenREST struct { type TokenREST struct {
svcaccts getter svcaccts getter
pods getter pods getter
secrets getter secrets getter
issuer token.TokenGenerator issuer token.TokenGenerator
auds []string auds []string
maxExpirationSeconds int64
} }
var _ = rest.NamedCreater(&TokenREST{}) var _ = rest.NamedCreater(&TokenREST{})
@ -111,6 +112,12 @@ func (r *TokenREST) Create(ctx context.Context, name string, obj runtime.Object,
if len(out.Spec.Audiences) == 0 { if len(out.Spec.Audiences) == 0 {
out.Spec.Audiences = r.auds out.Spec.Audiences = r.auds
} }
if r.maxExpirationSeconds > 0 && out.Spec.ExpirationSeconds > r.maxExpirationSeconds {
//only positive value is valid
out.Spec.ExpirationSeconds = r.maxExpirationSeconds
}
sc, pc := token.Claims(*svcacct, pod, secret, out.Spec.ExpirationSeconds, out.Spec.Audiences) sc, pc := token.Claims(*svcacct, pod, secret, out.Spec.ExpirationSeconds, out.Spec.Audiences)
tokdata, err := r.issuer.GenerateToken(sc, pc) tokdata, err := r.issuer.GenerateToken(sc, pc)
if err != nil { if err != nil {

View File

@ -20,6 +20,7 @@ import (
"crypto/ecdsa" "crypto/ecdsa"
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"fmt"
"strings" "strings"
"testing" "testing"
"time" "time"
@ -63,6 +64,12 @@ func TestServiceAccountTokenCreate(t *testing.T) {
const iss = "https://foo.bar.example.com" const iss = "https://foo.bar.example.com"
aud := []string{"api"} aud := []string{"api"}
maxExpirationSeconds := int64(60 * 60)
maxExpirationDuration, err := time.ParseDuration(fmt.Sprintf("%ds", maxExpirationSeconds))
if err != nil {
t.Fatalf("err: %v", err)
}
gcs := &clientset.Clientset{} gcs := &clientset.Clientset{}
// Start the server // Start the server
@ -77,6 +84,7 @@ func TestServiceAccountTokenCreate(t *testing.T) {
) )
masterConfig.ExtraConfig.ServiceAccountIssuer = serviceaccount.JWTTokenGenerator(iss, sk) masterConfig.ExtraConfig.ServiceAccountIssuer = serviceaccount.JWTTokenGenerator(iss, sk)
masterConfig.ExtraConfig.ServiceAccountAPIAudiences = aud masterConfig.ExtraConfig.ServiceAccountAPIAudiences = aud
masterConfig.ExtraConfig.ServiceAccountMaxExpiration = maxExpirationDuration
master, _, closeFn := framework.RunAMaster(masterConfig) master, _, closeFn := framework.RunAMaster(masterConfig)
defer closeFn() defer closeFn()
@ -438,6 +446,94 @@ func TestServiceAccountTokenCreate(t *testing.T) {
doTokenReview(t, cs, treq, true) doTokenReview(t, cs, treq, true)
}) })
t.Run("a token request within expiration time", func(t *testing.T) {
normalExpirationTime := maxExpirationSeconds - 10*60
treq := &authenticationv1.TokenRequest{
Spec: authenticationv1.TokenRequestSpec{
Audiences: []string{"api"},
ExpirationSeconds: &normalExpirationTime,
BoundObjectRef: &authenticationv1.BoundObjectReference{
Kind: "Secret",
APIVersion: "v1",
Name: secret.Name,
UID: secret.UID,
},
},
}
sa, del := createDeleteSvcAcct(t, cs, sa)
defer del()
originalSecret, originalDelSecret := createDeleteSecret(t, cs, secret)
defer originalDelSecret()
treq.Spec.BoundObjectRef.UID = originalSecret.UID
if treq, err = cs.CoreV1().ServiceAccounts(sa.Namespace).CreateToken(sa.Name, treq); err != nil {
t.Fatalf("err: %v", err)
}
checkPayload(t, treq.Status.Token, `"system:serviceaccount:myns:test-svcacct"`, "sub")
checkPayload(t, treq.Status.Token, `["api"]`, "aud")
checkPayload(t, treq.Status.Token, `null`, "kubernetes.io", "pod")
checkPayload(t, treq.Status.Token, `"test-secret"`, "kubernetes.io", "secret", "name")
checkPayload(t, treq.Status.Token, `"myns"`, "kubernetes.io", "namespace")
checkPayload(t, treq.Status.Token, `"test-svcacct"`, "kubernetes.io", "serviceaccount", "name")
checkExpiration(t, treq, normalExpirationTime)
doTokenReview(t, cs, treq, false)
originalDelSecret()
doTokenReview(t, cs, treq, true)
_, recreateDelSecret := createDeleteSecret(t, cs, secret)
defer recreateDelSecret()
doTokenReview(t, cs, treq, true)
})
t.Run("a token request with out-of-range expiration", func(t *testing.T) {
tooLongExpirationTime := maxExpirationSeconds + 10*60
treq := &authenticationv1.TokenRequest{
Spec: authenticationv1.TokenRequestSpec{
Audiences: []string{"api"},
ExpirationSeconds: &tooLongExpirationTime,
BoundObjectRef: &authenticationv1.BoundObjectReference{
Kind: "Secret",
APIVersion: "v1",
Name: secret.Name,
UID: secret.UID,
},
},
}
sa, del := createDeleteSvcAcct(t, cs, sa)
defer del()
originalSecret, originalDelSecret := createDeleteSecret(t, cs, secret)
defer originalDelSecret()
treq.Spec.BoundObjectRef.UID = originalSecret.UID
if treq, err = cs.CoreV1().ServiceAccounts(sa.Namespace).CreateToken(sa.Name, treq); err != nil {
t.Fatalf("err: %v", err)
}
checkPayload(t, treq.Status.Token, `"system:serviceaccount:myns:test-svcacct"`, "sub")
checkPayload(t, treq.Status.Token, `["api"]`, "aud")
checkPayload(t, treq.Status.Token, `null`, "kubernetes.io", "pod")
checkPayload(t, treq.Status.Token, `"test-secret"`, "kubernetes.io", "secret", "name")
checkPayload(t, treq.Status.Token, `"myns"`, "kubernetes.io", "namespace")
checkPayload(t, treq.Status.Token, `"test-svcacct"`, "kubernetes.io", "serviceaccount", "name")
checkExpiration(t, treq, maxExpirationSeconds)
doTokenReview(t, cs, treq, false)
originalDelSecret()
doTokenReview(t, cs, treq, true)
_, recreateDelSecret := createDeleteSecret(t, cs, secret)
defer recreateDelSecret()
doTokenReview(t, cs, treq, true)
})
} }
func doTokenReview(t *testing.T, cs externalclientset.Interface, treq *authenticationv1.TokenRequest, expectErr bool) { func doTokenReview(t *testing.T, cs externalclientset.Interface, treq *authenticationv1.TokenRequest, expectErr bool) {
@ -470,6 +566,16 @@ func checkPayload(t *testing.T, tok string, want string, parts ...string) {
} }
} }
func checkExpiration(t *testing.T, treq *authenticationv1.TokenRequest, expectedExpiration int64) {
t.Helper()
if treq.Spec.ExpirationSeconds == nil {
t.Errorf("unexpected nil expiration seconds.")
}
if *treq.Spec.ExpirationSeconds != expectedExpiration {
t.Errorf("unexpected expiration seconds.\nsaw:\t%d\nwant:\t%d", treq.Spec.ExpirationSeconds, expectedExpiration)
}
}
func getSubObject(t *testing.T, b string, parts ...string) string { func getSubObject(t *testing.T, b string, parts ...string) string {
t.Helper() t.Helper()
var obj interface{} var obj interface{}