[ k8s-operator 系列 ] 自动生成 webhook 证书并在过期时自动刷新

k8s webhook 要求必须使用 HTTPS,Operator webhook 的证书配置是个麻烦事。

众所周知 cert-manager 可以帮我们生成证书,但证书生成后还需要修改 ValidatingWebhookMutingWebhook

下面要介绍的是另一种方式:github.com/open-policy-agent/cert-controller,最初是在 open-policy-agent/gatekeeper 中看到这种用法,感觉非常 Geek。

Operator 内置:

  • 证书自动生成
  • 过期刷新
  • webhook 配置中的 caBundle 自动注入

不依赖外部组件,真正做到一键部署 Operator,非常方便分发 Operator。

使用

先启动 rotator:

setupFinished := make(chan struct{})
if !disableCertRotation {
  setupLog.Info("setting up cert rotation")
  err := rotator.AddRotator(mgr, &rotator.CertRotator{
    SecretKey: types.NamespacedName{
      Namespace: k8s.GetOperatorNamespace(),
      Name:      secretName,
    },
    CertDir:        certDir,
    CAName:         caName,
    CAOrganization: caOrganization,
    DNSName:        dnsName,
    IsReady:        setupFinished,
    Webhooks:       webhooks,
  })
  if err != nil {
    setupLog.Error(err, "unable to set up cert rotation")
    os.Exit(1)
  }
} else {
  close(setupFinished)
}

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

setupFinished 之后再启动 Reconciler 和 Webhook server:

go func() {
  <-setupFinished

  err := (&controllers.EtcdReconciler{
    Client: mgr.GetClient(),
    Log:    zaplog.Sugar().Named("controllers").Named("Etcd"),
    Scheme: mgr.GetScheme(),
  }).SetupWithManager(mgr)
  if err != nil {
    setupLog.Error(err, "unable to create controller", "controller", "Etcd")
    os.Exit(1)
  }

  if certDir != "" {
    err = (&dbv1.Etcd{}).SetupWebhookWithManager(mgr)
    if err != nil {
      setupLog.Error(err, "unable to SetupWebhookWithManager")
      os.Exit(1)
    }
  }
}()

controllerManager 有个特性,如果已经启动,则后面 Add 的 Runnable 直接启动:

// Add sets dependencies on i, and adds it to the list of Runnables to start.
func (cm *controllerManager) Add(r Runnable) error {
    // ...

    if shouldStart {
        // If already started, start the controller
        cm.startRunnable(r)
    }

    return nil
}

完整例子:win5do/etcd-operator (github.com)

原理

Webhook 使用的证书不要求一定是 k8s 集群 CA 根证书颁发的证书,所以我们可以使用自签证书。

生成自签 CA 根证书:

// CreateCACert creates the self-signed CA cert and private key that will
// be used to sign the server certificate
func (cr *CertRotator) CreateCACert(begin, end time.Time) (*KeyPairArtifacts, error) {
    // ...
    key, err := rsa.GenerateKey(rand.Reader, 2048)
    if err != nil {
        return nil, errors.Wrap(err, "generating key")
    }
    der, err := x509.CreateCertificate(rand.Reader, templ, templ, key.Public(), key)
    if err != nil {
        return nil, errors.Wrap(err, "creating certificate")
    }
    certPEM, keyPEM, err := pemEncode(der, key)
    if err != nil {
        return nil, errors.Wrap(err, "encoding PEM")
    }
    cert, err := x509.ParseCertificate(der)
    if err != nil {
        return nil, errors.Wrap(err, "parsing certificate")
    }

    return &KeyPairArtifacts{Cert: cert, Key: key, CertPEM: certPEM, KeyPEM: keyPEM}, nil
}

根据 service-name.namespace.svc 生成服务器 tls.key 和 tls.crt:

// CreateCertPEM takes the results of CreateCACert and uses it to create the
// PEM-encoded public certificate and private key, respectively
func (cr *CertRotator) CreateCertPEM(ca *KeyPairArtifacts, begin, end time.Time) ([]byte, []byte, error) {
    // ...
    key, err := rsa.GenerateKey(rand.Reader, 2048)
    if err != nil {
        return nil, nil, errors.Wrap(err, "generating key")
    }
    der, err := x509.CreateCertificate(rand.Reader, templ, ca.Cert, key.Public(), ca.Key)
    if err != nil {
        return nil, nil, errors.Wrap(err, "creating certificate")
    }
    certPEM, keyPEM, err := pemEncode(der, key)
    if err != nil {
        return nil, nil, errors.Wrap(err, "encoding PEM")
    }
    return certPEM, keyPEM, nil
}

