自動(dòng)管理 Admission Webhook TLS 證書(shū)
前面我們學(xué)習(xí)了如何開(kāi)發(fā)自己的準(zhǔn)入控制器 Webhook,這些準(zhǔn)入 Webhook 控制器調(diào)用自定義配置的 HTTP 回調(diào)服務(wù)來(lái)進(jìn)行其他檢查。但是,APIServer 僅通過(guò) HTTPS 與 Webhook 服務(wù)進(jìn)行通信,并且需要 TLS 證書(shū)的 CA 信息。所以對(duì)于如何處理該 Webhook 服務(wù)證書(shū)以及如何將 CA 信息自動(dòng)傳遞給 APIServer 帶來(lái)了一些麻煩。
前面我們是通過(guò) openssl(cfssl)來(lái)手動(dòng)生成的相關(guān)證書(shū),然后手動(dòng)配置給 Webhook 服務(wù)的,除此之外,我們也可以使用 cert-manager 來(lái)處理這些 TLS 證書(shū)和 CA。但是,cert-manager 本身是一個(gè)比較大的應(yīng)用程序,由許多 CRD 組成來(lái)處理其操作。僅安裝 cert-manager 來(lái)處理準(zhǔn)入 webhook TLS 證書(shū)和 CA 不是一個(gè)很好的做法。
另外一種做法就是我們可以使用自簽名證書(shū),然后通過(guò)使用 Init 容器來(lái)自行處理 CA,這就消除了對(duì)其他應(yīng)用程序(如 cert-manager)的依賴(lài)。接下來(lái)我們就來(lái)重點(diǎn)介紹下如何使用這種方式來(lái)管理相關(guān)證書(shū)。
初始化容器
這個(gè)初始化容器的主要功能是創(chuàng)建一個(gè)自簽名的 Webhook 服務(wù)證書(shū),并通過(guò) mutate/驗(yàn)證配置將 caBundle 提供給 APIServer。Webhook 服務(wù)如何使用該證書(shū)(通過(guò) Secret Volumes 或 emptyDir),取決于實(shí)際情況。這里我們這個(gè)初始化容器將運(yùn)行一個(gè)簡(jiǎn)單的 Go 二進(jìn)制文件來(lái)執(zhí)行這些功能。核心代碼如下所示:
package?main
import?(
?"bytes"
?cryptorand?"crypto/rand"
?"crypto/rsa"
?"crypto/x509"
?"crypto/x509/pkix"
?"encoding/pem"
?"fmt"
?log?"github.com/sirupsen/logrus"
?"math/big"
?"os"
?"time"
)
func?main()?{
?var?caPEM,?serverCertPEM,?serverPrivKeyPEM?*bytes.Buffer
?//?CA?config
?ca?:=?&x509.Certificate{
??SerialNumber:?big.NewInt(2021),
??Subject:?pkix.Name{
???Organization:?[]string{"ydzs.io"},
??},
??NotBefore:?????????????time.Now(),
??NotAfter:??????????????time.Now().AddDate(1,?0,?0),
??IsCA:??????????????????true,
??ExtKeyUsage:???????????[]x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth,?x509.ExtKeyUsageServerAuth},
??KeyUsage:??????????????x509.KeyUsageDigitalSignature?|?x509.KeyUsageCertSign,
??BasicConstraintsValid:?true,
?}
?//?CA?private?key
?caPrivKey,?err?:=?rsa.GenerateKey(cryptorand.Reader,?4096)
?if?err?!=?nil?{
??fmt.Println(err)
?}
?//?Self?signed?CA?certificate
?caBytes,?err?:=?x509.CreateCertificate(cryptorand.Reader,?ca,?ca,?&caPrivKey.PublicKey,?caPrivKey)
?if?err?!=?nil?{
??fmt.Println(err)
?}
?//?PEM?encode?CA?cert
?caPEM?=?new(bytes.Buffer)
?_?=?pem.Encode(caPEM,?&pem.Block{
??Type:??"CERTIFICATE",
??Bytes:?caBytes,
?})
?dnsNames?:=?[]string{"admission-registry",
??"admission-registry.default",?"admission-registry.default.svc"}
?commonName?:=?"admission-registry.default.svc"
?//?server?cert?config
?cert?:=?&x509.Certificate{
??DNSNames:?????dnsNames,
??SerialNumber:?big.NewInt(1658),
??Subject:?pkix.Name{
???CommonName:???commonName,
???Organization:?[]string{"ydzs.io"},
??},
??NotBefore:????time.Now(),
??NotAfter:?????time.Now().AddDate(1,?0,?0),
??SubjectKeyId:?[]byte{1,?2,?3,?4,?6},
??ExtKeyUsage:??[]x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth,?x509.ExtKeyUsageServerAuth},
??KeyUsage:?????x509.KeyUsageDigitalSignature,
?}
?//?server?private?key
?serverPrivKey,?err?:=?rsa.GenerateKey(cryptorand.Reader,?4096)
?if?err?!=?nil?{
??fmt.Println(err)
?}
?//?sign?the?server?cert
?serverCertBytes,?err?:=?x509.CreateCertificate(cryptorand.Reader,?cert,?ca,?&serverPrivKey.PublicKey,?caPrivKey)
?if?err?!=?nil?{
??fmt.Println(err)
?}
?//?PEM?encode?the?server?cert?and?key
?serverCertPEM?=?new(bytes.Buffer)
?_?=?pem.Encode(serverCertPEM,?&pem.Block{
??Type:??"CERTIFICATE",
??Bytes:?serverCertBytes,
?})
?serverPrivKeyPEM?=?new(bytes.Buffer)
?_?=?pem.Encode(serverPrivKeyPEM,?&pem.Block{
??Type:??"RSA?PRIVATE?KEY",
??Bytes:?x509.MarshalPKCS1PrivateKey(serverPrivKey),
?})
?err?=?os.MkdirAll("/etc/webhook/certs/",?0666)
?if?err?!=?nil?{
??log.Panic(err)
?}
?err?=?WriteFile("/etc/webhook/certs/tls.crt",?serverCertPEM)
?if?err?!=?nil?{
??log.Panic(err)
?}
?err?=?WriteFile("/etc/webhook/certs/tls.key",?serverPrivKeyPEM)
?if?err?!=?nil?{
??log.Panic(err)
?}
}
//?WriteFile?writes?data?in?the?file?at?the?given?path
func?WriteFile(filepath?string,?sCert?*bytes.Buffer)?error?{
?f,?err?:=?os.Create(filepath)
?if?err?!=?nil?{
??return?err
?}
?defer?f.Close()
?_,?err?=?f.Write(sCert.Bytes())
?if?err?!=?nil?{
??return?err
?}
?return?nil
}
在上面的代碼中我們通過(guò)生成自簽名的 CA 并簽署 Webhook 服務(wù)證書(shū)來(lái)提供服務(wù):
首先為 CA 創(chuàng)建一個(gè)配置 ca 為該 CA 創(chuàng)建一個(gè) RSA 私鑰 caPrivKey 生成一個(gè)自簽名的 CA、caByte 和 caPEM,在這里,caPEM 是 PEM 編碼的 caBytes,將是提供給 APIServer 的 CA_BUNDLE 數(shù)據(jù) 創(chuàng)建 webhook 服務(wù)證書(shū)的配置,即上面代碼中的 cert。該配置中的重要屬性是 DNSNames 和 commonName,要注意的是該名稱(chēng)必須是到達(dá) Webhook 服務(wù)的完整地址名稱(chēng) 然后為 Webhook 服務(wù)創(chuàng)建一個(gè) RS 私鑰 serverPrivKey使用上面代碼中的 ca 和 caPrivKey創(chuàng)建服務(wù)端證書(shū)serverCertBytes然后用 PEM 對(duì) serverPrivKey和serverCertBytes進(jìn)行編碼,這個(gè)serverPrivKeyPEM和serverCertPEM就是 TLS 證書(shū)和密鑰了,將由 Webhook 服務(wù)使用。
到這里我們就可以生成所需的證書(shū),密鑰和 CA_BUNDLE 數(shù)據(jù)了。然后我們將與同一 Pod 中的實(shí)際 Webhook 服務(wù)容器共享該服務(wù)器證書(shū)和密鑰。
一種方法是事先創(chuàng)建一個(gè)空的 Secret 資源,通過(guò)將該 Secret 作為環(huán)境變量傳遞來(lái)創(chuàng)建 Webhook 服務(wù),初始化容器將生成服務(wù)器證書(shū)和密鑰,并用證書(shū)和密鑰信息來(lái)填充該 Secret。此 Secret 將安裝到 Webhook 服務(wù)容器上,以使用 TLS 來(lái)啟動(dòng) HTTP 服務(wù)器。 第二種方法(在上面的代碼中使用)是使用 Kubernete 的本地 Pod 特定的 emptyDir 卷。該數(shù)據(jù)卷將在兩個(gè)容器之間共享,在上面的代碼中,我們可以看到 init 容器將這些證書(shū)和密鑰信息寫(xiě)入特定路徑的文件中,該路徑就是其中的一個(gè) emptyDir 卷,并且 Webhook 服務(wù)容器將從該路徑讀取用于 TLS 配置的證書(shū)和密鑰,并啟動(dòng) HTTP Webhook 服務(wù)器。請(qǐng)參考下圖:
Webhook 的 Pod 規(guī)范如下所示:
spec:
??initContainers:
????-?image:??init-image?name>
??????imagePullPolicy:?IfNotPresent
??????name:?webhook-init
??????volumeMounts:
????????-?mountPath:?/etc/webhook/certs
??????????name:?webhook-certs
??containers:
????-?image:??server?image?name>
??????imagePullPolicy:?IfNotPresent
??????name:?webhook-server
??????volumeMounts:
????????-?mountPath:?/etc/webhook/certs
??????????name:?webhook-certs
??????????readOnly:?true
??volumes:
????-?name:?webhook-certs
??????emptyDir:?{}
處理 CA Bundle
然后剩下的就只有使用 mutate/驗(yàn)證配置將 ?CA_BUNDLE 信息提供給 APIServer,這可以通過(guò)兩種方式完成:
使用 init 容器中的 ?client-go 在現(xiàn)有 MutatingWebhookConfiguration或ValidatingWebhookConfiguration中來(lái)修補(bǔ) CA_BUNDLE 數(shù)據(jù)。另一種方式使用配置中的 CA_BUNDLE 數(shù)據(jù)在 init 容器本身中直接創(chuàng)建 MutatingWebhookConfiguration或ValidatingWebhookConfiguration即可。
在這里,我們將通過(guò) init 容器來(lái)創(chuàng)建配置,通過(guò)動(dòng)態(tài)獲取某些參數(shù),例如 mutate 配置名稱(chēng),Webhook 服務(wù)名稱(chēng)和 Webhook 命名空間,我們都可以直接從 init 容器的環(huán)境變量中來(lái)獲取這些值:
initContainers:
-?image:??init-image?name>
??imagePullPolicy:?IfNotPresent
??name:?webhook-init
??volumeMounts:
????-?mountPath:?/etc/webhook/certs
??????name:?webhook-certs
??env:
????-?name:?MUTATE_CONFIG
??????value:?admission-registry-mutate
????-?name:?VALIDATE_CONFIG
??????value:?admission-registry
????-?name:?WEBHOOK_SERVICE
??????value:?admission-registry
????-?name:?WEBHOOK_NAMESPACE
??????value:??default
為了創(chuàng)建 MutatingWebhookConfiguration 或者 ValidatingWebhookConfiguration 資源對(duì)象,我們將以下代碼添加到上面的 init 容器代碼中。
package?main
import?(
?"bytes"
?"context"
?"os"
?admissionregistrationv1?"k8s.io/api/admissionregistration/v1"
?metav1?"k8s.io/apimachinery/pkg/apis/meta/v1"
?"k8s.io/client-go/kubernetes"
?"k8s.io/client-go/rest"
)
func?initKubeClient()?(*kubernetes.Clientset,?error)?{
?var?(
??err?error
??config?*rest.Config
?)
?if?config,?err?=?rest.InClusterConfig();?err?!=?nil?{
??return?nil,?err
?}
?//?創(chuàng)建?Clientset?對(duì)象
?clientset,?err?:=?kubernetes.NewForConfig(config)
?if?err?!=?nil?{
??return?nil,?err
?}
?return?clientset,?nil
}
func?CreateAdmissionConfig(caCert?*bytes.Buffer)?error?{
?var?(
??webhookNamespace,?_?=?os.LookupEnv("WEBHOOK_NAMESPACE")
??mutationCfgName,?_??=?os.LookupEnv("MUTATE_CONFIG")
??validateCfgName,?_?=?os.LookupEnv("VALIDATE_CONFIG")
??webhookService,?_?=?os.LookupEnv("WEBHOOK_SERVICE")
??validatePath,?_?=?os.LookupEnv("VALIDATE_PATH")
??mutationPath,?_?=?os.LookupEnv("MUTATE_PATH")
?)
?clientset,?err?:=?initKubeClient()
?if?err?!=?nil?{
??return?err
?}
?ctx?:=?context.Background()
?if?validateCfgName?!=?""?{
??validateConfig?:=?&admissionregistrationv1.ValidatingWebhookConfiguration{
???ObjectMeta:?metav1.ObjectMeta{
????Name:?validateCfgName,
???},
???Webhooks:?[]admissionregistrationv1.ValidatingWebhook{
????{
?????Name:?"io.ydzs.admission-registry",
?????ClientConfig:?admissionregistrationv1.WebhookClientConfig{
??????CABundle:?caCert.Bytes(),
??????Service:?&admissionregistrationv1.ServiceReference{
???????Name:?webhookService,
???????Namespace:?webhookNamespace,
???????Path:?&validatePath,
??????},
?????},
?????Rules:?[]admissionregistrationv1.RuleWithOperations{
??????{
???????Operations:?[]admissionregistrationv1.OperationType{admissionregistrationv1.Create},
???????Rule:?admissionregistrationv1.Rule{
????????APIGroups:???[]string{""},
????????APIVersions:?[]string{"v1"},
????????Resources:???[]string{"pods"},
???????},
??????},
?????},
?????FailurePolicy:?func()?*admissionregistrationv1.FailurePolicyType{
??????pt?:=?admissionregistrationv1.Fail
??????return?&pt
?????}(),
?????AdmissionReviewVersions:?[]string{"v1"},
?????SideEffects:?func()?*admissionregistrationv1.SideEffectClass?{
??????se?:=?admissionregistrationv1.SideEffectClassNone
??????return?&se
?????}(),
????},
???},
??}
??validateAdmissionClient?:=?clientset.AdmissionregistrationV1().ValidatingWebhookConfigurations()
??_,?err?:=?validateAdmissionClient.Get(ctx,?validateCfgName,?metav1.GetOptions{})
??if?err?!=?nil?{
???if?errors.IsNotFound(err)?{
????if?_,?err?=?validateAdmissionClient.Create(ctx,?validateConfig,?metav1.CreateOptions{});?err?!=?nil?{
?????return?err
????}
???}?else?{
????return?err
???}
??}?else?{
???if?_,?err?=?validateAdmissionClient.Update(ctx,?validateConfig,?metav1.UpdateOptions{});?err?!=?nil?{
????return?err
???}
??}
?}
?if?mutationCfgName?!=?""?{
??mutateConfig?:=?&admissionregistrationv1.MutatingWebhookConfiguration{
???ObjectMeta:?metav1.ObjectMeta{
????Name:?mutationCfgName,
???},
???Webhooks:?[]admissionregistrationv1.MutatingWebhook{{
????Name:?"io.ydzs.admission-registry-mutate",
????ClientConfig:?admissionregistrationv1.WebhookClientConfig{
?????CABundle:?caCert.Bytes(),?//?CA?bundle?created?earlier
?????Service:?&admissionregistrationv1.ServiceReference{
??????Name:??????webhookService,
??????Namespace:?webhookNamespace,
??????Path:??????&mutationPath,
?????},
????},
????Rules:?[]admissionregistrationv1.RuleWithOperations{{Operations:?[]admissionregistrationv1.OperationType{
?????admissionregistrationv1.Create},
?????Rule:?admissionregistrationv1.Rule{
??????APIGroups:???[]string{"apps",?""},
??????APIVersions:?[]string{"v1"},
??????Resources:???[]string{"deployments",?"services"},
?????},
????}},
????FailurePolicy:?func()?*admissionregistrationv1.FailurePolicyType{
?????pt?:=?admissionregistrationv1.Fail
?????return?&pt
????}(),
????AdmissionReviewVersions:?[]string{"v1"},
????SideEffects:?func()?*admissionregistrationv1.SideEffectClass?{
?????se?:=?admissionregistrationv1.SideEffectClassNone
?????return?&se
????}(),
???}},
??}
??mutateAdmissionClient?:=?clientset.AdmissionregistrationV1().MutatingWebhookConfigurations()
??_,?err?:=?mutateAdmissionClient.Get(ctx,?mutationCfgName,?metav1.GetOptions{})
??if?err?!=?nil?{
???if?errors.IsNotFound(err)?{
????if?_,?err?=?mutateAdmissionClient.Create(ctx,?mutateConfig,?metav1.CreateOptions{});?err?!=?nil?{
?????return?err
????}
???}?else?{
????return?err
???}
??}?else?{
???if?_,?err?=?mutateAdmissionClient.Update(ctx,?mutateConfig,?metav1.UpdateOptions{});?err?!=?nil?{
????return?err
???}
??}
?}
?return?nil
}
這里首先我們讀取環(huán)境變量,例如 webhookNamespace,接下來(lái),我們將使用 CA bundle 信息(先前創(chuàng)建)和其他必需信息來(lái)定義配置的資源對(duì)象結(jié)構(gòu)。最后,我們使用 client-go 來(lái)創(chuàng)建配置資源對(duì)象。對(duì)于 Pod 重新啟動(dòng)或刪除的情況,我們可以在 init 容器中添加額外的邏輯,例如首先刪除現(xiàn)有配置,然后再僅在創(chuàng)建或更新 CA bundle(如果配置已存在)之前刪除它們。
對(duì)于證書(shū)輪換的情況,對(duì)于向服務(wù)器容器提供此證書(shū)所采用的每種方法,方法將有所不同:
如果我們使用的是 emptyDir 卷,則方法將是僅重新啟動(dòng) Webhook Pod。由于 emptyDir 卷是臨時(shí)的,并且綁定到 Pod 的生命周期,因此在重新啟動(dòng)時(shí),將生成一個(gè)新證書(shū)并將其提供給服務(wù)器容器。如果已經(jīng)存在配置,則將在配置中添加新的 CA bundle。 如果我們正在使用 Secret 卷,則在重新啟動(dòng) Webhook Pod 時(shí),可以檢查 Secret 中現(xiàn)有證書(shū)的有效期,以決定是將現(xiàn)有證書(shū)用于服務(wù)器還是創(chuàng)建新證書(shū)。
在這兩種情況下,都需要重新啟動(dòng) Webhook Pod 才能觸發(fā)證書(shū)輪換/續(xù)訂過(guò)程。何時(shí)需要重新啟動(dòng) Webhook 容器以及如何重新啟動(dòng) Webhook 容器,將取決于實(shí)際情況??赡艿膸追N方法可以使用 Cronjob、controller 等來(lái)實(shí)現(xiàn)。
到這里我們的自定義 Webhook 已注冊(cè),APIServer 可以通過(guò) config 讀取到 CA bundle 信息,并且 Webhook 服務(wù)已準(zhǔn)備好按照 configs 中定義的規(guī)則處理 mutate/驗(yàn)證請(qǐng)求。
部署
最后將上面的證書(shū)生成應(yīng)用打包成一個(gè) Docker 鏡像,將上節(jié)課部署的 Webhook 服務(wù)刪除,重新使用如下所示的資源對(duì)象進(jìn)行部署即可:
apiVersion:?v1
kind:?ServiceAccount
metadata:
??name:?admission-registry-sa
---
apiVersion:?rbac.authorization.k8s.io/v1
kind:?ClusterRole
metadata:
??name:?admission-registry-role
rules:
-?verbs:?["*"]
??resources:?["validatingwebhookconfigurations",?"mutatingwebhookconfigurations"]
??apiGroups:?["admissionregistration.k8s.io"]
---
apiVersion:?rbac.authorization.k8s.io/v1
kind:?ClusterRoleBinding
metadata:
??name:?admission-registry-rolebinding
roleRef:
??apiGroup:?rbac.authorization.k8s.io
??kind:?ClusterRole
??name:?admission-registry-role
subjects:
-?kind:?ServiceAccount
??name:?admission-registry-sa
??namespace:?default
---
apiVersion:?apps/v1
kind:?Deployment
metadata:
??name:?admission-registry
??labels:
????app:?admission-registry
spec:
??selector:
????matchLabels:
??????app:?admission-registry
??template:
????metadata:
??????labels:
????????app:?admission-registry
????spec:
??????serviceAccountName:?admission-registry-sa
??????initContainers:
??????-?image:?cnych/admission-registry-tls:v0.0.3
????????imagePullPolicy:?IfNotPresent
????????name:?webhook-init
????????env:
????????-?name:?WEBHOOK_NAMESPACE
??????????value:?default
????????-?name:?MUTATE_CONFIG
??????????value:?admission-registry-mutate
????????-?name:?VALIDATE_CONFIG
??????????value:?admission-registry
????????-?name:?WEBHOOK_SERVICE
??????????value:?admission-registry
????????-?name:?VALIDATE_PATH
??????????value:?/validate
????????-?name:?MUTATE_PATH
??????????value:?/mutate
????????volumeMounts:
??????????-?mountPath:?/etc/webhook/certs
????????????name:?webhook-certs
??????containers:
??????-?name:?webhook
????????image:?cnych/admission-registry:v0.1.4
????????imagePullPolicy:?IfNotPresent
????????env:
????????-?name:?WHITELIST_REGISTRIES
??????????value:?"docker.io,gcr.io"
????????ports:
????????-?containerPort:?443
????????volumeMounts:
????????-?name:?webhook-certs
??????????mountPath:?/etc/webhook/certs
??????????readOnly:?true
??????volumes:
????????-?name:?webhook-certs
??????????emptyDir:?{}
---
apiVersion:?v1
kind:?Service
metadata:
??name:?admission-registry
??labels:
????app:?admission-registry
spec:
??ports:
????-?port:?443
??????targetPort:?443
??selector:
????app:?admission-registry
現(xiàn)在我們就不需要自己手動(dòng)去創(chuàng)建包含證書(shū)的 Secret 資源對(duì)象了,也不需要手動(dòng)去替換準(zhǔn)入控制器配置對(duì)象中的 CA bundle 信息了,這些都將通過(guò) Init 初始化容器來(lái)幫我們自動(dòng)完成。
由于初始化容器需要訪問(wèn) MutatingWebhookConfiguration 和 ValidatingWebhookConfiguration 這兩個(gè)資源對(duì)象,所以我們需要聲明對(duì)應(yīng)的 RBAC 權(quán)限。創(chuàng)建完成后的資源對(duì)象如下所示:
$?kubectl?get?pods?-l?app=admission-registry
NAME??????????????????????????????????READY???STATUS????RESTARTS???AGE
admission-registry-64f6b46cdc-vqbrl???1/1?????Running???0??????????96s
$?kubectl?exec?-it?admission-registry-64f6b46cdc-vqbrl?--?ls?/etc/webhook/certs
tls.crt??tls.key
$?kubectl?get?validatingwebhookconfiguration???????
NAME?????????????????????????????WEBHOOKS???AGE
admission-registry???????????????1??????????20s
???admission-registry?git:(main)???kubectl?get?mutatingwebhookconfigurations????????
NAME????????????????????????WEBHOOKS???AGE
admission-registry-mutate???1??????????24s
然后同樣再去測(cè)試一次即可,到這里我們就完成了使用初始化容器來(lái)管理 Admission Webhook 的 TLS 證書(shū)的功能,當(dāng)然上面的代碼擴(kuò)展性并不是很好,后續(xù)可以根據(jù)需要繼續(xù)優(yōu)化即可。
CKA 認(rèn)證培訓(xùn)
?點(diǎn)擊屏末?|?閱讀原文?|?即刻學(xué)習(xí)

