Skip to content

cert provisioner #22

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jun 21, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,18 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

package certprovisioner
package generator

// Certs hosts a private key, its corresponding serving certificate and
// Artifacts hosts a private key, its corresponding serving certificate and
// the CA certificate that signs the serving certificate.
type Certs struct {
type Artifacts struct {
Key []byte
Cert []byte
CACert []byte
}

// CertProvisioner is an interface to provision the serving certificate.
type CertProvisioner interface {
// ProvisionServingCert returns a Certs struct.
ProvisionServingCert() (*Certs, error)
// CertGenerator is an interface to provision the serving certificate.
type CertGenerator interface {
// Generate returns a Artifacts struct.
Generate(CommonName string) (*Artifacts, error)
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

package certprovisioner
package generator

import "fmt"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,16 @@ limitations under the License.
*/

/*
Package certprovisioner provides an interface and implementation to provision certificates.
Package generator provides an interface and implementation to provision certificates.

Create a implementation instance of certprovisioner.
Create an instance of CertGenerator.

cp := SelfSignedCertProvisioner{
CommonName: "foo.bar.com"
}
cg := SelfSignedCertGenerator{}

Provision the certificates.
certs, err := cp.ProvisionServingCert()
Generate the certificates.
certs, err := cg.Generate("foo.bar.com")
if err != nil {
// handle error
}
*/
package certprovisioner
package generator
39 changes: 39 additions & 0 deletions pkg/admission/cert/generator/fake/certgenerator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
Copyright 2018 The Kubernetes 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 fake

import (
"fmt"

"sigs.k8s.io/controller-runtime/pkg/admission/cert/generator"
)

// CertGenerator is a CertGenerator for testing.
type CertGenerator struct {
DNSNameToCertArtifacts map[string]*generator.Artifacts
}

var _ generator.CertGenerator = &CertGenerator{}

// Generate generates certificates by matching a common name.
func (cp *CertGenerator) Generate(commonName string) (*generator.Artifacts, error) {
certs, found := cp.DNSNameToCertArtifacts[commonName]
if !found {
return nil, fmt.Errorf("failed to find common name %q in the CertGenerator", commonName)
}
return certs, nil
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

package certprovisioner
package generator

import (
"crypto/x509"
Expand All @@ -28,21 +28,18 @@ func ServiceToCommonName(serviceNamespace, serviceName string) string {
return fmt.Sprintf("%s.%s.svc", serviceName, serviceNamespace)
}

// SelfSignedCertProvisioner implements the CertProvisioner interface.
// SelfSignedCertGenerator implements the CertGenerator interface.
// It provisions self-signed certificates.
type SelfSignedCertProvisioner struct {
// Required Common Name
CommonName string
}
type SelfSignedCertGenerator struct{}

var _ CertProvisioner = &SelfSignedCertProvisioner{}
var _ CertGenerator = &SelfSignedCertGenerator{}

// ProvisionServingCert creates and returns a CA certificate, certificate and
// Generate creates and returns a CA certificate, certificate and
// key for the server. serverKey and serverCert are used by the server
// to establish trust for clients, CA certificate is used by the
// client to verify the server authentication chain.
// The cert will be valid for 365 days.
func (cp *SelfSignedCertProvisioner) ProvisionServingCert() (*Certs, error) {
func (cp *SelfSignedCertGenerator) Generate(commonName string) (*Artifacts, error) {
signingKey, err := cert.NewPrivateKey()
if err != nil {
return nil, fmt.Errorf("failed to create the CA private key: %v", err)
Expand All @@ -57,15 +54,15 @@ func (cp *SelfSignedCertProvisioner) ProvisionServingCert() (*Certs, error) {
}
signedCert, err := cert.NewSignedCert(
cert.Config{
CommonName: cp.CommonName,
CommonName: commonName,
Usages: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
},
key, signingCert, signingKey,
)
if err != nil {
return nil, fmt.Errorf("failed to create the cert: %v", err)
}
return &Certs{
return &Artifacts{
Key: cert.EncodePrivateKeyPEM(key),
Cert: cert.EncodeCertPEM(signedCert),
CACert: cert.EncodeCertPEM(signingCert),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

package certprovisioner
package generator

import (
"crypto/x509"
Expand All @@ -24,8 +24,8 @@ import (

func TestProvisionServingCert(t *testing.T) {
cn := "mysvc.myns.svc"
cp := SelfSignedCertProvisioner{CommonName: cn}
certs, _ := cp.ProvisionServingCert()
cp := SelfSignedCertGenerator{}
certs, _ := cp.Generate(cn)

// First, create the set of root certificates. For this example we only
// have one. It's also possible to omit this in order to use the
Expand Down
187 changes: 187 additions & 0 deletions pkg/admission/cert/writer/certwriter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
/*
Copyright 2018 The Kubernetes 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 writer

import (
"bytes"
"crypto/tls"
"errors"
"fmt"
"log"
"net/url"

admissionregistrationv1beta1 "k8s.io/api/admissionregistration/v1beta1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/controller-runtime/pkg/admission/cert/generator"
"sigs.k8s.io/controller-runtime/pkg/client"
)

const (
// CACertName is the name of the CA certificate
CACertName = "ca-cert.pem"
// ServerKeyName is the name of the server private key
ServerKeyName = "key.pem"
// ServerCertName is the name of the serving certificate
ServerCertName = "cert.pem"
)

// CertWriter provides method to handle webhooks.
type CertWriter interface {
// EnsureCert ensures that the webhooks have proper certificates.
EnsureCerts(runtime.Object) error
}

// Options are options for configuring a CertWriter.
type Options struct {
Client client.Client
CertGenerator generator.CertGenerator
}

// NewCertWriter builds a new CertWriter using the provided options.
// By default, it builds a MultiCertWriter that is composed of a SecretCertWriter and a FSCertWriter.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This document is not correct, it doesn't do the file writer

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will happen in one week.
I can remove it for now.

func NewCertWriter(ops Options) (CertWriter, error) {
if ops.CertGenerator == nil {
ops.CertGenerator = &generator.SelfSignedCertGenerator{}
}
if ops.Client == nil {
// TODO: default the client if possible
return nil, errors.New("Options.Client is required")
}
s := &SecretCertWriter{
Client: ops.Client,
CertGenerator: ops.CertGenerator,
}
//f := &FSCertWriter{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this commented out?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It has not been implemented ans tested completely :)

// CertGenerator: ops.CertGenerator,
//}
return &MultiCertWriter{
CertWriters: []CertWriter{
s,
//f,
},
}, nil
}

// handleCommon ensures the given webhook has a proper certificate.
// It uses the given certReadWriter to read and (or) write the certificate.
func handleCommon(webhook *admissionregistrationv1beta1.Webhook, ch certReadWriter) error {
if webhook == nil {
return nil
}
if ch == nil {
return errors.New("certReaderWriter should not be nil")
}

certs, err := createIfNotExists(webhook.Name, ch)
if err != nil {
return err
}

// Recreate the cert if it's invalid.
if !validCert(certs) {
log.Printf("cert is invalid or expiring, regenerating a new one")
certs, err = ch.overwrite(webhook.Name)
if err != nil {
return err
}
}

// Ensure the CA bundle in the webhook configuration has the signing CA.
caBundle := webhook.ClientConfig.CABundle
caCert := certs.CACert
if !bytes.Contains(caBundle, caCert) {
webhook.ClientConfig.CABundle = append(caBundle, caCert...)
}
return nil
}

func createIfNotExists(webhookName string, ch certReadWriter) (*generator.Artifacts, error) {
// Try to read first
certs, err := ch.read(webhookName)
if apierrors.IsNotFound(err) {
// Create if not exists
certs, err = ch.write(webhookName)
switch {
// This may happen if there is another racer.
case apierrors.IsAlreadyExists(err):
certs, err = ch.read(webhookName)
if err != nil {
return certs, err
}
case err != nil:
return certs, err
}
} else if err != nil {
return certs, err
}
return certs, nil
}

// certReadWriter provides methods for reading and writing certificates.
type certReadWriter interface {
// read reads a wehbook name and returns the certs for it.
read(webhookName string) (*generator.Artifacts, error)
// write writes the certs and return the certs it wrote.
write(webhookName string) (*generator.Artifacts, error)
// overwrite overwrites the existing certs and return the certs it wrote.
overwrite(webhookName string) (*generator.Artifacts, error)
}

func validCert(certs *generator.Artifacts) bool {
// TODO:
// 1) validate the key and the cert are valid pair e.g. call crypto/tls.X509KeyPair()
// 2) validate the cert with the CA cert
// 3) validate the cert is for a certain DNSName
// e.g.
// c, err := tls.X509KeyPair(cert, key)
// err := c.Verify(options)
if certs == nil {
return false
}
_, err := tls.X509KeyPair(certs.Cert, certs.Key)
return err == nil
}

func getWebhooksFromObject(obj runtime.Object) ([]admissionregistrationv1beta1.Webhook, error) {
switch typed := obj.(type) {
case *admissionregistrationv1beta1.MutatingWebhookConfiguration:
return typed.Webhooks, nil
case *admissionregistrationv1beta1.ValidatingWebhookConfiguration:
return typed.Webhooks, nil
//case *unstructured.Unstructured:
// TODO: implement this if needed
default:
return nil, fmt.Errorf("unsupported type: %T, only support v1beta1.MutatingWebhookConfiguration and v1beta1.ValidatingWebhookConfiguration", typed)
}
}

func dnsNameForWebhook(config *admissionregistrationv1beta1.WebhookClientConfig) (string, error) {
if config.Service != nil && config.URL != nil {
return "", fmt.Errorf("service and URL can't be set at the same time in a webhook: %v", config)
}
if config.Service == nil && config.URL == nil {
return "", fmt.Errorf("one of service and URL need to be set in a webhook: %v", config)
}
if config.Service != nil {
return generator.ServiceToCommonName(config.Service.Namespace, config.Service.Name), nil
}
// config.URL != nil
u, err := url.Parse(*config.URL)
return u.Host, err

}
Loading