1 package apiext
2
3 import (
4
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/big"
14 "sync"
15 "time"
16
17
18 k8sTypesCoreV1 "k8s.io/api/core/v1"
19 k8sTypesMetaV1 "k8s.io/apimachinery/pkg/apis/meta/v1"
20
21
22 k8sClientCoreV1 "k8s.io/client-go/kubernetes/typed/core/v1"
23
24
25 k8sErrors "k8s.io/apimachinery/pkg/api/errors"
26 "k8s.io/client-go/rest"
27
28
29 "github.com/datawire/dlib/dlog"
30 )
31
32 const (
33 certValidDuration = 365 * 24 * time.Hour
34 caSecretName = "emissary-ingress-webhook-ca"
35 )
36
37
38 type CA struct {
39 Cert *x509.Certificate
40 Key *rsa.PrivateKey
41
42 cacheMu sync.Mutex
43 cache map[string]*tls.Certificate
44 }
45
46
47
48
49 func EnsureCA(ctx context.Context, restConfig *rest.Config, namespace string) (*CA, *k8sTypesCoreV1.Secret, error) {
50 coreClient, err := k8sClientCoreV1.NewForConfig(restConfig)
51 if err != nil {
52 return nil, nil, err
53 }
54 secretsClient := coreClient.Secrets(namespace)
55
56 for ctx.Err() == nil {
57
58 caSecret, err := secretsClient.Get(ctx, caSecretName, k8sTypesMetaV1.GetOptions{})
59 if err == nil {
60 ca, err := parseCA(caSecret)
61 if err != nil {
62 return nil, nil, err
63 }
64 return ca, caSecret, nil
65 }
66 if !k8sErrors.IsNotFound(err) {
67 return nil, nil, err
68 }
69
70
71 caSecret, err = genCASecret(namespace)
72 if err != nil {
73 return nil, nil, err
74 }
75 caSecret, err = secretsClient.Create(ctx, caSecret, k8sTypesMetaV1.CreateOptions{})
76 if err == nil {
77 ca, err := parseCA(caSecret)
78 if err != nil {
79 return nil, nil, err
80 }
81 return ca, caSecret, nil
82 }
83 if !k8sErrors.IsAlreadyExists(err) {
84 return nil, nil, err
85 }
86
87
88 }
89 return nil, nil, ctx.Err()
90 }
91
92 func parseCA(caSecret *k8sTypesCoreV1.Secret) (*CA, error) {
93
94 caKeyPEMBytes, ok := caSecret.Data[k8sTypesCoreV1.TLSPrivateKeyKey]
95 if !ok {
96 return nil, fmt.Errorf("no key found in CA secret")
97 }
98 caKeyBlock, _ := pem.Decode(caKeyPEMBytes)
99 _caKey, err := x509.ParsePKCS8PrivateKey(caKeyBlock.Bytes)
100 if err != nil {
101 return nil, fmt.Errorf("bad key loaded in CA secret: %w", err)
102 }
103 caKey, ok := _caKey.(*rsa.PrivateKey)
104 if !ok {
105 return nil, fmt.Errorf("key in CA secret is not an RSA key")
106 }
107
108
109 caCertPEMBytes, ok := caSecret.Data[k8sTypesCoreV1.TLSCertKey]
110 if !ok {
111 return nil, fmt.Errorf("no cert found in CA secret!")
112 }
113 caCertBlock, _ := pem.Decode(caCertPEMBytes)
114 caCert, err := x509.ParseCertificate(caCertBlock.Bytes)
115 if err != nil {
116 return nil, err
117 }
118
119 return &CA{
120 Cert: caCert,
121 Key: caKey,
122 }, nil
123 }
124
125
126
127 func genKey() (*rsa.PrivateKey, []byte, error) {
128 key, err := rsa.GenerateKey(rand.Reader, 4096)
129 if err != nil {
130 return nil, nil, err
131 }
132 derBytes, err := x509.MarshalPKCS8PrivateKey(key)
133 if err != nil {
134 return nil, nil, err
135 }
136 pemBytes := pem.EncodeToMemory(&pem.Block{
137 Type: "PRIVATE KEY",
138 Bytes: derBytes,
139 })
140 return key, pemBytes, nil
141 }
142
143
144 func genCACert(key *rsa.PrivateKey) ([]byte, error) {
145
146 notBefore := time.Now()
147 notAfter := notBefore.Add(certValidDuration)
148 serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
149 serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
150 if err != nil {
151 return nil, err
152 }
153
154 template := &x509.Certificate{
155 SerialNumber: serialNumber,
156 Subject: pkix.Name{
157 Organization: []string{"Ambassador Labs"},
158 },
159 NotBefore: notBefore,
160 NotAfter: notAfter,
161 IsCA: true,
162 ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
163 KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
164 BasicConstraintsValid: true,
165 }
166
167 derBytes, err := x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key)
168 if err != nil {
169 return nil, err
170 }
171
172 pemBytes := pem.EncodeToMemory(&pem.Block{
173 Type: "CERTIFICATE",
174 Bytes: derBytes,
175 })
176
177 return pemBytes, nil
178 }
179
180 func genCASecret(namespace string) (*k8sTypesCoreV1.Secret, error) {
181 key, keyPEMBytes, err := genKey()
182 if err != nil {
183 return nil, err
184 }
185 certPEMBytes, err := genCACert(key)
186 if err != nil {
187 return nil, err
188 }
189 return &k8sTypesCoreV1.Secret{
190 ObjectMeta: k8sTypesMetaV1.ObjectMeta{
191 Name: caSecretName,
192 Namespace: namespace,
193 },
194 Type: k8sTypesCoreV1.SecretTypeTLS,
195 Data: map[string][]byte{
196 k8sTypesCoreV1.TLSPrivateKeyKey: keyPEMBytes,
197 k8sTypesCoreV1.TLSCertKey: certPEMBytes,
198 },
199 }, nil
200 }
201
202 func (ca *CA) GenServerCert(ctx context.Context, hostname string) (*tls.Certificate, error) {
203 dlog.Debugf(ctx, "GenServerCert(ctx, %q)", hostname)
204 ca.cacheMu.Lock()
205 defer ca.cacheMu.Unlock()
206 if ca.cache == nil {
207 ca.cache = make(map[string]*tls.Certificate)
208 }
209 now := time.Now()
210 if cached, ok := ca.cache[hostname]; ok && cached != nil && cached.Leaf != nil {
211 if age, lifespan := now.Sub(cached.Leaf.NotBefore), cached.Leaf.NotAfter.Sub(cached.Leaf.NotBefore); age < 2*lifespan/3 {
212 dlog.Debugf(ctx, "GenServerCert(ctx, %q) => from cache (age=%v lifespan=%v)", hostname, age, lifespan)
213 return cached, nil
214 } else {
215 dlog.Debugf(ctx, "GenServerCert(ctx, %q) => cache entry too old (age=%v lifespan=%v)", hostname, age, lifespan)
216 }
217 }
218 dlog.Infof(ctx, "GenServerCert(ctx, %q) => generating new cert", hostname)
219
220 serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
221 serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
222 if err != nil {
223 return nil, err
224 }
225
226 priv, err := rsa.GenerateKey(rand.Reader, 4096)
227 if err != nil {
228 return nil, err
229 }
230
231 cert := &x509.Certificate{
232 SerialNumber: serialNumber,
233 Subject: pkix.Name{
234 Organization: []string{"Ambassador Labs"},
235 CommonName: "Webhook API",
236 },
237 NotBefore: now,
238 NotAfter: now.Add(certValidDuration),
239 KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
240 ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
241 BasicConstraintsValid: true,
242 DNSNames: []string{hostname},
243 }
244
245 certPEMBytes, err := x509.CreateCertificate(
246 rand.Reader,
247 cert,
248 ca.Cert,
249 priv.Public(),
250 ca.Key,
251 )
252 if err != nil {
253 return nil, err
254 }
255
256 certChain := &tls.Certificate{
257 Certificate: [][]byte{certPEMBytes},
258 PrivateKey: priv,
259 Leaf: cert,
260 }
261
262 ca.cache[hostname] = certChain
263 return certChain, nil
264 }
265
View as plain text