From 4a06b69579ed6f32e9b9369f85b711ac36b2eb14 Mon Sep 17 00:00:00 2001 From: deads2k Date: Tue, 21 Feb 2017 07:48:04 -0500 Subject: [PATCH] add client-ca to configmap in kube-public --- cmd/kube-apiserver/app/server.go | 25 ++++ pkg/master/BUILD | 3 + pkg/master/client_ca_hook.go | 124 +++++++++++++++++ pkg/master/client_ca_hook_test.go | 223 ++++++++++++++++++++++++++++++ pkg/master/master.go | 8 ++ 5 files changed, 383 insertions(+) create mode 100644 pkg/master/client_ca_hook.go create mode 100644 pkg/master/client_ca_hook_test.go diff --git a/cmd/kube-apiserver/app/server.go b/cmd/kube-apiserver/app/server.go index fcc5a6ea784..172fc66b1d7 100644 --- a/cmd/kube-apiserver/app/server.go +++ b/cmd/kube-apiserver/app/server.go @@ -22,6 +22,7 @@ package app import ( "crypto/tls" "fmt" + "io/ioutil" "net" "net/http" "net/url" @@ -331,9 +332,26 @@ func Run(s *options.ServerRunOptions) error { return err } + clientCA, err := readCAorNil(s.Authentication.ClientCert.ClientCA) + if err != nil { + return err + } + requestHeaderProxyCA, err := readCAorNil(s.Authentication.RequestHeader.ClientCAFile) + if err != nil { + return err + } config := &master.Config{ GenericConfig: genericConfig, + ClientCARegistrationHook: master.ClientCARegistrationHook{ + ClientCA: clientCA, + RequestHeaderUsernameHeaders: s.Authentication.RequestHeader.UsernameHeaders, + RequestHeaderGroupHeaders: s.Authentication.RequestHeader.GroupHeaders, + RequestHeaderExtraHeaderPrefixes: s.Authentication.RequestHeader.ExtraHeaderPrefixes, + RequestHeaderCA: requestHeaderProxyCA, + RequestHeaderAllowedNames: s.Authentication.RequestHeader.AllowedNames, + }, + APIResourceConfigSource: storageFactory.APIResourceConfigSource, StorageFactory: storageFactory, EnableCoreControllers: true, @@ -372,6 +390,13 @@ func Run(s *options.ServerRunOptions) error { return nil } +func readCAorNil(file string) ([]byte, error) { + if len(file) == 0 { + return nil, nil + } + return ioutil.ReadFile(file) +} + // PostProcessSpec adds removed definitions for backward compatibility func postProcessOpenAPISpecForBackwardCompatibility(s *spec.Swagger) (*spec.Swagger, error) { compatibilityMap := map[string]string{ diff --git a/pkg/master/BUILD b/pkg/master/BUILD index ecc134ffca1..24daf75f882 100644 --- a/pkg/master/BUILD +++ b/pkg/master/BUILD @@ -11,6 +11,7 @@ load( go_library( name = "go_default_library", srcs = [ + "client_ca_hook.go", "controller.go", "doc.go", "import_known_versions.go", @@ -90,6 +91,7 @@ go_library( go_test( name = "go_default_test", srcs = [ + "client_ca_hook_test.go", "controller_test.go", "import_known_versions_test.go", "master_openapi_test.go", @@ -125,6 +127,7 @@ go_test( "//vendor:k8s.io/apimachinery/pkg/apis/meta/v1", "//vendor:k8s.io/apimachinery/pkg/runtime", "//vendor:k8s.io/apimachinery/pkg/runtime/schema", + "//vendor:k8s.io/apimachinery/pkg/util/diff", "//vendor:k8s.io/apimachinery/pkg/util/intstr", "//vendor:k8s.io/apimachinery/pkg/util/net", "//vendor:k8s.io/apimachinery/pkg/util/sets", diff --git a/pkg/master/client_ca_hook.go b/pkg/master/client_ca_hook.go new file mode 100644 index 00000000000..0d898d67d5f --- /dev/null +++ b/pkg/master/client_ca_hook.go @@ -0,0 +1,124 @@ +/* +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 master + +import ( + "encoding/json" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + genericapiserver "k8s.io/apiserver/pkg/server" + "k8s.io/kubernetes/pkg/api" + coreclient "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/typed/core/internalversion" +) + +type ClientCARegistrationHook struct { + ClientCA []byte + + RequestHeaderUsernameHeaders []string + RequestHeaderGroupHeaders []string + RequestHeaderExtraHeaderPrefixes []string + RequestHeaderCA []byte + RequestHeaderAllowedNames []string +} + +func (h ClientCARegistrationHook) PostStartHook(hookContext genericapiserver.PostStartHookContext) error { + if len(h.ClientCA) == 0 && len(h.RequestHeaderCA) == 0 { + return nil + } + + client, err := coreclient.NewForConfig(hookContext.LoopbackClientConfig) + if err != nil { + utilruntime.HandleError(err) + return nil + } + + h.writeClientCAs(client) + return nil +} + +// writeClientCAs is here for unit testing with a fake client +func (h ClientCARegistrationHook) writeClientCAs(client coreclient.CoreInterface) { + if _, err := client.Namespaces().Create(&api.Namespace{ObjectMeta: metav1.ObjectMeta{Name: metav1.NamespaceSystem}}); err != nil && !apierrors.IsAlreadyExists(err) { + utilruntime.HandleError(err) + return + } + + data := map[string]string{} + if len(h.ClientCA) > 0 { + data["client-ca-file"] = string(h.ClientCA) + } + + if len(h.RequestHeaderCA) > 0 { + var err error + + data["requestheader-username-headers"], err = jsonSerializeStringSlice(h.RequestHeaderUsernameHeaders) + if err != nil { + utilruntime.HandleError(err) + return + } + data["requestheader-group-headers"], err = jsonSerializeStringSlice(h.RequestHeaderGroupHeaders) + if err != nil { + utilruntime.HandleError(err) + return + } + data["requestheader-extra-headers-prefix"], err = jsonSerializeStringSlice(h.RequestHeaderExtraHeaderPrefixes) + if err != nil { + utilruntime.HandleError(err) + return + } + data["requestheader-client-ca-file"] = string(h.RequestHeaderCA) + data["requestheader-allowed-names"], err = jsonSerializeStringSlice(h.RequestHeaderAllowedNames) + if err != nil { + utilruntime.HandleError(err) + return + } + } + + if err := writeConfigMap(client, "extension-apiserver-authentication", data); err != nil { + utilruntime.HandleError(err) + } + + return +} + +func jsonSerializeStringSlice(in []string) (string, error) { + out, err := json.Marshal(in) + if err != nil { + return "", err + } + return string(out), err +} + +func writeConfigMap(client coreclient.ConfigMapsGetter, name string, data map[string]string) error { + existing, err := client.ConfigMaps(metav1.NamespaceSystem).Get(name, metav1.GetOptions{}) + if apierrors.IsNotFound(err) { + _, err := client.ConfigMaps(metav1.NamespaceSystem).Create(&api.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{Namespace: metav1.NamespaceSystem, Name: name}, + Data: data, + }) + return err + } + if err != nil { + return err + } + + existing.Data = data + _, err = client.ConfigMaps(metav1.NamespaceSystem).Update(existing) + return err +} diff --git a/pkg/master/client_ca_hook_test.go b/pkg/master/client_ca_hook_test.go new file mode 100644 index 00000000000..b1f2483fd41 --- /dev/null +++ b/pkg/master/client_ca_hook_test.go @@ -0,0 +1,223 @@ +/* +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 master + +import ( + "reflect" + "testing" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/diff" + clienttesting "k8s.io/client-go/testing" + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/fake" +) + +func TestWriteClientCAs(t *testing.T) { + tests := []struct { + name string + hook ClientCARegistrationHook + preexistingObjs []runtime.Object + expectedConfigMaps map[string]*api.ConfigMap + expectUpdate bool + }{ + { + name: "basic", + hook: ClientCARegistrationHook{ + ClientCA: []byte("foo"), + RequestHeaderUsernameHeaders: []string{"alfa", "bravo", "charlie"}, + RequestHeaderGroupHeaders: []string{"delta"}, + RequestHeaderExtraHeaderPrefixes: []string{"echo", "foxtrot"}, + RequestHeaderCA: []byte("bar"), + RequestHeaderAllowedNames: []string{"first", "second"}, + }, + expectedConfigMaps: map[string]*api.ConfigMap{ + "extension-apiserver-authentication": { + ObjectMeta: metav1.ObjectMeta{Namespace: metav1.NamespaceSystem, Name: "extension-apiserver-authentication"}, + Data: map[string]string{ + "client-ca-file": "foo", + "requestheader-username-headers": `["alfa","bravo","charlie"]`, + "requestheader-group-headers": `["delta"]`, + "requestheader-extra-headers-prefix": `["echo","foxtrot"]`, + "requestheader-client-ca-file": "bar", + "requestheader-allowed-names": `["first","second"]`, + }, + }, + }, + }, + { + name: "skip extension-apiserver-authentication", + hook: ClientCARegistrationHook{ + RequestHeaderCA: []byte("bar"), + RequestHeaderAllowedNames: []string{"first", "second"}, + }, + expectedConfigMaps: map[string]*api.ConfigMap{ + "extension-apiserver-authentication": { + ObjectMeta: metav1.ObjectMeta{Namespace: metav1.NamespaceSystem, Name: "extension-apiserver-authentication"}, + Data: map[string]string{ + "requestheader-username-headers": `null`, + "requestheader-group-headers": `null`, + "requestheader-extra-headers-prefix": `null`, + "requestheader-client-ca-file": "bar", + "requestheader-allowed-names": `["first","second"]`, + }, + }, + }, + }, + { + name: "skip extension-apiserver-authentication", + hook: ClientCARegistrationHook{ + ClientCA: []byte("foo"), + }, + expectedConfigMaps: map[string]*api.ConfigMap{ + "extension-apiserver-authentication": { + ObjectMeta: metav1.ObjectMeta{Namespace: metav1.NamespaceSystem, Name: "extension-apiserver-authentication"}, + Data: map[string]string{ + "client-ca-file": "foo", + }, + }, + }, + }, + { + name: "empty allowed names", + hook: ClientCARegistrationHook{ + RequestHeaderCA: []byte("bar"), + }, + expectedConfigMaps: map[string]*api.ConfigMap{ + "extension-apiserver-authentication": { + ObjectMeta: metav1.ObjectMeta{Namespace: metav1.NamespaceSystem, Name: "extension-apiserver-authentication"}, + Data: map[string]string{ + "requestheader-username-headers": `null`, + "requestheader-group-headers": `null`, + "requestheader-extra-headers-prefix": `null`, + "requestheader-client-ca-file": "bar", + "requestheader-allowed-names": `null`, + }, + }, + }, + }, + { + name: "overwrite extension-apiserver-authentication", + hook: ClientCARegistrationHook{ + ClientCA: []byte("foo"), + }, + preexistingObjs: []runtime.Object{ + &api.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{Namespace: metav1.NamespaceSystem, Name: "extension-apiserver-authentication"}, + Data: map[string]string{ + "client-ca-file": "other", + }, + }, + }, + expectedConfigMaps: map[string]*api.ConfigMap{ + "extension-apiserver-authentication": { + ObjectMeta: metav1.ObjectMeta{Namespace: metav1.NamespaceSystem, Name: "extension-apiserver-authentication"}, + Data: map[string]string{ + "client-ca-file": "foo", + }, + }, + }, + expectUpdate: true, + }, + { + name: "overwrite extension-apiserver-authentication requestheader", + hook: ClientCARegistrationHook{ + RequestHeaderUsernameHeaders: []string{}, + RequestHeaderGroupHeaders: []string{}, + RequestHeaderExtraHeaderPrefixes: []string{}, + RequestHeaderCA: []byte("bar"), + RequestHeaderAllowedNames: []string{}, + }, + preexistingObjs: []runtime.Object{ + &api.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{Namespace: metav1.NamespaceSystem, Name: "extension-apiserver-authentication"}, + Data: map[string]string{ + "requestheader-username-headers": `null`, + "requestheader-group-headers": `null`, + "requestheader-extra-headers-prefix": `null`, + "requestheader-client-ca-file": "something", + "requestheader-allowed-names": `null`, + }, + }, + }, + expectedConfigMaps: map[string]*api.ConfigMap{ + "extension-apiserver-authentication": { + ObjectMeta: metav1.ObjectMeta{Namespace: metav1.NamespaceSystem, Name: "extension-apiserver-authentication"}, + Data: map[string]string{ + "requestheader-username-headers": `[]`, + "requestheader-group-headers": `[]`, + "requestheader-extra-headers-prefix": `[]`, + "requestheader-client-ca-file": "bar", + "requestheader-allowed-names": `[]`, + }, + }, + }, + expectUpdate: true, + }, + { + name: "namespace exists", + hook: ClientCARegistrationHook{ + ClientCA: []byte("foo"), + }, + preexistingObjs: []runtime.Object{ + &api.Namespace{ObjectMeta: metav1.ObjectMeta{Name: metav1.NamespaceSystem}}, + }, + expectedConfigMaps: map[string]*api.ConfigMap{ + "extension-apiserver-authentication": { + ObjectMeta: metav1.ObjectMeta{Namespace: metav1.NamespaceSystem, Name: "extension-apiserver-authentication"}, + Data: map[string]string{ + "client-ca-file": "foo", + }, + }, + }, + }, + } + + for _, test := range tests { + client := fake.NewSimpleClientset(test.preexistingObjs...) + test.hook.writeClientCAs(client.Core()) + + actualConfigMaps, updated := getFinalConfiMaps(client) + if !reflect.DeepEqual(test.expectedConfigMaps, actualConfigMaps) { + t.Errorf("%s: %v", test.name, diff.ObjectReflectDiff(test.expectedConfigMaps, actualConfigMaps)) + continue + } + if test.expectUpdate != updated { + t.Errorf("%s: expected %v, got %v", test.name, test.expectUpdate, updated) + continue + } + } +} + +func getFinalConfiMaps(client *fake.Clientset) (map[string]*api.ConfigMap, bool) { + ret := map[string]*api.ConfigMap{} + updated := false + + for _, action := range client.Actions() { + if action.Matches("create", "configmaps") { + obj := action.(clienttesting.CreateAction).GetObject().(*api.ConfigMap) + ret[obj.Name] = obj + } + if action.Matches("update", "configmaps") { + updated = true + obj := action.(clienttesting.UpdateAction).GetObject().(*api.ConfigMap) + ret[obj.Name] = obj + } + } + return ret, updated +} diff --git a/pkg/master/master.go b/pkg/master/master.go index d7a82eb10e2..6f44d4e5297 100644 --- a/pkg/master/master.go +++ b/pkg/master/master.go @@ -80,6 +80,8 @@ const ( type Config struct { GenericConfig *genericapiserver.Config + ClientCARegistrationHook ClientCARegistrationHook + APIResourceConfigSource serverstorage.APIResourceConfigSource StorageFactory serverstorage.StorageFactory EnableCoreControllers bool @@ -135,6 +137,8 @@ type EndpointReconcilerConfig struct { // Master contains state for a Kubernetes cluster master/api server. type Master struct { GenericAPIServer *genericapiserver.GenericAPIServer + + ClientCARegistrationHook ClientCARegistrationHook } type completedConfig struct { @@ -251,6 +255,10 @@ func (c completedConfig) New() (*Master, error) { m.installTunneler(c.Tunneler, corev1client.NewForConfigOrDie(c.GenericConfig.LoopbackClientConfig).Nodes()) } + if err := m.GenericAPIServer.AddPostStartHook("ca-registration", c.ClientCARegistrationHook.PostStartHook); err != nil { + glog.Fatalf("Error registering PostStartHook %q: %v", "ca-registration", err) + } + return m, nil }