diff --git a/cmd/kube-apiserver/app/options/options.go b/cmd/kube-apiserver/app/options/options.go index 70f83775b44b9..9f3e002592971 100644 --- a/cmd/kube-apiserver/app/options/options.go +++ b/cmd/kube-apiserver/app/options/options.go @@ -40,6 +40,7 @@ type APIServer struct { ServiceAccountKeyFile string ServiceAccountLookup bool WebhookTokenAuthnConfigFile string + WebhookTokenAuthnCacheTTL time.Duration } // NewAPIServer creates a new APIServer object with default parameters @@ -52,6 +53,7 @@ func NewAPIServer() *APIServer { EnableHttps: true, HTTPTimeout: time.Duration(5) * time.Second, }, + WebhookTokenAuthnCacheTTL: 2 * time.Minute, } return &s } @@ -66,6 +68,7 @@ func (s *APIServer) AddFlags(fs *pflag.FlagSet) { fs.StringVar(&s.ServiceAccountKeyFile, "service-account-key-file", s.ServiceAccountKeyFile, "File containing PEM-encoded x509 RSA private or public key, used to verify ServiceAccount tokens. If unspecified, --tls-private-key-file is used.") fs.BoolVar(&s.ServiceAccountLookup, "service-account-lookup", s.ServiceAccountLookup, "If true, validate ServiceAccount tokens exist in etcd as part of authentication.") fs.StringVar(&s.WebhookTokenAuthnConfigFile, "authentication-token-webhook-config-file", s.WebhookTokenAuthnConfigFile, "File with webhook configuration for token authentication in kubeconfig format. The API server will query the remote service to determine authentication for bearer tokens.") + fs.DurationVar(&s.WebhookTokenAuthnCacheTTL, "authentication-token-webhook-cache-ttl", s.WebhookTokenAuthnCacheTTL, "The duration to cache responses from the webhook token authenticator. Default is 2m") fs.BoolVar(&s.AllowPrivileged, "allow-privileged", s.AllowPrivileged, "If true, allow privileged containers.") fs.StringVar(&s.SSHUser, "ssh-user", s.SSHUser, "If non-empty, use secure SSH proxy to the nodes, using this user name") fs.StringVar(&s.SSHKeyfile, "ssh-keyfile", s.SSHKeyfile, "If non-empty, use secure SSH proxy to the nodes, using this user keyfile") diff --git a/cmd/kube-apiserver/app/server.go b/cmd/kube-apiserver/app/server.go index fd189a44fefa1..5ba29fac31509 100644 --- a/cmd/kube-apiserver/app/server.go +++ b/cmd/kube-apiserver/app/server.go @@ -190,6 +190,7 @@ func Run(s *options.APIServer) error { ServiceAccountTokenGetter: serviceAccountGetter, KeystoneURL: s.KeystoneURL, WebhookTokenAuthnConfigFile: s.WebhookTokenAuthnConfigFile, + WebhookTokenAuthnCacheTTL: s.WebhookTokenAuthnCacheTTL, }) if err != nil { diff --git a/docs/admin/kube-apiserver.md b/docs/admin/kube-apiserver.md index d3c034e74f8d6..669c15bd9fab7 100644 --- a/docs/admin/kube-apiserver.md +++ b/docs/admin/kube-apiserver.md @@ -56,6 +56,7 @@ kube-apiserver --advertise-address=: The IP address on which to advertise the apiserver to members of the cluster. This address must be reachable by the rest of the cluster. If blank, the --bind-address will be used. If --bind-address is unspecified, the host's default interface will be used. --allow-privileged[=false]: If true, allow privileged containers. --apiserver-count=1: The number of apiservers running in the cluster + --authentication-token-webhook-cache-ttl=2m0s: The duration to cache responses from the webhook token authenticator. Default is 2m --authentication-token-webhook-config-file="": File with webhook configuration for token authentication in kubeconfig format. The API server will query the remote service to determine authentication for bearer tokens. --authorization-mode="AlwaysAllow": Ordered list of plug-ins to do authorization on secure port. Comma-delimited list of: AlwaysAllow,AlwaysDeny,ABAC,Webhook --authorization-policy-file="": File with authorization policy in csv format, used with --authorization-mode=ABAC, on the secure port. @@ -120,7 +121,7 @@ kube-apiserver --watch-cache-sizes=[]: List of watch cache sizes for every resource (pods, nodes, etc.), comma separated. The individual override format: resource#size, where size is a number. It takes effect when watch-cache is enabled. ``` -###### Auto generated by spf13/cobra on 10-May-2016 +###### Auto generated by spf13/cobra on 17-May-2016 diff --git a/hack/verify-flags/known-flags.txt b/hack/verify-flags/known-flags.txt index 916910548247b..2ba0f6d180c24 100644 --- a/hack/verify-flags/known-flags.txt +++ b/hack/verify-flags/known-flags.txt @@ -18,6 +18,7 @@ api-token api-version apiserver-count auth-path +authentication-token-webhook-cache-ttl authentication-token-webhook-config-file authorization-mode authorization-policy-file diff --git a/pkg/apis/authentication.k8s.io/v1beta1/types.go b/pkg/apis/authentication.k8s.io/v1beta1/types.go index 7513541f6a042..fc136877a9efe 100644 --- a/pkg/apis/authentication.k8s.io/v1beta1/types.go +++ b/pkg/apis/authentication.k8s.io/v1beta1/types.go @@ -21,6 +21,8 @@ import ( ) // TokenReview attempts to authenticate a token to a known user. +// Note: TokenReview requests may be cached by the webhook token authenticator +// plugin in the kube-apiserver. type TokenReview struct { unversioned.TypeMeta `json:",inline"` diff --git a/pkg/apiserver/authenticator/authn.go b/pkg/apiserver/authenticator/authn.go index bbce5b3588ab5..56dfcf8ad1212 100644 --- a/pkg/apiserver/authenticator/authn.go +++ b/pkg/apiserver/authenticator/authn.go @@ -18,6 +18,7 @@ package authenticator import ( "crypto/rsa" + "time" "k8s.io/kubernetes/pkg/auth/authenticator" "k8s.io/kubernetes/pkg/auth/authenticator/bearertoken" @@ -47,6 +48,7 @@ type AuthenticatorConfig struct { ServiceAccountTokenGetter serviceaccount.ServiceAccountTokenGetter KeystoneURL string WebhookTokenAuthnConfigFile string + WebhookTokenAuthnCacheTTL time.Duration } // New returns an authenticator.Request or an error that supports the standard @@ -103,7 +105,7 @@ func New(config AuthenticatorConfig) (authenticator.Request, error) { } if len(config.WebhookTokenAuthnConfigFile) > 0 { - webhookTokenAuth, err := newWebhookTokenAuthenticator(config.WebhookTokenAuthnConfigFile) + webhookTokenAuth, err := newWebhookTokenAuthenticator(config.WebhookTokenAuthnConfigFile, config.WebhookTokenAuthnCacheTTL) if err != nil { return nil, err } @@ -198,8 +200,8 @@ func newAuthenticatorFromKeystoneURL(keystoneURL string) (authenticator.Request, return basicauth.New(keystoneAuthenticator), nil } -func newWebhookTokenAuthenticator(webhookConfigFile string) (authenticator.Request, error) { - webhookTokenAuthenticator, err := webhook.New(webhookConfigFile) +func newWebhookTokenAuthenticator(webhookConfigFile string, ttl time.Duration) (authenticator.Request, error) { + webhookTokenAuthenticator, err := webhook.New(webhookConfigFile, ttl) if err != nil { return nil, err } diff --git a/pkg/util/cache/lruexpirecache.go b/pkg/util/cache/lruexpirecache.go new file mode 100644 index 0000000000000..22f7f27679c27 --- /dev/null +++ b/pkg/util/cache/lruexpirecache.go @@ -0,0 +1,66 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cache + +import ( + "sync" + "time" + + "github.com/golang/groupcache/lru" +) + +type LRUExpireCache struct { + cache *lru.Cache + lock sync.RWMutex +} + +func NewLRUExpireCache(maxSize int) *LRUExpireCache { + return &LRUExpireCache{cache: lru.New(maxSize)} +} + +type cacheEntry struct { + value interface{} + expireTime time.Time +} + +func (c *LRUExpireCache) Add(key lru.Key, value interface{}, ttl time.Duration) { + c.lock.Lock() + defer c.lock.Unlock() + c.cache.Add(key, &cacheEntry{value, time.Now().Add(ttl)}) + // Remove entry from cache after ttl. + time.AfterFunc(ttl, func() { c.remove(key) }) +} + +func (c *LRUExpireCache) Get(key lru.Key) (interface{}, bool) { + c.lock.RLock() + defer c.lock.RUnlock() + e, ok := c.cache.Get(key) + if !ok { + return nil, false + } + if time.Now().After(e.(*cacheEntry).expireTime) { + go c.remove(key) + return nil, false + } + return e.(*cacheEntry).value, true +} + +func (c *LRUExpireCache) remove(key lru.Key) { + c.lock.Lock() + defer c.lock.Unlock() + c.cache.Remove(key) +} diff --git a/pkg/util/cache/lruexpirecache_test.go b/pkg/util/cache/lruexpirecache_test.go new file mode 100644 index 0000000000000..07465ea5643a5 --- /dev/null +++ b/pkg/util/cache/lruexpirecache_test.go @@ -0,0 +1,63 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cache + +import ( + "testing" + "time" + + "github.com/golang/groupcache/lru" +) + +func expectEntry(t *testing.T, c *LRUExpireCache, key lru.Key, value interface{}) { + result, ok := c.Get(key) + if !ok || result != value { + t.Errorf("Expected cache[%v]: %v, got %v", key, value, result) + } +} + +func expectNotEntry(t *testing.T, c *LRUExpireCache, key lru.Key) { + if result, ok := c.Get(key); ok { + t.Errorf("Expected cache[%v] to be empty, got %v", key, result) + } +} + +func TestSimpleGet(t *testing.T) { + c := NewLRUExpireCache(10) + c.Add("long-lived", "12345", 10*time.Hour) + expectEntry(t, c, "long-lived", "12345") +} + +func TestExpiredGet(t *testing.T) { + c := NewLRUExpireCache(10) + c.Add("short-lived", "12345", 0*time.Second) + expectNotEntry(t, c, "short-lived") +} + +func TestLRUOverflow(t *testing.T) { + c := NewLRUExpireCache(4) + c.Add("elem1", "1", 10*time.Hour) + c.Add("elem2", "2", 10*time.Hour) + c.Add("elem3", "3", 10*time.Hour) + c.Add("elem4", "4", 10*time.Hour) + c.Add("elem5", "5", 10*time.Hour) + expectNotEntry(t, c, "elem1") + expectEntry(t, c, "elem2", "2") + expectEntry(t, c, "elem3", "3") + expectEntry(t, c, "elem4", "4") + expectEntry(t, c, "elem5", "5") +} diff --git a/plugin/pkg/auth/authenticator/token/webhook/webhook.go b/plugin/pkg/auth/authenticator/token/webhook/webhook.go index adc8e5fb361e6..ad298a39aaed1 100644 --- a/plugin/pkg/auth/authenticator/token/webhook/webhook.go +++ b/plugin/pkg/auth/authenticator/token/webhook/webhook.go @@ -18,10 +18,13 @@ limitations under the License. package webhook import ( + "time" + "k8s.io/kubernetes/pkg/api/unversioned" "k8s.io/kubernetes/pkg/apis/authentication.k8s.io/v1beta1" "k8s.io/kubernetes/pkg/auth/authenticator" "k8s.io/kubernetes/pkg/auth/user" + "k8s.io/kubernetes/pkg/util/cache" "k8s.io/kubernetes/plugin/pkg/webhook" _ "k8s.io/kubernetes/pkg/apis/authentication.k8s.io/install" @@ -36,30 +39,36 @@ var _ authenticator.Token = (*WebhookTokenAuthenticator)(nil) type WebhookTokenAuthenticator struct { *webhook.GenericWebhook + responseCache *cache.LRUExpireCache + ttl time.Duration } // New creates a new WebhookTokenAuthenticator from the provided kubeconfig file. -func New(kubeConfigFile string) (*WebhookTokenAuthenticator, error) { +func New(kubeConfigFile string, ttl time.Duration) (*WebhookTokenAuthenticator, error) { gw, err := webhook.NewGenericWebhook(kubeConfigFile, groupVersions) if err != nil { return nil, err } - return &WebhookTokenAuthenticator{gw}, nil + return &WebhookTokenAuthenticator{gw, cache.NewLRUExpireCache(1024), ttl}, nil } -// AuthenticateToken +// AuthenticateToken implements the authenticator.Token interface. func (w *WebhookTokenAuthenticator) AuthenticateToken(token string) (user.Info, bool, error) { r := &v1beta1.TokenReview{ - Spec: v1beta1.TokenReviewSpec{ - Token: token, - }, - } - result := w.RestClient.Post().Body(r).Do() - if err := result.Error(); err != nil { - return nil, false, err + Spec: v1beta1.TokenReviewSpec{Token: token}, } - if err := result.Into(r); err != nil { - return nil, false, err + if entry, ok := w.responseCache.Get(r.Spec); ok { + r.Status = entry.(v1beta1.TokenReviewStatus) + } else { + result := w.RestClient.Post().Body(r).Do() + if err := result.Error(); err != nil { + return nil, false, err + } + spec := r.Spec + if err := result.Into(r); err != nil { + return nil, false, err + } + go w.responseCache.Add(spec, r.Status, w.ttl) } if !r.Status.Authenticated { return nil, false, nil diff --git a/plugin/pkg/auth/authenticator/token/webhook/webhook_test.go b/plugin/pkg/auth/authenticator/token/webhook/webhook_test.go index 94e832624afb0..9dfcb8d0346db 100644 --- a/plugin/pkg/auth/authenticator/token/webhook/webhook_test.go +++ b/plugin/pkg/auth/authenticator/token/webhook/webhook_test.go @@ -140,7 +140,7 @@ func newTokenAuthenticator(serverURL string, clientCert, clientKey, ca []byte) ( if err := json.NewEncoder(tempfile).Encode(config); err != nil { return nil, err } - return New(p) + return New(p, 0) } func TestTLSConfig(t *testing.T) { diff --git a/test/integration/auth_test.go b/test/integration/auth_test.go index 7b7c441eb8593..5b1a3400b751e 100644 --- a/test/integration/auth_test.go +++ b/test/integration/auth_test.go @@ -35,6 +35,7 @@ import ( "strconv" "strings" "testing" + "time" "k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/api/testapi" @@ -85,7 +86,7 @@ func getTestWebhookTokenAuth(serverURL string) (authenticator.Request, error) { if err := json.NewEncoder(kubecfgFile).Encode(config); err != nil { return nil, err } - webhookTokenAuth, err := webhook.New(kubecfgFile.Name()) + webhookTokenAuth, err := webhook.New(kubecfgFile.Name(), 2*time.Minute) if err != nil { return nil, err }