Skip to content

Commit cd79900

Browse files
author
Mengqi Yu
committed
cert provisioner
1 parent 872f043 commit cd79900

File tree

9 files changed

+689
-13
lines changed

9 files changed

+689
-13
lines changed
Lines changed: 367 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,367 @@
1+
/*
2+
Copyright 2018 The Kubernetes 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 certoutput
18+
19+
import (
20+
"bytes"
21+
"fmt"
22+
"net/url"
23+
"strings"
24+
25+
admissionregistrationv1beta1 "k8s.io/api/admissionregistration/v1beta1"
26+
corev1 "k8s.io/api/core/v1"
27+
apierrors "k8s.io/apimachinery/pkg/api/errors"
28+
"k8s.io/apimachinery/pkg/api/meta"
29+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
30+
"k8s.io/apimachinery/pkg/runtime"
31+
apitypes "k8s.io/apimachinery/pkg/types"
32+
33+
"github.com/kubernetes-sigs/controller-runtime/pkg/admission/certprovisioner"
34+
"github.com/kubernetes-sigs/controller-runtime/pkg/client"
35+
)
36+
37+
var (
38+
// Use an annotation in the following format:
39+
// secret.certprovisioner.kubernetes.io/<webhook-name>: <secret-namespace>/<secret-name>
40+
// the webhook cert manager library will provision the certificate for the webhook by
41+
// storing it in the specified secret.
42+
SecretCertInjectionAnnotationKeyPrefix = "secret.certprovisioner.kubernetes.io/"
43+
44+
// Use an annotation in the following format:
45+
// local.certprovisioner.kubernetes.io/<webhook-name>: path/to/certs/
46+
// the webhook cert manager library will provision the certificate for the webhook by
47+
// storing it under the specified path.
48+
// format: local.certprovisioner.kubernetes.io/webhookName: path/to/certs/
49+
LocalCertInjectionAnnotationKeyPrefix = "local.certprovisioner.kubernetes.io/"
50+
51+
CACertName = "ca-cert.pem"
52+
ServerKeyName = "key.pem"
53+
ServerCertName = "cert.pem"
54+
)
55+
56+
type Options struct {
57+
Type *CertOutputType
58+
}
59+
60+
const (
61+
SecretType CertOutputType = "secret-certoutput-type"
62+
)
63+
64+
type CertOutputType string
65+
66+
// CertsOutput provides method to handle webhooks.
67+
type CertsOutput interface {
68+
// Handle process one webhook.
69+
Handle(webhook *admissionregistrationv1beta1.Webhook) error
70+
}
71+
72+
func New(webhookConfig runtime.Object, client client.Client, cp certprovisioner.CertProvisioner, op Options) (CertsOutput, error) {
73+
if op.Type == nil {
74+
t := SecretType
75+
op.Type = &t
76+
}
77+
switch *op.Type {
78+
case SecretType:
79+
return newSecretCertsReadWriter(webhookConfig, client, cp)
80+
default:
81+
return nil, fmt.Errorf("unknown CertOutputType: %v", op.Type)
82+
}
83+
}
84+
85+
func newSecretCertsReadWriter(webhookConfig runtime.Object, client client.Client, cp certprovisioner.CertProvisioner) (CertsOutput, error) {
86+
webhookToSecret := map[string]apitypes.NamespacedName{}
87+
accessor, err := meta.Accessor(webhookConfig)
88+
if err != nil {
89+
return nil, err
90+
}
91+
annotations := accessor.GetAnnotations()
92+
if annotations == nil {
93+
return nil, nil
94+
}
95+
96+
for k, v := range annotations {
97+
if strings.HasPrefix(k, SecretCertInjectionAnnotationKeyPrefix) {
98+
webhookName := strings.TrimPrefix(k, SecretCertInjectionAnnotationKeyPrefix)
99+
webhookToSecret[webhookName] = apitypes.NewNamespacedNameFromString(v)
100+
}
101+
}
102+
103+
return &secretCertsOutput{
104+
client: client,
105+
webhookConfig: webhookConfig,
106+
webhookToSecrets: webhookToSecret,
107+
certInput: cp,
108+
}, nil
109+
}
110+
111+
var _ CertsOutput = &secretCertsOutput{}
112+
113+
type secretCertsOutput struct {
114+
client client.Client
115+
116+
// The webhookConfiguration it is going to handle.
117+
webhookConfig runtime.Object
118+
// A map from wehbook name to the individual webhook.
119+
webhookMap map[string]*admissionregistrationv1beta1.Webhook
120+
// A map from webhook name to the service namespace and name.
121+
webhookToSecrets map[string]apitypes.NamespacedName
122+
123+
certInput certprovisioner.CertProvisioner
124+
}
125+
126+
// syncSecretWithWebhook ensures the certificate and CA exist and valid for the given webhook.
127+
// syncSecretWithWebhook will modify the passed-in webhook.
128+
func (s *secretCertsOutput) Handle(webhook *admissionregistrationv1beta1.Webhook) error {
129+
return handleCommon(webhook, s)
130+
}
131+
132+
func handleCommon(webhook *admissionregistrationv1beta1.Webhook, ch certsHandler) error {
133+
134+
webhookName := webhook.Name
135+
if ch.skip(webhookName) {
136+
return nil
137+
}
138+
139+
certs, err := ch.read(webhookName)
140+
if apierrors.IsNotFound(err) {
141+
certs, err = ch.write(webhookName)
142+
if err != nil {
143+
return err
144+
}
145+
} else if err != nil {
146+
return err
147+
}
148+
149+
// Recreate the cert if it's invalid.
150+
if !validCertInSecret(certs) {
151+
certs, err = ch.write(webhookName)
152+
if err != nil {
153+
return err
154+
}
155+
if err != nil {
156+
return err
157+
}
158+
}
159+
160+
// Ensure the CA bundle in the webhook configuration has the signing CA.
161+
caBundle := webhook.ClientConfig.CABundle
162+
caCert := certs.CACert
163+
if !bytes.Contains(caBundle, caCert) {
164+
webhook.ClientConfig.CABundle = append(caBundle, caCert...)
165+
}
166+
return nil
167+
}
168+
169+
// certsHandler provides methods for handling certificates for webhook.
170+
type certsHandler interface {
171+
// skip returns if the webhook should be skipped.
172+
skip(webhookName string) bool
173+
// read reads a wehbook name and returns the certs for it.
174+
read(webhookName string) (*certprovisioner.Certs, error)
175+
// write writes the certs and return the certs it wrote.
176+
write(webhookName string) (*certprovisioner.Certs, error)
177+
}
178+
179+
var _ certsHandler = &secretCertsOutput{}
180+
181+
func (s *secretCertsOutput) skip(webhookName string) bool {
182+
_, found := s.webhookToSecrets[webhookName]
183+
return !found
184+
}
185+
186+
func (s *secretCertsOutput) write(webhookName string) (
187+
*certprovisioner.Certs, error) {
188+
sec, found := s.webhookToSecrets[webhookName]
189+
if !found {
190+
return nil, fmt.Errorf("failed to find the secret name by the webhook name: %q", webhookName)
191+
}
192+
193+
webhook := s.webhookMap[webhookName]
194+
commonName, err := webhookClientConfigToCommonName(&webhook.ClientConfig)
195+
if err != nil {
196+
return nil, err
197+
}
198+
199+
secret := &corev1.Secret{}
200+
err = s.client.Get(nil, sec, secret)
201+
if apierrors.IsNotFound(err) {
202+
certs, err := s.certInput.ProvisionServingCert(commonName)
203+
if err != nil {
204+
return nil, err
205+
}
206+
secret = certsToSecret(certs, sec)
207+
// TODO fix and enable it
208+
//err = setOwnerRef(secret, s.webhookConfig)
209+
err = s.client.Create(nil, secret)
210+
return certs, err
211+
} else if err != nil {
212+
return nil, err
213+
}
214+
215+
certs, err := secretToCerts(secret)
216+
if err != nil {
217+
return nil, err
218+
}
219+
// Recreate the cert if it's invalid.
220+
if !validCertInSecret(certs) {
221+
certs, err := s.certInput.ProvisionServingCert(commonName)
222+
if err != nil {
223+
return nil, err
224+
}
225+
secret = certsToSecret(certs, sec)
226+
// TODO fix and enable it
227+
//err = setOwnerRef(secret, s.webhookConfig)
228+
err = s.client.Update(nil, secret)
229+
return certs, err
230+
}
231+
return certs, nil
232+
}
233+
234+
func (s *secretCertsOutput) read(webhookName string) (*certprovisioner.Certs, error) {
235+
sec, found := s.webhookToSecrets[webhookName]
236+
if !found {
237+
return nil, fmt.Errorf("failed to find the secret name by the webhook name: %q", webhookName)
238+
}
239+
secret := &corev1.Secret{}
240+
err := s.client.Get(nil, sec, secret)
241+
if err != nil {
242+
return nil, err
243+
}
244+
return secretToCerts(secret)
245+
}
246+
247+
// Mark the webhook as the owner of the secret by setting the ownerReference in the secret.
248+
func setOwnerRef(secret, webhookConfig runtime.Object) error {
249+
accessor, err := meta.Accessor(webhookConfig)
250+
// TODO: typeAccessor.GetAPIVersion() and typeAccessor.GetKind() returns empty apiVersion and Kind, fix it.
251+
typeAccessor, err := meta.TypeAccessor(webhookConfig)
252+
if err != nil {
253+
return err
254+
}
255+
blockOwnerDeletion := false
256+
// Due to
257+
// https://github.com/kubernetes/kubernetes/blob/5da925ad4fd070e687dc5255c177d5e7d542edd7/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/controller_ref.go#L35
258+
isController := true
259+
ownerRef := metav1.OwnerReference{
260+
APIVersion: typeAccessor.GetAPIVersion(),
261+
Kind: typeAccessor.GetKind(),
262+
Name: accessor.GetName(),
263+
UID: accessor.GetUID(),
264+
BlockOwnerDeletion: &blockOwnerDeletion,
265+
Controller: &isController,
266+
}
267+
secretAccessor, err := meta.Accessor(secret)
268+
if err != nil {
269+
return err
270+
}
271+
secretAccessor.SetOwnerReferences([]metav1.OwnerReference{ownerRef})
272+
return nil
273+
}
274+
275+
func validCertInSecret(certs *certprovisioner.Certs) bool {
276+
// TODO: 1) validate the key and the cert are valid pair e.g. call crypto/tls.X509KeyPair()
277+
// 2) validate the cert with the CA cert
278+
// 3) validate the cert is for a certain DNSName
279+
// e.g.
280+
// c, err := tls.X509KeyPair(cert, key)
281+
// err := c.Verify(options)
282+
283+
return true
284+
}
285+
286+
func secretToCerts(secret *corev1.Secret) (*certprovisioner.Certs, error) {
287+
checkList := []string{CACertName, ServerCertName, ServerKeyName}
288+
for _, key := range checkList {
289+
if _, ok := secret.Data[key]; !ok {
290+
return nil, fmt.Errorf("failed to find required key: %q in the secret", key)
291+
}
292+
}
293+
return &certprovisioner.Certs{
294+
CACert: secret.Data[CACertName],
295+
Cert: secret.Data[ServerCertName],
296+
Key: secret.Data[ServerKeyName],
297+
}, nil
298+
}
299+
300+
func certsToSecret(certs *certprovisioner.Certs, sec apitypes.NamespacedName) *corev1.Secret {
301+
return &corev1.Secret{
302+
TypeMeta: metav1.TypeMeta{
303+
APIVersion: "v1",
304+
Kind: "Secret",
305+
},
306+
ObjectMeta: metav1.ObjectMeta{
307+
Namespace: sec.Namespace,
308+
Name: sec.Name,
309+
},
310+
Data: map[string][]byte{
311+
CACertName: certs.CACert,
312+
ServerKeyName: certs.Key,
313+
ServerCertName: certs.Cert,
314+
},
315+
}
316+
}
317+
318+
func webhookClientConfigToCommonName(config *admissionregistrationv1beta1.WebhookClientConfig) (string, error) {
319+
if config.Service != nil && config.URL != nil {
320+
return "", fmt.Errorf("service and URL can't be set at the same time in a webhook: %#v", config)
321+
}
322+
if config.Service == nil && config.URL == nil {
323+
return "", fmt.Errorf("one of service and URL need to be set in a webhook: %#v", config)
324+
}
325+
if config.Service != nil {
326+
return certprovisioner.ServiceToCommonName(config.Service.Namespace, config.Service.Name), nil
327+
}
328+
if config.URL != nil {
329+
u, err := url.Parse(*config.URL)
330+
return u.Host, err
331+
}
332+
return "", nil
333+
}
334+
335+
//// fsCertsOutput deals with the local FS.
336+
//// This is designed for running as static pod on the master node.
337+
//type fsCertsOutput struct {
338+
// // The webhookConfiguration it is going to handle.
339+
// webhookConfig runtime.Object
340+
// // A map from webhook name to the path to write the certificates.
341+
// WebhookToPath map[string]string
342+
//}
343+
//
344+
//var _ CertsOutput = &fsCertsOutput{}
345+
//
346+
//func (s *fsCertsOutput) Handle(webhook *admissionregistrationv1beta1.Webhook) error {
347+
// // TODO: implement this
348+
// return nil
349+
//}
350+
//
351+
//var _ certsHandler = &fsCertsOutput{}
352+
//
353+
//func (s *fsCertsOutput) skip(webhookName string) bool {
354+
// // TODO: implement this
355+
// return true
356+
//}
357+
//
358+
//func (s *fsCertsOutput) write(webhookName string) (
359+
// *certprovisioner.Certs, error) {
360+
// // TODO: implement this
361+
// return nil, nil
362+
//}
363+
//
364+
//func (s *fsCertsOutput) read(webhookName string) (*certprovisioner.Certs, error) {
365+
// // TODO: implement this
366+
// return nil, nil
367+
//}

0 commit comments

Comments
 (0)