Skip to content

Commit b4085d8

Browse files
committed
tls: rotate self-signed key/certificate
Signed-off-by: zhangzujian <zhangzujian.7@gmail.com>
1 parent 4c984b2 commit b4085d8

File tree

3 files changed

+263
-3
lines changed

3 files changed

+263
-3
lines changed

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ require (
5454
k8s.io/api v0.32.5
5555
k8s.io/apiextensions-apiserver v0.32.5
5656
k8s.io/apimachinery v0.32.5
57+
k8s.io/apiserver v0.32.5
5758
k8s.io/client-go v12.0.0+incompatible
5859
k8s.io/component-base v0.32.5
5960
k8s.io/klog/v2 v2.130.1
@@ -271,7 +272,6 @@ require (
271272
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
272273
gopkg.in/yaml.v3 v3.0.1 // indirect
273274
howett.net/plist v1.0.1 // indirect
274-
k8s.io/apiserver v0.32.5 // indirect
275275
k8s.io/cli-runtime v0.32.5 // indirect
276276
k8s.io/cloud-provider v0.32.5 // indirect
277277
k8s.io/cluster-bootstrap v0.32.5 // indirect

pkg/metrics/dynamic_cert_key.go

Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
package metrics
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"crypto/rand"
7+
"crypto/rsa"
8+
"crypto/tls"
9+
"crypto/x509"
10+
"crypto/x509/pkix"
11+
"encoding/pem"
12+
"fmt"
13+
"math"
14+
"math/big"
15+
"net"
16+
"sync/atomic"
17+
"time"
18+
19+
"k8s.io/apimachinery/pkg/util/runtime"
20+
"k8s.io/apimachinery/pkg/util/wait"
21+
"k8s.io/apiserver/pkg/server/dynamiccertificates"
22+
certutil "k8s.io/client-go/util/cert"
23+
"k8s.io/client-go/util/keyutil"
24+
"k8s.io/klog/v2"
25+
netutil "k8s.io/utils/net"
26+
"k8s.io/utils/ptr"
27+
)
28+
29+
const caCommonName = "self-signed-ca"
30+
31+
func tlsGetConfigForClient(config *tls.Config) (func(*tls.ClientHelloInfo) (*tls.Config, error), error) {
32+
caKey, err := rsa.GenerateKey(rand.Reader, 2048)
33+
if err != nil {
34+
return nil, fmt.Errorf("failed to generate CA key: %w", err)
35+
}
36+
37+
caCert, err := certutil.NewSelfSignedCACert(certutil.Config{CommonName: caCommonName}, caKey)
38+
if err != nil {
39+
return nil, fmt.Errorf("failed to create self-signed CA cert: %w", err)
40+
}
41+
42+
caBundle := pem.EncodeToMemory(&pem.Block{Type: certutil.CertificateBlockType, Bytes: caCert.Raw})
43+
caProvider, err := dynamiccertificates.NewStaticCAContent(caCommonName, caBundle)
44+
if err != nil {
45+
return nil, fmt.Errorf("failed to create static CA content provider: %w", err)
46+
}
47+
48+
certKeyProvider, err := NewDynamicInMemoryCertKeyPairContent("localhost", caCert, caKey, []net.IP{{127, 0, 0, 1}}, nil)
49+
if err != nil {
50+
return nil, fmt.Errorf("failed to create dynamic in-memory cert/key pair content provider: %w", err)
51+
}
52+
53+
controller := dynamiccertificates.NewDynamicServingCertificateController(config, caProvider, certKeyProvider, nil, nil)
54+
caProvider.AddListener(controller)
55+
certKeyProvider.AddListener(controller)
56+
57+
// generate a context from stopCh. This is to avoid modifying files which are relying on apiserver
58+
// TODO: See if we can pass ctx to the current method
59+
stopCh := make(chan struct{})
60+
ctx, cancel := context.WithCancel(context.Background())
61+
go func() {
62+
select {
63+
case <-stopCh:
64+
cancel() // stopCh closed, so cancel our context
65+
case <-ctx.Done():
66+
}
67+
}()
68+
69+
if controller, ok := certKeyProvider.(dynamiccertificates.ControllerRunner); ok {
70+
if err = controller.RunOnce(ctx); err != nil {
71+
klog.Fatalf("Initial population of default serving certificate failed: %v", err)
72+
}
73+
go controller.Run(ctx, 1)
74+
}
75+
76+
if err = controller.RunOnce(); err != nil {
77+
return nil, fmt.Errorf("failed to run initial serving certificate population: %w", err)
78+
}
79+
go controller.Run(1, stopCh)
80+
81+
return controller.GetConfigForClient, nil
82+
}
83+
84+
func GenerateSelfSignedCertKey(host string, caCert *x509.Certificate, caKey *rsa.PrivateKey, alternateIPs []net.IP, alternateDNS []string) ([]byte, []byte, *time.Time, error) {
85+
validFrom := time.Now().Add(-time.Hour) // valid an hour earlier to avoid flakes due to clock skew
86+
maxAge := time.Hour * 24 * 365 // one year self-signed certs
87+
88+
priv, err := rsa.GenerateKey(rand.Reader, 2048)
89+
if err != nil {
90+
return nil, nil, nil, err
91+
}
92+
// returns a uniform random value in [0, max-1), then add 1 to serial to make it a uniform random value in [1, max).
93+
serial, err := rand.Int(rand.Reader, new(big.Int).SetInt64(math.MaxInt64-1))
94+
if err != nil {
95+
return nil, nil, nil, err
96+
}
97+
serial = new(big.Int).Add(serial, big.NewInt(1))
98+
template := x509.Certificate{
99+
SerialNumber: serial,
100+
Subject: pkix.Name{
101+
CommonName: fmt.Sprintf("%s@%d", host, time.Now().Unix()),
102+
},
103+
NotBefore: validFrom,
104+
NotAfter: validFrom.Add(maxAge),
105+
106+
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
107+
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
108+
BasicConstraintsValid: true,
109+
}
110+
111+
if ip := netutil.ParseIPSloppy(host); ip != nil {
112+
template.IPAddresses = append(template.IPAddresses, ip)
113+
} else {
114+
template.DNSNames = append(template.DNSNames, host)
115+
}
116+
117+
template.IPAddresses = append(template.IPAddresses, alternateIPs...)
118+
template.DNSNames = append(template.DNSNames, alternateDNS...)
119+
120+
derBytes, err := x509.CreateCertificate(rand.Reader, &template, caCert, &priv.PublicKey, caKey)
121+
if err != nil {
122+
return nil, nil, nil, err
123+
}
124+
125+
// Generate cert
126+
certBuffer := bytes.Buffer{}
127+
if err := pem.Encode(&certBuffer, &pem.Block{Type: certutil.CertificateBlockType, Bytes: derBytes}); err != nil {
128+
return nil, nil, nil, err
129+
}
130+
131+
// Generate key
132+
keyBuffer := bytes.Buffer{}
133+
if err := pem.Encode(&keyBuffer, &pem.Block{Type: keyutil.RSAPrivateKeyBlockType, Bytes: x509.MarshalPKCS1PrivateKey(priv)}); err != nil {
134+
return nil, nil, nil, err
135+
}
136+
137+
return certBuffer.Bytes(), keyBuffer.Bytes(), ptr.To(template.NotAfter), nil
138+
}
139+
140+
type DynamicInMemoryCertKeyPairContent struct {
141+
host string
142+
caCert *x509.Certificate
143+
caKey *rsa.PrivateKey
144+
alternateIPs []net.IP
145+
alternateDNS []string
146+
147+
// certKeyPair is a certKeyPair that contains the last read, non-zero length content of the key and cert
148+
certKeyPair atomic.Value
149+
150+
expireTime time.Time
151+
152+
listeners []dynamiccertificates.Listener
153+
}
154+
155+
var (
156+
_ dynamiccertificates.CertKeyContentProvider = &DynamicInMemoryCertKeyPairContent{}
157+
_ dynamiccertificates.ControllerRunner = &DynamicInMemoryCertKeyPairContent{}
158+
)
159+
160+
func NewDynamicInMemoryCertKeyPairContent(host string, caCert *x509.Certificate, caKey *rsa.PrivateKey, alternateIPs []net.IP, alternateDNS []string) (dynamiccertificates.CertKeyContentProvider, error) {
161+
ret := &DynamicInMemoryCertKeyPairContent{
162+
host: host,
163+
caCert: caCert,
164+
caKey: caKey,
165+
alternateIPs: alternateIPs,
166+
alternateDNS: alternateDNS,
167+
}
168+
if err := ret.generateCertKeyPair(); err != nil {
169+
return nil, err
170+
}
171+
172+
return ret, nil
173+
}
174+
175+
// AddListener adds a listener to be notified when the serving cert content changes.
176+
func (c *DynamicInMemoryCertKeyPairContent) AddListener(listener dynamiccertificates.Listener) {
177+
c.listeners = append(c.listeners, listener)
178+
}
179+
180+
func (c *DynamicInMemoryCertKeyPairContent) generateCertKeyPair() error {
181+
if !c.expireTime.IsZero() && time.Now().Before(c.expireTime.Add(-2*time.Hour)) {
182+
return nil
183+
}
184+
185+
klog.Infof("Generating new cert/key pair for %s", c.host)
186+
cert, key, expire, err := GenerateSelfSignedCertKey(c.host, c.caCert, c.caKey, c.alternateIPs, c.alternateDNS)
187+
if err != nil {
188+
return fmt.Errorf("failed to generate self-signed cert/key pair: %w", err)
189+
}
190+
191+
c.certKeyPair.Store(&certKeyPair{
192+
cert: cert,
193+
key: key,
194+
})
195+
c.expireTime = *expire
196+
klog.Infof("Loaded newly generated cert/key pair with expiration time %s", c.expireTime.Local().Format(time.RFC3339))
197+
198+
for _, listener := range c.listeners {
199+
listener.Enqueue()
200+
}
201+
202+
return nil
203+
}
204+
205+
// RunOnce runs a single sync loop
206+
func (c *DynamicInMemoryCertKeyPairContent) RunOnce(ctx context.Context) error {
207+
return c.generateCertKeyPair()
208+
}
209+
210+
// Run starts the controller and blocks until context is killed.
211+
func (c *DynamicInMemoryCertKeyPairContent) Run(ctx context.Context, _ int) {
212+
defer runtime.HandleCrash()
213+
214+
klog.Info("Starting dynamic in-memory cert/key pair controller")
215+
defer klog.Info("Shutting down dynamic in-memory cert/key pair controller")
216+
217+
go wait.Until(func() {
218+
if err := c.generateCertKeyPair(); err != nil {
219+
klog.ErrorS(err, "Failed to generate cert/key pair, will retry later")
220+
}
221+
}, time.Hour, ctx.Done())
222+
223+
<-ctx.Done()
224+
}
225+
226+
// Name is just an identifier
227+
func (c *DynamicInMemoryCertKeyPairContent) Name() string {
228+
return ""
229+
}
230+
231+
// CurrentCertKeyContent provides cert and key byte content
232+
func (c *DynamicInMemoryCertKeyPairContent) CurrentCertKeyContent() ([]byte, []byte) {
233+
certKeyPair := c.certKeyPair.Load().(*certKeyPair)
234+
return certKeyPair.cert, certKeyPair.key
235+
}
236+
237+
// certKeyPair holds the content for the cert and key
238+
type certKeyPair struct {
239+
cert []byte
240+
key []byte
241+
}
242+
243+
func (c *certKeyPair) Equal(rhs *certKeyPair) bool {
244+
if c == nil || rhs == nil {
245+
return c == rhs
246+
}
247+
248+
return bytes.Equal(c.key, rhs.key) && bytes.Equal(c.cert, rhs.cert)
249+
}

pkg/metrics/server.go

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,18 @@ func Run(ctx context.Context, config *rest.Config, addr string, secureServing, w
113113
return err
114114
}
115115

116+
tlsConfig := &tls.Config{
117+
MinVersion: minVersion,
118+
MaxVersion: maxVersion,
119+
CipherSuites: cipherSuites,
120+
}
121+
getConfigForClient, err := tlsGetConfigForClient(tlsConfig)
122+
if err != nil {
123+
err = fmt.Errorf("failed to set GetConfigForClient for TLS config: %w", err)
124+
klog.Error(err)
125+
return err
126+
}
127+
116128
client, err := rest.HTTPClientFor(config)
117129
if err != nil {
118130
klog.Error(err)
@@ -127,8 +139,7 @@ func Run(ctx context.Context, config *rest.Config, addr string, secureServing, w
127139
options.FilterProvider = filterProvider
128140
options.TLSOpts = []func(*tls.Config){
129141
func(c *tls.Config) {
130-
c.MinVersion, c.MaxVersion = minVersion, maxVersion
131-
c.CipherSuites = cipherSuites
142+
c.GetConfigForClient = getConfigForClient
132143
},
133144
}
134145
}

0 commit comments

Comments
 (0)