Skip to content

Simplifying Admission Webhook Certificate Management with Cert Rotator Library

Following the suckless admission webhook setup guide, it turns out that certificate management can be further simplified using the cert rotator library created by the Open Policy Agent team.

According to the project:

The purpose of the Certificate Controller library is to provide an easy way for controller authors to bootstrap webhooks while making it possible for users to use more customizable projects like cert-manager should they desire to do so. Its purpose is not to be a fully-featured certificate solution, but a simple solution that allows webhook authors to avoid having a hard dependency on the existence of any third-party certificate generation solution.

The cert rotator library takes good care of several aspects, namely:

  • CA generation
  • Certificate and key generation for the webhook server
  • Certificate rotation
  • All the certificates are managed in a designated Kubernetes secret file

Practically, if you are writing the webhook controller using Kubebuilder, you can have a pkg/cert/cert.go file with the following content:

package cert

import (
    "fmt"

    cert "github.com/open-policy-agent/cert-controller/pkg/rotator"
    "k8s.io/apimachinery/pkg/types"
    ctrl "sigs.k8s.io/controller-runtime"
)

const (
    caName = "your-ca-name" // XXX: MODIFY IT
    caOrg  = "your-org" // XXX: MODIFY IT
)

type CertRotatorConfig struct {
    ServiceName             string
    SecretName              string
    SecretNamespace         string
    CertDir                 string
    MutatingWebhookConfName string
}

// +kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch;update
// +kubebuilder:rbac:groups="admissionregistration.k8s.io",resources=mutatingwebhookconfigurations,verbs=get;list;watch;update

// CertsManager creates certs for webhooks.
func CertsManager(mgr ctrl.Manager, cfg CertRotatorConfig, setupFinish chan struct{}) error {
    // dnsName is the format of <service name>.<namespace>.svc
    var dnsName = fmt.Sprintf("%s.%s.svc", cfg.ServiceName, cfg.SecretNamespace)

    return cert.AddRotator(mgr, &cert.CertRotator{
        SecretKey: types.NamespacedName{
            Namespace: cfg.SecretNamespace,
            Name:      cfg.SecretName,
        },
        CertDir:        cfg.CertDir,
        CAName:         caName,
        CAOrganization: caOrg,
        DNSName:        dnsName,
        IsReady:        setupFinish,
        Webhooks: []cert.WebhookInfo{
            {
                Type: cert.Mutating,
                Name: cfg.MutatingWebhookConfName,
            },
        },
    })
}

On the controller manager's main function, you will need to initialize the cert rotator like the following:

    // ...
    certsReady := make(chan struct{})

    if err = cert.CertsManager(mgr, cert.CertRotatorConfig{
        ServiceName:             serviceName,
        SecretName:              secretName,
        SecretNamespace:         secretNamespace,
        CertDir:                 certPath,
        MutatingWebhookConfName: mutatingWebhookConfName,
    }, certsReady); err != nil {
        setupLog.Error(err, "unable to setup cert rotation")
        os.Exit(1)
    }

    // certs gen won't finish until the controller manager is started
    // thus we need to run this in a goroutine
    go func() {
        // can't register the webhook until the certs are ready, so we run this in sequence
        setupLog.Info("waiting for the cert generation to complete")
        <-certsReady
        setupLog.Info("certs ready")

        webhooks.RegisterPodMutator(mgr)
    }()

    // ...

    setupLog.Info("starting manager")
    if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil {
        setupLog.Error(err, "problem running manager")
        os.Exit(1)
    }

As the comment in the code suggests, the certificate generation won't be ready until the controller manager is up and running (which is a bit of a twisty chicken and egg problem). As a result, we need to run the webhook registration, which depends on the certificate generation, in a separate goroutine.

It's important to note that you still need to bootstrap the Kubernetes secret before leveraging the cert rotator library. This can be achieved through various deployment tools such as Helm charts or Terraform configurations. Once the initial secret is in place, the cert rotator library will handle the rest of the certificate management lifecycle, including rotation and renewal.