Skip to content

Commit 1874bc6

Browse files
enjk8s-publishing-bot
authored andcommitted
exec auth: support TLS config caching
This change updates the transport.Config .Dial and .TLS.GetCert fields to use a struct wrapper. This indirection via a pointer allows the functions to be compared and thus makes them valid to use as map keys. This change is then leveraged by the existing global exec auth and TLS config caches to return the same authenticator and TLS config even when distinct but identical rest configs were used to create distinct clientsets. Signed-off-by: Monis Khan <mok@microsoft.com> Kubernetes-commit: 01044903860c7e2463888c48db91270dc9c7281d
1 parent db7e2d8 commit 1874bc6

File tree

7 files changed

+354
-15
lines changed

7 files changed

+354
-15
lines changed

plugin/pkg/client/auth/exec/exec.go

+20-10
Original file line numberDiff line numberDiff line change
@@ -199,14 +199,18 @@ func newAuthenticator(c *cache, isTerminalFunc func(int) bool, config *api.ExecC
199199
now: time.Now,
200200
environ: os.Environ,
201201

202-
defaultDialer: defaultDialer,
203-
connTracker: connTracker,
202+
connTracker: connTracker,
204203
}
205204

206205
for _, env := range config.Env {
207206
a.env = append(a.env, env.Name+"="+env.Value)
208207
}
209208

209+
// these functions are made comparable and stored in the cache so that repeated clientset
210+
// construction with the same rest.Config results in a single TLS cache and Authenticator
211+
a.getCert = &transport.GetCertHolder{GetCert: a.cert}
212+
a.dial = &transport.DialHolder{Dial: defaultDialer.DialContext}
213+
210214
return c.put(key, a), nil
211215
}
212216

