Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cache Webhook Authentication responses #25694

Merged
merged 2 commits into from
May 21, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions cmd/kube-apiserver/app/options/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -52,6 +53,7 @@ func NewAPIServer() *APIServer {
EnableHttps: true,
HTTPTimeout: time.Duration(5) * time.Second,
},
WebhookTokenAuthnCacheTTL: 2 * time.Minute,
}
return &s
}
Expand All @@ -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")
Expand Down
1 change: 1 addition & 0 deletions cmd/kube-apiserver/app/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ func Run(s *options.APIServer) error {
ServiceAccountTokenGetter: serviceAccountGetter,
KeystoneURL: s.KeystoneURL,
WebhookTokenAuthnConfigFile: s.WebhookTokenAuthnConfigFile,
WebhookTokenAuthnCacheTTL: s.WebhookTokenAuthnCacheTTL,
})

if err != nil {
Expand Down
3 changes: 2 additions & 1 deletion docs/admin/kube-apiserver.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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 -->
Expand Down
1 change: 1 addition & 0 deletions hack/verify-flags/known-flags.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions pkg/apis/authentication.k8s.io/v1beta1/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`

Expand Down
8 changes: 5 additions & 3 deletions pkg/apiserver/authenticator/authn.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package authenticator

import (
"crypto/rsa"
"time"

"k8s.io/kubernetes/pkg/auth/authenticator"
"k8s.io/kubernetes/pkg/auth/authenticator/bearertoken"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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
}
Expand Down
66 changes: 66 additions & 0 deletions pkg/util/cache/lruexpirecache.go
Original file line number Diff line number Diff line change
@@ -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 changes: 63 additions & 0 deletions pkg/util/cache/lruexpirecache_test.go
Original file line number Diff line number Diff line change
@@ -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")
}
33 changes: 21 additions & 12 deletions plugin/pkg/auth/authenticator/token/webhook/webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Conditional cast? I'm still uneasy about dealing with specific serialized versions in this code, unlike the rest of the codebase

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cache should only contain TokenReviewStatus objects. If we get something that isn't, we must be in a pretty bad state, and I don't know if there is a better answer than crashing.

} 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
3 changes: 2 additions & 1 deletion test/integration/auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import (
"strconv"
"strings"
"testing"
"time"

"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/api/testapi"
Expand Down Expand Up @@ -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
}
Expand Down