Skip to content

Commit

Permalink
apiserver: add a bootstrap token authenticator for TLS bootstrapping
Browse files Browse the repository at this point in the history
  • Loading branch information
ericchiang committed Feb 14, 2017
1 parent 436fa5c commit 70fa725
Show file tree
Hide file tree
Showing 6 changed files with 319 additions and 15 deletions.
2 changes: 2 additions & 0 deletions cmd/kube-apiserver/app/BUILD
Expand Up @@ -20,6 +20,7 @@ go_library(
"//pkg/apis/batch:go_default_library",
"//pkg/capabilities:go_default_library",
"//pkg/client/clientset_generated/internalclientset:go_default_library",
"//pkg/client/informers/informers_generated/internalversion:go_default_library",
"//pkg/cloudprovider:go_default_library",
"//pkg/cloudprovider/providers:go_default_library",
"//pkg/controller/informers:go_default_library",
Expand Down Expand Up @@ -64,6 +65,7 @@ go_library(
"//vendor:k8s.io/apiserver/pkg/admission",
"//vendor:k8s.io/apiserver/pkg/server",
"//vendor:k8s.io/apiserver/pkg/server/filters",
"//vendor:k8s.io/apiserver/plugin/pkg/authenticator/token/bootstrap",
],
)

Expand Down
41 changes: 27 additions & 14 deletions cmd/kube-apiserver/app/server.go
Expand Up @@ -35,6 +35,7 @@ import (
"github.com/spf13/cobra"
"github.com/spf13/pflag"

"k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/openapi"
"k8s.io/apimachinery/pkg/runtime/schema"
utilerrors "k8s.io/apimachinery/pkg/util/errors"
Expand All @@ -44,11 +45,13 @@ import (
"k8s.io/apiserver/pkg/admission"
genericapiserver "k8s.io/apiserver/pkg/server"
"k8s.io/apiserver/pkg/server/filters"
"k8s.io/apiserver/plugin/pkg/authenticator/token/bootstrap"
"k8s.io/kubernetes/cmd/kube-apiserver/app/options"
"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/apis/batch"
"k8s.io/kubernetes/pkg/capabilities"
"k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset"
"k8s.io/kubernetes/pkg/client/informers/informers_generated/internalversion"
"k8s.io/kubernetes/pkg/cloudprovider"
"k8s.io/kubernetes/pkg/controller/informers"
serviceaccountcontroller "k8s.io/kubernetes/pkg/controller/serviceaccount"
Expand Down Expand Up @@ -241,6 +244,25 @@ func Run(s *options.ServerRunOptions) error {
}
}

client, err := internalclientset.NewForConfig(genericConfig.LoopbackClientConfig)
if err != nil {
kubeAPIVersions := os.Getenv("KUBE_API_VERSIONS")
if len(kubeAPIVersions) == 0 {
return fmt.Errorf("failed to create clientset: %v", err)
}

// KUBE_API_VERSIONS is used in test-update-storage-objects.sh, disabling a number of API
// groups. This leads to a nil client above and undefined behaviour further down.
// TODO: get rid of KUBE_API_VERSIONS or define sane behaviour if set
glog.Errorf("Failed to create clientset with KUBE_API_VERSIONS=%q. KUBE_API_VERSIONS is only for testing. Things will break.", kubeAPIVersions)
}

// NOTE(ericchang): Internal informers should switch to using
// 'pkg/client/informers/informers_generated', the second informer
// created here.
sharedInformers := informers.NewSharedInformerFactory(nil, client, 10*time.Minute)
internalSharedInformers := internalversion.NewSharedInformerFactory(client, 10*time.Minute)

authenticatorConfig := s.Authentication.ToAuthenticationConfig()
if s.Authentication.ServiceAccounts.Lookup {
// If we need to look up service accounts and tokens,
Expand All @@ -251,26 +273,16 @@ func Run(s *options.ServerRunOptions) error {
}
authenticatorConfig.ServiceAccountTokenGetter = serviceaccountcontroller.NewGetterFromStorageInterface(storageConfig, storageFactory.ResourcePrefix(api.Resource("serviceaccounts")), storageFactory.ResourcePrefix(api.Resource("secrets")))
}
authenticatorConfig.BootstrapTokenAuthenticator = bootstrap.NewTokenAuthenticator(
internalSharedInformers.Core().InternalVersion().Secrets(),
v1.NamespaceSystem,
)

apiAuthenticator, securityDefinitions, err := authenticatorConfig.New()
if err != nil {
return fmt.Errorf("invalid Authentication Config: %v", err)
}

client, err := internalclientset.NewForConfig(genericConfig.LoopbackClientConfig)
if err != nil {
kubeAPIVersions := os.Getenv("KUBE_API_VERSIONS")
if len(kubeAPIVersions) == 0 {
return fmt.Errorf("failed to create clientset: %v", err)
}

// KUBE_API_VERSIONS is used in test-update-storage-objects.sh, disabling a number of API
// groups. This leads to a nil client above and undefined behaviour further down.
// TODO: get rid of KUBE_API_VERSIONS or define sane behaviour if set
glog.Errorf("Failed to create clientset with KUBE_API_VERSIONS=%q. KUBE_API_VERSIONS is only for testing. Things will break.", kubeAPIVersions)
}
sharedInformers := informers.NewSharedInformerFactory(nil, client, 10*time.Minute)

authorizationConfig := s.Authorization.ToAuthorizationConfig(sharedInformers)
apiAuthorizer, err := authorizationConfig.New()
if err != nil {
Expand Down Expand Up @@ -351,6 +363,7 @@ func Run(s *options.ServerRunOptions) error {
}

sharedInformers.Start(wait.NeverStop)
internalSharedInformers.Start(wait.NeverStop)
m.GenericAPIServer.PrepareRun().Run(wait.NeverStop)
return nil
}
Expand Down
6 changes: 5 additions & 1 deletion pkg/kubeapiserver/authenticator/config.go
Expand Up @@ -66,7 +66,8 @@ type AuthenticatorConfig struct {
RequestHeaderConfig *authenticatorfactory.RequestHeaderConfig

// TODO, this is the only non-serializable part of the entire config. Factor it out into a clientconfig
ServiceAccountTokenGetter serviceaccount.ServiceAccountTokenGetter
ServiceAccountTokenGetter serviceaccount.ServiceAccountTokenGetter
BootstrapTokenAuthenticator authenticator.Token
}

// New returns an authenticator.Request or an error that supports the standard
Expand Down Expand Up @@ -128,6 +129,9 @@ func (config AuthenticatorConfig) New() (authenticator.Request, *spec.SecurityDe
authenticators = append(authenticators, tokenAuth)
hasTokenAuth = true
}
if config.BootstrapTokenAuthenticator != nil {
authenticators = append(authenticators, bearertoken.New(config.BootstrapTokenAuthenticator))
}
if len(config.ServiceAccountKeyFiles) > 0 {
serviceAccountAuth, err := newServiceAccountAuthenticator(config.ServiceAccountKeyFiles, config.ServiceAccountLookup, config.ServiceAccountTokenGetter)
if err != nil {
Expand Down
@@ -0,0 +1,100 @@
/*
Copyright 2017 The Kubernetes Authors.
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 bootstrap provides a token authenticator for TLS bootstrap secrets.
*/
package bootstrap

import (
"fmt"

"k8s.io/apimachinery/pkg/labels"
"k8s.io/apiserver/pkg/authentication/user"
"k8s.io/kubernetes/pkg/client/informers/informers_generated/internalversion/core/internalversion"
listers "k8s.io/kubernetes/pkg/client/listers/core/internalversion"
)

const (
SecretType = "bootstrap.kubernetes.io/token"

TokenID = "token-id"
TokenSecret = "token-secret"

BootstrapUserPrefix = "system:bootstrap:"
BootstrapGroup = "system:bootstrappers"
)

// NewTokenAuthenticator initializes a bootstrap token authenticator.
func NewTokenAuthenticator(informer internalversion.SecretInformer, namespace string) *TokenAuthenticator {
return &TokenAuthenticator{informer.Lister().Secrets(namespace)}
}

// TokenAuthenticator authenticates bootstrap tokens from secrets in the API server.
type TokenAuthenticator struct {
lister listers.SecretNamespaceLister

// TODO(ericchiang): Does the SecretLister do the caching or do we do it here?
}

// AuthenticateToken tries to match the provided token to a bootstrap token secret
// in the "kube-system" namespace. If found, it authenticates the token in the
// "system:bootstrappers" group and with the "system:bootstrap:(token-id)" username.
//
// All secrets must be of type "bootstrap.kubernetes.io/token". An example secret:
//
// apiVersion: v1
// kind: Secret
// metadata:
// name: bootstrap-token-( token id )
// namespace: kube-system
// data:
// token-secret: ( private part of token )
// token-id: ( token id )
// type: bootstrap.kubernetes.io/token
//
func (t *TokenAuthenticator) AuthenticateToken(token string) (user.Info, bool, error) {
secrets, err := t.lister.List(labels.Everything())
if err != nil {
return nil, false, err
}

for _, secret := range secrets {
if secret.Type != SecretType || secret.Data == nil {
continue
}

ts, ok := secret.Data[TokenSecret]
if !ok || len(ts) == 0 {
continue
}

id, ok := secret.Data[TokenID]
if !ok || len(id) == 0 {
continue
}

if token != fmt.Sprintf("%s:%s", id, ts) {
continue
}

return &user.DefaultInfo{
Name: BootstrapUserPrefix + string(id),
Groups: []string{BootstrapGroup},
}, true, nil
}
return nil, false, nil
}
@@ -0,0 +1,156 @@
/*
Copyright 2017 The Kubernetes Authors.
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 bootstrap

import (
"reflect"
"testing"
"time"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apiserver/pkg/authentication/user"
"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/api/v1"
"k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/fake"
"k8s.io/kubernetes/pkg/client/informers/informers_generated/internalversion"
"k8s.io/kubernetes/pkg/controller"
)

type secretLister struct {
secrets []v1.Secret
}

func (s secretLister) ListSecrets(namespace string) (*v1.SecretList, error) {
var list v1.SecretList
if namespace == "kube-system" {
list.Items = s.secrets
}
return &list, nil
}

func TestTokenAuthenticator(t *testing.T) {
tests := []struct {
name string

secrets []runtime.Object
token string

wantNotFound bool
wantUser *user.DefaultInfo
}{
{
name: "valid token",
secrets: []runtime.Object{
&api.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "foo",
Namespace: "kube-system",
},
Data: map[string][]byte{
TokenID: []byte("node1"),
TokenSecret: []byte("foobar"),
},
Type: SecretType,
},
},
token: "node1:foobar",
wantUser: &user.DefaultInfo{
Name: "system:bootstrap:node1",
Groups: []string{"system:bootstrappers"},
},
},
{
name: "wrong namespace",
secrets: []runtime.Object{
&api.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "foo",
Namespace: "wrong-namespace",
},
Data: map[string][]byte{
TokenID: []byte("node1"),
TokenSecret: []byte("foobar"),
},
Type: SecretType,
},
},
token: "node1:foobar",
wantNotFound: true,
},
{
name: "wrong token",
secrets: []runtime.Object{
&api.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "foo",
Namespace: "kube-system",
},
Data: map[string][]byte{
TokenID: []byte("node1"),
TokenSecret: []byte("foobar"),
},
Type: SecretType,
},
},
token: "node1:barfoo",
wantNotFound: true,
},
}

for _, test := range tests {
func() {
f := internalversion.NewSharedInformerFactory(
fake.NewSimpleClientset(test.secrets...),
controller.NoResyncPeriodFunc(),
)
informer := f.Core().InternalVersion().Secrets()
a := NewTokenAuthenticator(informer)

c := make(chan struct{})
f.Start(c)
defer close(c)

// Without this sleep, the informer doesn't do its initial sync.
time.Sleep(5 * time.Millisecond)

u, found, err := a.AuthenticateToken(test.token)
if err != nil {
t.Errorf("test %q returned an error: %v", test.name, err)
return
}

if !found {
if !test.wantNotFound {
t.Errorf("test %q expected to get user", test.name)
}
return
}

if test.wantNotFound {
t.Errorf("test %q expected to not get a user", test.name)
return
}

gotUser := u.(*user.DefaultInfo)

if !reflect.DeepEqual(gotUser, test.wantUser) {
t.Errorf("test %q want user=%#v, got=%#v", test.name, test.wantUser, gotUser)
}
}()
}
}

0 comments on commit 70fa725

Please sign in to comment.