写入到 secret 中, 并等待 mount:

// ensureCertsMounted ensure the cert files exist.
func (cr *CertRotator) ensureCertsMounted() {
    checkFn := func() (bool, error) {
        certFile := cr.CertDir + "/" + certName
        _, err := os.Stat(certFile)
        if err == nil {
            return true, nil
        }
        return false, nil
    }
    if err := wait.ExponentialBackoff(wait.Backoff{
        Duration: 1 * time.Second,
        Factor:   2,
        Jitter:   1,
        Steps:    10,
    }, checkFn); err != nil {
        crLog.Error(err, "max retries for checking certs existence")
        close(cr.certsNotMounted)
        return
    }
    crLog.Info(fmt.Sprintf("certs are ready in %s", cr.CertDir))
    close(cr.certsMounted)
}

启动一个 Reconceile 监听 secret,如果有更新则将 caBundle 注入到 webhook config 中:

func injectCertToWebhook(wh *unstructured.Unstructured, certPem []byte) error {
    webhooks, found, err := unstructured.NestedSlice(wh.Object, "webhooks")
    if err != nil {
        return err
    }
    if !found {
        return errors.New("`webhooks` field not found in ValidatingWebhookConfiguration")
    }
    for i, h := range webhooks {
        hook, ok := h.(map[string]interface{})
        if !ok {
            return errors.Errorf("webhook %d is not well-formed", i)
        }
        if err := unstructured.SetNestedField(hook, base64.StdEncoding.EncodeToString(certPem), "clientConfig", "caBundle"); err != nil {
            return err
        }
        webhooks[i] = hook
    }
    if err := unstructured.SetNestedSlice(wh.Object, webhooks, "webhooks"); err != nil {
        return err
    }
    return nil
}

无风的内存空间
编程相关,随手记下
652 声望
58 粉丝
0 条评论
推荐阅读
张晋涛:我的 2022 总结
大家好,我是张晋涛。2022 年已经结束,我每年都会惯例的做个小回顾,今年因为阳了在恢复身体,一直拖到了今天才写。生活在 2022 年初做回顾的时候,觉得 2021 是魔幻的一年,但现在看来 2022 年其实更加魔幻。一...

张晋涛6阅读 1k评论 2

封面图
http 和 https 的通信过程及区别
🎈 两者的区别端口: http 端口号是80, https 端口号是443传输协议: http 是超文本传输协议,属于明文传输; https 是安全的超文本传输协议,是经过 SSL 加密后的传输协议安全性: https 使用了 TLS/SSL 加密,...

tiny极客2阅读 2.8k评论 2

封面图
使用kubeasz部署高可用kubernetes集群
本实验采用kubeasz作为kubernetes环境部署工具,它是一个基于二进制方式部署和利用ansible-playbook实现自动化来快速部署高可用kubernetes集群的工具,详细介绍请查看kubeasz官方。本实验用到的所有虚拟机默认软...

李朝阳4阅读 711

Jvm调优与微服务资源分配
在没有接触微服务之前,我们的java程序一般都部署在WebLogic、Tomcat这类应用服务器上,这些应用服务器本身也是基于Jvm虚拟机的。一般我们统一对应用服务器做Jvm参数调优(分配多大内存,线程池限制等),而不用...

KerryWu阅读 5.7k

不背锅运维:一文搞清楚应用发布到k8s集群的基本流程
❝关于标签的主要作用:标记、过滤、关联(主要体现在deployment、pod、service,3者标签保持一致),可设定多个标签,建议设定至少2个标签,一个为项目标签,一个为应用标签。❞

不背锅运维1阅读 719

10分钟学会使用 Loki 日志聚合系统
Loki 通过使用类似 Prometheus 的标签索引机制来存储和查询日志数据,这使得它能够快速地进行分布式查询和聚合,而不需要将所有数据都从存储中加载到内存中。Loki还使用了压缩和切割日志数据的方法来减少存储空间...

Rainbond1阅读 450

封面图
gitlab-ce将https修改为http
索性我们禁用gitlab的https功能,将期恢复为http。后期我们再在部署一个nginx进行数据转发,然后在nginx上起用https并设置证书。这样应该就规避了gitlab的证书错误问题。

myskies1阅读 817

652 声望
58 粉丝
宣传栏