-
Notifications
You must be signed in to change notification settings - Fork 2k
/
dynamic_source.go
258 lines (220 loc) · 7.49 KB
/
dynamic_source.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
/*
Copyright 2020 The cert-manager Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package tls
import (
"context"
"crypto"
"crypto/tls"
"crypto/x509"
"fmt"
"sync"
"time"
"github.com/go-logr/logr"
"k8s.io/apimachinery/pkg/util/wait"
cmapi "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1"
logf "github.com/cert-manager/cert-manager/pkg/logs"
"github.com/cert-manager/cert-manager/pkg/server/tls/authority"
"github.com/cert-manager/cert-manager/pkg/util/pki"
)
// DynamicSource provides certificate data for a golang HTTP server by
// automatically generating certificates using an authority.SignFunc.
type DynamicSource struct {
// DNSNames that will be set on certificates this source produces.
DNSNames []string
// The authority used to sign certificate templates.
Authority *authority.DynamicAuthority
log logr.Logger
cachedCertificate *tls.Certificate
lock sync.Mutex
}
var _ CertificateSource = &DynamicSource{}
func (f *DynamicSource) Run(ctx context.Context) error {
f.log = logf.FromContext(ctx)
// Run the authority in a separate goroutine
authorityErrChan := make(chan error)
go func() {
defer close(authorityErrChan)
authorityErrChan <- f.Authority.Run(ctx)
}()
nextRenewCh := make(chan time.Time, 1)
// initially fetch a certificate from the signing CA
interval := time.Second
if err := wait.PollUntilContextCancel(ctx, interval, false, func(ctx context.Context) (done bool, err error) {
// check for errors from the authority here too, to prevent retrying
// if the authority has failed to start
select {
case err, ok := <-authorityErrChan:
if err != nil {
return true, fmt.Errorf("failed to run certificate authority: %w", err)
}
if !ok {
return true, context.Canceled
}
default:
// this case avoids blocking if the authority is still running
}
if err := f.regenerateCertificate(nextRenewCh); err != nil {
f.log.Error(err, "Failed to generate initial serving certificate, retrying...", "interval", interval)
return false, nil
}
return true, nil
}); err != nil {
// In case of an error, the stopCh is closed; wait for authorityErrChan to be closed too
<-authorityErrChan
return err
}
// watch for changes to the root CA
rotationChan := f.Authority.WatchRotation(ctx.Done())
renewalChan := func() <-chan struct{} {
ch := make(chan struct{})
go func() {
defer close(ch)
var renewMoment time.Time
select {
case renewMoment = <-nextRenewCh:
// We recevieved a renew moment
default:
// This should never happen
panic("Unreacheable")
}
for {
timer := time.NewTimer(time.Until(renewMoment))
defer timer.Stop()
select {
case <-ctx.Done():
return
case <-timer.C:
// Try to send a message on ch, but also allow for a stop signal or
// a new renewMoment to be received
select {
case <-ctx.Done():
return
case ch <- struct{}{}:
// Message was sent on channel
case renewMoment = <-nextRenewCh:
// We recevieved a renew moment, next loop iteration will update the timer
}
case renewMoment = <-nextRenewCh:
// We recevieved a renew moment, next loop iteration will update the timer
}
}
}()
return ch
}()
// check the current certificate every 10s in case it needs updating
if err := wait.PollUntilContextCancel(ctx, time.Second*10, true, func(ctx context.Context) (done bool, err error) {
// regenerate the serving certificate if the root CA has been rotated
select {
// if the authority has stopped for whatever reason, exit and return the error
case err, ok := <-authorityErrChan:
if err != nil {
return true, fmt.Errorf("failed to run certificate authority: %w", err)
}
if !ok {
return true, context.Canceled
}
// trigger regeneration if the root CA has been rotated
case _, ok := <-rotationChan:
if !ok {
return true, context.Canceled
}
f.log.V(logf.InfoLevel).Info("Detected root CA rotation - regenerating serving certificates")
if err := f.regenerateCertificate(nextRenewCh); err != nil {
f.log.Error(err, "Failed to regenerate serving certificate")
// Return an error here and stop the source running - this case should never
// occur, and if it does, indicates some form of internal error.
return false, err
}
// trigger regeneration if a renewal is required
case <-renewalChan:
f.log.V(logf.InfoLevel).Info("cert-manager webhook certificate requires renewal, regenerating", "DNSNames", f.DNSNames)
if err := f.regenerateCertificate(nextRenewCh); err != nil {
f.log.Error(err, "Failed to regenerate serving certificate")
// Return an error here and stop the source running - this case should never
// occur, and if it does, indicates some form of internal error.
return false, err
}
case <-ctx.Done():
return true, context.Canceled
}
return false, nil
}); err != nil {
// In case of an error, the stopCh is closed; wait for all channels to close
<-authorityErrChan
<-rotationChan
<-renewalChan
return err
}
return nil
}
func (f *DynamicSource) GetCertificate(*tls.ClientHelloInfo) (*tls.Certificate, error) {
f.lock.Lock()
defer f.lock.Unlock()
if f.cachedCertificate == nil {
return nil, ErrNotAvailable
}
return f.cachedCertificate, nil
}
func (f *DynamicSource) Healthy() bool {
return f.cachedCertificate != nil
}
// regenerateCertificate will trigger the cached certificate and private key to
// be regenerated by requesting a new certificate from the authority.
func (f *DynamicSource) regenerateCertificate(nextRenew chan<- time.Time) error {
f.log.V(logf.DebugLevel).Info("Generating new ECDSA private key")
pk, err := pki.GenerateECPrivateKey(384)
if err != nil {
return err
}
// create the certificate template to be signed
template := &x509.Certificate{
Version: 3,
PublicKeyAlgorithm: x509.ECDSA,
PublicKey: pk.Public(),
DNSNames: f.DNSNames,
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
}
f.log.V(logf.DebugLevel).Info("Signing new serving certificate")
cert, err := f.Authority.Sign(template)
if err != nil {
return err
}
f.log.V(logf.DebugLevel).Info("Signed new serving certificate")
if err := f.updateCertificate(pk, cert, nextRenew); err != nil {
return err
}
return nil
}
func (f *DynamicSource) updateCertificate(pk crypto.Signer, cert *x509.Certificate, nextRenew chan<- time.Time) error {
f.lock.Lock()
defer f.lock.Unlock()
pkData, err := pki.EncodePrivateKey(pk, cmapi.PKCS8)
if err != nil {
return err
}
certData, err := pki.EncodeX509(cert)
if err != nil {
return err
}
bundle, err := tls.X509KeyPair(certData, pkData)
if err != nil {
return err
}
f.cachedCertificate = &bundle
certDuration := cert.NotAfter.Sub(cert.NotBefore)
// renew the certificate 1/3 of the time before its expiry
nextRenew <- cert.NotAfter.Add(certDuration / -3)
f.log.V(logf.InfoLevel).Info("Updated cert-manager TLS certificate", "DNSNames", f.DNSNames)
return nil
}