Merge pull request #25694 from cjcullen/authncache

Automatic merge from submit-queue

Cache Webhook Authentication responses

Add a simple LRU cache w/ 2 minute TTL to the webhook authenticator.

Kubectl is a little spammy, w/ >= 4 API requests per command. This also prevents a single unauthenticated user from being able to DOS the remote authenticator.
This commit is contained in:
k8s-merge-robot 2016-05-21 10:48:38 -07:00
commit 346f965871
11 changed files with 167 additions and 18 deletions

View File

@ -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")

View File

@ -190,6 +190,7 @@ func Run(s *options.APIServer) error {
ServiceAccountTokenGetter: serviceAccountGetter,
KeystoneURL: s.KeystoneURL,
WebhookTokenAuthnConfigFile: s.WebhookTokenAuthnConfigFile,
WebhookTokenAuthnCacheTTL: s.WebhookTokenAuthnCacheTTL,
})
if err != nil {

View File

@ -56,6 +56,7 @@ kube-apiserver
--advertise-address=<nil>: 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
<!-- BEGIN MUNGE: GENERATED_ANALYTICS -->

View File

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

View File

@ -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"`

View File

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

66
pkg/util/cache/lruexpirecache.go vendored Normal file
View File

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

63
pkg/util/cache/lruexpirecache_test.go vendored Normal file
View File

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

View File

@ -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,
},
Spec: v1beta1.TokenReviewSpec{Token: token},
}
result := w.RestClient.Post().Body(r).Do()
if err := result.Error(); err != nil {
return nil, false, err
}
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

View File

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

View File

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