Merge pull request #41814 from deads2k/agg-06-cas

Automatic merge from submit-queue

add client-ca to configmap in kube-public

Client CA information is not secret and it's required for any API server trying to terminate a TLS connection.  This pull adds the information to configmaps in `kube-public` that look like this:


```yaml
apiVersion: v1
data:
  client-ca.crt: |
    -----BEGIN CERTIFICATE-----
    -----END CERTIFICATE-----
  requestheader-allowed-names: '["system:auth-proxy"]'
  requestheader-client-ca-file: |
    -----BEGIN CERTIFICATE-----
    -----END CERTIFICATE-----
  requestheader-extra-headers-prefix: '["X-Remote-Extra-"]'
  requestheader-group-headers: '["X-Remote-Group"]'
  requestheader-username-headers: '["X-Remote-User"]'
kind: ConfigMap
metadata:
  creationTimestamp: 2017-02-22T17:54:37Z
  name: extension-apiserver-authentication
  namespace: kube-system
  resourceVersion: "6"
  selfLink: /api/v1/namespaces/kube-system/configmaps/extension-apiserver-authentication
  uid: fa1dd328-f927-11e6-8b0e-28d2447dc82b

```

@kubernetes/sig-auth-api-reviews @liggitt @kubernetes/sig-api-machinery-pr-reviews @lavalamp @sttts 


There will need to be a corresponding pull for permissions
pull/6/head
Kubernetes Submit Queue 2017-02-26 09:32:44 -08:00 committed by GitHub
commit 1519422aba
5 changed files with 383 additions and 0 deletions

View File

@ -22,6 +22,7 @@ package app
import (
"crypto/tls"
"fmt"
"io/ioutil"
"net"
"net/http"
"net/url"
@ -327,9 +328,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,
@ -367,6 +385,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{

View File

@ -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",

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}