@@ -9,10 +9,13 @@ import (
9
9
"github.com/F5Networks/k8s-bigip-ctlr/v2/pkg/health"
10
10
bigIPPrometheus "github.com/F5Networks/k8s-bigip-ctlr/v2/pkg/prometheus"
11
11
log "github.com/F5Networks/k8s-bigip-ctlr/v2/pkg/vlogger"
12
+ "github.com/fsnotify/fsnotify"
12
13
"github.com/prometheus/client_golang/prometheus/promhttp"
13
14
"net/http"
14
15
"os"
16
+ "path/filepath"
15
17
"sync"
18
+ "sync/atomic"
16
19
"time"
17
20
)
18
21
@@ -28,50 +31,100 @@ type webHook struct {
28
31
29
32
func (ctlr * Controller ) startWebhook () {
30
33
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
+
31
63
webhookMux := http .NewServeMux ()
32
64
webhookMux .HandleFunc ("/mutate" , ctlr .handleMutate )
33
65
webhookMux .HandleFunc ("/validate" , ctlr .handleValidate )
34
66
ctlr .webhookServer = webHook {
35
67
Server : & http.Server {
36
- Addr : ctlr .agentParams .HttpsAddress ,
37
- Handler : webhookMux ,
68
+ Addr : ctlr .agentParams .HttpsAddress ,
69
+ Handler : webhookMux ,
70
+ TLSConfig : tlsCfg ,
38
71
},
39
72
address : ctlr .agentParams .HttpsAddress ,
40
73
}
41
74
webhookShutdownCh := make (chan struct {})
42
75
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
-
57
76
// Graceful shutdown goroutine
58
77
go func () {
59
78
<- webhookShutdownCh
60
79
ctx , cancel := context .WithTimeout (context .Background (), 5 * time .Second )
61
80
defer cancel ()
62
81
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 )
64
83
} else {
65
- log .Infof ("Webhook server gracefully stopped" )
84
+ log .Infof ("[ Webhook] server gracefully stopped" )
66
85
}
67
86
}()
68
- log .Infof ("Starting webhook server on :%s" , ctlr .agentParams .HttpsAddress )
87
+ log .Infof ("[Webhook] starting webhook server on :%s" , ctlr .agentParams .HttpsAddress )
69
88
if err := ctlr .webhookServer .GetWebhookServer ().ListenAndServeTLS (certFile , keyFile ); err != nil && err != http .ErrServerClosed {
70
89
log .Errorf ("Webhook server failed: %v" , err )
71
90
}
72
91
})
73
92
}
74
93
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
+
75
128
func (ctlr * Controller ) CISHealthCheck () {
76
129
healthCheckOnce .Do (func () {
77
130
healthMux := http .NewServeMux ()
@@ -114,38 +167,6 @@ func (ctlr *Controller) CISHealthCheck() {
114
167
})
115
168
}
116
169
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
-
149
170
func (ctlr * Controller ) CISHealthCheckHandler () http.Handler {
150
171
return http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
151
172
clusterConfig := ctlr .multiClusterHandler .getClusterConfig (ctlr .multiClusterHandler .LocalClusterName )
@@ -188,3 +209,43 @@ func (w webHook) IsWebhookServerRunning() bool {
188
209
func (w webHook ) GetWebhookServer () * http.Server {
189
210
return w .Server
190
211
}
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