diff --git a/pkg/client/helper.go b/pkg/client/helper.go index 0302d666750..f1939f10eb4 100644 --- a/pkg/client/helper.go +++ b/pkg/client/helper.go @@ -18,6 +18,7 @@ package client import ( "fmt" + "io/ioutil" "net/http" "net/url" "path" @@ -62,8 +63,20 @@ type Config struct { // Server requires TLS client certificate authentication CertFile string - KeyFile string - CAFile string + // Server requires TLS client certificate authentication + KeyFile string + // Trusted root certificates for server + CAFile string + + // CertData holds PEM-encoded bytes (typically read from a client certificate file). + // CertData takes precedence over CertFile + CertData []byte + // KeyData holds PEM-encoded bytes (typically read from a client certificate key file). + // KeyData takes precedence over KeyFile + KeyData []byte + // CAData holds PEM-encoded bytes (typically read from a root certificates bundle). + // CAData takes precedence over CAFile + CAData []byte // Server should be accessed without verifying the TLS // certificate. For testing only. @@ -85,6 +98,16 @@ type KubeletConfig struct { KeyFile string // TLS Configuration, only applies if EnableHttps is true. CAFile string + + // CertData holds PEM-encoded bytes (typically read from a client certificate file). + // CertData takes precedence over CertFile + CertData []byte + // KeyData holds PEM-encoded bytes (typically read from a client certificate key file). + // KeyData takes precedence over KeyFile + KeyData []byte + // CAData holds PEM-encoded bytes (typically read from a root certificates bundle). + // CAData takes precedence over CAFile + CAData []byte } // New creates a Kubernetes client for the given config. This client works with pods, @@ -185,29 +208,48 @@ func RESTClientFor(config *Config) (*RESTClient, error) { // or transport level security defined by the provided Config. Will return the // default http.DefaultTransport if no special case behavior is needed. func TransportFor(config *Config) (http.RoundTripper, error) { + hasCA := len(config.CAFile) > 0 || len(config.CAData) > 0 + hasCert := len(config.CertFile) > 0 || len(config.CertData) > 0 + // Set transport level security - if config.Transport != nil && (config.CAFile != "" || config.CertFile != "" || config.Insecure) { + if config.Transport != nil && (hasCA || hasCert || config.Insecure) { return nil, fmt.Errorf("using a custom transport with TLS certificate options or the insecure flag is not allowed") } - if config.CAFile != "" && config.Insecure { + if hasCA && config.Insecure { return nil, fmt.Errorf("specifying a root certificates file with the insecure flag is not allowed") } var transport http.RoundTripper switch { case config.Transport != nil: transport = config.Transport - case config.CertFile != "": - t, err := NewClientCertTLSTransport(config.CertFile, config.KeyFile, config.CAFile) - if err != nil { + case hasCert: + var ( + certData, keyData, caData []byte + err error + ) + if certData, err = dataFromSliceOrFile(config.CertData, config.CertFile); err != nil { return nil, err } - transport = t - case config.CAFile != "": - t, err := NewTLSTransport(config.CAFile) - if err != nil { + if keyData, err = dataFromSliceOrFile(config.KeyData, config.KeyFile); err != nil { + return nil, err + } + if caData, err = dataFromSliceOrFile(config.CAData, config.CAFile); err != nil { + return nil, err + } + if transport, err = NewClientCertTLSTransport(certData, keyData, caData); err != nil { + return nil, err + } + case hasCA: + var ( + caData []byte + err error + ) + if caData, err = dataFromSliceOrFile(config.CAData, config.CAFile); err != nil { + return nil, err + } + if transport, err = NewTLSTransport(caData); err != nil { return nil, err } - transport = t case config.Insecure: transport = NewUnsafeTLSTransport() default: @@ -231,6 +273,19 @@ func TransportFor(config *Config) (http.RoundTripper, error) { return transport, nil } +// dataFromSliceOrFile returns data from the slice (if non-empty), or from the file, +// or an error if an error occurred reading the file +func dataFromSliceOrFile(data []byte, file string) ([]byte, error) { + if len(data) > 0 { + return data, nil + } + fileData, err := ioutil.ReadFile(file) + if err != nil { + return []byte{}, err + } + return fileData, nil +} + // DefaultServerURL converts a host, host:port, or URL string to the default base server API path // to use with a Client at a given API version following the standard conventions for a // Kubernetes API. diff --git a/pkg/client/helper_test.go b/pkg/client/helper_test.go index 5ed4a6cb232..6eeaaf185da 100644 --- a/pkg/client/helper_test.go +++ b/pkg/client/helper_test.go @@ -21,14 +21,138 @@ import ( "testing" ) +const ( + rootCACert = `-----BEGIN CERTIFICATE----- +MIIC4DCCAcqgAwIBAgIBATALBgkqhkiG9w0BAQswIzEhMB8GA1UEAwwYMTAuMTMu +MTI5LjEwNkAxNDIxMzU5MDU4MB4XDTE1MDExNTIxNTczN1oXDTE2MDExNTIxNTcz +OFowIzEhMB8GA1UEAwwYMTAuMTMuMTI5LjEwNkAxNDIxMzU5MDU4MIIBIjANBgkq +hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAunDRXGwsiYWGFDlWH6kjGun+PshDGeZX +xtx9lUnL8pIRWH3wX6f13PO9sktaOWW0T0mlo6k2bMlSLlSZgG9H6og0W6gLS3vq +s4VavZ6DbXIwemZG2vbRwsvR+t4G6Nbwelm6F8RFnA1Fwt428pavmNQ/wgYzo+T1 +1eS+HiN4ACnSoDSx3QRWcgBkB1g6VReofVjx63i0J+w8Q/41L9GUuLqquFxu6ZnH +60vTB55lHgFiDLjA1FkEz2dGvGh/wtnFlRvjaPC54JH2K1mPYAUXTreoeJtLJKX0 +ycoiyB24+zGCniUmgIsmQWRPaOPircexCp1BOeze82BT1LCZNTVaxQIDAQABoyMw +ITAOBgNVHQ8BAf8EBAMCAKQwDwYDVR0TAQH/BAUwAwEB/zALBgkqhkiG9w0BAQsD +ggEBADMxsUuAFlsYDpF4fRCzXXwrhbtj4oQwcHpbu+rnOPHCZupiafzZpDu+rw4x +YGPnCb594bRTQn4pAu3Ac18NbLD5pV3uioAkv8oPkgr8aUhXqiv7KdDiaWm6sbAL +EHiXVBBAFvQws10HMqMoKtO8f1XDNAUkWduakR/U6yMgvOPwS7xl0eUTqyRB6zGb +K55q2dejiFWaFqB/y78txzvz6UlOZKE44g2JAVoJVM6kGaxh33q8/FmrL4kuN3ut +W+MmJCVDvd4eEqPwbp7146ZWTqpIJ8lvA6wuChtqV8lhAPka2hD/LMqY8iXNmfXD +uml0obOEy+ON91k+SWTJ3ggmF/U= +-----END CERTIFICATE-----` + + certData = `-----BEGIN CERTIFICATE----- +MIIC6jCCAdSgAwIBAgIBCzALBgkqhkiG9w0BAQswIzEhMB8GA1UEAwwYMTAuMTMu +MTI5LjEwNkAxNDIxMzU5MDU4MB4XDTE1MDExNTIyMDEzMVoXDTE2MDExNTIyMDEz +MlowGzEZMBcGA1UEAxMQb3BlbnNoaWZ0LWNsaWVudDCCASIwDQYJKoZIhvcNAQEB +BQADggEPADCCAQoCggEBAKtdhz0+uCLXw5cSYns9rU/XifFSpb/x24WDdrm72S/v +b9BPYsAStiP148buylr1SOuNi8sTAZmlVDDIpIVwMLff+o2rKYDicn9fjbrTxTOj +lI4pHJBH+JU3AJ0tbajupioh70jwFS0oYpwtneg2zcnE2Z4l6mhrj2okrc5Q1/X2 +I2HChtIU4JYTisObtin10QKJX01CLfYXJLa8upWzKZ4/GOcHG+eAV3jXWoXidtjb +1Usw70amoTZ6mIVCkiu1QwCoa8+ycojGfZhvqMsAp1536ZcCul+Na+AbCv4zKS7F +kQQaImVrXdUiFansIoofGlw/JNuoKK6ssVpS5Ic3pgcCAwEAAaM1MDMwDgYDVR0P +AQH/BAQDAgCgMBMGA1UdJQQMMAoGCCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAwCwYJ +KoZIhvcNAQELA4IBAQCKLREH7bXtXtZ+8vI6cjD7W3QikiArGqbl36bAhhWsJLp/ +p/ndKz39iFNaiZ3GlwIURWOOKx3y3GA0x9m8FR+Llthf0EQ8sUjnwaknWs0Y6DQ3 +jjPFZOpV3KPCFrdMJ3++E3MgwFC/Ih/N2ebFX9EcV9Vcc6oVWMdwT0fsrhu683rq +6GSR/3iVX1G/pmOiuaR0fNUaCyCfYrnI4zHBDgSfnlm3vIvN2lrsR/DQBakNL8DJ +HBgKxMGeUPoneBv+c8DMXIL0EhaFXRlBv9QW45/GiAIOuyFJ0i6hCtGZpJjq4OpQ +BRjCI+izPzFTjsxD4aORE+WOkyWFCGPWKfNejfw0 +-----END CERTIFICATE-----` + + keyData = `-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAq12HPT64ItfDlxJiez2tT9eJ8VKlv/HbhYN2ubvZL+9v0E9i +wBK2I/Xjxu7KWvVI642LyxMBmaVUMMikhXAwt9/6jaspgOJyf1+NutPFM6OUjikc +kEf4lTcAnS1tqO6mKiHvSPAVLShinC2d6DbNycTZniXqaGuPaiStzlDX9fYjYcKG +0hTglhOKw5u2KfXRAolfTUIt9hcktry6lbMpnj8Y5wcb54BXeNdaheJ22NvVSzDv +RqahNnqYhUKSK7VDAKhrz7JyiMZ9mG+oywCnXnfplwK6X41r4BsK/jMpLsWRBBoi +ZWtd1SIVqewiih8aXD8k26gorqyxWlLkhzemBwIDAQABAoIBAD2XYRs3JrGHQUpU +FkdbVKZkvrSY0vAZOqBTLuH0zUv4UATb8487anGkWBjRDLQCgxH+jucPTrztekQK +aW94clo0S3aNtV4YhbSYIHWs1a0It0UdK6ID7CmdWkAj6s0T8W8lQT7C46mWYVLm +5mFnCTHi6aB42jZrqmEpC7sivWwuU0xqj3Ml8kkxQCGmyc9JjmCB4OrFFC8NNt6M +ObvQkUI6Z3nO4phTbpxkE1/9dT0MmPIF7GhHVzJMS+EyyRYUDllZ0wvVSOM3qZT0 +JMUaBerkNwm9foKJ1+dv2nMKZZbJajv7suUDCfU44mVeaEO+4kmTKSGCGjjTBGkr +7L1ySDECgYEA5ElIMhpdBzIivCuBIH8LlUeuzd93pqssO1G2Xg0jHtfM4tz7fyeI +cr90dc8gpli24dkSxzLeg3Tn3wIj/Bu64m2TpZPZEIlukYvgdgArmRIPQVxerYey +OkrfTNkxU1HXsYjLCdGcGXs5lmb+K/kuTcFxaMOs7jZi7La+jEONwf8CgYEAwCs/ +rUOOA0klDsWWisbivOiNPII79c9McZCNBqncCBfMUoiGe8uWDEO4TFHN60vFuVk9 +8PkwpCfvaBUX+ajvbafIfHxsnfk1M04WLGCeqQ/ym5Q4sQoQOcC1b1y9qc/xEWfg +nIUuia0ukYRpl7qQa3tNg+BNFyjypW8zukUAC/kCgYB1/Kojuxx5q5/oQVPrx73k +2bevD+B3c+DYh9MJqSCNwFtUpYIWpggPxoQan4LwdsmO0PKzocb/ilyNFj4i/vII +NToqSc/WjDFpaDIKyuu9oWfhECye45NqLWhb/6VOuu4QA/Nsj7luMhIBehnEAHW+ +GkzTKM8oD1PxpEG3nPKXYQKBgQC6AuMPRt3XBl1NkCrpSBy/uObFlFaP2Enpf39S +3OZ0Gv0XQrnSaL1kP8TMcz68rMrGX8DaWYsgytstR4W+jyy7WvZwsUu+GjTJ5aMG +77uEcEBpIi9CBzivfn7hPccE8ZgqPf+n4i6q66yxBJflW5xhvafJqDtW2LcPNbW/ +bvzdmQKBgExALRUXpq+5dbmkdXBHtvXdRDZ6rVmrnjy4nI5bPw+1GqQqk6uAR6B/ +F6NmLCQOO4PDG/cuatNHIr2FrwTmGdEL6ObLUGWn9Oer9gJhHVqqsY5I4sEPo4XX +stR0Yiw0buV6DL/moUO0HIM9Bjh96HJp+LxiIS6UCdIhMPp5HoQa +-----END RSA PRIVATE KEY-----` +) + func TestTransportFor(t *testing.T) { testCases := map[string]struct { Config *Config Err bool + TLS bool Default bool }{ "default transport": { - Config: &Config{}, + Default: true, + Config: &Config{}, + }, + + "ca transport": { + TLS: true, + Config: &Config{ + CAData: []byte(rootCACert), + }, + }, + "bad ca file transport": { + Err: true, + Config: &Config{ + CAFile: "invalid file", + }, + }, + "ca data overriding bad ca file transport": { + TLS: true, + Config: &Config{ + CAData: []byte(rootCACert), + CAFile: "invalid file", + }, + }, + + "cert transport": { + TLS: true, + Config: &Config{ + CertData: []byte(certData), + KeyData: []byte(keyData), + CAData: []byte(rootCACert), + }, + }, + "bad cert data transport": { + Err: true, + Config: &Config{ + CertData: []byte(certData), + KeyData: []byte("bad key data"), + CAData: []byte(rootCACert), + }, + }, + "bad file cert transport": { + Err: true, + Config: &Config{ + CertData: []byte(certData), + KeyFile: "invalid file", + CAData: []byte(rootCACert), + }, + }, + "key data overriding bad file cert transport": { + TLS: true, + Config: &Config{ + CertData: []byte(certData), + KeyData: []byte(keyData), + KeyFile: "invalid file", + CAData: []byte(rootCACert), + }, }, } for k, testCase := range testCases { @@ -41,8 +165,26 @@ func TestTransportFor(t *testing.T) { t.Errorf("%s: unexpected error: %v", k, err) continue } - if testCase.Default && transport != http.DefaultTransport { + + switch { + case testCase.Default && transport != http.DefaultTransport: t.Errorf("%s: expected the default transport, got %#v", k, transport) + continue + case !testCase.Default && transport == http.DefaultTransport: + t.Errorf("%s: expected non-default transport, got %#v", k, transport) + continue + } + + // We only know how to check TLSConfig on http.Transports + if transport, ok := transport.(*http.Transport); ok { + switch { + case testCase.TLS && transport.TLSClientConfig == nil: + t.Errorf("%s: expected TLSClientConfig, got %#v", k, transport) + continue + case !testCase.TLS && transport.TLSClientConfig != nil: + t.Errorf("%s: expected no TLSClientConfig, got %#v", k, transport) + continue + } } } } diff --git a/pkg/client/kubelet.go b/pkg/client/kubelet.go index ef76bd4cc77..503f409282c 100644 --- a/pkg/client/kubelet.go +++ b/pkg/client/kubelet.go @@ -61,17 +61,33 @@ type HTTPKubeletClient struct { func NewKubeletClient(config *KubeletConfig) (KubeletClient, error) { transport := http.DefaultTransport if config.CertFile != "" { - t, err := NewClientCertTLSTransport(config.CertFile, config.KeyFile, config.CAFile) - if err != nil { + var ( + certData, keyData, caData []byte + err error + ) + if certData, err = dataFromSliceOrFile(config.CertData, config.CertFile); err != nil { + return nil, err + } + if keyData, err = dataFromSliceOrFile(config.KeyData, config.KeyFile); err != nil { + return nil, err + } + if caData, err = dataFromSliceOrFile(config.CAData, config.CAFile); err != nil { + return nil, err + } + if transport, err = NewClientCertTLSTransport(certData, keyData, caData); err != nil { return nil, err } - transport = t } else if config.CAFile != "" { - t, err := NewTLSTransport(config.CAFile) - if err != nil { + var ( + caData []byte + err error + ) + if caData, err = dataFromSliceOrFile(config.CAData, config.CAFile); err != nil { + return nil, err + } + if transport, err = NewTLSTransport(caData); err != nil { return nil, err } - transport = t } c := &http.Client{Transport: transport} diff --git a/pkg/client/restclient_test.go b/pkg/client/restclient_test.go index d0e2e1ebb6f..073e1c4bb8c 100644 --- a/pkg/client/restclient_test.go +++ b/pkg/client/restclient_test.go @@ -102,7 +102,7 @@ func TestSetDefaults(t *testing.T) { case err != nil: continue } - if *val != testCase.After { + if !reflect.DeepEqual(*val, testCase.After) { t.Errorf("unexpected result object: %#v", val) } } diff --git a/pkg/client/transport.go b/pkg/client/transport.go index 6462f95d462..4d171bea5f2 100644 --- a/pkg/client/transport.go +++ b/pkg/client/transport.go @@ -20,7 +20,6 @@ import ( "crypto/tls" "crypto/x509" "fmt" - "io/ioutil" "net/http" ) @@ -55,17 +54,13 @@ func (rt *bearerAuthRoundTripper) RoundTrip(req *http.Request) (*http.Response, return rt.rt.RoundTrip(req) } -func NewClientCertTLSTransport(certFile, keyFile, caFile string) (*http.Transport, error) { - cert, err := tls.LoadX509KeyPair(certFile, keyFile) - if err != nil { - return nil, err - } - data, err := ioutil.ReadFile(caFile) +func NewClientCertTLSTransport(certData, keyData, caData []byte) (*http.Transport, error) { + cert, err := tls.X509KeyPair(certData, keyData) if err != nil { return nil, err } certPool := x509.NewCertPool() - certPool.AppendCertsFromPEM(data) + certPool.AppendCertsFromPEM(caData) return &http.Transport{ TLSClientConfig: &tls.Config{ // Change default from SSLv3 to TLSv1.0 (because of POODLE vulnerability) @@ -80,13 +75,9 @@ func NewClientCertTLSTransport(certFile, keyFile, caFile string) (*http.Transpor }, nil } -func NewTLSTransport(caFile string) (*http.Transport, error) { - data, err := ioutil.ReadFile(caFile) - if err != nil { - return nil, err - } +func NewTLSTransport(caData []byte) (*http.Transport, error) { certPool := x509.NewCertPool() - certPool.AppendCertsFromPEM(data) + certPool.AppendCertsFromPEM(caData) return &http.Transport{ TLSClientConfig: &tls.Config{ // Change default from SSLv3 to TLSv1.0 (because of POODLE vulnerability)