Skip to content

Commit a99852f

Browse files
committed
feat: support dynamic TLS certificate reload on update without restart
1 parent 7e93350 commit a99852f

21 files changed

+2774
-51
lines changed

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ require (
77
github.com/f5devcentral/go-bigip v0.0.0-20250731061239-628be0470a84
88
github.com/f5devcentral/go-bigip/f5teem v0.0.0-20250731061239-628be0470a84
99
github.com/f5devcentral/mockhttpclient v0.0.0-20210630101009-cc12e8b81051
10+
github.com/fsnotify/fsnotify v1.4.9
1011
github.com/google/uuid v1.3.0
1112
github.com/miekg/dns v1.1.42
1213
github.com/onsi/ginkgo/v2 v2.19.1

pkg/controller/server.go

Lines changed: 112 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,13 @@ import (
99
"github.com/F5Networks/k8s-bigip-ctlr/v2/pkg/health"
1010
bigIPPrometheus "github.com/F5Networks/k8s-bigip-ctlr/v2/pkg/prometheus"
1111
log "github.com/F5Networks/k8s-bigip-ctlr/v2/pkg/vlogger"
12+
"github.com/fsnotify/fsnotify"
1213
"github.com/prometheus/client_golang/prometheus/promhttp"
1314
"net/http"
1415
"os"
16+
"path/filepath"
1517
"sync"
18+
"sync/atomic"
1619
"time"
1720
)
1821

@@ -28,50 +31,100 @@ type webHook struct {
2831

2932
func (ctlr *Controller) startWebhook() {
3033
webhookServerOnce.Do(func() {
34+
// Initial cert load
35+
cert, err := loadAndValidateTLSCertificate(certFile, keyFile)
36+
if err != nil {
37+
log.Errorf("[Webhook] TLS cert load failed: %v", err)
38+
return
39+
}
40+
41+
// This will be updated when cert changes
42+
var currentCert atomic.Value
43+
currentCert.Store(cert)
44+
45+
// Watch for changes
46+
go watchCertFiles(certFile, keyFile, func() {
47+
newCert, err := loadAndValidateTLSCertificate(certFile, keyFile)
48+
if err != nil {
49+
log.Errorf("[Webhook] Failed to reload webhook TLS cert: %v", err)
50+
return
51+
}
52+
currentCert.Store(newCert)
53+
log.Debugf("[Webhook] TLS cert reloaded")
54+
})
55+
56+
tlsCfg := &tls.Config{
57+
GetCertificate: func(*tls.ClientHelloInfo) (*tls.Certificate, error) {
58+
c := currentCert.Load().(tls.Certificate)
59+
return &c, nil
60+
},
61+
}
62+
3163
webhookMux := http.NewServeMux()
3264
webhookMux.HandleFunc("/mutate", ctlr.handleMutate)
3365
webhookMux.HandleFunc("/validate", ctlr.handleValidate)
3466
ctlr.webhookServer = webHook{
3567
Server: &http.Server{
36-
Addr: ctlr.agentParams.HttpsAddress,
37-
Handler: webhookMux,
68+
Addr: ctlr.agentParams.HttpsAddress,
69+
Handler: webhookMux,
70+
TLSConfig: tlsCfg,
3871
},
3972
address: ctlr.agentParams.HttpsAddress,
4073
}
4174
webhookShutdownCh := make(chan struct{})
4275

43-
// Check cert/key existence and validity before starting server
44-
if _, err := os.Stat(certFile); err != nil {
45-
log.Errorf("Webhook server failed as TLS certificate file not found: %s, error: %v", certFile, err)
46-
return
47-
}
48-
if _, err := os.Stat(keyFile); err != nil {
49-
log.Errorf("Webhook server failed as TLS key file not found: %s, error: %v", keyFile, err)
50-
return
51-
}
52-
if err := validateTLSCertificate(certFile, keyFile); err != nil {
53-
log.Errorf("Webhook server failed as Invalid TLS certificate or key: %v", err)
54-
return
55-
}
56-
5776
// Graceful shutdown goroutine
5877
go func() {
5978
<-webhookShutdownCh
6079
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
6180
defer cancel()
6281
if err := ctlr.webhookServer.GetWebhookServer().Shutdown(ctx); err != nil {
63-
log.Errorf("Webhook server graceful shutdown failed: %v", err)
82+
log.Errorf("[Webhook] server graceful shutdown failed: %v", err)
6483
} else {
65-
log.Infof("Webhook server gracefully stopped")
84+
log.Infof("[Webhook] server gracefully stopped")
6685
}
6786
}()
68-
log.Infof("Starting webhook server on :%s", ctlr.agentParams.HttpsAddress)
87+
log.Infof("[Webhook] starting webhook server on :%s", ctlr.agentParams.HttpsAddress)
6988
if err := ctlr.webhookServer.GetWebhookServer().ListenAndServeTLS(certFile, keyFile); err != nil && err != http.ErrServerClosed {
7089
log.Errorf("Webhook server failed: %v", err)
7190
}
7291
})
7392
}
7493

94+
// loadAndValidateTLSCertificate reads and validates the TLS certificate and key files.
95+
func loadAndValidateTLSCertificate(certPath, keyPath string) (tls.Certificate, error) {
96+
certPEM, err := os.ReadFile(certPath)
97+
if err != nil {
98+
return tls.Certificate{}, fmt.Errorf("could not read cert file: %w", err)
99+
}
100+
101+
_, err = os.ReadFile(keyPath)
102+
if err != nil {
103+
return tls.Certificate{}, fmt.Errorf("could not read key file: %w", err)
104+
}
105+
106+
block, _ := pem.Decode(certPEM)
107+
if block == nil {
108+
return tls.Certificate{}, fmt.Errorf("failed to parse certificate PEM")
109+
}
110+
111+
parsedCert, err := x509.ParseCertificate(block.Bytes)
112+
if err != nil {
113+
return tls.Certificate{}, fmt.Errorf("failed to parse certificate: %w", err)
114+
}
115+
116+
now := time.Now()
117+
if now.Before(parsedCert.NotBefore) {
118+
return tls.Certificate{}, fmt.Errorf("certificate is not valid yet (NotBefore: %v)", parsedCert.NotBefore)
119+
}
120+
if now.After(parsedCert.NotAfter) {
121+
return tls.Certificate{}, fmt.Errorf("certificate is expired (NotAfter: %v)", parsedCert.NotAfter)
122+
}
123+
124+
// If valid, load keypair as usual
125+
return tls.LoadX509KeyPair(certPath, keyPath)
126+
}
127+
75128
func (ctlr *Controller) CISHealthCheck() {
76129
healthCheckOnce.Do(func() {
77130
healthMux := http.NewServeMux()
@@ -114,38 +167,6 @@ func (ctlr *Controller) CISHealthCheck() {
114167
})
115168
}
116169

117-
// validateTLSCertificate checks if the cert/key files are valid and not expired
118-
func validateTLSCertificate(certPath, keyPath string) error {
119-
cert, err := os.ReadFile(certPath)
120-
if err != nil {
121-
return fmt.Errorf("could not read cert file: %w", err)
122-
}
123-
key, err := os.ReadFile(keyPath)
124-
if err != nil {
125-
return fmt.Errorf("could not read key file: %w", err)
126-
}
127-
_, err = tls.X509KeyPair(cert, key)
128-
if err != nil {
129-
return fmt.Errorf("invalid TLS key pair: %w", err)
130-
}
131-
// Check for expiration
132-
block, _ := pem.Decode(cert)
133-
if block == nil {
134-
return fmt.Errorf("failed to parse certificate PEM")
135-
}
136-
parsedCert, err := x509.ParseCertificate(block.Bytes)
137-
if err != nil {
138-
return fmt.Errorf("failed to parse certificate: %w", err)
139-
}
140-
if time.Now().After(parsedCert.NotAfter) {
141-
return fmt.Errorf("certificate is expired (NotAfter: %v)", parsedCert.NotAfter)
142-
}
143-
if time.Now().Before(parsedCert.NotBefore) {
144-
return fmt.Errorf("certificate is not valid yet (NotBefore: %v)", parsedCert.NotBefore)
145-
}
146-
return nil
147-
}
148-
149170
func (ctlr *Controller) CISHealthCheckHandler() http.Handler {
150171
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
151172
clusterConfig := ctlr.multiClusterHandler.getClusterConfig(ctlr.multiClusterHandler.LocalClusterName)
@@ -188,3 +209,43 @@ func (w webHook) IsWebhookServerRunning() bool {
188209
func (w webHook) GetWebhookServer() *http.Server {
189210
return w.Server
190211
}
212+
213+
// watchCertFiles monitors the certificate and key files for changes and reloads them when modified.
214+
func watchCertFiles(certPath, keyPath string, certsReload func()) {
215+
absCertPath, _ := filepath.Abs(certPath)
216+
absKeyPath, _ := filepath.Abs(keyPath)
217+
218+
watcher, err := fsnotify.NewWatcher()
219+
if err != nil {
220+
fmt.Printf("[Webhook] fsnotify init failed: %v\n", err)
221+
return
222+
}
223+
224+
defer watcher.Close()
225+
226+
certDir := filepath.Dir(absCertPath)
227+
keyDir := filepath.Dir(absKeyPath)
228+
_ = watcher.Add(certDir)
229+
230+
if certDir != keyDir {
231+
_ = watcher.Add(keyDir)
232+
}
233+
234+
log.Debugf("[Webhook] Watching certificate file: %s and key file: %s for changes...", certPath, keyPath)
235+
236+
for {
237+
select {
238+
case event, ok := <-watcher.Events:
239+
if !ok {
240+
return
241+
}
242+
if event.Op&(fsnotify.Write|fsnotify.Create|fsnotify.Remove|fsnotify.Rename) != 0 {
243+
certsReload()
244+
}
245+
case err, ok := <-watcher.Errors:
246+
if ok {
247+
log.Errorf("[Webhook] fsnotify error: %v\n", err)
248+
}
249+
}
250+
}
251+
}

0 commit comments

Comments
 (0)