Unverified Commit f701b752 authored by Kubernetes Submit Queue's avatar Kubernetes Submit Queue Committed by GitHub

Merge pull request #61803 from awly/client-auth-exec-tls

Automatic merge from submit-queue (batch tested with PRs 61803, 64305, 64170, 64361, 64339). If you want to cherry-pick this change to another branch, please follow the instructions <a href="https://github.com/kubernetes/community/blob/master/contributors/devel/cherry-picks.md">here</a>. Add TLS support to exec authenticator plugin **What this PR does / why we need it**: https://github.com/kubernetes/community/blob/master/contributors/design-proposals/auth/kubectl-exec-plugins.md#tls-client-certificate-support Allows exec plugin to return raw TLS key/cert data. This data populates transport.Config.TLS field. This requires a change to AuthProvider interface to expose TLS configs, not only RoundTripper. **Which issue(s) this PR fixes** *(optional, in `fixes #<issue number>(, fixes #<issue_number>, ...)` format, will close the issue(s) when PR gets merged)*: Fixes #61421 **Special notes for your reviewer**: **Release note**: ```release-note Exec authenticator plugin supports TLS client certificates. ```
parents ea92879f cd89f947
...@@ -2011,6 +2011,10 @@ ...@@ -2011,6 +2011,10 @@
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}, },
{ {
"ImportPath": "k8s.io/client-go/util/connrotation",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
{
"ImportPath": "k8s.io/client-go/util/flowcontrol", "ImportPath": "k8s.io/client-go/util/flowcontrol",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}, },
......
...@@ -1739,6 +1739,10 @@ ...@@ -1739,6 +1739,10 @@
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}, },
{ {
"ImportPath": "k8s.io/client-go/util/connrotation",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
{
"ImportPath": "k8s.io/client-go/util/flowcontrol", "ImportPath": "k8s.io/client-go/util/flowcontrol",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}, },
......
...@@ -57,7 +57,14 @@ type ExecCredentialStatus struct { ...@@ -57,7 +57,14 @@ type ExecCredentialStatus struct {
// +optional // +optional
ExpirationTimestamp *metav1.Time ExpirationTimestamp *metav1.Time
// Token is a bearer token used by the client for request authentication. // Token is a bearer token used by the client for request authentication.
// +optional
Token string Token string
// PEM-encoded client TLS certificate.
// +optional
ClientCertificateData string
// PEM-encoded client TLS private key.
// +optional
ClientKeyData string
} }
// Response defines metadata about a failed request, including HTTP status code and // Response defines metadata about a failed request, including HTTP status code and
......
...@@ -52,12 +52,20 @@ type ExecCredentialSpec struct { ...@@ -52,12 +52,20 @@ type ExecCredentialSpec struct {
} }
// ExecCredentialStatus holds credentials for the transport to use. // ExecCredentialStatus holds credentials for the transport to use.
//
// Token and ClientKeyData are sensitive fields. This data should only be
// transmitted in-memory between client and exec plugin process. Exec plugin
// itself should at least be protected via file permissions.
type ExecCredentialStatus struct { type ExecCredentialStatus struct {
// ExpirationTimestamp indicates a time when the provided credentials expire. // ExpirationTimestamp indicates a time when the provided credentials expire.
// +optional // +optional
ExpirationTimestamp *metav1.Time `json:"expirationTimestamp,omitempty"` ExpirationTimestamp *metav1.Time `json:"expirationTimestamp,omitempty"`
// Token is a bearer token used by the client for request authentication. // Token is a bearer token used by the client for request authentication.
Token string `json:"token,omitempty"` Token string `json:"token,omitempty"`
// PEM-encoded client TLS certificates (including intermediates, if any).
ClientCertificateData string `json:"clientCertificateData,omitempty"`
// PEM-encoded private key for the above certificate.
ClientKeyData string `json:"clientKeyData,omitempty"`
} }
// Response defines metadata about a failed request, including HTTP status code and // Response defines metadata about a failed request, including HTTP status code and
......
...@@ -99,6 +99,8 @@ func Convert_clientauthentication_ExecCredentialSpec_To_v1alpha1_ExecCredentialS ...@@ -99,6 +99,8 @@ func Convert_clientauthentication_ExecCredentialSpec_To_v1alpha1_ExecCredentialS
func autoConvert_v1alpha1_ExecCredentialStatus_To_clientauthentication_ExecCredentialStatus(in *ExecCredentialStatus, out *clientauthentication.ExecCredentialStatus, s conversion.Scope) error { func autoConvert_v1alpha1_ExecCredentialStatus_To_clientauthentication_ExecCredentialStatus(in *ExecCredentialStatus, out *clientauthentication.ExecCredentialStatus, s conversion.Scope) error {
out.ExpirationTimestamp = (*v1.Time)(unsafe.Pointer(in.ExpirationTimestamp)) out.ExpirationTimestamp = (*v1.Time)(unsafe.Pointer(in.ExpirationTimestamp))
out.Token = in.Token out.Token = in.Token
out.ClientCertificateData = in.ClientCertificateData
out.ClientKeyData = in.ClientKeyData
return nil return nil
} }
...@@ -110,6 +112,8 @@ func Convert_v1alpha1_ExecCredentialStatus_To_clientauthentication_ExecCredentia ...@@ -110,6 +112,8 @@ func Convert_v1alpha1_ExecCredentialStatus_To_clientauthentication_ExecCredentia
func autoConvert_clientauthentication_ExecCredentialStatus_To_v1alpha1_ExecCredentialStatus(in *clientauthentication.ExecCredentialStatus, out *ExecCredentialStatus, s conversion.Scope) error { func autoConvert_clientauthentication_ExecCredentialStatus_To_v1alpha1_ExecCredentialStatus(in *clientauthentication.ExecCredentialStatus, out *ExecCredentialStatus, s conversion.Scope) error {
out.ExpirationTimestamp = (*v1.Time)(unsafe.Pointer(in.ExpirationTimestamp)) out.ExpirationTimestamp = (*v1.Time)(unsafe.Pointer(in.ExpirationTimestamp))
out.Token = in.Token out.Token = in.Token
out.ClientCertificateData = in.ClientCertificateData
out.ClientKeyData = in.ClientKeyData
return nil return nil
} }
......
...@@ -15,6 +15,8 @@ go_library( ...@@ -15,6 +15,8 @@ go_library(
"//vendor/k8s.io/client-go/pkg/apis/clientauthentication:go_default_library", "//vendor/k8s.io/client-go/pkg/apis/clientauthentication:go_default_library",
"//vendor/k8s.io/client-go/pkg/apis/clientauthentication/v1alpha1:go_default_library", "//vendor/k8s.io/client-go/pkg/apis/clientauthentication/v1alpha1:go_default_library",
"//vendor/k8s.io/client-go/tools/clientcmd/api:go_default_library", "//vendor/k8s.io/client-go/tools/clientcmd/api:go_default_library",
"//vendor/k8s.io/client-go/transport:go_default_library",
"//vendor/k8s.io/client-go/util/connrotation:go_default_library",
], ],
) )
...@@ -24,8 +26,11 @@ go_test( ...@@ -24,8 +26,11 @@ go_test(
data = glob(["testdata/**"]), data = glob(["testdata/**"]),
embed = [":go_default_library"], embed = [":go_default_library"],
deps = [ deps = [
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
"//vendor/k8s.io/client-go/pkg/apis/clientauthentication:go_default_library", "//vendor/k8s.io/client-go/pkg/apis/clientauthentication:go_default_library",
"//vendor/k8s.io/client-go/tools/clientcmd/api:go_default_library", "//vendor/k8s.io/client-go/tools/clientcmd/api:go_default_library",
"//vendor/k8s.io/client-go/transport:go_default_library",
], ],
) )
......
...@@ -18,11 +18,15 @@ package exec ...@@ -18,11 +18,15 @@ package exec
import ( import (
"bytes" "bytes"
"context"
"crypto/tls"
"fmt" "fmt"
"io" "io"
"net"
"net/http" "net/http"
"os" "os"
"os/exec" "os/exec"
"reflect"
"sync" "sync"
"time" "time"
...@@ -35,6 +39,8 @@ import ( ...@@ -35,6 +39,8 @@ import (
"k8s.io/client-go/pkg/apis/clientauthentication" "k8s.io/client-go/pkg/apis/clientauthentication"
"k8s.io/client-go/pkg/apis/clientauthentication/v1alpha1" "k8s.io/client-go/pkg/apis/clientauthentication/v1alpha1"
"k8s.io/client-go/tools/clientcmd/api" "k8s.io/client-go/tools/clientcmd/api"
"k8s.io/client-go/transport"
"k8s.io/client-go/util/connrotation"
) )
const execInfoEnv = "KUBERNETES_EXEC_INFO" const execInfoEnv = "KUBERNETES_EXEC_INFO"
...@@ -147,14 +153,55 @@ type Authenticator struct { ...@@ -147,14 +153,55 @@ type Authenticator struct {
// The mutex also guards calling the plugin. Since the plugin could be // The mutex also guards calling the plugin. Since the plugin could be
// interactive we want to make sure it's only called once. // interactive we want to make sure it's only called once.
mu sync.Mutex mu sync.Mutex
cachedToken string cachedCreds *credentials
exp time.Time exp time.Time
onRotate func()
} }
// WrapTransport instruments an existing http.RoundTripper with credentials returned type credentials struct {
// by the plugin. token string
func (a *Authenticator) WrapTransport(rt http.RoundTripper) http.RoundTripper { cert *tls.Certificate
return &roundTripper{a, rt} }
// UpdateTransportConfig updates the transport.Config to use credentials
// returned by the plugin.
func (a *Authenticator) UpdateTransportConfig(c *transport.Config) error {
wt := c.WrapTransport
c.WrapTransport = func(rt http.RoundTripper) http.RoundTripper {
if wt != nil {
rt = wt(rt)
}
return &roundTripper{a, rt}
}
getCert := c.TLS.GetCert
c.TLS.GetCert = func() (*tls.Certificate, error) {
// If previous GetCert is present and returns a valid non-nil
// certificate, use that. Otherwise use cert from exec plugin.
if getCert != nil {
cert, err := getCert()
if err != nil {
return nil, err
}
if cert != nil {
return cert, nil
}
}
return a.cert()
}
var dial func(ctx context.Context, network, addr string) (net.Conn, error)
if c.Dial != nil {
dial = c.Dial
} else {
dial = (&net.Dialer{Timeout: 30 * time.Second, KeepAlive: 30 * time.Second}).DialContext
}
d := connrotation.NewDialer(dial)
a.onRotate = d.CloseAll
c.Dial = d.DialContext
return nil
} }
type roundTripper struct { type roundTripper struct {
...@@ -169,11 +216,13 @@ func (r *roundTripper) RoundTrip(req *http.Request) (*http.Response, error) { ...@@ -169,11 +216,13 @@ func (r *roundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
return r.base.RoundTrip(req) return r.base.RoundTrip(req)
} }
token, err := r.a.token() creds, err := r.a.getCreds()
if err != nil { if err != nil {
return nil, fmt.Errorf("getting token: %v", err) return nil, fmt.Errorf("getting credentials: %v", err)
}
if creds.token != "" {
req.Header.Set("Authorization", "Bearer "+creds.token)
} }
req.Header.Set("Authorization", "Bearer "+token)
res, err := r.base.RoundTrip(req) res, err := r.base.RoundTrip(req)
if err != nil { if err != nil {
...@@ -184,47 +233,60 @@ func (r *roundTripper) RoundTrip(req *http.Request) (*http.Response, error) { ...@@ -184,47 +233,60 @@ func (r *roundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
Header: res.Header, Header: res.Header,
Code: int32(res.StatusCode), Code: int32(res.StatusCode),
} }
if err := r.a.refresh(token, resp); err != nil { if err := r.a.maybeRefreshCreds(creds, resp); err != nil {
glog.Errorf("refreshing token: %v", err) glog.Errorf("refreshing credentials: %v", err)
} }
} }
return res, nil return res, nil
} }
func (a *Authenticator) tokenExpired() bool { func (a *Authenticator) credsExpired() bool {
if a.exp.IsZero() { if a.exp.IsZero() {
return false return false
} }
return a.now().After(a.exp) return a.now().After(a.exp)
} }
func (a *Authenticator) token() (string, error) { func (a *Authenticator) cert() (*tls.Certificate, error) {
creds, err := a.getCreds()
if err != nil {
return nil, err
}
return creds.cert, nil
}
func (a *Authenticator) getCreds() (*credentials, error) {
a.mu.Lock() a.mu.Lock()
defer a.mu.Unlock() defer a.mu.Unlock()
if a.cachedToken != "" && !a.tokenExpired() { if a.cachedCreds != nil && !a.credsExpired() {
return a.cachedToken, nil return a.cachedCreds, nil
} }
return a.getToken(nil) if err := a.refreshCredsLocked(nil); err != nil {
return nil, err
}
return a.cachedCreds, nil
} }
// refresh executes the plugin to force a rotation of the token. // maybeRefreshCreds executes the plugin to force a rotation of the
func (a *Authenticator) refresh(token string, r *clientauthentication.Response) error { // credentials, unless they were rotated already.
func (a *Authenticator) maybeRefreshCreds(creds *credentials, r *clientauthentication.Response) error {
a.mu.Lock() a.mu.Lock()
defer a.mu.Unlock() defer a.mu.Unlock()
if token != a.cachedToken { // Since we're not making a new pointer to a.cachedCreds in getCreds, no
// Token already rotated. // need to do deep comparison.
if creds != a.cachedCreds {
// Credentials already rotated.
return nil return nil
} }
_, err := a.getToken(r) return a.refreshCredsLocked(r)
return err
} }
// getToken executes the plugin and reads the credentials from stdout. It must be // refreshCredsLocked executes the plugin and reads the credentials from
// called while holding the Authenticator's mutex. // stdout. It must be called while holding the Authenticator's mutex.
func (a *Authenticator) getToken(r *clientauthentication.Response) (string, error) { func (a *Authenticator) refreshCredsLocked(r *clientauthentication.Response) error {
cred := &clientauthentication.ExecCredential{ cred := &clientauthentication.ExecCredential{
Spec: clientauthentication.ExecCredentialSpec{ Spec: clientauthentication.ExecCredentialSpec{
Response: r, Response: r,
...@@ -234,7 +296,7 @@ func (a *Authenticator) getToken(r *clientauthentication.Response) (string, erro ...@@ -234,7 +296,7 @@ func (a *Authenticator) getToken(r *clientauthentication.Response) (string, erro
data, err := runtime.Encode(codecs.LegacyCodec(a.group), cred) data, err := runtime.Encode(codecs.LegacyCodec(a.group), cred)
if err != nil { if err != nil {
return "", fmt.Errorf("encode ExecCredentials: %v", err) return fmt.Errorf("encode ExecCredentials: %v", err)
} }
env := append(a.environ(), a.env...) env := append(a.environ(), a.env...)
...@@ -250,23 +312,26 @@ func (a *Authenticator) getToken(r *clientauthentication.Response) (string, erro ...@@ -250,23 +312,26 @@ func (a *Authenticator) getToken(r *clientauthentication.Response) (string, erro
} }
if err := cmd.Run(); err != nil { if err := cmd.Run(); err != nil {
return "", fmt.Errorf("exec: %v", err) return fmt.Errorf("exec: %v", err)
} }
_, gvk, err := codecs.UniversalDecoder(a.group).Decode(stdout.Bytes(), nil, cred) _, gvk, err := codecs.UniversalDecoder(a.group).Decode(stdout.Bytes(), nil, cred)
if err != nil { if err != nil {
return "", fmt.Errorf("decode stdout: %v", err) return fmt.Errorf("decoding stdout: %v", err)
} }
if gvk.Group != a.group.Group || gvk.Version != a.group.Version { if gvk.Group != a.group.Group || gvk.Version != a.group.Version {
return "", fmt.Errorf("exec plugin is configured to use API version %s, plugin returned version %s", return fmt.Errorf("exec plugin is configured to use API version %s, plugin returned version %s",
a.group, schema.GroupVersion{Group: gvk.Group, Version: gvk.Version}) a.group, schema.GroupVersion{Group: gvk.Group, Version: gvk.Version})
} }
if cred.Status == nil { if cred.Status == nil {
return "", fmt.Errorf("exec plugin didn't return a status field") return fmt.Errorf("exec plugin didn't return a status field")
} }
if cred.Status.Token == "" { if cred.Status.Token == "" && cred.Status.ClientCertificateData == "" && cred.Status.ClientKeyData == "" {
return "", fmt.Errorf("exec plugin didn't return a token") return fmt.Errorf("exec plugin didn't return a token or cert/key pair")
}
if (cred.Status.ClientCertificateData == "") != (cred.Status.ClientKeyData == "") {
return fmt.Errorf("exec plugin returned only certificate or key, not both")
} }
if cred.Status.ExpirationTimestamp != nil { if cred.Status.ExpirationTimestamp != nil {
...@@ -274,7 +339,24 @@ func (a *Authenticator) getToken(r *clientauthentication.Response) (string, erro ...@@ -274,7 +339,24 @@ func (a *Authenticator) getToken(r *clientauthentication.Response) (string, erro
} else { } else {
a.exp = time.Time{} a.exp = time.Time{}
} }
a.cachedToken = cred.Status.Token
return a.cachedToken, nil newCreds := &credentials{
token: cred.Status.Token,
}
if cred.Status.ClientKeyData != "" && cred.Status.ClientCertificateData != "" {
cert, err := tls.X509KeyPair([]byte(cred.Status.ClientCertificateData), []byte(cred.Status.ClientKeyData))
if err != nil {
return fmt.Errorf("failed parsing client key/certificate: %v", err)
}
newCreds.cert = &cert
}
oldCreds := a.cachedCreds
a.cachedCreds = newCreds
// Only close all connections when TLS cert rotates. Token rotation doesn't
// need the extra noise.
if a.onRotate != nil && oldCreds != nil && !reflect.DeepEqual(oldCreds.cert, a.cachedCreds.cert) {
a.onRotate()
}
return nil
} }
#!/bin/sh -e #!/bin/bash -e
# Copyright 2018 The Kubernetes Authors. # Copyright 2018 The Kubernetes Authors.
# #
......
...@@ -59,39 +59,10 @@ func HTTPWrappersForConfig(config *Config, rt http.RoundTripper) (http.RoundTrip ...@@ -59,39 +59,10 @@ func HTTPWrappersForConfig(config *Config, rt http.RoundTripper) (http.RoundTrip
// TransportConfig converts a client config to an appropriate transport config. // TransportConfig converts a client config to an appropriate transport config.
func (c *Config) TransportConfig() (*transport.Config, error) { func (c *Config) TransportConfig() (*transport.Config, error) {
wt := c.WrapTransport conf := &transport.Config{
if c.ExecProvider != nil {
provider, err := exec.GetAuthenticator(c.ExecProvider)
if err != nil {
return nil, err
}
if wt != nil {
previousWT := wt
wt = func(rt http.RoundTripper) http.RoundTripper {
return provider.WrapTransport(previousWT(rt))
}
} else {
wt = provider.WrapTransport
}
}
if c.AuthProvider != nil {
provider, err := GetAuthProvider(c.Host, c.AuthProvider, c.AuthConfigPersister)
if err != nil {
return nil, err
}
if wt != nil {
previousWT := wt
wt = func(rt http.RoundTripper) http.RoundTripper {
return provider.WrapTransport(previousWT(rt))
}
} else {
wt = provider.WrapTransport
}
}
return &transport.Config{
UserAgent: c.UserAgent, UserAgent: c.UserAgent,
Transport: c.Transport, Transport: c.Transport,
WrapTransport: wt, WrapTransport: c.WrapTransport,
TLS: transport.TLSConfig{ TLS: transport.TLSConfig{
Insecure: c.Insecure, Insecure: c.Insecure,
ServerName: c.ServerName, ServerName: c.ServerName,
...@@ -111,5 +82,29 @@ func (c *Config) TransportConfig() (*transport.Config, error) { ...@@ -111,5 +82,29 @@ func (c *Config) TransportConfig() (*transport.Config, error) {
Extra: c.Impersonate.Extra, Extra: c.Impersonate.Extra,
}, },
Dial: c.Dial, Dial: c.Dial,
}, nil }
if c.ExecProvider != nil {
provider, err := exec.GetAuthenticator(c.ExecProvider)
if err != nil {
return nil, err
}
if err := provider.UpdateTransportConfig(conf); err != nil {
return nil, err
}
}
if c.AuthProvider != nil {
provider, err := GetAuthProvider(c.Host, c.AuthProvider, c.AuthConfigPersister)
if err != nil {
return nil, err
}
wt := conf.WrapTransport
if wt != nil {
conf.WrapTransport = func(rt http.RoundTripper) http.RoundTripper {
return provider.WrapTransport(wt(rt))
}
} else {
conf.WrapTransport = provider.WrapTransport
}
}
return conf, nil
} }
...@@ -43,6 +43,7 @@ type tlsCacheKey struct { ...@@ -43,6 +43,7 @@ type tlsCacheKey struct {
caData string caData string
certData string certData string
keyData string keyData string
getCert string
serverName string serverName string
dial string dial string
} }
...@@ -52,7 +53,7 @@ func (t tlsCacheKey) String() string { ...@@ -52,7 +53,7 @@ func (t tlsCacheKey) String() string {
if len(t.keyData) > 0 { if len(t.keyData) > 0 {
keyText = "<redacted>" keyText = "<redacted>"
} }
return fmt.Sprintf("insecure:%v, caData:%#v, certData:%#v, keyData:%s, serverName:%s, dial:%s", t.insecure, t.caData, t.certData, keyText, t.serverName, t.dial) return fmt.Sprintf("insecure:%v, caData:%#v, certData:%#v, keyData:%s, getCert: %s, serverName:%s, dial:%s", t.insecure, t.caData, t.certData, keyText, t.getCert, t.serverName, t.dial)
} }
func (c *tlsTransportCache) get(config *Config) (http.RoundTripper, error) { func (c *tlsTransportCache) get(config *Config) (http.RoundTripper, error) {
...@@ -109,6 +110,7 @@ func tlsConfigKey(c *Config) (tlsCacheKey, error) { ...@@ -109,6 +110,7 @@ func tlsConfigKey(c *Config) (tlsCacheKey, error) {
caData: string(c.TLS.CAData), caData: string(c.TLS.CAData),
certData: string(c.TLS.CertData), certData: string(c.TLS.CertData),
keyData: string(c.TLS.KeyData), keyData: string(c.TLS.KeyData),
getCert: fmt.Sprintf("%p", c.TLS.GetCert),
serverName: c.TLS.ServerName, serverName: c.TLS.ServerName,
dial: fmt.Sprintf("%p", c.Dial), dial: fmt.Sprintf("%p", c.Dial),
}, nil }, nil
......
...@@ -18,6 +18,7 @@ package transport ...@@ -18,6 +18,7 @@ package transport
import ( import (
"context" "context"
"crypto/tls"
"net" "net"
"net/http" "net/http"
"testing" "testing"
...@@ -54,6 +55,7 @@ func TestTLSConfigKey(t *testing.T) { ...@@ -54,6 +55,7 @@ func TestTLSConfigKey(t *testing.T) {
// Make sure config fields that affect the tls config affect the cache key // Make sure config fields that affect the tls config affect the cache key
dialer := net.Dialer{} dialer := net.Dialer{}
getCert := func() (*tls.Certificate, error) { return nil, nil }
uniqueConfigurations := map[string]*Config{ uniqueConfigurations := map[string]*Config{
"no tls": {}, "no tls": {},
"dialer": {Dial: dialer.DialContext}, "dialer": {Dial: dialer.DialContext},
...@@ -106,6 +108,24 @@ func TestTLSConfigKey(t *testing.T) { ...@@ -106,6 +108,24 @@ func TestTLSConfigKey(t *testing.T) {
KeyData: []byte{1}, KeyData: []byte{1},
}, },
}, },
"getCert1": {
TLS: TLSConfig{
KeyData: []byte{1},
GetCert: getCert,
},
},
"getCert2": {
TLS: TLSConfig{
KeyData: []byte{1},
GetCert: func() (*tls.Certificate, error) { return nil, nil },
},
},
"getCert1, key 2": {
TLS: TLSConfig{
KeyData: []byte{2},
GetCert: getCert,
},
},
} }
for nameA, valueA := range uniqueConfigurations { for nameA, valueA := range uniqueConfigurations {
for nameB, valueB := range uniqueConfigurations { for nameB, valueB := range uniqueConfigurations {
......
...@@ -18,6 +18,7 @@ package transport ...@@ -18,6 +18,7 @@ package transport
import ( import (
"context" "context"
"crypto/tls"
"net" "net"
"net/http" "net/http"
) )
...@@ -84,7 +85,12 @@ func (c *Config) HasTokenAuth() bool { ...@@ -84,7 +85,12 @@ func (c *Config) HasTokenAuth() bool {
// HasCertAuth returns whether the configuration has certificate authentication or not. // HasCertAuth returns whether the configuration has certificate authentication or not.
func (c *Config) HasCertAuth() bool { func (c *Config) HasCertAuth() bool {
return len(c.TLS.CertData) != 0 || len(c.TLS.CertFile) != 0 return (len(c.TLS.CertData) != 0 || len(c.TLS.CertFile) != 0) && (len(c.TLS.KeyData) != 0 || len(c.TLS.KeyFile) != 0)
}
// HasCertCallbacks returns whether the configuration has certificate callback or not.
func (c *Config) HasCertCallback() bool {
return c.TLS.GetCert != nil
} }
// TLSConfig holds the information needed to set up a TLS transport. // TLSConfig holds the information needed to set up a TLS transport.
...@@ -99,4 +105,6 @@ type TLSConfig struct { ...@@ -99,4 +105,6 @@ type TLSConfig struct {
CAData []byte // Bytes of the PEM-encoded server trusted root certificates. Supercedes CAFile. CAData []byte // Bytes of the PEM-encoded server trusted root certificates. Supercedes CAFile.
CertData []byte // Bytes of the PEM-encoded client certificate. Supercedes CertFile. CertData []byte // Bytes of the PEM-encoded client certificate. Supercedes CertFile.
KeyData []byte // Bytes of the PEM-encoded client key. Supercedes KeyFile. KeyData []byte // Bytes of the PEM-encoded client key. Supercedes KeyFile.
GetCert func() (*tls.Certificate, error) // Callback that returns a TLS client certificate. CertData, CertFile, KeyData and KeyFile supercede this field.
} }
...@@ -28,7 +28,7 @@ import ( ...@@ -28,7 +28,7 @@ import (
// or transport level security defined by the provided Config. // or transport level security defined by the provided Config.
func New(config *Config) (http.RoundTripper, error) { func New(config *Config) (http.RoundTripper, error) {
// Set transport level security // Set transport level security
if config.Transport != nil && (config.HasCA() || config.HasCertAuth() || config.TLS.Insecure) { if config.Transport != nil && (config.HasCA() || config.HasCertAuth() || config.HasCertCallback() || config.TLS.Insecure) {
return nil, fmt.Errorf("using a custom transport with TLS certificate options or the insecure flag is not allowed") return nil, fmt.Errorf("using a custom transport with TLS certificate options or the insecure flag is not allowed")
} }
...@@ -52,7 +52,7 @@ func New(config *Config) (http.RoundTripper, error) { ...@@ -52,7 +52,7 @@ func New(config *Config) (http.RoundTripper, error) {
// TLSConfigFor returns a tls.Config that will provide the transport level security defined // TLSConfigFor returns a tls.Config that will provide the transport level security defined
// by the provided Config. Will return nil if no transport level security is requested. // by the provided Config. Will return nil if no transport level security is requested.
func TLSConfigFor(c *Config) (*tls.Config, error) { func TLSConfigFor(c *Config) (*tls.Config, error) {
if !(c.HasCA() || c.HasCertAuth() || c.TLS.Insecure || len(c.TLS.ServerName) > 0) { if !(c.HasCA() || c.HasCertAuth() || c.HasCertCallback() || c.TLS.Insecure || len(c.TLS.ServerName) > 0) {
return nil, nil return nil, nil
} }
if c.HasCA() && c.TLS.Insecure { if c.HasCA() && c.TLS.Insecure {
...@@ -75,12 +75,40 @@ func TLSConfigFor(c *Config) (*tls.Config, error) { ...@@ -75,12 +75,40 @@ func TLSConfigFor(c *Config) (*tls.Config, error) {
tlsConfig.RootCAs = rootCertPool(c.TLS.CAData) tlsConfig.RootCAs = rootCertPool(c.TLS.CAData)
} }
var staticCert *tls.Certificate
if c.HasCertAuth() { if c.HasCertAuth() {
// If key/cert were provided, verify them before setting up
// tlsConfig.GetClientCertificate.
cert, err := tls.X509KeyPair(c.TLS.CertData, c.TLS.KeyData) cert, err := tls.X509KeyPair(c.TLS.CertData, c.TLS.KeyData)
if err != nil { if err != nil {
return nil, err return nil, err
} }
tlsConfig.Certificates = []tls.Certificate{cert} staticCert = &cert
}
if c.HasCertAuth() || c.HasCertCallback() {
tlsConfig.GetClientCertificate = func(*tls.CertificateRequestInfo) (*tls.Certificate, error) {
// Note: static key/cert data always take precedence over cert
// callback.
if staticCert != nil {
return staticCert, nil
}
if c.HasCertCallback() {
cert, err := c.TLS.GetCert()
if err != nil {
return nil, err
}
// GetCert may return empty value, meaning no cert.
if cert != nil {
return cert, nil
}
}
// Both c.TLS.CertData/KeyData were unset and GetCert didn't return
// anything. Return an empty tls.Certificate, no client cert will
// be sent to the server.
return &tls.Certificate{}, nil
}
} }
return tlsConfig, nil return tlsConfig, nil
......
...@@ -17,6 +17,8 @@ limitations under the License. ...@@ -17,6 +17,8 @@ limitations under the License.
package transport package transport
import ( import (
"crypto/tls"
"errors"
"net/http" "net/http"
"testing" "testing"
) )
...@@ -94,6 +96,8 @@ func TestNew(t *testing.T) { ...@@ -94,6 +96,8 @@ func TestNew(t *testing.T) {
Config *Config Config *Config
Err bool Err bool
TLS bool TLS bool
TLSCert bool
TLSErr bool
Default bool Default bool
}{ }{
"default transport": { "default transport": {
...@@ -135,7 +139,8 @@ func TestNew(t *testing.T) { ...@@ -135,7 +139,8 @@ func TestNew(t *testing.T) {
}, },
"cert transport": { "cert transport": {
TLS: true, TLS: true,
TLSCert: true,
Config: &Config{ Config: &Config{
TLS: TLSConfig{ TLS: TLSConfig{
CAData: []byte(rootCACert), CAData: []byte(rootCACert),
...@@ -165,7 +170,8 @@ func TestNew(t *testing.T) { ...@@ -165,7 +170,8 @@ func TestNew(t *testing.T) {
}, },
}, },
"key data overriding bad file cert transport": { "key data overriding bad file cert transport": {
TLS: true, TLS: true,
TLSCert: true,
Config: &Config{ Config: &Config{
TLS: TLSConfig{ TLS: TLSConfig{
CAData: []byte(rootCACert), CAData: []byte(rootCACert),
...@@ -175,37 +181,108 @@ func TestNew(t *testing.T) { ...@@ -175,37 +181,108 @@ func TestNew(t *testing.T) {
}, },
}, },
}, },
"callback cert and key": {
TLS: true,
TLSCert: true,
Config: &Config{
TLS: TLSConfig{
CAData: []byte(rootCACert),
GetCert: func() (*tls.Certificate, error) {
crt, err := tls.X509KeyPair([]byte(certData), []byte(keyData))
return &crt, err
},
},
},
},
"cert callback error": {
TLS: true,
TLSCert: true,
TLSErr: true,
Config: &Config{
TLS: TLSConfig{
CAData: []byte(rootCACert),
GetCert: func() (*tls.Certificate, error) {
return nil, errors.New("GetCert failure")
},
},
},
},
"cert data overrides empty callback result": {
TLS: true,
TLSCert: true,
Config: &Config{
TLS: TLSConfig{
CAData: []byte(rootCACert),
GetCert: func() (*tls.Certificate, error) {
return nil, nil
},
CertData: []byte(certData),
KeyData: []byte(keyData),
},
},
},
"callback returns nothing": {
TLS: true,
TLSCert: true,
Config: &Config{
TLS: TLSConfig{
CAData: []byte(rootCACert),
GetCert: func() (*tls.Certificate, error) {
return nil, nil
},
},
},
},
} }
for k, testCase := range testCases { for k, testCase := range testCases {
transport, err := New(testCase.Config) t.Run(k, func(t *testing.T) {
switch { rt, err := New(testCase.Config)
case testCase.Err && err == nil: switch {
t.Errorf("%s: unexpected non-error", k) case testCase.Err && err == nil:
continue t.Fatal("unexpected non-error")
case !testCase.Err && err != nil: case !testCase.Err && err != nil:
t.Errorf("%s: unexpected error: %v", k, err) t.Fatalf("unexpected error: %v", err)
continue }
} if testCase.Err {
return
switch { }
case testCase.Default && transport != http.DefaultTransport:
t.Errorf("%s: expected the default transport, got %#v", k, transport) switch {
continue case testCase.Default && rt != http.DefaultTransport:
case !testCase.Default && transport == http.DefaultTransport: t.Fatalf("got %#v, expected the default transport", rt)
t.Errorf("%s: expected non-default transport, got %#v", k, transport) case !testCase.Default && rt == http.DefaultTransport:
continue t.Fatalf("got %#v, expected non-default transport", rt)
} }
// We only know how to check TLSConfig on http.Transports // We only know how to check TLSConfig on http.Transports
if transport, ok := transport.(*http.Transport); ok { transport := rt.(*http.Transport)
switch { switch {
case testCase.TLS && transport.TLSClientConfig == nil: case testCase.TLS && transport.TLSClientConfig == nil:
t.Errorf("%s: expected TLSClientConfig, got %#v", k, transport) t.Fatalf("got %#v, expected TLSClientConfig", transport)
continue
case !testCase.TLS && transport.TLSClientConfig != nil: case !testCase.TLS && transport.TLSClientConfig != nil:
t.Errorf("%s: expected no TLSClientConfig, got %#v", k, transport) t.Fatalf("got %#v, expected no TLSClientConfig", transport)
continue }
if !testCase.TLS {
return
}
switch {
case testCase.TLSCert && transport.TLSClientConfig.GetClientCertificate == nil:
t.Fatalf("got %#v, expected TLSClientConfig.GetClientCertificate", transport.TLSClientConfig)
case !testCase.TLSCert && transport.TLSClientConfig.GetClientCertificate != nil:
t.Fatalf("got %#v, expected no TLSClientConfig.GetClientCertificate", transport.TLSClientConfig)
}
if !testCase.TLSCert {
return
}
_, err = transport.TLSClientConfig.GetClientCertificate(nil)
switch {
case testCase.TLSErr && err == nil:
t.Error("got nil error from GetClientCertificate, expected non-nil")
case !testCase.TLSErr && err != nil:
t.Errorf("got error from GetClientCertificate: %q, expected nil", err)
} }
} })
} }
} }
...@@ -1655,6 +1655,10 @@ ...@@ -1655,6 +1655,10 @@
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}, },
{ {
"ImportPath": "k8s.io/client-go/util/connrotation",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
{
"ImportPath": "k8s.io/client-go/util/flowcontrol", "ImportPath": "k8s.io/client-go/util/flowcontrol",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}, },
......
...@@ -475,6 +475,10 @@ ...@@ -475,6 +475,10 @@
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}, },
{ {
"ImportPath": "k8s.io/client-go/util/connrotation",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
{
"ImportPath": "k8s.io/client-go/util/flowcontrol", "ImportPath": "k8s.io/client-go/util/flowcontrol",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}, },
......
...@@ -1623,6 +1623,10 @@ ...@@ -1623,6 +1623,10 @@
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}, },
{ {
"ImportPath": "k8s.io/client-go/util/connrotation",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
{
"ImportPath": "k8s.io/client-go/util/flowcontrol", "ImportPath": "k8s.io/client-go/util/flowcontrol",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}, },
......
...@@ -1047,6 +1047,10 @@ ...@@ -1047,6 +1047,10 @@
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}, },
{ {
"ImportPath": "k8s.io/client-go/util/connrotation",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
{
"ImportPath": "k8s.io/client-go/util/flowcontrol", "ImportPath": "k8s.io/client-go/util/flowcontrol",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}, },
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment