Skip to content

Commit 01449fd

Browse files
authored
Fix server startup failure when client-ca is empty or not provided (#3742)
1 parent afb9e80 commit 01449fd

2 files changed

Lines changed: 143 additions & 12 deletions

File tree

pkg/lib/server/server.go

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package server
33
import (
44
"context"
55
"crypto/tls"
6+
"crypto/x509"
67
"fmt"
78
"net/http"
89
"path/filepath"
@@ -16,6 +17,11 @@ import (
1617
"sigs.k8s.io/controller-runtime/pkg/metrics/filters"
1718
)
1819

20+
// certPoolGetter is an interface for getting a certificate pool
21+
type certPoolGetter interface {
22+
GetCertPool() *x509.CertPool
23+
}
24+
1925
// Option applies a configuration option to the given config.
2026
type Option func(s *serverConfig)
2127

@@ -94,6 +100,10 @@ func (sc *serverConfig) getAddress(tlsEnabled bool) string {
94100
return ":8080"
95101
}
96102

103+
func (sc *serverConfig) clientCAEnabled() bool {
104+
return sc.clientCAPath != nil && *sc.clientCAPath != ""
105+
}
106+
97107
func (sc serverConfig) getListenAndServeFunc() (func() error, error) {
98108
tlsEnabled, err := sc.tlsEnabled()
99109
if err != nil {
@@ -168,15 +178,23 @@ func (sc serverConfig) getListenAndServeFunc() (func() error, error) {
168178
return nil, fmt.Errorf("error creating cert file watcher: %v", err)
169179
}
170180
csw.Run(context.Background())
171-
certPoolStore, err := filemonitor.NewCertPoolStore(*sc.clientCAPath)
172-
if err != nil {
173-
return nil, fmt.Errorf("certificate monitoring for client-ca failed: %v", err)
174-
}
175-
cpsw, err := filemonitor.NewWatch(sc.logger, []string{filepath.Dir(*sc.clientCAPath)}, certPoolStore.HandleCABundleUpdate)
176-
if err != nil {
177-
return nil, fmt.Errorf("error creating cert file watcher: %v", err)
181+
182+
// Only setup client CA monitoring if clientCAPath is provided
183+
var certPoolStore certPoolGetter
184+
if sc.clientCAEnabled() {
185+
cps, err := filemonitor.NewCertPoolStore(*sc.clientCAPath)
186+
if err != nil {
187+
return nil, fmt.Errorf("certificate monitoring for client-ca failed: %v", err)
188+
}
189+
cpsw, err := filemonitor.NewWatch(sc.logger, []string{filepath.Dir(*sc.clientCAPath)}, cps.HandleCABundleUpdate)
190+
if err != nil {
191+
return nil, fmt.Errorf("error creating cert file watcher: %v", err)
192+
}
193+
cpsw.Run(context.Background())
194+
certPoolStore = cps
195+
} else {
196+
sc.logger.Info("No client CA provided, client certificate verification disabled")
178197
}
179-
cpsw.Run(context.Background())
180198

181199
s.TLSConfig = &tls.Config{
182200
GetCertificate: func(_ *tls.ClientHelloInfo) (*tls.Certificate, error) {
@@ -187,11 +205,15 @@ func (sc serverConfig) getListenAndServeFunc() (func() error, error) {
187205
if cert := certStore.GetCertificate(); cert != nil {
188206
certs = append(certs, *cert)
189207
}
190-
return &tls.Config{
208+
tlsCfg := &tls.Config{
191209
Certificates: certs,
192-
ClientCAs: certPoolStore.GetCertPool(),
193-
ClientAuth: tls.VerifyClientCertIfGiven,
194-
}, nil
210+
}
211+
// Only configure client CA verification if certPoolStore is available
212+
if certPoolStore != nil {
213+
tlsCfg.ClientCAs = certPoolStore.GetCertPool()
214+
tlsCfg.ClientAuth = tls.VerifyClientCertIfGiven
215+
}
216+
return tlsCfg, nil
195217
},
196218
NextProtos: []string{"http/1.1"}, // Disable HTTP/2 for security
197219
}

pkg/lib/server/server_test.go

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,115 @@ func TestGetListenAndServeFunc_WithoutKubeConfig(t *testing.T) {
111111
assert.NoError(t, err, "GetListenAndServeFunc should succeed without kubeConfig")
112112
}
113113

114+
// TestGetListenAndServeFunc_WithEmptyClientCA tests that the server
115+
// starts successfully when TLS is enabled but client-ca is empty
116+
func TestGetListenAndServeFunc_WithEmptyClientCA(t *testing.T) {
117+
// Generate test certificates dynamically
118+
caCert, caKey, err := generateCA()
119+
require.NoError(t, err)
120+
121+
serverCert, serverKey, err := generateServerCert(caCert, caKey, "localhost")
122+
require.NoError(t, err)
123+
124+
tmpDir, err := os.MkdirTemp("", "server-test-*")
125+
require.NoError(t, err)
126+
defer os.RemoveAll(tmpDir)
127+
128+
tlsCertPath := filepath.Join(tmpDir, "tls.crt")
129+
tlsKeyPath := filepath.Join(tmpDir, "tls.key")
130+
emptyClientCAPath := "" // Empty client CA path
131+
132+
err = os.WriteFile(tlsCertPath, serverCert, 0644)
133+
require.NoError(t, err)
134+
err = os.WriteFile(tlsKeyPath, serverKey, 0600)
135+
require.NoError(t, err)
136+
137+
logger := logrus.New()
138+
logger.SetOutput(io.Discard)
139+
140+
// Test with TLS enabled but empty client CA - should succeed
141+
_, err = GetListenAndServeFunc(
142+
WithLogger(logger),
143+
WithTLS(&tlsCertPath, &tlsKeyPath, &emptyClientCAPath),
144+
WithDebug(false),
145+
)
146+
147+
assert.NoError(t, err, "GetListenAndServeFunc should succeed with empty client-ca")
148+
}
149+
150+
// TestGetListenAndServeFunc_WithNilClientCA tests that the server
151+
// starts successfully when TLS is enabled but client-ca pointer is nil
152+
func TestGetListenAndServeFunc_WithNilClientCA(t *testing.T) {
153+
// Generate test certificates dynamically
154+
caCert, caKey, err := generateCA()
155+
require.NoError(t, err)
156+
157+
serverCert, serverKey, err := generateServerCert(caCert, caKey, "localhost")
158+
require.NoError(t, err)
159+
160+
tmpDir, err := os.MkdirTemp("", "server-test-*")
161+
require.NoError(t, err)
162+
defer os.RemoveAll(tmpDir)
163+
164+
tlsCertPath := filepath.Join(tmpDir, "tls.crt")
165+
tlsKeyPath := filepath.Join(tmpDir, "tls.key")
166+
167+
err = os.WriteFile(tlsCertPath, serverCert, 0644)
168+
require.NoError(t, err)
169+
err = os.WriteFile(tlsKeyPath, serverKey, 0600)
170+
require.NoError(t, err)
171+
172+
logger := logrus.New()
173+
logger.SetOutput(io.Discard)
174+
175+
// Test with TLS enabled but nil client CA pointer - should succeed
176+
_, err = GetListenAndServeFunc(
177+
WithLogger(logger),
178+
WithTLS(&tlsCertPath, &tlsKeyPath, nil),
179+
WithDebug(false),
180+
)
181+
182+
assert.NoError(t, err, "GetListenAndServeFunc should succeed with nil client-ca pointer")
183+
}
184+
185+
// TestClientCAEnabled tests the clientCAEnabled helper function
186+
func TestClientCAEnabled(t *testing.T) {
187+
tests := []struct {
188+
name string
189+
clientCAPath *string
190+
expected bool
191+
}{
192+
{
193+
name: "nil pointer",
194+
clientCAPath: nil,
195+
expected: false,
196+
},
197+
{
198+
name: "empty string",
199+
clientCAPath: strPtr(""),
200+
expected: false,
201+
},
202+
{
203+
name: "valid path",
204+
clientCAPath: strPtr("/path/to/ca.crt"),
205+
expected: true,
206+
},
207+
}
208+
209+
for _, tt := range tests {
210+
t.Run(tt.name, func(t *testing.T) {
211+
sc := &serverConfig{
212+
clientCAPath: tt.clientCAPath,
213+
}
214+
assert.Equal(t, tt.expected, sc.clientCAEnabled(), "clientCAEnabled result should match expected")
215+
})
216+
}
217+
}
218+
219+
func strPtr(s string) *string {
220+
return &s
221+
}
222+
114223
// TestHTTPClientHasTLSConfig verifies that rest.HTTPClientFor creates a client
115224
// with proper TLS configuration including CA certificates
116225
func TestHTTPClientHasTLSConfig(t *testing.T) {

0 commit comments

Comments
 (0)