diff --git a/cmd/kubelet/app/options/options.go b/cmd/kubelet/app/options/options.go index 250da221f3..9fbf45ea2f 100644 --- a/cmd/kubelet/app/options/options.go +++ b/cmd/kubelet/app/options/options.go @@ -442,6 +442,10 @@ func AddKubeletConfigFlags(fs *pflag.FlagSet, c *kubeletconfig.KubeletConfigurat "If --tls-cert-file and --tls-private-key-file are not provided, a self-signed certificate and key "+ "are generated for the public address and saved to the directory passed to --cert-dir.") fs.StringVar(&c.TLSPrivateKeyFile, "tls-private-key-file", c.TLSPrivateKeyFile, "File containing x509 private key matching --tls-cert-file.") + fs.StringSliceVar(&c.TLSCipherSuites, "tls-cipher-suites", c.TLSCipherSuites, + "Comma-separated list of cipher suites for the server. "+ + "Values are from tls package constants (https://golang.org/pkg/crypto/tls/#pkg-constants). "+ + "If omitted, the default Go cipher suites will be used") fs.Int32Var(&c.RegistryPullQPS, "registry-qps", c.RegistryPullQPS, "If > 0, limit registry pull QPS to this value. If 0, unlimited.") fs.Int32Var(&c.RegistryBurst, "registry-burst", c.RegistryBurst, "Maximum size of a bursty pulls, temporarily allows pulls to burst to this number, while still not exceeding registry-qps. Only used if --registry-qps > 0") diff --git a/cmd/kubelet/app/server.go b/cmd/kubelet/app/server.go index a3eb633f98..598ef1489e 100644 --- a/cmd/kubelet/app/server.go +++ b/cmd/kubelet/app/server.go @@ -567,12 +567,19 @@ func InitializeTLS(kf *options.KubeletFlags, kc *kubeletconfiginternal.KubeletCo glog.V(4).Infof("Using self-signed cert (%s, %s)", kc.TLSCertFile, kc.TLSPrivateKeyFile) } } + + tlsCipherSuites, err := flag.TLSCipherSuites(kc.TLSCipherSuites) + if err != nil { + return nil, err + } + tlsOptions := &server.TLSOptions{ Config: &tls.Config{ // Can't use SSLv3 because of POODLE and BEAST // Can't use TLSv1.0 because of POODLE and BEAST using CBC cipher // Can't use TLSv1.1 because of RC4 cipher usage - MinVersion: tls.VersionTLS12, + MinVersion: tls.VersionTLS12, + CipherSuites: tlsCipherSuites, }, CertFile: kc.TLSCertFile, KeyFile: kc.TLSPrivateKeyFile, diff --git a/pkg/kubelet/apis/kubeletconfig/helpers_test.go b/pkg/kubelet/apis/kubeletconfig/helpers_test.go index 59c3b5d05b..b04c43c673 100644 --- a/pkg/kubelet/apis/kubeletconfig/helpers_test.go +++ b/pkg/kubelet/apis/kubeletconfig/helpers_test.go @@ -175,6 +175,7 @@ var ( "HairpinMode", "HealthzBindAddress", "HealthzPort", + "TLSCipherSuites[*]", "IPTablesDropBit", "IPTablesMasqueradeBit", "ImageGCHighThresholdPercent", diff --git a/pkg/kubelet/apis/kubeletconfig/types.go b/pkg/kubelet/apis/kubeletconfig/types.go index aee0077d47..ac9b1ff9df 100644 --- a/pkg/kubelet/apis/kubeletconfig/types.go +++ b/pkg/kubelet/apis/kubeletconfig/types.go @@ -85,6 +85,9 @@ type KubeletConfiguration struct { // tlsPrivateKeyFile is the ile containing x509 private key matching // tlsCertFile. TLSPrivateKeyFile string + // TLSCipherSuites is the list of allowed cipher suites for the server. + // Values are from tls package constants (https://golang.org/pkg/crypto/tls/#pkg-constants). + TLSCipherSuites []string // authentication specifies how requests to the Kubelet's server are authenticated Authentication KubeletAuthentication // authorization specifies how requests to the Kubelet's server are authorized diff --git a/pkg/kubelet/apis/kubeletconfig/v1alpha1/types.go b/pkg/kubelet/apis/kubeletconfig/v1alpha1/types.go index 8d4ffe4a61..bedac22264 100644 --- a/pkg/kubelet/apis/kubeletconfig/v1alpha1/types.go +++ b/pkg/kubelet/apis/kubeletconfig/v1alpha1/types.go @@ -85,6 +85,9 @@ type KubeletConfiguration struct { // tlsPrivateKeyFile is the ile containing x509 private key matching // tlsCertFile. TLSPrivateKeyFile string `json:"tlsPrivateKeyFile"` + // TLSCipherSuites is the list of allowed cipher suites for the server. + // Values are from tls package constants (https://golang.org/pkg/crypto/tls/#pkg-constants). + TLSCipherSuites []string `json:"tlsCipherSuites"` // authentication specifies how requests to the Kubelet's server are authenticated Authentication KubeletAuthentication `json:"authentication"` // authorization specifies how requests to the Kubelet's server are authorized diff --git a/pkg/kubelet/apis/kubeletconfig/v1alpha1/zz_generated.conversion.go b/pkg/kubelet/apis/kubeletconfig/v1alpha1/zz_generated.conversion.go index 9368d7c0a2..1c800e9848 100644 --- a/pkg/kubelet/apis/kubeletconfig/v1alpha1/zz_generated.conversion.go +++ b/pkg/kubelet/apis/kubeletconfig/v1alpha1/zz_generated.conversion.go @@ -158,6 +158,7 @@ func autoConvert_v1alpha1_KubeletConfiguration_To_kubeletconfig_KubeletConfigura } out.TLSCertFile = in.TLSCertFile out.TLSPrivateKeyFile = in.TLSPrivateKeyFile + out.TLSCipherSuites = *(*[]string)(unsafe.Pointer(&in.TLSCipherSuites)) if err := Convert_v1alpha1_KubeletAuthentication_To_kubeletconfig_KubeletAuthentication(&in.Authentication, &out.Authentication, s); err != nil { return err } @@ -279,6 +280,7 @@ func autoConvert_kubeletconfig_KubeletConfiguration_To_v1alpha1_KubeletConfigura } out.TLSCertFile = in.TLSCertFile out.TLSPrivateKeyFile = in.TLSPrivateKeyFile + out.TLSCipherSuites = *(*[]string)(unsafe.Pointer(&in.TLSCipherSuites)) if err := Convert_kubeletconfig_KubeletAuthentication_To_v1alpha1_KubeletAuthentication(&in.Authentication, &out.Authentication, s); err != nil { return err } diff --git a/pkg/kubelet/apis/kubeletconfig/v1alpha1/zz_generated.deepcopy.go b/pkg/kubelet/apis/kubeletconfig/v1alpha1/zz_generated.deepcopy.go index 700a997a93..301aa272d3 100644 --- a/pkg/kubelet/apis/kubeletconfig/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/kubelet/apis/kubeletconfig/v1alpha1/zz_generated.deepcopy.go @@ -132,6 +132,11 @@ func (in *KubeletConfiguration) DeepCopyInto(out *KubeletConfiguration) { **out = **in } } + if in.TLSCipherSuites != nil { + in, out := &in.TLSCipherSuites, &out.TLSCipherSuites + *out = make([]string, len(*in)) + copy(*out, *in) + } in.Authentication.DeepCopyInto(&out.Authentication) out.Authorization = in.Authorization if in.RegistryPullQPS != nil { diff --git a/pkg/kubelet/apis/kubeletconfig/zz_generated.deepcopy.go b/pkg/kubelet/apis/kubeletconfig/zz_generated.deepcopy.go index eb788f3b4c..b150467d4b 100644 --- a/pkg/kubelet/apis/kubeletconfig/zz_generated.deepcopy.go +++ b/pkg/kubelet/apis/kubeletconfig/zz_generated.deepcopy.go @@ -105,6 +105,11 @@ func (in *KubeletConfiguration) DeepCopyInto(out *KubeletConfiguration) { } } } + if in.TLSCipherSuites != nil { + in, out := &in.TLSCipherSuites, &out.TLSCipherSuites + *out = make([]string, len(*in)) + copy(*out, *in) + } out.Authentication = in.Authentication out.Authorization = in.Authorization if in.ClusterDNS != nil { diff --git a/staging/src/k8s.io/apiserver/pkg/server/options/serving.go b/staging/src/k8s.io/apiserver/pkg/server/options/serving.go index 43042fde97..9606fa0cb4 100644 --- a/staging/src/k8s.io/apiserver/pkg/server/options/serving.go +++ b/staging/src/k8s.io/apiserver/pkg/server/options/serving.go @@ -51,6 +51,9 @@ type SecureServingOptions struct { ServerCert GeneratableKeyCert // SNICertKeys are named CertKeys for serving secure traffic with SNI support. SNICertKeys []utilflag.NamedCertKey + // CipherSuites is the list of allowed cipher suites for the server. + // Values are from tls package constants (https://golang.org/pkg/crypto/tls/#pkg-constants). + CipherSuites []string } type CertKey struct { @@ -134,6 +137,11 @@ func (s *SecureServingOptions) AddFlags(fs *pflag.FlagSet) { "Controllers. This must be a valid PEM-encoded CA bundle. Altneratively, the certificate authority "+ "can be appended to the certificate provided by --tls-cert-file.") + fs.StringSliceVar(&s.CipherSuites, "tls-cipher-suites", s.CipherSuites, + "Comma-separated list of cipher suites for the server. "+ + "Values are from tls package constants (https://golang.org/pkg/crypto/tls/#pkg-constants). "+ + "If omitted, the default Go cipher suites will be used") + fs.Var(utilflag.NewNamedCertKeyArray(&s.SNICertKeys), "tls-sni-cert-key", ""+ "A pair of x509 certificate and private key file paths, optionally suffixed with a list of "+ "domain patterns which are fully qualified domain names, possibly with prefixed wildcard "+ @@ -233,6 +241,14 @@ func (s *SecureServingOptions) applyServingInfoTo(c *server.Config) error { } } + if len(s.CipherSuites) != 0 { + cipherSuites, err := utilflag.TLSCipherSuites(s.CipherSuites) + if err != nil { + return err + } + secureServingInfo.CipherSuites = cipherSuites + } + // load SNI certs namedTLSCerts := make([]server.NamedTLSCert, 0, len(s.SNICertKeys)) for _, nck := range s.SNICertKeys { diff --git a/staging/src/k8s.io/apiserver/pkg/util/flag/BUILD b/staging/src/k8s.io/apiserver/pkg/util/flag/BUILD index 7bf5456311..25bf080aae 100644 --- a/staging/src/k8s.io/apiserver/pkg/util/flag/BUILD +++ b/staging/src/k8s.io/apiserver/pkg/util/flag/BUILD @@ -9,6 +9,7 @@ load( go_test( name = "go_default_test", srcs = [ + "ciphersuites_flag_test.go", "colon_separated_multimap_string_string_test.go", "langle_separated_map_string_string_test.go", "map_string_bool_test.go", @@ -23,6 +24,7 @@ go_test( go_library( name = "go_default_library", srcs = [ + "ciphersuites_flag.go", "colon_separated_multimap_string_string.go", "configuration_map.go", "flags.go", diff --git a/staging/src/k8s.io/apiserver/pkg/util/flag/ciphersuites_flag.go b/staging/src/k8s.io/apiserver/pkg/util/flag/ciphersuites_flag.go new file mode 100644 index 0000000000..c2ce311e4f --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/util/flag/ciphersuites_flag.go @@ -0,0 +1,64 @@ +/* +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 flag + +import ( + "crypto/tls" + "fmt" +) + +// ciphers maps strings into tls package cipher constants in +// https://golang.org/pkg/crypto/tls/#pkg-constants +var ciphers = map[string]uint16{ + "TLS_RSA_WITH_RC4_128_SHA": tls.TLS_RSA_WITH_RC4_128_SHA, + "TLS_RSA_WITH_3DES_EDE_CBC_SHA": tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA, + "TLS_RSA_WITH_AES_128_CBC_SHA": tls.TLS_RSA_WITH_AES_128_CBC_SHA, + "TLS_RSA_WITH_AES_256_CBC_SHA": tls.TLS_RSA_WITH_AES_256_CBC_SHA, + "TLS_RSA_WITH_AES_128_CBC_SHA256": tls.TLS_RSA_WITH_AES_128_CBC_SHA256, + "TLS_RSA_WITH_AES_128_GCM_SHA256": tls.TLS_RSA_WITH_AES_128_GCM_SHA256, + "TLS_RSA_WITH_AES_256_GCM_SHA384": tls.TLS_RSA_WITH_AES_256_GCM_SHA384, + "TLS_ECDHE_ECDSA_WITH_RC4_128_SHA": tls.TLS_ECDHE_ECDSA_WITH_RC4_128_SHA, + "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, + "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, + "TLS_ECDHE_RSA_WITH_RC4_128_SHA": tls.TLS_ECDHE_RSA_WITH_RC4_128_SHA, + "TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA, + "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, + "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, + "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256, + "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256": tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256, + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256": tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384": tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384": tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305": tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, + "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305": tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, +} + +func TLSCipherSuites(cipherNames []string) ([]uint16, error) { + if len(cipherNames) == 0 { + return nil, nil + } + ciphersIntSlice := make([]uint16, 0) + for _, cipher := range cipherNames { + intValue, ok := ciphers[cipher] + if !ok { + return nil, fmt.Errorf("Cipher suite %s not supported or doesn't exist", cipher) + } + ciphersIntSlice = append(ciphersIntSlice, intValue) + } + return ciphersIntSlice, nil +} diff --git a/staging/src/k8s.io/apiserver/pkg/util/flag/ciphersuites_flag_test.go b/staging/src/k8s.io/apiserver/pkg/util/flag/ciphersuites_flag_test.go new file mode 100644 index 0000000000..b050238a34 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/util/flag/ciphersuites_flag_test.go @@ -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 flag + +import ( + "crypto/tls" + "fmt" + "go/importer" + "reflect" + "strings" + "testing" +) + +func TestStrToUInt16(t *testing.T) { + tests := []struct { + flag []string + expected []uint16 + expected_error bool + }{ + { + // Happy case + flag: []string{"TLS_RSA_WITH_RC4_128_SHA", "TLS_RSA_WITH_AES_128_CBC_SHA", "TLS_ECDHE_RSA_WITH_RC4_128_SHA", "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA"}, + expected: []uint16{tls.TLS_RSA_WITH_RC4_128_SHA, tls.TLS_RSA_WITH_AES_128_CBC_SHA, tls.TLS_ECDHE_RSA_WITH_RC4_128_SHA, tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA}, + expected_error: false, + }, + { + // One flag only + flag: []string{"TLS_RSA_WITH_RC4_128_SHA"}, + expected: []uint16{tls.TLS_RSA_WITH_RC4_128_SHA}, + expected_error: false, + }, + { + // Empty flag + flag: []string{}, + expected: nil, + expected_error: false, + }, + { + // Duplicated flag + flag: []string{"TLS_RSA_WITH_RC4_128_SHA", "TLS_RSA_WITH_AES_128_CBC_SHA", "TLS_ECDHE_RSA_WITH_RC4_128_SHA", "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", "TLS_RSA_WITH_RC4_128_SHA"}, + expected: []uint16{tls.TLS_RSA_WITH_RC4_128_SHA, tls.TLS_RSA_WITH_AES_128_CBC_SHA, tls.TLS_ECDHE_RSA_WITH_RC4_128_SHA, tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, tls.TLS_RSA_WITH_RC4_128_SHA}, + expected_error: false, + }, + { + // Invalid flag + flag: []string{"foo"}, + expected: nil, + expected_error: true, + }, + } + + for i, test := range tests { + uIntFlags, err := TLSCipherSuites(test.flag) + if reflect.DeepEqual(uIntFlags, test.expected) == false { + t.Errorf("%d: expected %+v, got %+v", i, test.expected, uIntFlags) + } + if test.expected_error && err == nil { + t.Errorf("%d: expecting error, got %+v", i, err) + } + } +} + +func TestConstantMaps(t *testing.T) { + pkg, err := importer.Default().Import("crypto/tls") + if err != nil { + fmt.Printf("error: %s\n", err.Error()) + return + } + discoveredCiphers := map[string]bool{} + for _, declName := range pkg.Scope().Names() { + if strings.HasPrefix(declName, "TLS_RSA_") || strings.HasPrefix(declName, "TLS_ECDHE_") { + discoveredCiphers[declName] = true + } + } + + for k := range discoveredCiphers { + if _, ok := ciphers[k]; !ok { + t.Errorf("discovered cipher tls.%s not in ciphers map", k) + } + } + for k := range ciphers { + if _, ok := discoveredCiphers[k]; !ok { + t.Errorf("ciphers map has %s not in tls package", k) + } + } +} diff --git a/test/integration/BUILD b/test/integration/BUILD index 4b27062dc7..fd5e761565 100644 --- a/test/integration/BUILD +++ b/test/integration/BUILD @@ -58,6 +58,7 @@ filegroup( "//test/integration/secrets:all-srcs", "//test/integration/serviceaccount:all-srcs", "//test/integration/storageclasses:all-srcs", + "//test/integration/tls:all-srcs", "//test/integration/ttlcontroller:all-srcs", "//test/integration/volume:all-srcs", ], diff --git a/test/integration/tls/BUILD b/test/integration/tls/BUILD new file mode 100644 index 0000000000..b5f331bede --- /dev/null +++ b/test/integration/tls/BUILD @@ -0,0 +1,35 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_test") + +go_test( + name = "go_default_test", + size = "large", + srcs = [ + "ciphers_test.go", + "main_test.go", + ], + importpath = "k8s.io/kubernetes/test/integration/tls", + tags = ["integration"], + deps = [ + "//cmd/kube-apiserver/app:go_default_library", + "//cmd/kube-apiserver/app/options:go_default_library", + "//test/integration/framework:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/util/wait:go_default_library", + "//vendor/k8s.io/apiserver/pkg/server:go_default_library", + "//vendor/k8s.io/client-go/kubernetes:go_default_library", + "//vendor/k8s.io/client-go/rest:go_default_library", + ], +) + +filegroup( + name = "package-srcs", + srcs = glob(["**"]), + tags = ["automanaged"], + visibility = ["//visibility:private"], +) + +filegroup( + name = "all-srcs", + srcs = [":package-srcs"], + tags = ["automanaged"], + visibility = ["//visibility:public"], +) diff --git a/test/integration/tls/ciphers_test.go b/test/integration/tls/ciphers_test.go new file mode 100644 index 0000000000..3945711011 --- /dev/null +++ b/test/integration/tls/ciphers_test.go @@ -0,0 +1,168 @@ +/* +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 tls + +import ( + "crypto/tls" + "fmt" + "io/ioutil" + "net" + "net/http" + "os" + "sync/atomic" + "testing" + "time" + + "k8s.io/apimachinery/pkg/util/wait" + genericapiserver "k8s.io/apiserver/pkg/server" + client "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" + "k8s.io/kubernetes/cmd/kube-apiserver/app" + "k8s.io/kubernetes/cmd/kube-apiserver/app/options" + "k8s.io/kubernetes/test/integration/framework" +) + +func runBasicSecureAPIServer(t *testing.T, ciphers []string) (uint32, error) { + certDir, _ := ioutil.TempDir("", "test-integration-tls") + defer os.RemoveAll(certDir) + _, defaultServiceClusterIPRange, _ := net.ParseCIDR("10.0.0.0/24") + kubeClientConfigValue := atomic.Value{} + var kubePort uint32 + + go func() { + // always get a fresh port in case something claimed the old one + freePort, err := framework.FindFreeLocalPort() + if err != nil { + t.Fatal(err) + } + + atomic.StoreUint32(&kubePort, uint32(freePort)) + + kubeAPIServerOptions := options.NewServerRunOptions() + kubeAPIServerOptions.SecureServing.BindAddress = net.ParseIP("127.0.0.1") + kubeAPIServerOptions.SecureServing.BindPort = freePort + kubeAPIServerOptions.SecureServing.ServerCert.CertDirectory = certDir + kubeAPIServerOptions.SecureServing.CipherSuites = ciphers + kubeAPIServerOptions.InsecureServing.BindPort = 0 + kubeAPIServerOptions.Etcd.StorageConfig.ServerList = []string{framework.GetEtcdURL()} + kubeAPIServerOptions.ServiceClusterIPRange = *defaultServiceClusterIPRange + + tunneler, proxyTransport, err := app.CreateNodeDialer(kubeAPIServerOptions) + if err != nil { + t.Fatal(err) + } + kubeAPIServerConfig, sharedInformers, versionedInformers, _, _, err := app.CreateKubeAPIServerConfig(kubeAPIServerOptions, tunneler, proxyTransport) + if err != nil { + t.Fatal(err) + } + kubeAPIServerConfig.ExtraConfig.EnableCoreControllers = false + kubeClientConfigValue.Store(kubeAPIServerConfig.GenericConfig.LoopbackClientConfig) + + kubeAPIServer, err := app.CreateKubeAPIServer(kubeAPIServerConfig, genericapiserver.EmptyDelegate, sharedInformers, versionedInformers) + if err != nil { + t.Fatal(err) + } + + if err := kubeAPIServer.GenericAPIServer.PrepareRun().Run(wait.NeverStop); err != nil { + t.Log(err) + } + time.Sleep(100 * time.Millisecond) + }() + + // Ensure server is ready + err := wait.PollImmediate(100*time.Millisecond, 10*time.Second, func() (done bool, err error) { + obj := kubeClientConfigValue.Load() + if obj == nil { + return false, nil + } + kubeClientConfig := kubeClientConfigValue.Load().(*rest.Config) + kubeClientConfig.ContentType = "" + kubeClientConfig.AcceptContentTypes = "" + kubeClient, err := client.NewForConfig(kubeClientConfig) + if err != nil { + // this happens because we race the API server start + t.Log(err) + return false, nil + } + if _, err := kubeClient.Discovery().ServerVersion(); err != nil { + return false, nil + } + return true, nil + }) + if err != nil { + return 0, err + } + + securePort := atomic.LoadUint32(&kubePort) + return securePort, nil +} + +func TestAPICiphers(t *testing.T) { + + basicServerCiphers := []string{"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305", "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305", "TLS_RSA_WITH_AES_128_CBC_SHA", "TLS_RSA_WITH_AES_256_CBC_SHA", "TLS_RSA_WITH_AES_128_GCM_SHA256", "TLS_RSA_WITH_AES_256_GCM_SHA384", "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA", "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA", "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA"} + + kubePort, err := runBasicSecureAPIServer(t, basicServerCiphers) + if err != nil { + t.Fatal(err) + } + + tests := []struct { + clientCiphers []uint16 + expectedError bool + }{ + { + // Not supported cipher + clientCiphers: []uint16{tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA}, + expectedError: true, + }, + { + // Supported cipher + clientCiphers: []uint16{tls.TLS_RSA_WITH_AES_256_CBC_SHA}, + expectedError: false, + }, + } + + for i, test := range tests { + runTestAPICiphers(t, i, kubePort, test.clientCiphers, test.expectedError) + } +} + +func runTestAPICiphers(t *testing.T, testID int, kubePort uint32, clientCiphers []uint16, expectedError bool) { + + tr := &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + CipherSuites: clientCiphers, + }, + } + client := &http.Client{Transport: tr} + req, err := http.NewRequest("GET", fmt.Sprintf("https://127.0.0.1:%d", kubePort), nil) + if err != nil { + t.Fatal(err) + } + resp, err := client.Do(req) + + if expectedError == true && err == nil { + t.Fatalf("%d: expecting error for cipher test, client cipher is supported and it should't", testID) + } else if err != nil && expectedError == false { + t.Fatalf("%d: not expecting error by client with cipher failed: %+v", testID, err) + } + + if err == nil { + defer resp.Body.Close() + } +} diff --git a/test/integration/tls/main_test.go b/test/integration/tls/main_test.go new file mode 100644 index 0000000000..c9a1d9fa98 --- /dev/null +++ b/test/integration/tls/main_test.go @@ -0,0 +1,27 @@ +/* +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 tls + +import ( + "testing" + + "k8s.io/kubernetes/test/integration/framework" +) + +func TestMain(m *testing.M) { + framework.EtcdMain(m.Run) +}