Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
apiserver: add a bootstrap token authenticator for TLS bootstrapping
- Loading branch information
1 parent
436fa5c
commit 70fa725
Showing
6 changed files
with
319 additions
and
15 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
100 changes: 100 additions & 0 deletions
100
staging/src/k8s.io/apiserver/plugin/pkg/authenticator/token/bootstrap/bootstrap.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} |
156 changes: 156 additions & 0 deletions
156
staging/src/k8s.io/apiserver/plugin/pkg/authenticator/token/bootstrap/bootstrap_test.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
} | ||
}() | ||
} | ||
} |
Oops, something went wrong.