diff --git a/cmd/kube-controller-manager/app/BUILD b/cmd/kube-controller-manager/app/BUILD index 5138b40f062..55550e41058 100644 --- a/cmd/kube-controller-manager/app/BUILD +++ b/cmd/kube-controller-manager/app/BUILD @@ -14,6 +14,7 @@ go_library( "apps.go", "autoscaling.go", "batch.go", + "bootstrap.go", "certificates.go", "controllermanager.go", "core.go", @@ -42,6 +43,7 @@ go_library( "//pkg/cloudprovider/providers/photon:go_default_library", "//pkg/cloudprovider/providers/vsphere:go_default_library", "//pkg/controller:go_default_library", + "//pkg/controller/bootstrap:go_default_library", "//pkg/controller/certificates:go_default_library", "//pkg/controller/cronjob:go_default_library", "//pkg/controller/daemon:go_default_library", diff --git a/cmd/kube-controller-manager/app/bootstrap.go b/cmd/kube-controller-manager/app/bootstrap.go new file mode 100644 index 00000000000..05ff566fd88 --- /dev/null +++ b/cmd/kube-controller-manager/app/bootstrap.go @@ -0,0 +1,35 @@ +/* +Copyright 2016 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 app + +import "k8s.io/kubernetes/pkg/controller/bootstrap" + +func startBootstrapSignerController(ctx ControllerContext) (bool, error) { + go bootstrap.NewBootstrapSigner( + ctx.ClientBuilder.ClientGoClientOrDie("bootstrap-signer"), + bootstrap.DefaultBootstrapSignerOptions(), + ).Run(ctx.Stop) + return true, nil +} + +func startTokenCleanerController(ctx ControllerContext) (bool, error) { + go bootstrap.NewTokenCleaner( + ctx.ClientBuilder.ClientGoClientOrDie("token-cleaner"), + bootstrap.DefaultTokenCleanerOptions(), + ).Run(ctx.Stop) + return true, nil +} diff --git a/cmd/kube-controller-manager/app/controllermanager.go b/cmd/kube-controller-manager/app/controllermanager.go index 19dc599bd49..0b34ae7e532 100644 --- a/cmd/kube-controller-manager/app/controllermanager.go +++ b/cmd/kube-controller-manager/app/controllermanager.go @@ -270,7 +270,10 @@ func KnownControllers() []string { return sets.StringKeySet(newControllerInitializers()).List() } -var ControllersDisabledByDefault = sets.NewString() +var ControllersDisabledByDefault = sets.NewString( + "bootstrapsigner", + "tokencleaner", +) func newControllerInitializers() map[string]InitFunc { controllers := map[string]InitFunc{} @@ -291,6 +294,8 @@ func newControllerInitializers() map[string]InitFunc { controllers["cronjob"] = startCronJobController controllers["certificatesigningrequests"] = startCSRController controllers["ttl"] = startTTLController + controllers["bootstrapsigner"] = startBootstrapSignerController + controllers["tokencleaner"] = startTokenCleanerController return controllers } diff --git a/cmd/kubeadm/app/cmd/BUILD b/cmd/kubeadm/app/cmd/BUILD index 73cb8dd6785..dc1b464a3b3 100644 --- a/cmd/kubeadm/app/cmd/BUILD +++ b/cmd/kubeadm/app/cmd/BUILD @@ -36,6 +36,7 @@ go_library( "//cmd/kubeadm/app/preflight:go_default_library", "//cmd/kubeadm/app/util:go_default_library", "//pkg/api:go_default_library", + "//pkg/bootstrap/api:go_default_library", "//pkg/kubectl:go_default_library", "//pkg/kubectl/cmd/util:go_default_library", "//pkg/util/initsystem:go_default_library", @@ -48,6 +49,7 @@ go_library( "//vendor:k8s.io/apimachinery/pkg/runtime", "//vendor:k8s.io/apimachinery/pkg/util/net", "//vendor:k8s.io/apiserver/pkg/util/flag", + "//vendor:k8s.io/client-go/pkg/api", "//vendor:k8s.io/client-go/util/cert", ], ) diff --git a/cmd/kubeadm/app/cmd/token.go b/cmd/kubeadm/app/cmd/token.go index c6c9957863d..996d674a0d6 100644 --- a/cmd/kubeadm/app/cmd/token.go +++ b/cmd/kubeadm/app/cmd/token.go @@ -29,11 +29,12 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/fields" + "k8s.io/client-go/pkg/api" kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm" kubemaster "k8s.io/kubernetes/cmd/kubeadm/app/master" "k8s.io/kubernetes/cmd/kubeadm/app/phases/kubeconfig" kubeadmutil "k8s.io/kubernetes/cmd/kubeadm/app/util" - "k8s.io/kubernetes/pkg/api" + bootstrapapi "k8s.io/kubernetes/pkg/bootstrap/api" "k8s.io/kubernetes/pkg/kubectl" ) @@ -164,7 +165,7 @@ func RunListTokens(out io.Writer, errW io.Writer, cmd *cobra.Command) error { tokenSelector := fields.SelectorFromSet( map[string]string{ - api.SecretTypeField: string(api.SecretTypeBootstrapToken), + api.SecretTypeField: string(bootstrapapi.SecretTypeBootstrapToken), }, ) listOptions := metav1.ListOptions{ diff --git a/cmd/kubeadm/app/util/BUILD b/cmd/kubeadm/app/util/BUILD index 8e369653048..acde1dbe7eb 100644 --- a/cmd/kubeadm/app/util/BUILD +++ b/cmd/kubeadm/app/util/BUILD @@ -21,8 +21,8 @@ go_library( "//cmd/kubeadm/app/apis/kubeadm:go_default_library", "//cmd/kubeadm/app/apis/kubeadm/v1alpha1:go_default_library", "//cmd/kubeadm/app/preflight:go_default_library", - "//pkg/api:go_default_library", "//pkg/api/v1:go_default_library", + "//pkg/bootstrap/api:go_default_library", "//pkg/client/clientset_generated/clientset:go_default_library", "//vendor:k8s.io/apimachinery/pkg/api/errors", "//vendor:k8s.io/apimachinery/pkg/apis/meta/v1", diff --git a/cmd/kubeadm/app/util/tokens.go b/cmd/kubeadm/app/util/tokens.go index e173cd32577..051fe5e777e 100644 --- a/cmd/kubeadm/app/util/tokens.go +++ b/cmd/kubeadm/app/util/tokens.go @@ -29,8 +29,8 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm" kubeadmapiext "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/v1alpha1" - "k8s.io/kubernetes/pkg/api" v1 "k8s.io/kubernetes/pkg/api/v1" + bootstrapapi "k8s.io/kubernetes/pkg/bootstrap/api" "k8s.io/kubernetes/pkg/client/clientset_generated/clientset" ) @@ -154,7 +154,9 @@ func UpdateOrCreateToken(client *clientset.Clientset, d *kubeadmapi.TokenDiscove ObjectMeta: metav1.ObjectMeta{ Name: secretName, }, - Type: api.SecretTypeBootstrapToken, + // TODO(jbeda): convert kubeadm to client-go + // https://github.com/kubernetes/kubeadm/issues/52 + Type: v1.SecretType(bootstrapapi.SecretTypeBootstrapToken), Data: encodeTokenSecretData(d, tokenDuration), } if _, err := client.Secrets(metav1.NamespaceSystem).Create(secret); err == nil { diff --git a/hack/.linted_packages b/hack/.linted_packages index 0a91d4c10d4..1e8a9c89280 100644 --- a/hack/.linted_packages +++ b/hack/.linted_packages @@ -87,6 +87,7 @@ pkg/apis/rbac/install pkg/apis/rbac/v1alpha1 pkg/apis/storage/install pkg/apis/storage/validation +pkg/bootstrap/api pkg/client/conditions pkg/client/informers/informers_generated/apps pkg/client/informers/informers_generated/apps/internalversion diff --git a/pkg/BUILD b/pkg/BUILD index e7451b72e77..d8acf431ecd 100644 --- a/pkg/BUILD +++ b/pkg/BUILD @@ -31,6 +31,7 @@ filegroup( "//pkg/apis/storage:all-srcs", "//pkg/auth/authorizer/abac:all-srcs", "//pkg/auth/user:all-srcs", + "//pkg/bootstrap/api:all-srcs", "//pkg/capabilities:all-srcs", "//pkg/client/chaosclient:all-srcs", "//pkg/client/clientset_generated/clientset:all-srcs", diff --git a/pkg/api/types.go b/pkg/api/types.go index 457a5f64566..80e6f430421 100644 --- a/pkg/api/types.go +++ b/pkg/api/types.go @@ -189,6 +189,8 @@ const ( NamespaceNone string = "" // NamespaceSystem is the system namespace where we place system components. NamespaceSystem string = "kube-system" + // NamespacePublic is the namespace where we place public info (ConfigMaps) + NamespacePublic string = "kube-public" // TerminationMessagePathDefault means the default path to capture the application termination message running in a container TerminationMessagePathDefault string = "/dev/termination-log" ) @@ -3344,9 +3346,6 @@ const ( // - Secret.Data["token"] - a token that identifies the service account to the API SecretTypeServiceAccountToken SecretType = "kubernetes.io/service-account-token" - // SecretTypeBootstrapToken is the key for tokens used by kubeadm to validate cluster info during discovery. - SecretTypeBootstrapToken = "bootstrap.kubernetes.io/token" - // ServiceAccountNameKey is the key of the required annotation for SecretTypeServiceAccountToken secrets ServiceAccountNameKey = "kubernetes.io/service-account.name" // ServiceAccountUIDKey is the key of the required annotation for SecretTypeServiceAccountToken secrets diff --git a/pkg/bootstrap/api/BUILD b/pkg/bootstrap/api/BUILD new file mode 100644 index 00000000000..192e9698062 --- /dev/null +++ b/pkg/bootstrap/api/BUILD @@ -0,0 +1,31 @@ +package(default_visibility = ["//visibility:public"]) + +licenses(["notice"]) + +load( + "@io_bazel_rules_go//go:def.bzl", + "go_library", +) + +go_library( + name = "go_default_library", + srcs = [ + "doc.go", + "types.go", + ], + tags = ["automanaged"], + deps = ["//vendor:k8s.io/client-go/pkg/api/v1"], +) + +filegroup( + name = "package-srcs", + srcs = glob(["**"]), + tags = ["automanaged"], + visibility = ["//visibility:private"], +) + +filegroup( + name = "all-srcs", + srcs = [":package-srcs"], + tags = ["automanaged"], +) diff --git a/pkg/bootstrap/api/doc.go b/pkg/bootstrap/api/doc.go new file mode 100644 index 00000000000..9fed79198db --- /dev/null +++ b/pkg/bootstrap/api/doc.go @@ -0,0 +1,20 @@ +/* +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 api (pkg/bootstrap/api) contains constants and types needed for +// bootstrap tokens as maintained by the BootstrapSigner and TokenCleaner +// controllers (in pkg/controller/bootstrap) +package api // import "k8s.io/kubernetes/pkg/bootstrap/api" diff --git a/pkg/bootstrap/api/types.go b/pkg/bootstrap/api/types.go new file mode 100644 index 00000000000..8f046792a56 --- /dev/null +++ b/pkg/bootstrap/api/types.go @@ -0,0 +1,48 @@ +/* +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 api + +import ( + "k8s.io/client-go/pkg/api/v1" +) + +const ( + // SecretTypeBootstrapToken is used during the automated bootstrap process (first + // implemented by kubeadm). It stores tokens that are used to sign well known + // ConfigMaps. They may also eventually be used for authentication. + SecretTypeBootstrapToken v1.SecretType = "bootstrap.kubernetes.io/token" + + // BootstrapTokenIDKey is the id of this token. This can be transmitted in the + // clear and encoded in the name of the secret. It should be a random 6 + // character string. Required + BootstrapTokenIDKey = "token-id" + + // BootstrapTokenSecretKey is the actual secret. Typically this is a random 16 + // character string. Required. + BootstrapTokenSecretKey = "token-secret" + + // BootstrapTokenExpirationKey is when this token should be expired and no + // longer used. A controller will delete this resource after this time. This + // is an absolute UTC time using RFC3339. If this cannot be parsed, the token + // should be considered invalid. Optional. + BootstrapTokenExpirationKey = "expiration" + + // BootstrapTokenUsageSigningKey signals that this token should be used to + // sign configs as part of the bootstrap process. Value must be "true". Any + // other value is assumed to be false. Optional. + BootstrapTokenUsageSigningKey = "usage-bootstrap-signing" +) diff --git a/pkg/controller/BUILD b/pkg/controller/BUILD index 9f4848a9c7f..78daa3da1c6 100644 --- a/pkg/controller/BUILD +++ b/pkg/controller/BUILD @@ -84,6 +84,7 @@ filegroup( name = "all-srcs", srcs = [ ":package-srcs", + "//pkg/controller/bootstrap:all-srcs", "//pkg/controller/certificates:all-srcs", "//pkg/controller/cloud:all-srcs", "//pkg/controller/cronjob:all-srcs", diff --git a/pkg/controller/bootstrap/BUILD b/pkg/controller/bootstrap/BUILD new file mode 100644 index 00000000000..185e348b65c --- /dev/null +++ b/pkg/controller/bootstrap/BUILD @@ -0,0 +1,75 @@ +package(default_visibility = ["//visibility:public"]) + +licenses(["notice"]) + +load( + "@io_bazel_rules_go//go:def.bzl", + "go_library", + "go_test", +) + +go_test( + name = "go_default_test", + srcs = [ + "bootstrapsigner_test.go", + "common_test.go", + "jws_test.go", + "tokencleaner_test.go", + "util_test.go", + ], + library = ":go_default_library", + tags = ["automanaged"], + deps = [ + "//pkg/bootstrap/api:go_default_library", + "//vendor:github.com/davecgh/go-spew/spew", + "//vendor:k8s.io/apimachinery/pkg/apis/meta/v1", + "//vendor:k8s.io/apimachinery/pkg/runtime/schema", + "//vendor:k8s.io/client-go/kubernetes/fake", + "//vendor:k8s.io/client-go/pkg/api", + "//vendor:k8s.io/client-go/pkg/api/v1", + "//vendor:k8s.io/client-go/testing", + ], +) + +go_library( + name = "go_default_library", + srcs = [ + "bootstrapsigner.go", + "doc.go", + "jws.go", + "tokencleaner.go", + "util.go", + ], + tags = ["automanaged"], + deps = [ + "//pkg/bootstrap/api:go_default_library", + "//pkg/util/metrics:go_default_library", + "//vendor:github.com/golang/glog", + "//vendor:github.com/square/go-jose", + "//vendor:k8s.io/apimachinery/pkg/api/errors", + "//vendor:k8s.io/apimachinery/pkg/apis/meta/v1", + "//vendor:k8s.io/apimachinery/pkg/fields", + "//vendor:k8s.io/apimachinery/pkg/runtime", + "//vendor:k8s.io/apimachinery/pkg/util/runtime", + "//vendor:k8s.io/apimachinery/pkg/util/wait", + "//vendor:k8s.io/apimachinery/pkg/watch", + "//vendor:k8s.io/client-go/kubernetes", + "//vendor:k8s.io/client-go/pkg/api", + "//vendor:k8s.io/client-go/pkg/api/v1", + "//vendor:k8s.io/client-go/tools/cache", + "//vendor:k8s.io/client-go/util/workqueue", + ], +) + +filegroup( + name = "package-srcs", + srcs = glob(["**"]), + tags = ["automanaged"], + visibility = ["//visibility:private"], +) + +filegroup( + name = "all-srcs", + srcs = [":package-srcs"], + tags = ["automanaged"], +) diff --git a/pkg/controller/bootstrap/bootstrapsigner.go b/pkg/controller/bootstrap/bootstrapsigner.go new file mode 100644 index 00000000000..64218511672 --- /dev/null +++ b/pkg/controller/bootstrap/bootstrapsigner.go @@ -0,0 +1,303 @@ +/* +Copyright 2016 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 ( + "strings" + "time" + + "github.com/golang/glog" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/fields" + "k8s.io/apimachinery/pkg/runtime" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/apimachinery/pkg/watch" + clientset "k8s.io/client-go/kubernetes" + "k8s.io/client-go/pkg/api" + "k8s.io/client-go/pkg/api/v1" + "k8s.io/client-go/tools/cache" + "k8s.io/client-go/util/workqueue" + bootstrapapi "k8s.io/kubernetes/pkg/bootstrap/api" + "k8s.io/kubernetes/pkg/util/metrics" +) + +const ( + configMapClusterInfo = "cluster-info" + kubeConfigKey = "kubeconfig" + signaturePrefix = "jws-kubeconfig-" +) + +// BootstrapSignerOptions contains options for the BootstrapSigner +type BootstrapSignerOptions struct { + + // ConfigMapNamespace is the namespace of the ConfigMap + ConfigMapNamespace string + + // ConfigMapName is the name for the ConfigMap + ConfigMapName string + + // TokenSecretNamespace string is the namespace for token Secrets. + TokenSecretNamespace string + + // ConfigMapResynce is the time.Duration at which to fully re-list configmaps. + // If zero, re-list will be delayed as long as possible + ConfigMapResync time.Duration + + // SecretResync is the time.Duration at which to fully re-list secrets. + // If zero, re-list will be delayed as long as possible + SecretResync time.Duration +} + +// DefaultBootstrapSignerOptions returns a set of default options for creating a +// BootstrapSigner +func DefaultBootstrapSignerOptions() BootstrapSignerOptions { + return BootstrapSignerOptions{ + ConfigMapNamespace: api.NamespacePublic, + ConfigMapName: configMapClusterInfo, + TokenSecretNamespace: api.NamespaceSystem, + } +} + +// BootstrapSigner is a controller that signs a ConfigMap with a set of tokens. +type BootstrapSigner struct { + client clientset.Interface + configMapKey string + secretNamespace string + + configMaps cache.Store + secrets cache.Store + + // syncQueue handles synchronizing updates to the ConfigMap. We'll only ever + // have one item (Named ) in this queue. We are using it + // serializes and collapses updates as they can come from both the ConfigMap + // and Secrets controllers. + syncQueue workqueue.Interface + + // Since we join two objects, we'll watch both of them with controllers. + configMapsController cache.Controller + secretsController cache.Controller +} + +// NewBootstrapSigner returns a new *BootstrapSigner. +// +// TODO: Switch to shared informers +func NewBootstrapSigner(cl clientset.Interface, options BootstrapSignerOptions) *BootstrapSigner { + e := &BootstrapSigner{ + client: cl, + configMapKey: options.ConfigMapNamespace + "/" + options.ConfigMapName, + secretNamespace: options.TokenSecretNamespace, + syncQueue: workqueue.NewNamed("bootstrap_signer_queue"), + } + if cl.Core().RESTClient().GetRateLimiter() != nil { + metrics.RegisterMetricAndTrackRateLimiterUsage("bootstrap_signer", cl.Core().RESTClient().GetRateLimiter()) + } + configMapSelector := fields.SelectorFromSet(map[string]string{api.ObjectNameField: options.ConfigMapName}) + e.configMaps, e.configMapsController = cache.NewInformer( + &cache.ListWatch{ + ListFunc: func(lo metav1.ListOptions) (runtime.Object, error) { + lo.FieldSelector = configMapSelector.String() + return e.client.Core().ConfigMaps(options.ConfigMapNamespace).List(lo) + }, + WatchFunc: func(lo metav1.ListOptions) (watch.Interface, error) { + lo.FieldSelector = configMapSelector.String() + return e.client.Core().ConfigMaps(options.ConfigMapNamespace).Watch(lo) + }, + }, + &v1.ConfigMap{}, + options.ConfigMapResync, + cache.ResourceEventHandlerFuncs{ + AddFunc: func(_ interface{}) { e.pokeConfigMapSync() }, + UpdateFunc: func(_, _ interface{}) { e.pokeConfigMapSync() }, + }, + ) + + secretSelector := fields.SelectorFromSet(map[string]string{api.SecretTypeField: string(bootstrapapi.SecretTypeBootstrapToken)}) + e.secrets, e.secretsController = cache.NewInformer( + &cache.ListWatch{ + ListFunc: func(lo metav1.ListOptions) (runtime.Object, error) { + lo.FieldSelector = secretSelector.String() + return e.client.Core().Secrets(e.secretNamespace).List(lo) + }, + WatchFunc: func(lo metav1.ListOptions) (watch.Interface, error) { + lo.FieldSelector = secretSelector.String() + return e.client.Core().Secrets(e.secretNamespace).Watch(lo) + }, + }, + &v1.Secret{}, + options.SecretResync, + cache.ResourceEventHandlerFuncs{ + AddFunc: func(_ interface{}) { e.pokeConfigMapSync() }, + UpdateFunc: func(_, _ interface{}) { e.pokeConfigMapSync() }, + DeleteFunc: func(_ interface{}) { e.pokeConfigMapSync() }, + }, + ) + return e +} + +// Run runs controller loops and returns when they are done +func (e *BootstrapSigner) Run(stopCh <-chan struct{}) { + go e.configMapsController.Run(stopCh) + go e.secretsController.Run(stopCh) + go wait.Until(e.serviceConfigMapQueue, 0, stopCh) + <-stopCh +} + +func (e *BootstrapSigner) pokeConfigMapSync() { + e.syncQueue.Add(e.configMapKey) +} + +func (e *BootstrapSigner) serviceConfigMapQueue() { + key, quit := e.syncQueue.Get() + if quit { + return + } + defer e.syncQueue.Done(key) + + e.signConfigMap() +} + +// signConfigMap computes the signatures on our latest cached objects and writes +// back if necessary. +func (e *BootstrapSigner) signConfigMap() { + origCM := e.getConfigMap() + + if origCM == nil { + return + } + + var needUpdate = false + + newCM, err := copyConfigMap(origCM) + if err != nil { + utilruntime.HandleError(err) + return + } + + // First capture the config we are signing + content, ok := newCM.Data[kubeConfigKey] + if !ok { + glog.V(3).Infof("No %s key in %s/%s ConfigMap", kubeConfigKey, origCM.Namespace, origCM.Name) + return + } + + // Next remove and save all existing signatures + sigs := map[string]string{} + for key, value := range newCM.Data { + if strings.HasPrefix(key, signaturePrefix) { + tokenID := strings.TrimPrefix(key, signaturePrefix) + sigs[tokenID] = value + delete(newCM.Data, key) + } + } + + // Now recompute signatures and store them on the new map + tokens := e.getTokens() + for tokenID, tokenValue := range tokens { + sig, err := computeDetachedSig(content, tokenID, tokenValue) + if err != nil { + utilruntime.HandleError(err) + } + + // Check to see if this signature is changed or new. + oldSig, _ := sigs[tokenID] + if sig != oldSig { + needUpdate = true + } + delete(sigs, tokenID) + + newCM.Data[signaturePrefix+tokenID] = sig + } + + // If we have signatures left over we know that some signatures were + // removed. We now need to update the ConfigMap + if len(sigs) != 0 { + needUpdate = true + } + + if needUpdate { + e.updateConfigMap(newCM) + } +} + +func (e *BootstrapSigner) updateConfigMap(cm *v1.ConfigMap) { + _, err := e.client.Core().ConfigMaps(cm.Namespace).Update(cm) + if err != nil && !apierrors.IsConflict(err) && !apierrors.IsNotFound(err) { + glog.V(3).Infof("Error updating ConfigMap: %v", err) + } +} + +// getConfigMap gets the ConfigMap we are interested in +func (e *BootstrapSigner) getConfigMap() *v1.ConfigMap { + configMap, exists, err := e.configMaps.GetByKey(e.configMapKey) + + // If we can't get the configmap just return nil. The resync will eventually + // sync things up. + if err != nil { + utilruntime.HandleError(err) + return nil + } + + if exists { + return configMap.(*v1.ConfigMap) + } + return nil +} + +func (e *BootstrapSigner) listSecrets() []*v1.Secret { + secrets := e.secrets.List() + + items := []*v1.Secret{} + for _, obj := range secrets { + items = append(items, obj.(*v1.Secret)) + } + return items +} + +// getTokens returns a map of tokenID->tokenSecret. It ensures the token is +// valid for signing. +func (e *BootstrapSigner) getTokens() map[string]string { + ret := map[string]string{} + secretObjs := e.listSecrets() + for _, secret := range secretObjs { + tokenID, tokenSecret, ok := validateSecretForSigning(secret) + if !ok { + continue + } + + // Check and warn for duplicate secrets. Behavior here will be undefined. + if _, ok := ret[tokenID]; ok { + glog.V(3).Infof("Duplicate bootstrap tokens found for id %s, ignoring on in %s/%s", tokenID, secret.Namespace, secret.Name) + continue + } + + // This secret looks good, add it to the list. + ret[tokenID] = tokenSecret + } + + return ret +} + +func copyConfigMap(orig *v1.ConfigMap) (*v1.ConfigMap, error) { + newCMObj, err := api.Scheme.DeepCopy(orig) + if err != nil { + return nil, err + } + return newCMObj.(*v1.ConfigMap), nil +} diff --git a/pkg/controller/bootstrap/bootstrapsigner_test.go b/pkg/controller/bootstrap/bootstrapsigner_test.go new file mode 100644 index 00000000000..f196e3e2519 --- /dev/null +++ b/pkg/controller/bootstrap/bootstrapsigner_test.go @@ -0,0 +1,137 @@ +/* +Copyright 2016 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 ( + "testing" + + "github.com/davecgh/go-spew/spew" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/kubernetes/fake" + "k8s.io/client-go/pkg/api" + "k8s.io/client-go/pkg/api/v1" + core "k8s.io/client-go/testing" +) + +func init() { + spew.Config.DisableMethods = true +} + +func newBootstrapSigner() (*BootstrapSigner, *fake.Clientset) { + options := DefaultBootstrapSignerOptions() + cl := fake.NewSimpleClientset() + return NewBootstrapSigner(cl, options), cl +} + +func newConfigMap(tokenID, signature string) *v1.ConfigMap { + ret := &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: metav1.NamespacePublic, + Name: configMapClusterInfo, + ResourceVersion: "1", + }, + Data: map[string]string{ + kubeConfigKey: "payload", + }, + } + if len(tokenID) > 0 { + ret.Data[signaturePrefix+tokenID] = signature + } + return ret +} + +func TestNoConfigMap(t *testing.T) { + signer, cl := newBootstrapSigner() + signer.signConfigMap() + verifyActions(t, []core.Action{}, cl.Actions()) +} + +func TestSimpleSign(t *testing.T) { + signer, cl := newBootstrapSigner() + + cm := newConfigMap("", "") + signer.configMaps.Add(cm) + + secret := newTokenSecret("tokenID", "tokenSecret") + addSecretSigningUsage(secret, "true") + signer.secrets.Add(secret) + + signer.signConfigMap() + + expected := []core.Action{ + core.NewUpdateAction(schema.GroupVersionResource{Version: "v1", Resource: "configmaps"}, + api.NamespacePublic, + newConfigMap("tokenID", "eyJhbGciOiJIUzI1NiIsImtpZCI6InRva2VuSUQifQ..QAvK9DAjF0hSyASEkH1MOTB5rJMmbWEY9j-z1NSYILE")), + } + + verifyActions(t, expected, cl.Actions()) +} + +func TestNoSignNeeded(t *testing.T) { + signer, cl := newBootstrapSigner() + + cm := newConfigMap("tokenID", "eyJhbGciOiJIUzI1NiIsImtpZCI6InRva2VuSUQifQ..QAvK9DAjF0hSyASEkH1MOTB5rJMmbWEY9j-z1NSYILE") + signer.configMaps.Add(cm) + + secret := newTokenSecret("tokenID", "tokenSecret") + addSecretSigningUsage(secret, "true") + signer.secrets.Add(secret) + + signer.signConfigMap() + + verifyActions(t, []core.Action{}, cl.Actions()) +} + +func TestUpdateSignature(t *testing.T) { + signer, cl := newBootstrapSigner() + + cm := newConfigMap("tokenID", "old signature") + signer.configMaps.Add(cm) + + secret := newTokenSecret("tokenID", "tokenSecret") + addSecretSigningUsage(secret, "true") + signer.secrets.Add(secret) + + signer.signConfigMap() + + expected := []core.Action{ + core.NewUpdateAction(schema.GroupVersionResource{Version: "v1", Resource: "configmaps"}, + api.NamespacePublic, + newConfigMap("tokenID", "eyJhbGciOiJIUzI1NiIsImtpZCI6InRva2VuSUQifQ..QAvK9DAjF0hSyASEkH1MOTB5rJMmbWEY9j-z1NSYILE")), + } + + verifyActions(t, expected, cl.Actions()) +} + +func TestRemoveSignature(t *testing.T) { + signer, cl := newBootstrapSigner() + + cm := newConfigMap("tokenID", "old signature") + signer.configMaps.Add(cm) + + signer.signConfigMap() + + expected := []core.Action{ + core.NewUpdateAction(schema.GroupVersionResource{Version: "v1", Resource: "configmaps"}, + api.NamespacePublic, + newConfigMap("", "")), + } + + verifyActions(t, expected, cl.Actions()) +} diff --git a/pkg/controller/bootstrap/common_test.go b/pkg/controller/bootstrap/common_test.go new file mode 100644 index 00000000000..71f231de9fc --- /dev/null +++ b/pkg/controller/bootstrap/common_test.go @@ -0,0 +1,74 @@ +/* +Copyright 2016 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 ( + "testing" + + "github.com/davecgh/go-spew/spew" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/pkg/api" + "k8s.io/client-go/pkg/api/v1" + core "k8s.io/client-go/testing" + bootstrapapi "k8s.io/kubernetes/pkg/bootstrap/api" +) + +func newTokenSecret(tokenID, tokenSecret string) *v1.Secret { + return &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: metav1.NamespaceSystem, + Name: "secretName", + ResourceVersion: "1", + }, + Type: bootstrapapi.SecretTypeBootstrapToken, + Data: map[string][]byte{ + bootstrapapi.BootstrapTokenIDKey: []byte(tokenID), + bootstrapapi.BootstrapTokenSecretKey: []byte(tokenSecret), + }, + } +} + +func addSecretExpiration(s *v1.Secret, expiration string) { + s.Data[bootstrapapi.BootstrapTokenExpirationKey] = []byte(expiration) +} + +func addSecretSigningUsage(s *v1.Secret, value string) { + s.Data[bootstrapapi.BootstrapTokenUsageSigningKey] = []byte(value) +} + +func verifyActions(t *testing.T, expected, actual []core.Action) { + for i, a := range actual { + if len(expected) < i+1 { + t.Errorf("%d unexpected actions: %s", len(actual)-len(expected), spew.Sdump(actual[i:])) + break + } + + e := expected[i] + if !api.Semantic.DeepEqual(e, a) { + t.Errorf("Expected\n\t%s\ngot\n\t%s", spew.Sdump(e), spew.Sdump(a)) + continue + } + } + + if len(expected) > len(actual) { + t.Errorf("%d additional expected actions", len(expected)-len(actual)) + for _, a := range expected[len(actual):] { + t.Logf(" %s", spew.Sdump(a)) + } + } +} diff --git a/pkg/controller/bootstrap/doc.go b/pkg/controller/bootstrap/doc.go new file mode 100644 index 00000000000..c548cec54e5 --- /dev/null +++ b/pkg/controller/bootstrap/doc.go @@ -0,0 +1,20 @@ +/* +Copyright 2016 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 automatic processes necessary for bootstraping. +// This includes managing and expiring tokens along with signing well known +// configmaps with those tokens. +package bootstrap // import "k8s.io/kubernetes/pkg/controller/bootstrap" diff --git a/pkg/controller/bootstrap/jws.go b/pkg/controller/bootstrap/jws.go new file mode 100644 index 00000000000..e9d7e099511 --- /dev/null +++ b/pkg/controller/bootstrap/jws.go @@ -0,0 +1,64 @@ +/* +Copyright 2016 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 ( + "fmt" + "strings" + + jose "github.com/square/go-jose" +) + +// computeDetachedSig takes content and token details and computes a detached +// JWS signature. This is described in Appendix F of RFC 7515. Basically, this +// is a regular JWS with the content part of the signature elided. +func computeDetachedSig(content, tokenID, tokenSecret string) (string, error) { + jwk := &jose.JsonWebKey{ + Key: []byte(tokenSecret), + KeyID: tokenID, + } + + signer, err := jose.NewSigner(jose.HS256, jwk) + if err != nil { + return "", nil + } + + jws, err := signer.Sign([]byte(content)) + if err != nil { + return "", nil + } + + fullSig, err := jws.CompactSerialize() + if err != nil { + return "", nil + } + return stripContent(fullSig) +} + +// stripContent will remove the content part of a compact JWS +// +// The `go-jose` library doesn't support generating signatures with "detatched" +// content. To make up for this we take the full compact signature, break it +// apart and put it back together without the content section. +func stripContent(fullSig string) (string, error) { + parts := strings.Split(fullSig, ".") + if len(parts) != 3 { + return "", fmt.Errorf("Compact JWS format must have three parts") + } + + return parts[0] + ".." + parts[2], nil +} diff --git a/pkg/controller/bootstrap/jws_test.go b/pkg/controller/bootstrap/jws_test.go new file mode 100644 index 00000000000..3add9d4c835 --- /dev/null +++ b/pkg/controller/bootstrap/jws_test.go @@ -0,0 +1,53 @@ +/* +Copyright 2016 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 "testing" + +const ( + content = "Hello from the other side. I must have called a thousand times." + secret = "my voice is my passcode" + id = "joshua" +) + +func TestComputeDetachedSig(t *testing.T) { + sig, err := computeDetachedSig(content, id, secret) + if err != nil { + t.Errorf("Error when computing signature: %v", err) + } + if sig != "eyJhbGciOiJIUzI1NiIsImtpZCI6Impvc2h1YSJ9..VShe2taLd-YTrmWuRkcL_8QTNDHYxQIEBsAYYiIj1_8" { + t.Errorf("Wrong signature. Got: %v", sig) + } + + // Try with null content + sig, err = computeDetachedSig("", id, secret) + if err != nil { + t.Errorf("Error when computing signature: %v", err) + } + if sig != "eyJhbGciOiJIUzI1NiIsImtpZCI6Impvc2h1YSJ9..7Ui1ALizW4jXphVUB7xUqC9vLYLL9RZeOFfVLoB7Tgk" { + t.Errorf("Wrong signature. Got: %v", sig) + } + + // Try with no secret + sig, err = computeDetachedSig(content, id, "") + if err != nil { + t.Errorf("Error when computing signature: %v", err) + } + if sig != "eyJhbGciOiJIUzI1NiIsImtpZCI6Impvc2h1YSJ9..UfkqvDGiIFxrMnFseDj9LYJOLNrvjW8aHhF71mvvAs8" { + t.Errorf("Wrong signature. Got: %v", sig) + } +} diff --git a/pkg/controller/bootstrap/tokencleaner.go b/pkg/controller/bootstrap/tokencleaner.go new file mode 100644 index 00000000000..dbab79c4b90 --- /dev/null +++ b/pkg/controller/bootstrap/tokencleaner.go @@ -0,0 +1,118 @@ +/* +Copyright 2016 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 ( + "time" + + "github.com/golang/glog" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/fields" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/watch" + clientset "k8s.io/client-go/kubernetes" + "k8s.io/client-go/pkg/api" + "k8s.io/client-go/pkg/api/v1" + "k8s.io/client-go/tools/cache" + bootstrapapi "k8s.io/kubernetes/pkg/bootstrap/api" + "k8s.io/kubernetes/pkg/util/metrics" +) + +// TokenCleanerOptions contains options for the TokenCleaner +type TokenCleanerOptions struct { + // TokenSecretNamespace string is the namespace for token Secrets. + TokenSecretNamespace string + + // SecretResync is the time.Duration at which to fully re-list secrets. + // If zero, re-list will be delayed as long as possible + SecretResync time.Duration +} + +// DefaultTokenCleanerOptions returns a set of default options for creating a +// TokenCleaner +func DefaultTokenCleanerOptions() TokenCleanerOptions { + return TokenCleanerOptions{} +} + +// TokenCleaner is a controller that deletes expired tokens +type TokenCleaner struct { + tokenSecretNamespace string + + client clientset.Interface + + secrets cache.Store + secretsController cache.Controller +} + +// NewTokenCleaner returns a new *NewTokenCleaner. +// +// TODO: Switch to shared informers +func NewTokenCleaner(cl clientset.Interface, options TokenCleanerOptions) *TokenCleaner { + e := &TokenCleaner{ + client: cl, + tokenSecretNamespace: options.TokenSecretNamespace, + } + if cl.Core().RESTClient().GetRateLimiter() != nil { + metrics.RegisterMetricAndTrackRateLimiterUsage("token_cleaner", cl.Core().RESTClient().GetRateLimiter()) + } + + secretSelector := fields.SelectorFromSet(map[string]string{api.SecretTypeField: string(bootstrapapi.SecretTypeBootstrapToken)}) + e.secrets, e.secretsController = cache.NewInformer( + &cache.ListWatch{ + ListFunc: func(lo metav1.ListOptions) (runtime.Object, error) { + lo.FieldSelector = secretSelector.String() + return e.client.Core().Secrets(e.tokenSecretNamespace).List(lo) + }, + WatchFunc: func(lo metav1.ListOptions) (watch.Interface, error) { + lo.FieldSelector = secretSelector.String() + return e.client.Core().Secrets(e.tokenSecretNamespace).Watch(lo) + }, + }, + &v1.Secret{}, + options.SecretResync, + cache.ResourceEventHandlerFuncs{ + AddFunc: e.evalSecret, + UpdateFunc: func(oldSecret, newSecret interface{}) { e.evalSecret(newSecret) }, + }, + ) + return e +} + +// Run runs controller loops and returns when they are done +func (tc *TokenCleaner) Run(stopCh <-chan struct{}) { + go tc.secretsController.Run(stopCh) + <-stopCh +} + +func (tc *TokenCleaner) evalSecret(o interface{}) { + secret := o.(*v1.Secret) + if isSecretExpired(secret) { + glog.V(3).Infof("Deleting expired secret %s/%s", secret.Namespace, secret.Name) + var options *metav1.DeleteOptions + if len(secret.UID) > 0 { + options = &metav1.DeleteOptions{Preconditions: &metav1.Preconditions{UID: &secret.UID}} + } + err := tc.client.Core().Secrets(secret.Namespace).Delete(secret.Name, options) + // NotFound isn't a real error (it's already been deleted) + // Conflict isn't a real error (the UID precondition failed) + if err != nil && !apierrors.IsConflict(err) && !apierrors.IsNotFound(err) { + glog.V(3).Infof("Error deleting Secret: %v", err) + } + } +} diff --git a/pkg/controller/bootstrap/tokencleaner_test.go b/pkg/controller/bootstrap/tokencleaner_test.go new file mode 100644 index 00000000000..f6de895d2bb --- /dev/null +++ b/pkg/controller/bootstrap/tokencleaner_test.go @@ -0,0 +1,85 @@ +/* +Copyright 2016 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 ( + "testing" + "time" + + "github.com/davecgh/go-spew/spew" + + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/kubernetes/fake" + "k8s.io/client-go/pkg/api" + core "k8s.io/client-go/testing" +) + +func init() { + spew.Config.DisableMethods = true +} + +func newTokenCleaner() (*TokenCleaner, *fake.Clientset) { + options := DefaultTokenCleanerOptions() + cl := fake.NewSimpleClientset() + return NewTokenCleaner(cl, options), cl +} + +func TestCleanerNoExpiration(t *testing.T) { + cleaner, cl := newTokenCleaner() + + secret := newTokenSecret("tokenID", "tokenSecret") + cleaner.secrets.Add(secret) + + cleaner.evalSecret(secret) + + expected := []core.Action{} + + verifyActions(t, expected, cl.Actions()) +} + +func TestCleanerExpired(t *testing.T) { + cleaner, cl := newTokenCleaner() + + secret := newTokenSecret("tokenID", "tokenSecret") + addSecretExpiration(secret, timeString(-time.Hour)) + cleaner.secrets.Add(secret) + + cleaner.evalSecret(secret) + + expected := []core.Action{ + core.NewDeleteAction( + schema.GroupVersionResource{Version: "v1", Resource: "secrets"}, + api.NamespaceSystem, + secret.ObjectMeta.Name), + } + + verifyActions(t, expected, cl.Actions()) +} + +func TestCleanerNotExpired(t *testing.T) { + cleaner, cl := newTokenCleaner() + + secret := newTokenSecret("tokenID", "tokenSecret") + addSecretExpiration(secret, timeString(time.Hour)) + cleaner.secrets.Add(secret) + + cleaner.evalSecret(secret) + + expected := []core.Action{} + + verifyActions(t, expected, cl.Actions()) +} diff --git a/pkg/controller/bootstrap/util.go b/pkg/controller/bootstrap/util.go new file mode 100644 index 00000000000..f15ce01d886 --- /dev/null +++ b/pkg/controller/bootstrap/util.go @@ -0,0 +1,85 @@ +/* +Copyright 2016 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 ( + "time" + + "github.com/golang/glog" + + "k8s.io/client-go/pkg/api/v1" + bootstrapapi "k8s.io/kubernetes/pkg/bootstrap/api" +) + +// getSecretString gets a string value from a secret. If there is an error or +// if the key doesn't exist, an empty string is returned. +func getSecretString(secret *v1.Secret, key string) string { + data, ok := secret.Data[key] + if !ok { + return "" + } + + return string(data) +} + +func validateSecretForSigning(secret *v1.Secret) (tokenID, tokenSecret string, ok bool) { + tokenID = getSecretString(secret, bootstrapapi.BootstrapTokenIDKey) + if len(tokenID) == 0 { + glog.V(3).Infof("No %s key in %s/%s Secret", bootstrapapi.BootstrapTokenIDKey, secret.Namespace, secret.Name) + return "", "", false + } + + tokenSecret = getSecretString(secret, bootstrapapi.BootstrapTokenSecretKey) + if len(tokenSecret) == 0 { + glog.V(3).Infof("No %s key in %s/%s Secret", bootstrapapi.BootstrapTokenSecretKey, secret.Namespace, secret.Name) + return "", "", false + } + + // Ensure this secret hasn't expired. The TokenCleaner should remove this + // but if that isn't working or it hasn't gotten there yet we should check + // here. + if isSecretExpired(secret) { + return "", "", false + } + + // Make sure this secret can be used for signing + okToSign := getSecretString(secret, bootstrapapi.BootstrapTokenUsageSigningKey) + if okToSign != "true" { + return "", "", false + } + + return tokenID, tokenSecret, true +} + +// isSecretExpired returns true if the Secret is expired. +func isSecretExpired(secret *v1.Secret) bool { + expiration := getSecretString(secret, bootstrapapi.BootstrapTokenExpirationKey) + if len(expiration) > 0 { + expTime, err2 := time.Parse(time.RFC3339, expiration) + if err2 != nil { + glog.V(3).Infof("Unparseable expiration time (%s) in %s/%s Secret: %v. Treating as expired.", + expiration, secret.Namespace, secret.Name, err2) + return true + } + if time.Now().After(expTime) { + glog.V(3).Infof("Expired bootstrap token in %s/%s Secret: %v", + secret.Namespace, secret.Name, expiration) + return true + } + } + return false +} diff --git a/pkg/controller/bootstrap/util_test.go b/pkg/controller/bootstrap/util_test.go new file mode 100644 index 00000000000..0bdea2c9a64 --- /dev/null +++ b/pkg/controller/bootstrap/util_test.go @@ -0,0 +1,137 @@ +/* +Copyright 2016 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 ( + "testing" + "time" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/pkg/api/v1" + bootstrapapi "k8s.io/kubernetes/pkg/bootstrap/api" +) + +const ( + givenTokenID = "tokenID" + givenTokenSecret = "tokenSecret" +) + +func timeString(delta time.Duration) string { + return time.Now().Add(delta).Format(time.RFC3339) +} + +func TestValidateSecretForSigning(t *testing.T) { + cases := []struct { + description string + tokenID string + tokenSecret string + okToSign string + expiration string + valid bool + }{ + { + "Signing token with no exp", + givenTokenID, givenTokenSecret, "true", "", true, + }, + { + "Signing token with valid exp", + givenTokenID, givenTokenSecret, "true", timeString(time.Hour), true, + }, + { + "Expired signing token", + givenTokenID, givenTokenSecret, "true", timeString(-time.Hour), false, + }, + { + "Signing token with bad exp", + givenTokenID, givenTokenSecret, "true", "garbage", false, + }, + { + "Signing token without signing bit", + givenTokenID, givenTokenSecret, "", "garbage", false, + }, + { + "Signing token with bad signing bit", + givenTokenID, givenTokenSecret, "", "", false, + }, + { + "Signing token with no ID", + "", givenTokenSecret, "true", "", false, + }, + { + "Signing token with no secret", + givenTokenID, "", "true", "", false, + }, + } + + for _, tc := range cases { + secret := &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: metav1.NamespaceSystem, + Name: "secretName", + ResourceVersion: "1", + }, + Type: bootstrapapi.SecretTypeBootstrapToken, + Data: map[string][]byte{ + bootstrapapi.BootstrapTokenIDKey: []byte(tc.tokenID), + bootstrapapi.BootstrapTokenSecretKey: []byte(tc.tokenSecret), + bootstrapapi.BootstrapTokenUsageSigningKey: []byte(tc.okToSign), + bootstrapapi.BootstrapTokenExpirationKey: []byte(tc.expiration), + }, + } + + tokenID, tokenSecret, ok := validateSecretForSigning(secret) + if ok != tc.valid { + t.Errorf("%s: Unexpected validation failure. Expected %v, got %v", tc.description, tc.valid, ok) + } + if ok { + if tokenID != tc.tokenID { + t.Errorf("%s: Unexpected Token ID. Expected %q, got %q", tc.description, givenTokenID, tokenID) + } + if tokenSecret != tc.tokenSecret { + t.Errorf("%s: Unexpected Token Secret. Expected %q, got %q", tc.description, givenTokenSecret, tokenSecret) + } + } + } + +} + +func TestValidateSecret(t *testing.T) { + secret := &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: metav1.NamespaceSystem, + Name: "secretName", + ResourceVersion: "1", + }, + Type: bootstrapapi.SecretTypeBootstrapToken, + Data: map[string][]byte{ + bootstrapapi.BootstrapTokenIDKey: []byte(givenTokenID), + bootstrapapi.BootstrapTokenSecretKey: []byte(givenTokenSecret), + bootstrapapi.BootstrapTokenUsageSigningKey: []byte("true"), + }, + } + + tokenID, tokenSecret, ok := validateSecretForSigning(secret) + if !ok { + t.Errorf("Unexpected validation failure.") + } + if tokenID != givenTokenID { + t.Errorf("Unexpected Token ID. Expected %q, got %q", givenTokenID, tokenID) + } + if tokenSecret != givenTokenSecret { + t.Errorf("Unexpected Token Secret. Expected %q, got %q", givenTokenSecret, tokenSecret) + } +} diff --git a/pkg/master/controller.go b/pkg/master/controller.go index c98adc89b6f..4751deb8859 100644 --- a/pkg/master/controller.go +++ b/pkg/master/controller.go @@ -41,9 +41,10 @@ import ( const kubernetesServiceName = "kubernetes" -// Controller is the controller manager for the core bootstrap Kubernetes controller -// loops, which manage creating the "kubernetes" service, the "default" and "kube-system" -// namespace, and provide the IP repair check on service IPs +// Controller is the controller manager for the core bootstrap Kubernetes +// controller loops, which manage creating the "kubernetes" service, the +// "default", "kube-system" and "kube-public" namespaces, and provide the IP +// repair check on service IPs type Controller struct { ServiceClient coreclient.ServicesGetter NamespaceClient coreclient.NamespacesGetter @@ -84,7 +85,7 @@ func (c *Config) NewBootstrapController(legacyRESTStorage corerest.LegacyRESTSto EndpointReconciler: c.EndpointReconcilerConfig.Reconciler, EndpointInterval: c.EndpointReconcilerConfig.Interval, - SystemNamespaces: []string{metav1.NamespaceSystem}, + SystemNamespaces: []string{metav1.NamespaceSystem, metav1.NamespacePublic}, SystemNamespacesInterval: 1 * time.Minute, ServiceClusterIPRegistry: legacyRESTStorage.ServiceClusterIPAllocator, diff --git a/plugin/pkg/admission/namespace/lifecycle/admission.go b/plugin/pkg/admission/namespace/lifecycle/admission.go index a7f86352a80..52a56bd2e4c 100644 --- a/plugin/pkg/admission/namespace/lifecycle/admission.go +++ b/plugin/pkg/admission/namespace/lifecycle/admission.go @@ -51,7 +51,7 @@ const ( func init() { admission.RegisterPlugin(PluginName, func(config io.Reader) (admission.Interface, error) { - return NewLifecycle(sets.NewString(metav1.NamespaceDefault, metav1.NamespaceSystem)) + return NewLifecycle(sets.NewString(metav1.NamespaceDefault, metav1.NamespaceSystem, metav1.NamespacePublic)) }) } diff --git a/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/types.go b/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/types.go index af78044bdf0..e9bade7690a 100644 --- a/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/types.go +++ b/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/types.go @@ -73,7 +73,7 @@ type ListMeta struct { // These are internal finalizer values for Kubernetes-like APIs, must be qualified name unless defined here const ( - FinalizerOrphan string = "orphan" + FinalizerOrphan string = "orphan" ) // ObjectMeta is metadata that all persisted resources must have, which includes all objects @@ -233,6 +233,8 @@ const ( NamespaceNone string = "" // NamespaceSystem is the system namespace where we place system components. NamespaceSystem string = "kube-system" + // NamespacePublic is the namespace where we place public info (ConfigMaps) + NamespacePublic string = "kube-public" ) // OwnerReference contains enough information to let you identify an owning diff --git a/staging/src/k8s.io/client-go/_vendor/k8s.io/apimachinery/pkg/apis/meta/v1/types.go b/staging/src/k8s.io/client-go/_vendor/k8s.io/apimachinery/pkg/apis/meta/v1/types.go index 11fc5b60f3e..653e8e714a0 100644 --- a/staging/src/k8s.io/client-go/_vendor/k8s.io/apimachinery/pkg/apis/meta/v1/types.go +++ b/staging/src/k8s.io/client-go/_vendor/k8s.io/apimachinery/pkg/apis/meta/v1/types.go @@ -228,6 +228,8 @@ const ( NamespaceNone string = "" // NamespaceSystem is the system namespace where we place system components. NamespaceSystem string = "kube-system" + // NamespacePublic is the namespace where we place public info (ConfigMaps) + NamespacePublic string = "kube-public" ) // OwnerReference contains enough information to let you identify an owning diff --git a/staging/src/k8s.io/client-go/pkg/api/types.go b/staging/src/k8s.io/client-go/pkg/api/types.go index 2e0ac770777..3fbdb1029b4 100644 --- a/staging/src/k8s.io/client-go/pkg/api/types.go +++ b/staging/src/k8s.io/client-go/pkg/api/types.go @@ -189,6 +189,8 @@ const ( NamespaceNone string = "" // NamespaceSystem is the system namespace where we place system components. NamespaceSystem string = "kube-system" + // NamespacePublic is the namespace where we place public info (ConfigMaps) + NamespacePublic string = "kube-public" // TerminationMessagePathDefault means the default path to capture the application termination message running in a container TerminationMessagePathDefault string = "/dev/termination-log" ) @@ -3344,9 +3346,6 @@ const ( // - Secret.Data["token"] - a token that identifies the service account to the API SecretTypeServiceAccountToken SecretType = "kubernetes.io/service-account-token" - // SecretTypeBootstrapToken is the key for tokens used by kubeadm to validate cluster info during discovery. - SecretTypeBootstrapToken = "bootstrap.kubernetes.io/token" - // ServiceAccountNameKey is the key of the required annotation for SecretTypeServiceAccountToken secrets ServiceAccountNameKey = "kubernetes.io/service-account.name" // ServiceAccountUIDKey is the key of the required annotation for SecretTypeServiceAccountToken secrets