From d90e7409e4db9101032263527f72f55aa41cf782 Mon Sep 17 00:00:00 2001 From: Jordan Liggitt Date: Sat, 16 May 2015 23:29:18 -0400 Subject: [PATCH] Prevent auth recursion for service account tokens --- cmd/kube-apiserver/app/server.go | 2 +- pkg/apiserver/authn.go | 18 ++++-- pkg/serviceaccount/jwt.go | 13 ++-- pkg/serviceaccount/jwt_test.go | 3 +- pkg/serviceaccount/tokengetter.go | 81 ++++++++++++++++++++++++ test/integration/service_account_test.go | 3 +- 6 files changed, 105 insertions(+), 15 deletions(-) create mode 100644 pkg/serviceaccount/tokengetter.go diff --git a/cmd/kube-apiserver/app/server.go b/cmd/kube-apiserver/app/server.go index 11c4b3f42d..3939b6711a 100644 --- a/cmd/kube-apiserver/app/server.go +++ b/cmd/kube-apiserver/app/server.go @@ -300,7 +300,7 @@ func (s *APIServer) Run(_ []string) error { if s.ServiceAccountKeyFile == "" && s.TLSPrivateKeyFile != "" { s.ServiceAccountKeyFile = s.TLSPrivateKeyFile } - authenticator, err := apiserver.NewAuthenticator(s.BasicAuthFile, s.ClientCAFile, s.TokenAuthFile, s.ServiceAccountKeyFile, s.ServiceAccountLookup, client) + authenticator, err := apiserver.NewAuthenticator(s.BasicAuthFile, s.ClientCAFile, s.TokenAuthFile, s.ServiceAccountKeyFile, s.ServiceAccountLookup, helper) if err != nil { glog.Fatalf("Invalid Authentication Config: %v", err) } diff --git a/pkg/apiserver/authn.go b/pkg/apiserver/authn.go index 3114cced40..99359c9250 100644 --- a/pkg/apiserver/authn.go +++ b/pkg/apiserver/authn.go @@ -21,8 +21,8 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/auth/authenticator" "github.com/GoogleCloudPlatform/kubernetes/pkg/auth/authenticator/bearertoken" - "github.com/GoogleCloudPlatform/kubernetes/pkg/client" "github.com/GoogleCloudPlatform/kubernetes/pkg/serviceaccount" + "github.com/GoogleCloudPlatform/kubernetes/pkg/tools" "github.com/GoogleCloudPlatform/kubernetes/pkg/util" "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/auth/authenticator/password/passwordfile" "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/auth/authenticator/request/basicauth" @@ -32,7 +32,7 @@ import ( ) // NewAuthenticator returns an authenticator.Request or an error -func NewAuthenticator(basicAuthFile, clientCAFile, tokenFile, serviceAccountKeyFile string, serviceAccountLookup bool, client client.Interface) (authenticator.Request, error) { +func NewAuthenticator(basicAuthFile, clientCAFile, tokenFile, serviceAccountKeyFile string, serviceAccountLookup bool, helper tools.EtcdHelper) (authenticator.Request, error) { var authenticators []authenticator.Request if len(basicAuthFile) > 0 { @@ -60,7 +60,7 @@ func NewAuthenticator(basicAuthFile, clientCAFile, tokenFile, serviceAccountKeyF } if len(serviceAccountKeyFile) > 0 { - serviceAccountAuth, err := newServiceAccountAuthenticator(serviceAccountKeyFile, serviceAccountLookup, client) + serviceAccountAuth, err := newServiceAccountAuthenticator(serviceAccountKeyFile, serviceAccountLookup, helper) if err != nil { return nil, err } @@ -98,12 +98,20 @@ func newAuthenticatorFromTokenFile(tokenAuthFile string) (authenticator.Request, } // newServiceAccountAuthenticator returns an authenticator.Request or an error -func newServiceAccountAuthenticator(keyfile string, lookup bool, client client.Interface) (authenticator.Request, error) { +func newServiceAccountAuthenticator(keyfile string, lookup bool, helper tools.EtcdHelper) (authenticator.Request, error) { publicKey, err := serviceaccount.ReadPublicKey(keyfile) if err != nil { return nil, err } - tokenAuthenticator := serviceaccount.JWTTokenAuthenticator([]*rsa.PublicKey{publicKey}, lookup, client) + + var serviceAccountGetter serviceaccount.ServiceAccountTokenGetter + if lookup { + // If we need to look up service accounts and tokens, + // go directly to etcd to avoid recursive auth insanity + serviceAccountGetter = serviceaccount.NewGetterFromEtcdHelper(helper) + } + + tokenAuthenticator := serviceaccount.JWTTokenAuthenticator([]*rsa.PublicKey{publicKey}, lookup, serviceAccountGetter) return bearertoken.New(tokenAuthenticator), nil } diff --git a/pkg/serviceaccount/jwt.go b/pkg/serviceaccount/jwt.go index 5a40c30cda..28bc9588df 100644 --- a/pkg/serviceaccount/jwt.go +++ b/pkg/serviceaccount/jwt.go @@ -29,7 +29,6 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/auth/authenticator" "github.com/GoogleCloudPlatform/kubernetes/pkg/auth/user" - "github.com/GoogleCloudPlatform/kubernetes/pkg/client" ) const ( @@ -124,15 +123,15 @@ func (j *jwtTokenGenerator) GenerateToken(serviceAccount api.ServiceAccount, sec // JWTTokenAuthenticator authenticates tokens as JWT tokens produced by JWTTokenGenerator // Token signatures are verified using each of the given public keys until one works (allowing key rotation) -// If lookup is true, the service account and secret referenced as claims inside the token are retrieved and verified using the given client -func JWTTokenAuthenticator(keys []*rsa.PublicKey, lookup bool, client client.Interface) authenticator.Token { - return &jwtTokenAuthenticator{keys, lookup, client} +// If lookup is true, the service account and secret referenced as claims inside the token are retrieved and verified with the provided ServiceAccountTokenGetter +func JWTTokenAuthenticator(keys []*rsa.PublicKey, lookup bool, getter ServiceAccountTokenGetter) authenticator.Token { + return &jwtTokenAuthenticator{keys, lookup, getter} } type jwtTokenAuthenticator struct { keys []*rsa.PublicKey lookup bool - client client.Interface + getter ServiceAccountTokenGetter } func (j *jwtTokenAuthenticator) AuthenticateToken(token string) (user.Info, bool, error) { @@ -199,7 +198,7 @@ func (j *jwtTokenAuthenticator) AuthenticateToken(token string) (user.Info, bool if j.lookup { // Make sure token hasn't been invalidated by deletion of the secret - secret, err := j.client.Secrets(namespace).Get(secretName) + secret, err := j.getter.GetSecret(namespace, secretName) if err != nil { return nil, false, errors.New("Token has been invalidated") } @@ -208,7 +207,7 @@ func (j *jwtTokenAuthenticator) AuthenticateToken(token string) (user.Info, bool } // Make sure service account still exists (name and UID) - serviceAccount, err := j.client.ServiceAccounts(namespace).Get(serviceAccountName) + serviceAccount, err := j.getter.GetServiceAccount(namespace, serviceAccountName) if err != nil { return nil, false, err } diff --git a/pkg/serviceaccount/jwt_test.go b/pkg/serviceaccount/jwt_test.go index c0de29f08a..c53aa91f2d 100644 --- a/pkg/serviceaccount/jwt_test.go +++ b/pkg/serviceaccount/jwt_test.go @@ -214,7 +214,8 @@ func TestTokenGenerateAndValidate(t *testing.T) { } for k, tc := range testCases { - authenticator := JWTTokenAuthenticator(tc.Keys, tc.Client != nil, tc.Client) + getter := NewGetterFromClient(tc.Client) + authenticator := JWTTokenAuthenticator(tc.Keys, tc.Client != nil, getter) user, ok, err := authenticator.AuthenticateToken(token) if (err != nil) != tc.ExpectedErr { diff --git a/pkg/serviceaccount/tokengetter.go b/pkg/serviceaccount/tokengetter.go new file mode 100644 index 0000000000..d1e8d6b780 --- /dev/null +++ b/pkg/serviceaccount/tokengetter.go @@ -0,0 +1,81 @@ +/* +Copyright 2014 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 serviceaccount + +import ( + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/client" + "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/secret" + secretetcd "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/secret/etcd" + "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/serviceaccount" + serviceaccountetcd "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/serviceaccount/etcd" + "github.com/GoogleCloudPlatform/kubernetes/pkg/tools" +) + +// ServiceAccountTokenGetter defines functions to retrieve a named service account and secret +type ServiceAccountTokenGetter interface { + GetServiceAccount(namespace, name string) (*api.ServiceAccount, error) + GetSecret(namespace, name string) (*api.Secret, error) +} + +// clientGetter implements ServiceAccountTokenGetter using a client.Interface +type clientGetter struct { + client client.Interface +} + +// NewGetterFromClient returns a ServiceAccountTokenGetter that +// uses the specified client to retrieve service accounts and secrets. +// The client should NOT authenticate using a service account token +// the returned getter will be used to retrieve, or recursion will result. +func NewGetterFromClient(c client.Interface) ServiceAccountTokenGetter { + return clientGetter{c} +} +func (c clientGetter) GetServiceAccount(namespace, name string) (*api.ServiceAccount, error) { + return c.client.ServiceAccounts(namespace).Get(name) +} +func (c clientGetter) GetSecret(namespace, name string) (*api.Secret, error) { + return c.client.Secrets(namespace).Get(name) +} + +// registryGetter implements ServiceAccountTokenGetter using a service account and secret registry +type registryGetter struct { + serviceAccounts serviceaccount.Registry + secrets secret.Registry +} + +// NewGetterFromRegistries returns a ServiceAccountTokenGetter that +// uses the specified registries to retrieve service accounts and secrets. +func NewGetterFromRegistries(serviceAccounts serviceaccount.Registry, secrets secret.Registry) ServiceAccountTokenGetter { + return ®istryGetter{serviceAccounts, secrets} +} +func (r *registryGetter) GetServiceAccount(namespace, name string) (*api.ServiceAccount, error) { + ctx := api.WithNamespace(api.NewContext(), namespace) + return r.serviceAccounts.GetServiceAccount(ctx, name) +} +func (r *registryGetter) GetSecret(namespace, name string) (*api.Secret, error) { + ctx := api.WithNamespace(api.NewContext(), namespace) + return r.secrets.GetSecret(ctx, name) +} + +// NewGetterFromEtcdHelper returns a ServiceAccountTokenGetter that +// uses the specified helper to retrieve service accounts and secrets. +func NewGetterFromEtcdHelper(helper tools.EtcdHelper) ServiceAccountTokenGetter { + return NewGetterFromRegistries( + serviceaccount.NewRegistry(serviceaccountetcd.NewStorage(helper)), + secret.NewRegistry(secretetcd.NewStorage(helper)), + ) +} diff --git a/test/integration/service_account_test.go b/test/integration/service_account_test.go index 77b8a9d451..44b7c7472f 100644 --- a/test/integration/service_account_test.go +++ b/test/integration/service_account_test.go @@ -366,7 +366,8 @@ func startServiceAccountTestServer(t *testing.T) (*client.Client, client.Config, return nil, false, nil }) serviceAccountKey, err := rsa.GenerateKey(rand.Reader, 2048) - serviceAccountTokenAuth := serviceaccount.JWTTokenAuthenticator([]*rsa.PublicKey{&serviceAccountKey.PublicKey}, true, rootClient) + serviceAccountTokenGetter := serviceaccount.NewGetterFromClient(rootClient) + serviceAccountTokenAuth := serviceaccount.JWTTokenAuthenticator([]*rsa.PublicKey{&serviceAccountKey.PublicKey}, true, serviceAccountTokenGetter) authenticator := union.New( bearertoken.New(rootTokenAuth), bearertoken.New(serviceAccountTokenAuth),