|
| 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