forked from operator-framework/operator-sdk
-
Notifications
You must be signed in to change notification settings - Fork 0
/
tls.go
384 lines (350 loc) · 12.6 KB
/
tls.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
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
// Copyright 2018 The Operator-SDK 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 tlsutil
import (
"crypto/rsa"
"crypto/x509"
"errors"
"fmt"
"io/ioutil"
"strings"
"k8s.io/api/core/v1"
apiErrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/kubernetes"
)
// CertType defines the type of the cert.
type CertType int
const (
// ClientAndServingCert defines both client and serving cert.
ClientAndServingCert CertType = iota
// ServingCert defines a serving cert.
ServingCert
// ClientCert defines a client cert.
ClientCert
)
// CertConfig configures how to generate the Cert.
type CertConfig struct {
// CertName is the name of the cert.
CertName string
// Optional CertType. Serving, client or both; defaults to both.
CertType CertType
// Optional CommonName is the common name of the cert; defaults to "".
CommonName string
// Optional Organization is Organization of the cert; defaults to "".
Organization []string
// Optional CA Key, if user wants to provide custom CA key via a file path.
CAKey string
// Optional CA Certificate, if user wants to provide custom CA cert via file path.
CACert string
// TODO: consider to add passed in SAN fields.
}
// CertGenerator is an operator specific TLS tool that generates TLS assets for the deploying a user's application.
type CertGenerator interface {
// GenerateCert generates a secret containing TLS encryption key and cert, a Secret
// containing the CA key, and a ConfigMap containing the CA Certificate given the Custom
// Resource(CR) "cr", the Kubernetes Service "Service", and the CertConfig "config".
//
// GenerateCert creates and manages TLS key and cert and CA with the following:
// CA creation and management:
// - If CA is not given:
// - A unique CA is generated for the CR.
// - CA's key is packaged into a Secret as shown below.
// - CA's cert is packaged in a ConfigMap as shown below.
// - The CA Secret and ConfigMap are created on the k8s cluster in the CR's namespace before
// returned to the user. The CertGenerator manages the CA Secret and ConfigMap to ensure it's
// unqiue per CR.
// - If CA is given:
// - CA's key is packaged into a Secret as shown below.
// - CA's cert is packaged in a ConfigMap as shown below.
// - The CA Secret and ConfigMap are returned but not created in the K8s cluster in the CR's
// namespace. The CertGenerator doesn't manage the CA because the user controls the lifecycle
// of the CA.
//
// TLS Key and Cert Creation and Management:
// - A unique TLS cert and key pair is generated per CR + CertConfig.CertName.
// - The CA is used to generate and sign the TLS cert.
// - The signing process uses the passed in "service" to set the Subject Alternative Names(SAN)
// for the certificate. We assume that the deployed applications are typically communicated
// with via a Kubernetes Service. The SAN is set to the FQDN of the service
// `<service-name>.<service-namespace>.svc.cluster.local`.
// - Once TLS key and cert are created, they are packaged into a secret as shown below.
// - Finally, the secret are created on the k8s cluster in the CR's namespace before returned to
// the user. The CertGenerator manages this secret to ensure that it is unique per CR +
// CertConfig.CertName.
//
// TLS encryption key and cert Secret format:
// kind: Secret
// apiVersion: v1
// metadata:
// name: <cr-kind>-<cr-name>-<CertConfig.CertName>
// namespace: <cr-namespace>
// data:
// tls.crt: ...
// tls.key: ...
//
// CA Certificate ConfigMap format:
// kind: ConfigMap
// apiVersion: v1
// metadata:
// name: <cr-kind>-<cr-name>-ca
// namespace: <cr-namespace>
// data:
// ca.crt: ...
//
// CA Key Secret format:
// kind: Secret
// apiVersion: v1
// metadata:
// name: <cr-kind>-<cr-name>-ca
// namespace: <cr-namespace>
// data:
// ca.key: ..
GenerateCert(cr runtime.Object, service *v1.Service, config *CertConfig) (*v1.Secret, *v1.ConfigMap, *v1.Secret, error)
}
const (
// TLSPrivateCAKeyKey is the key for the private CA key field.
TLSPrivateCAKeyKey = "ca.key"
// TLSCertKey is the key for tls CA certificates.
TLSCACertKey = "ca.crt"
)
// NewSDKCertGenerator constructs a new CertGenerator given the kubeClient.
func NewSDKCertGenerator(kubeClient kubernetes.Interface) CertGenerator {
return &SDKCertGenerator{KubeClient: kubeClient}
}
type SDKCertGenerator struct {
KubeClient kubernetes.Interface
}
// GenerateCert returns a secret containing the TLS encryption key and cert,
// a ConfigMap containing the CA Certificate and a Secret containing the CA key or it
// returns a error incase something goes wrong.
func (scg *SDKCertGenerator) GenerateCert(cr runtime.Object, service *v1.Service, config *CertConfig) (*v1.Secret, *v1.ConfigMap, *v1.Secret, error) {
if err := verifyConfig(config); err != nil {
return nil, nil, nil, err
}
k, n, ns, err := toKindNameNamespace(cr)
if err != nil {
return nil, nil, nil, err
}
appSecretName := ToAppSecretName(k, n, config.CertName)
appSecret, err := getAppSecretInCluster(scg.KubeClient, appSecretName, ns)
if err != nil {
return nil, nil, nil, err
}
caSecretAndConfigMapName := ToCASecretAndConfigMapName(k, n)
var (
caSecret *v1.Secret
caConfigMap *v1.ConfigMap
)
caSecret, caConfigMap, err = getCASecretAndConfigMapInCluster(scg.KubeClient, caSecretAndConfigMapName, ns)
if err != nil {
return nil, nil, nil, err
}
if config.CAKey != "" && config.CACert != "" {
// custom CA provided by the user.
customCAKeyData, err := ioutil.ReadFile(config.CAKey)
if err != nil {
return nil, nil, nil, fmt.Errorf("error reading CA Key from the given file name: %v", err)
}
customCACertData, err := ioutil.ReadFile(config.CACert)
if err != nil {
return nil, nil, nil, fmt.Errorf("error reading CA Cert from the given file name: %v", err)
}
customCAKey, err := parsePEMEncodedPrivateKey(customCAKeyData)
if err != nil {
return nil, nil, nil, fmt.Errorf("error parsing CA Key from the given file name: %v", err)
}
customCACert, err := parsePEMEncodedCert(customCACertData)
if err != nil {
return nil, nil, nil, fmt.Errorf("error parsing CA Cert from the given file name: %v", err)
}
caSecret, caConfigMap = toCASecretAndConfigmap(customCAKey, customCACert, caSecretAndConfigMapName)
} else if config.CAKey != "" || config.CACert != "" {
// if only one of the custom CA Key or Cert is provided
return nil, nil, nil, ErrCAKeyAndCACertReq
}
hasAppSecret := appSecret != nil
hasCASecretAndConfigMap := caSecret != nil && caConfigMap != nil
switch {
case hasAppSecret && hasCASecretAndConfigMap:
return appSecret, caConfigMap, caSecret, nil
case hasAppSecret && !hasCASecretAndConfigMap:
return nil, nil, nil, ErrCANotFound
case !hasAppSecret && hasCASecretAndConfigMap:
// Note: if a custom CA is passed in my the user it takes preference over an already
// generated CA secret and CA configmap that might exist in the cluster
caKey, err := parsePEMEncodedPrivateKey(caSecret.Data[TLSPrivateCAKeyKey])
if err != nil {
return nil, nil, nil, err
}
caCert, err := parsePEMEncodedCert([]byte(caConfigMap.Data[TLSCACertKey]))
if err != nil {
return nil, nil, nil, err
}
key, err := newPrivateKey()
if err != nil {
return nil, nil, nil, err
}
cert, err := newSignedCertificate(config, service, key, caCert, caKey)
if err != nil {
return nil, nil, nil, err
}
appSecret, err := scg.KubeClient.CoreV1().Secrets(ns).Create(toTLSSecret(key, cert, appSecretName))
if err != nil {
return nil, nil, nil, err
}
return appSecret, caConfigMap, caSecret, nil
case !hasAppSecret && !hasCASecretAndConfigMap:
// If no custom CAKey and CACert are provided we have to generate them
caKey, err := newPrivateKey()
if err != nil {
return nil, nil, nil, err
}
caCert, err := newSelfSignedCACertificate(caKey)
if err != nil {
return nil, nil, nil, err
}
caSecret, caConfigMap := toCASecretAndConfigmap(caKey, caCert, caSecretAndConfigMapName)
caSecret, err = scg.KubeClient.CoreV1().Secrets(ns).Create(caSecret)
if err != nil {
return nil, nil, nil, err
}
caConfigMap, err = scg.KubeClient.CoreV1().ConfigMaps(ns).Create(caConfigMap)
if err != nil {
return nil, nil, nil, err
}
key, err := newPrivateKey()
if err != nil {
return nil, nil, nil, err
}
cert, err := newSignedCertificate(config, service, key, caCert, caKey)
if err != nil {
return nil, nil, nil, err
}
appSecret, err := scg.KubeClient.CoreV1().Secrets(ns).Create(toTLSSecret(key, cert, appSecretName))
if err != nil {
return nil, nil, nil, err
}
return appSecret, caConfigMap, caSecret, nil
default:
return nil, nil, nil, ErrInternal
}
}
func verifyConfig(config *CertConfig) error {
if config == nil {
return errors.New("nil CertConfig not allowed")
}
if config.CertName == "" {
return errors.New("empty CertConfig.CertName not allowed")
}
return nil
}
func ToAppSecretName(kind, name, certName string) string {
return strings.ToLower(kind) + "-" + name + "-" + certName
}
func ToCASecretAndConfigMapName(kind, name string) string {
return strings.ToLower(kind) + "-" + name + "-ca"
}
func getAppSecretInCluster(kubeClient kubernetes.Interface, name, namespace string) (*v1.Secret, error) {
se, err := kubeClient.CoreV1().Secrets(namespace).Get(name, metav1.GetOptions{})
if err != nil && !apiErrors.IsNotFound(err) {
return nil, err
}
if apiErrors.IsNotFound(err) {
return nil, nil
}
return se, nil
}
// getCASecretAndConfigMapInCluster gets CA secret and configmap of the given name and namespace.
// it only returns both if they are found and nil if both are not found. In the case if only one of them is found,
// then we error out because we expect either both CA secret and configmap exit or not.
//
// NOTE: both the CA secret and configmap have the same name with template `<cr-kind>-<cr-name>-ca` which is what the
// input parameter `name` refers to.
func getCASecretAndConfigMapInCluster(kubeClient kubernetes.Interface, name, namespace string) (*v1.Secret, *v1.ConfigMap, error) {
hasConfigMap := true
cm, err := kubeClient.CoreV1().ConfigMaps(namespace).Get(name, metav1.GetOptions{})
if err != nil && !apiErrors.IsNotFound(err) {
return nil, nil, err
}
if apiErrors.IsNotFound(err) {
hasConfigMap = false
}
hasSecret := true
se, err := kubeClient.CoreV1().Secrets(namespace).Get(name, metav1.GetOptions{})
if err != nil && !apiErrors.IsNotFound(err) {
return nil, nil, err
}
if apiErrors.IsNotFound(err) {
hasSecret = false
}
if hasConfigMap != hasSecret {
// TODO: this case can happen if creating CA configmap succeeds and creating CA secret failed. We need to handle this case properly.
return nil, nil, fmt.Errorf("expect either both ca configmap and secret both exist or not exist, but got hasCAConfigmap==%v and hasCASecret==%v", hasConfigMap, hasSecret)
}
if hasConfigMap == false {
return nil, nil, nil
}
return se, cm, nil
}
func toKindNameNamespace(cr runtime.Object) (string, string, string, error) {
a := meta.NewAccessor()
k, err := a.Kind(cr)
if err != nil {
return "", "", "", err
}
n, err := a.Name(cr)
if err != nil {
return "", "", "", err
}
ns, err := a.Namespace(cr)
if err != nil {
return "", "", "", err
}
return k, n, ns, nil
}
// toTLSSecret returns a client/server "kubernetes.io/tls" secret.
// TODO: add owner ref.
func toTLSSecret(key *rsa.PrivateKey, cert *x509.Certificate, name string) *v1.Secret {
return &v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: name,
},
Data: map[string][]byte{
v1.TLSPrivateKeyKey: encodePrivateKeyPEM(key),
v1.TLSCertKey: encodeCertificatePEM(cert),
},
Type: v1.SecretTypeTLS,
}
}
// TODO: add owner ref.
func toCASecretAndConfigmap(key *rsa.PrivateKey, cert *x509.Certificate, name string) (*v1.Secret, *v1.ConfigMap) {
return &v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: name,
},
Data: map[string][]byte{
TLSPrivateCAKeyKey: encodePrivateKeyPEM(key),
},
}, &v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: name,
},
Data: map[string]string{
TLSCACertKey: string(encodeCertificatePEM(cert)),
},
}
}