mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-24 04:06:03 +00:00
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:
commit
346f965871
@ -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")
|
||||
|
@ -190,6 +190,7 @@ func Run(s *options.APIServer) error {
|
||||
ServiceAccountTokenGetter: serviceAccountGetter,
|
||||
KeystoneURL: s.KeystoneURL,
|
||||
WebhookTokenAuthnConfigFile: s.WebhookTokenAuthnConfigFile,
|
||||
WebhookTokenAuthnCacheTTL: s.WebhookTokenAuthnCacheTTL,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
|
@ -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 -->
|
||||
|
@ -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
|
||||
|
@ -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"`
|
||||
|
||||
|
@ -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
66
pkg/util/cache/lruexpirecache.go
vendored
Normal 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
63
pkg/util/cache/lruexpirecache_test.go
vendored
Normal 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")
|
||||
}
|
@ -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
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user