@@ -261,8 +265,6 @@ type Authenticator struct {
261265
now func() time.Time
262266
environ func() []string
263267

264-
// defaultDialer is used for clients which don't specify a custom dialer
265-
defaultDialer *connrotation.Dialer
266268
// connTracker tracks all connections opened that we need to close when rotating a client certificate
267269
connTracker *connrotation.ConnectionTracker
268270

@@ -273,6 +275,12 @@ type Authenticator struct {
273275
mu sync.Mutex
274276
cachedCreds *credentials
275277
exp time.Time
278+
279+
// getCert makes Authenticator.cert comparable to support TLS config caching
280+
getCert *transport.GetCertHolder
281+
// dial is used for clients which do not specify a custom dialer
282+
// it is comparable to support TLS config caching
283+
dial *transport.DialHolder
276284
}
277285

278286
type credentials struct {
@@ -300,18 +308,20 @@ func (a *Authenticator) UpdateTransportConfig(c *transport.Config) error {
300308
if c.HasCertCallback() {
301309
return errors.New("can't add TLS certificate callback: transport.Config.TLS.GetCert already set")
302310
}
303-
c.TLS.GetCert = a.cert
311+
c.TLS.GetCert = a.getCert.GetCert
312+
c.TLS.GetCertHolder = a.getCert // comparable for TLS config caching
304313

305-
var d *connrotation.Dialer
306314
if c.Dial != nil {
307315
// if c has a custom dialer, we have to wrap it
308-
d = connrotation.NewDialerWithTracker(c.Dial, a.connTracker)
316+
// TLS config caching is not supported for this config
317+
d := connrotation.NewDialerWithTracker(c.Dial, a.connTracker)
318+
c.Dial = d.DialContext
319+
c.DialHolder = nil
309320
} else {
310-
d = a.defaultDialer
321+
c.Dial = a.dial.Dial
322+
c.DialHolder = a.dial // comparable for TLS config caching
311323
}
312324

313-
c.Dial = d.DialContext
314-
315325
return nil
316326
}
317327

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
/*
2+
Copyright 2022 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package exec_test // separate package to prevent circular import
18+
19+
import (
20+
"context"
21+
"testing"
22+
"time"
23+
24+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
25+
utilnet "k8s.io/apimachinery/pkg/util/net"
26+
clientset "k8s.io/client-go/kubernetes"
27+
"k8s.io/client-go/rest"
28+
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
29+
)
30+
31+
// TestExecTLSCache asserts the semantics of the TLS cache when exec auth is used.
32+
//
33+
// In particular, when:
34+
// - multiple identical rest configs exist as distinct objects, and
35+
// - these rest configs use exec auth, and
36+
// - these rest configs are used to create distinct clientsets, then
37+
//
38+
// the underlying TLS config is shared between those clientsets.
39+
func TestExecTLSCache(t *testing.T) {
40+
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
41+
t.Cleanup(cancel)
42+
43+
config1 := &rest.Config{
44+
Host: "https://localhost",
45+
ExecProvider: &clientcmdapi.ExecConfig{
46+
Command: "./testdata/test-plugin.sh",
47+
APIVersion: "client.authentication.k8s.io/v1",
48+
InteractiveMode: clientcmdapi.IfAvailableExecInteractiveMode,
49+
},
50+
}
51+
client1 := clientset.NewForConfigOrDie(config1)
52+
53+
config2 := &rest.Config{
54+
Host: "https://localhost",
55+
ExecProvider: &clientcmdapi.ExecConfig{
56+
Command: "./testdata/test-plugin.sh",
57+
APIVersion: "client.authentication.k8s.io/v1",
58+
InteractiveMode: clientcmdapi.IfAvailableExecInteractiveMode,
59+
},
60+
}
61+
client2 := clientset.NewForConfigOrDie(config2)
62+
63+
config3 := &rest.Config{
64+
Host: "https://localhost",
65+
ExecProvider: &clientcmdapi.ExecConfig{
66+
Command: "./testdata/test-plugin.sh",
67+
Args: []string{"make this exec auth different"},
68+
APIVersion: "client.authentication.k8s.io/v1",
69+
InteractiveMode: clientcmdapi.IfAvailableExecInteractiveMode,
70+
},
71+
}
72+
client3 := clientset.NewForConfigOrDie(config3)
73+
74+
_, _ = client1.CoreV1().Nodes().List(ctx, metav1.ListOptions{})
75+
_, _ = client2.CoreV1().Namespaces().List(ctx, metav1.ListOptions{})
76+
_, _ = client3.CoreV1().PersistentVolumes().List(ctx, metav1.ListOptions{})
77+
78+
rt1 := client1.RESTClient().(*rest.RESTClient).Client.Transport
79+
rt2 := client2.RESTClient().(*rest.RESTClient).Client.Transport
80+
rt3 := client3.RESTClient().(*rest.RESTClient).Client.Transport
81+
82+
tlsConfig1, err := utilnet.TLSClientConfig(rt1)
83+
if err != nil {
84+
t.Fatal(err)
85+
}
86+
tlsConfig2, err := utilnet.TLSClientConfig(rt2)
87+
if err != nil {
88+
t.Fatal(err)
89+
}
90+
tlsConfig3, err := utilnet.TLSClientConfig(rt3)
91+
if err != nil {
92+
t.Fatal(err)
93+
}
94+
95+
if tlsConfig1 == nil || tlsConfig2 == nil || tlsConfig3 == nil {
96+
t.Fatal("expected non-nil TLS configs")
97+
}
98+
99+
if tlsConfig1 != tlsConfig2 {
100+
t.Fatal("expected the same TLS config for matching exec config via rest config")
101+
}
102+
103+
if tlsConfig1 == tlsConfig3 {
104+
t.Fatal("expected different TLS config for non-matching exec config via rest config")
105+
}
106+
}

transport/cache.go

+21-4
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ limitations under the License.
1717
package transport
1818

1919
import (
20+
"context"
2021
"fmt"
2122
"net"
2223
"net/http"
@@ -55,14 +56,18 @@ type tlsCacheKey struct {
5556
serverName string
5657
nextProtos string
5758
disableCompression bool
59+
// these functions are wrapped to allow them to be used as map keys
60+
getCert *GetCertHolder
61+
dial *DialHolder
5862
}
5963

6064
func (t tlsCacheKey) String() string {
6165
keyText := "<none>"
6266
if len(t.keyData) > 0 {
6367
keyText = "<redacted>"
6468
}
65-
return fmt.Sprintf("insecure:%v, caData:%#v, certData:%#v, keyData:%s, serverName:%s, disableCompression:%t", t.insecure, t.caData, t.certData, keyText, t.serverName, t.disableCompression)
69+
return fmt.Sprintf("insecure:%v, caData:%#v, certData:%#v, keyData:%s, serverName:%s, disableCompression:%t, getCert:%p, dial:%p",
70+
t.insecure, t.caData, t.certData, keyText, t.serverName, t.disableCompression, t.getCert, t.dial)
6671
}
6772

6873
func (c *tlsTransportCache) get(config *Config) (http.RoundTripper, error) {
@@ -92,8 +97,10 @@ func (c *tlsTransportCache) get(config *Config) (http.RoundTripper, error) {
9297
return http.DefaultTransport, nil
9398
}
9499

95-
dial := config.Dial
96-
if dial == nil {
100+
var dial func(ctx context.Context, network, address string) (net.Conn, error)
101+
if config.Dial != nil {
102+
dial = config.Dial
103+
} else {
97104
dial = (&net.Dialer{
98105
Timeout: 30 * time.Second,
99106
KeepAlive: 30 * time.Second,
@@ -138,17 +145,27 @@ func tlsConfigKey(c *Config) (tlsCacheKey, bool, error) {
138145
return tlsCacheKey{}, false, err
139146
}
140147

141-
if c.TLS.GetCert != nil || c.Dial != nil || c.Proxy != nil {
148+
if c.Proxy != nil {
142149
// cannot determine equality for functions
143150
return tlsCacheKey{}, false, nil
144151
}
152+
if c.Dial != nil && c.DialHolder == nil {
153+
// cannot determine equality for dial function that doesn't have non-nil DialHolder set as well
154+
return tlsCacheKey{}, false, nil
155+
}
156+
if c.TLS.GetCert != nil && c.TLS.GetCertHolder == nil {
157+
// cannot determine equality for getCert function that doesn't have non-nil GetCertHolder set as well
158+
return tlsCacheKey{}, false, nil
159+
}
145160

146161
k := tlsCacheKey{
147162
insecure: c.TLS.Insecure,
148163
caData: string(c.TLS.CAData),
149164
serverName: c.TLS.ServerName,
150165
nextProtos: strings.Join(c.TLS.NextProtos, ","),
151166
disableCompression: c.DisableCompression,
167+
getCert: c.TLS.GetCertHolder,
168+
dial: c.DialHolder,
152169
}
153170

154171
if c.TLS.ReloadTLSFiles {

transport/cache_test.go

+16
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"crypto/tls"
2222
"net"
2323
"net/http"
24+
"net/url"
2425
"testing"
2526
)
2627

@@ -58,16 +59,24 @@ func TestTLSConfigKey(t *testing.T) {
5859
t.Errorf("Expected identical cache keys for %q and %q, got:\n\t%s\n\t%s", nameA, nameB, keyA, keyB)
5960
continue
6061
}
62+
if keyA != (tlsCacheKey{}) {
63+
t.Errorf("Expected empty cache keys for %q and %q, got:\n\t%s\n\t%s", nameA, nameB, keyA, keyB)
64+
continue
65+
}
6166
}
6267
}
6368

6469
// Make sure config fields that affect the tls config affect the cache key
6570
dialer := net.Dialer{}
6671
getCert := func() (*tls.Certificate, error) { return nil, nil }
72+
getCertHolder := &GetCertHolder{GetCert: getCert}
6773
uniqueConfigurations := map[string]*Config{
74+
"proxy": {Proxy: func(request *http.Request) (*url.URL, error) { return nil, nil }},
6875
"no tls": {},
6976
"dialer": {Dial: dialer.DialContext},
7077
"dialer2": {Dial: func(ctx context.Context, network, address string) (net.Conn, error) { return nil, nil }},
78+
"dialer3": {Dial: dialer.DialContext, DialHolder: &DialHolder{Dial: dialer.DialContext}},
79+
"dialer4": {Dial: func(ctx context.Context, network, address string) (net.Conn, error) { return nil, nil }, DialHolder: &DialHolder{Dial: func(ctx context.Context, network, address string) (net.Conn, error) { return nil, nil }}},
7180
"insecure": {TLS: TLSConfig{Insecure: true}},
7281
"cadata 1": {TLS: TLSConfig{CAData: []byte{1}}},
7382
"cadata 2": {TLS: TLSConfig{CAData: []byte{2}}},
@@ -128,6 +137,13 @@ func TestTLSConfigKey(t *testing.T) {
128137
GetCert: func() (*tls.Certificate, error) { return nil, nil },
129138
},
130139
},
140+
"getCert3": {
141+
TLS: TLSConfig{
142+
KeyData: []byte{1},
143+
GetCert: getCert,
144+
GetCertHolder: getCertHolder,
145+
},
146+
},
131147
"getCert1, key 2": {
132148
TLS: TLSConfig{
133149
KeyData: []byte{2},

transport/config.go

+20-1
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,11 @@ type Config struct {
6868
WrapTransport WrapperFunc
6969

7070
// Dial specifies the dial function for creating unencrypted TCP connections.
71+
// If specified, this transport will be non-cacheable unless DialHolder is also set.
7172
Dial func(ctx context.Context, network, address string) (net.Conn, error)
73+
// DialHolder can be populated to make transport configs cacheable.
74+
// If specified, DialHolder.Dial must be equal to Dial.
75+
DialHolder *DialHolder
7276

7377
// Proxy is the proxy func to be used for all requests made by this
7478
// transport. If Proxy is nil, http.ProxyFromEnvironment is used. If Proxy
@@ -78,6 +82,11 @@ type Config struct {
7882
Proxy func(*http.Request) (*url.URL, error)
7983
}
8084

85+
// DialHolder is used to make the wrapped function comparable so that it can be used as a map key.
86+
type DialHolder struct {
87+
Dial func(ctx context.Context, network, address string) (net.Conn, error)
88+
}
89+
8190
// ImpersonationConfig has all the available impersonation options
8291
type ImpersonationConfig struct {
8392
// UserName matches user.Info.GetName()
@@ -143,5 +152,15 @@ type TLSConfig struct {
143152
// To use only http/1.1, set to ["http/1.1"].
144153
NextProtos []string
145154

146-
GetCert func() (*tls.Certificate, error) // Callback that returns a TLS client certificate. CertData, CertFile, KeyData and KeyFile supercede this field.
155+
// Callback that returns a TLS client certificate. CertData, CertFile, KeyData and KeyFile supercede this field.
156+
// If specified, this transport is non-cacheable unless CertHolder is populated.
157+
GetCert func() (*tls.Certificate, error)
158+
// CertHolder can be populated to make transport configs that set GetCert cacheable.
159+
// If set, CertHolder.GetCert must be equal to GetCert.
160+
GetCertHolder *GetCertHolder
161+
}
162+
163+
// GetCertHolder is used to make the wrapped function comparable so that it can be used as a map key.
164+
type GetCertHolder struct {
165+
GetCert func() (*tls.Certificate, error)
147166
}

transport/transport.go

+25
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
"fmt"
2525
"io/ioutil"
2626
"net/http"
27+
"reflect"
2728
"sync"
2829
"time"
2930

@@ -39,6 +40,10 @@ func New(config *Config) (http.RoundTripper, error) {
3940
return nil, fmt.Errorf("using a custom transport with TLS certificate options or the insecure flag is not allowed")
4041
}
4142

43+
if !isValidHolders(config) {
44+
return nil, fmt.Errorf("misconfigured holder for dialer or cert callback")
45+
}
46+
4247
var (
4348
rt http.RoundTripper
4449
err error
@@ -56,6 +61,26 @@ func New(config *Config) (http.RoundTripper, error) {
5661
return HTTPWrappersForConfig(config, rt)
5762
}
5863

64+
func isValidHolders(config *Config) bool {
65+
if config.TLS.GetCertHolder != nil {
66+
if config.TLS.GetCertHolder.GetCert == nil ||
67+
config.TLS.GetCert == nil ||
68+
reflect.ValueOf(config.TLS.GetCertHolder.GetCert).Pointer() != reflect.ValueOf(config.TLS.GetCert).Pointer() {
69+
return false
70+
}
71+
}
72+
73+
if config.DialHolder != nil {
74+
if config.DialHolder.Dial == nil ||
75+
config.Dial == nil ||
76+
reflect.ValueOf(config.DialHolder.Dial).Pointer() != reflect.ValueOf(config.Dial).Pointer() {
77+
return false
78+
}
79+
}
80+
81+
return true
82+
}
83+
5984
// TLSConfigFor returns a tls.Config that will provide the transport level security defined
6085
// by the provided Config. Will return nil if no transport level security is requested.
6186
func TLSConfigFor(c *Config) (*tls.Config, error) {

0 commit comments

Comments
 (0)