Skip to content

Commit b23654f

Browse files
authored
Merge pull request #100 from JoshVanL/root-ca-file-watcher
Root CA file watcher
2 parents 27cb19b + 23a7b39 commit b23654f

File tree

6 files changed

+378
-58
lines changed

6 files changed

+378
-58
lines changed

pkg/controller/configmap.go

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ func AddConfigMapController(ctx context.Context, log logr.Logger, opts Options)
111111

112112
// If the CA roots change then reconcile all ConfigMaps
113113
Watches(&source.Channel{Source: c.tls.SubscribeRootCAsEvent()}, handler.EnqueueRequestsFromMapFunc(
114-
func(obj client.Object) []reconcile.Request {
114+
func(_ client.Object) []reconcile.Request {
115115
var namespaceList corev1.NamespaceList
116116
if err := c.lister.List(ctx, &namespaceList); err != nil {
117117
c.log.Error(err, "failed to list namespaces, exiting...")
@@ -152,8 +152,7 @@ func (c *configmap) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resul
152152
return ctrl.Result{}, nil
153153
}
154154

155-
rootCAsPEMBytes, _ := c.tls.RootCAs()
156-
rootCAsPEM := string(rootCAsPEMBytes)
155+
rootCAsPEM := string(c.tls.RootCAs().PEM)
157156

158157
// Check ConfigMap exists, and has the correct data.
159158
var configMap corev1.ConfigMap
@@ -209,7 +208,7 @@ func (c *configmap) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resul
209208
}
210209

211210
if updateNeeded {
212-
log.V(3).Info("updating ConfigMap")
211+
log.Info("updating ConfigMap data")
213212
return ctrl.Result{}, c.client.Update(ctx, &configMap)
214213
}
215214

pkg/server/server.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ func New(log logr.Logger, restConfig *rest.Config, cm certmanager.Signer, tls tl
8888

8989
return &Server{
9090
opts: opts,
91-
log: log.WithName("grpc_server").WithValues("serving_addr", opts.ServingAddress),
91+
log: log.WithName("grpc-server").WithValues("serving-addr", opts.ServingAddress),
9292
auther: auther,
9393
cm: cm,
9494
tls: tls,
@@ -230,10 +230,10 @@ func (s *Server) parseCertificateBundle(bundle certmanager.Bundle) ([]string, er
230230
intermediatePool.AddCert(intermediate)
231231
}
232232

233-
rootCAsPEM, rootCAsPool := s.tls.RootCAs()
233+
rootCAs := s.tls.RootCAs()
234234
opts := x509.VerifyOptions{
235235
Intermediates: intermediatePool,
236-
Roots: rootCAsPool,
236+
Roots: rootCAs.CertPool,
237237
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
238238
}
239239
if _, err := respCerts[0].Verify(opts); err != nil {
@@ -250,5 +250,5 @@ func (s *Server) parseCertificateBundle(bundle certmanager.Bundle) ([]string, er
250250
certChain = append(certChain, string(certEncoded))
251251
}
252252

253-
return append(certChain, string(rootCAsPEM)), nil
253+
return append(certChain, string(rootCAs.PEM)), nil
254254
}

pkg/tls/fake/fake.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,37 +24,38 @@ import (
2424
"sigs.k8s.io/controller-runtime/pkg/event"
2525

2626
cmtls "github.com/cert-manager/istio-csr/pkg/tls"
27+
"github.com/cert-manager/istio-csr/pkg/tls/rootca"
2728
)
2829

2930
var _ cmtls.Interface = &FakeTLS{}
3031

3132
// FakeTLS is a fake implementation of tls.Interface that can be used for testing.
3233
type FakeTLS struct {
3334
funcTrustDomain func() string
34-
funcRootCAs func() ([]byte, *x509.CertPool)
35+
funcRootCAs func() rootca.RootCAs
3536
funcConfig func(ctx context.Context) (*tls.Config, error)
3637
funcSubscribeRootCAsEvent func() <-chan event.GenericEvent
3738
}
3839

3940
func New() *FakeTLS {
4041
return &FakeTLS{
4142
funcTrustDomain: func() string { return "" },
42-
funcRootCAs: func() ([]byte, *x509.CertPool) { return nil, nil },
43+
funcRootCAs: func() rootca.RootCAs { return rootca.RootCAs{} },
4344
funcConfig: func(_ context.Context) (*tls.Config, error) { return nil, nil },
4445
funcSubscribeRootCAsEvent: func() <-chan event.GenericEvent { return make(chan event.GenericEvent) },
4546
}
4647
}
4748

4849
func (f *FakeTLS) WithRootCAs(rootCAsPEM []byte, rootCAsPool *x509.CertPool) *FakeTLS {
49-
f.funcRootCAs = func() ([]byte, *x509.CertPool) { return rootCAsPEM, rootCAsPool }
50+
f.funcRootCAs = func() rootca.RootCAs { return rootca.RootCAs{PEM: rootCAsPEM, CertPool: rootCAsPool} }
5051
return f
5152
}
5253

5354
func (f *FakeTLS) TrustDomain() string {
5455
return f.funcTrustDomain()
5556
}
5657

57-
func (f *FakeTLS) RootCAs() ([]byte, *x509.CertPool) {
58+
func (f *FakeTLS) RootCAs() rootca.RootCAs {
5859
return f.funcRootCAs()
5960
}
6061

pkg/tls/rootca/rootca.go

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
/*
2+
Copyright 2021 The cert-manager 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 rootca
18+
19+
import (
20+
"bytes"
21+
"context"
22+
"crypto/x509"
23+
"fmt"
24+
"os"
25+
26+
"github.com/fsnotify/fsnotify"
27+
"github.com/go-logr/logr"
28+
"github.com/jetstack/cert-manager/pkg/util/pki"
29+
)
30+
31+
// RootCAs is a Root CAs bundle that contains raw PEM encoded CAs, as well as
32+
// their x509.CertPool encoding.
33+
type RootCAs struct {
34+
// PEM is the raw PEM encoding of the CA certificates.
35+
PEM []byte
36+
37+
// CertPool is the x509.CertPool encoding of the CA certificates.
38+
CertPool *x509.CertPool
39+
}
40+
41+
// watcher is used for loading and watching a file that contains a root CAs
42+
// bundle.
43+
type watcher struct {
44+
log logr.Logger
45+
46+
filepath string
47+
rootCAsPEM []byte
48+
broadcastChan chan RootCAs
49+
}
50+
51+
// Watch watches the given filepath for changes, and writes to the returned
52+
// channel the new state when it changes. The first event is the initial state
53+
// of the root CAs file.
54+
func Watch(ctx context.Context, log logr.Logger, filepath string) (<-chan RootCAs, error) {
55+
w := &watcher{
56+
log: log.WithName("root-ca-watcher").WithValues("file", filepath),
57+
filepath: filepath,
58+
broadcastChan: make(chan RootCAs),
59+
}
60+
61+
w.log.Info("loading root CAs bundle")
62+
rootCAs, err := w.loadRootCAsFile()
63+
if err != nil {
64+
return nil, fmt.Errorf("failed to load root CA bundle: %w", err)
65+
}
66+
67+
watcher, err := fsnotify.NewWatcher()
68+
if err != nil {
69+
return nil, fmt.Errorf("failed to create file watch: %w", err)
70+
}
71+
72+
if err := watcher.Add(w.filepath); err != nil {
73+
return nil, fmt.Errorf("failed to add root CAs file for watching %q: %w", w.filepath, err)
74+
}
75+
76+
go func() {
77+
defer watcher.Close()
78+
79+
// Send initial root CAs state
80+
w.broadcastChan <- *rootCAs
81+
w.rootCAsPEM = rootCAs.PEM
82+
83+
for {
84+
select {
85+
case <-ctx.Done():
86+
w.log.Info("closing root CAs file watcher")
87+
return
88+
89+
case event := <-watcher.Events:
90+
w.log.V(3).Info("received event from file watcher", "event", event.Op.String())
91+
92+
// Watch for remove events, since this is actually the syslink being
93+
// changed in the volume mount.
94+
if event.Op == fsnotify.Remove {
95+
watcher.Remove(event.Name)
96+
if err := watcher.Add(w.filepath); err != nil {
97+
w.log.Error(err, "failed to add new file watch")
98+
}
99+
w.reloadConfig()
100+
}
101+
102+
// Also allow normal files to be modified and reloaded.
103+
if event.Op&fsnotify.Write == fsnotify.Write {
104+
w.reloadConfig()
105+
}
106+
107+
case err := <-watcher.Errors:
108+
w.log.Error(err, "errors watching root CAs file")
109+
}
110+
}
111+
}()
112+
113+
return w.broadcastChan, nil
114+
}
115+
116+
// reloadConfig will load root CAs file, and if changed from the current state,
117+
// will broadcast the update.
118+
func (w *watcher) reloadConfig() {
119+
rootCAs, err := w.loadRootCAsFile()
120+
if err != nil {
121+
w.log.Error(err, "failed to load root CAs file")
122+
return
123+
}
124+
125+
if rootCAs == nil {
126+
w.log.V(3).Info("no root CA changes on file")
127+
} else {
128+
w.log.Info("root CAs changed on file, broadcasting update")
129+
w.rootCAsPEM = rootCAs.PEM
130+
w.broadcastChan <- *rootCAs
131+
}
132+
}
133+
134+
// loadRootCAsFile will read the root CAs from the configured file, and if
135+
// changed from the previous state, will return the updated root CAs. Will
136+
// return nil if there has been no state change.
137+
func (w *watcher) loadRootCAsFile() (*RootCAs, error) {
138+
rootCAsPEM, err := os.ReadFile(w.filepath)
139+
if err != nil {
140+
return nil, fmt.Errorf("failed to read root CAs certificate file %q: %w", w.filepath, err)
141+
}
142+
143+
// If the root CAs PEM hasn't changed, return nil
144+
if bytes.Equal(rootCAsPEM, w.rootCAsPEM) {
145+
return nil, nil
146+
}
147+
148+
w.log.Info("updating root CAs from file")
149+
150+
rootCAsCerts, err := pki.DecodeX509CertificateChainBytes(rootCAsPEM)
151+
if err != nil {
152+
return nil, fmt.Errorf("failed to decode root CAs in certificate file %q: %w", w.filepath, err)
153+
}
154+
155+
rootCAsPool := x509.NewCertPool()
156+
for _, rootCert := range rootCAsCerts {
157+
rootCAsPool.AddCert(rootCert)
158+
}
159+
160+
return &RootCAs{rootCAsPEM, rootCAsPool}, nil
161+
}

0 commit comments

Comments
 (0)