Kubernetes 对 API 访问提供了三种安全访问控制措施:认证、授权和 Admission Control。认证解决用户是谁的问题,授权解决用户能做什么的问题,Admission Control 则是资源管理方面的作用。通过合理的权限管理,能够保证系统的安全可靠。
K8S中Webhook的调用原理为首先向K8S集群中注册一个Admission Webhook(Validating / Mutating),所谓注册是向K8S集群注册一个地址,而实际Webhook服务可能跑在Pod里,也可能跑在开发机上;当创建资源的时候会调用这些Webhook进行修改或验证,最后持久化到ETCD中。
本文主要讲讲Admission中ValidatingAdmissionWebhook和MutatingAdmissionWebhook。
我们知道k8s在各个方面都具备可扩展性,比如通过cni实现多种网络模型,通过csi实现多种存储引擎,通过cri实现多种容器运行时等等。而AdmissionWebhook就是另外一种可扩展的手段。 除了已编译的Admission插件外,可以开发自己的Admission插件作为扩展,并在运行时配置为webhook。
Admission webhooks是HTTP回调,它接收Admission请求并对它们做一些事情。可以定义两种类型的Admission webhook,ValidatingAdmissionWebhook和MutatingAdmissionWebhook。
如果启用了MutatingAdmission,当开始创建一种k8s资源对象的时候,创建请求会发到你所编写的controller中,然后我们就可以做一系列的操作。比如我们的场景中,我们会统一做一些功能性增强,当业务开发创建了新的deployment,我们会执行一些注入的操作,比如敏感信息aksk,或是一些优化的init脚本。
而与此类似,只不过ValidatingAdmissionWebhook 是按照你自定义的逻辑是否允许资源的创建。比如,我们在实际生产k8s集群中,处于稳定性考虑,我们要求创建的deployment 必须设置request和limit。
官方提供了有个demo。大家可以详细研究,核心思想就是:
webhook处理apiservers发送的AdmissionReview请求,并将其决定作为AdmissionReview对象发送回去。
package main
import (
"encoding/json"
"flag"
"fmt"
"io/ioutil"
"net/http"
"k8s.io/api/admission/v1beta1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/klog"
// TODO: try this library to see if it generates correct json patch
// https://github.com/mattbaird/jsonpatch
)
// toAdmissionResponse is a helper function to create an AdmissionResponse
// with an embedded error
func toAdmissionResponse(err error) *v1beta1.AdmissionResponse {
return &v1beta1.AdmissionResponse{
Result: &metav1.Status{
Message: err.Error(),
},
}
}
// admitFunc is the type we use for all of our validators and mutators
type admitFunc func(v1beta1.AdmissionReview) *v1beta1.AdmissionResponse
// serve handles the http portion of a request prior to handing to an admit
// function
func serve(w http.ResponseWriter, r *http.Request, admit admitFunc) {
var body []byte
if r.Body != nil {
if data, err := ioutil.ReadAll(r.Body); err == nil {
body = data
}
}
// verify the content type is accurate
contentType := r.Header.Get("Content-Type")
if contentType != "application/json" {
klog.Errorf("contentType=%s, expect application/json", contentType)
return
}
klog.V(2).Info(fmt.Sprintf("handling request: %s", body))
// The AdmissionReview that was sent to the webhook
requestedAdmissionReview := v1beta1.AdmissionReview{}
// The AdmissionReview that will be returned
responseAdmissionReview := v1beta1.AdmissionReview{}
deserializer := codecs.UniversalDeserializer()
if _, _, err := deserializer.Decode(body, nil, &requestedAdmissionReview); err != nil {
klog.Error(err)
responseAdmissionReview.Response = toAdmissionResponse(err)
} else {
// pass to admitFunc
responseAdmissionReview.Response = admit(requestedAdmissionReview)
}
// Return the same UID
responseAdmissionReview.Response.UID = requestedAdmissionReview.Request.UID
klog.V(2).Info(fmt.Sprintf("sending response: %v", responseAdmissionReview.Response))
respBytes, err := json.Marshal(responseAdmissionReview)
if err != nil {
klog.Error(err)
}
if _, err := w.Write(respBytes); err != nil {
klog.Error(err)
}
}
func serveAlwaysDeny(w http.ResponseWriter, r *http.Request) {
serve(w, r, alwaysDeny)
}
func serveAddLabel(w http.ResponseWriter, r *http.Request) {
serve(w, r, addLabel)
}
func servePods(w http.ResponseWriter, r *http.Request) {
serve(w, r, admitPods)
}
func serveAttachingPods(w http.ResponseWriter, r *http.Request) {
serve(w, r, denySpecificAttachment)
}
func serveMutatePods(w http.ResponseWriter, r *http.Request) {
serve(w, r, mutatePods)
}
func serveConfigmaps(w http.ResponseWriter, r *http.Request) {
serve(w, r, admitConfigMaps)
}
func serveMutateConfigmaps(w http.ResponseWriter, r *http.Request) {
serve(w, r, mutateConfigmaps)
}
func serveCustomResource(w http.ResponseWriter, r *http.Request) {
serve(w, r, admitCustomResource)
}
func serveMutateCustomResource(w http.ResponseWriter, r *http.Request) {
serve(w, r, mutateCustomResource)
}
func serveCRD(w http.ResponseWriter, r *http.Request) {
serve(w, r, admitCRD)
}
func main() {
var config Config
config.addFlags()
flag.Parse()
http.HandleFunc("/always-deny", serveAlwaysDeny)
http.HandleFunc("/add-label", serveAddLabel)
http.HandleFunc("/pods", servePods)
http.HandleFunc("/pods/attach", serveAttachingPods)
http.HandleFunc("/mutating-pods", serveMutatePods)
http.HandleFunc("/configmaps", serveConfigmaps)
http.HandleFunc("/mutating-configmaps", serveMutateConfigmaps)
http.HandleFunc("/custom-resource", serveCustomResource)
http.HandleFunc("/mutating-custom-resource", serveMutateCustomResource)
http.HandleFunc("/crd", serveCRD)
server := &http.Server{
Addr: ":443",
TLSConfig: configTLS(config),
}
server.ListenAndServeTLS("", "")
}
您可以通过ValidatingWebhookConfiguration或MutatingWebhookConfiguration动态配置哪些资源受入口webhooks的限制。
具体示例如下:
apiVersion: admissionregistration.k8s.io/v1beta1
kind: ValidatingWebhookConfiguration
metadata:
name:
webhooks:
- name:
rules:
- apiGroups:
- ""
apiVersions:
- v1
operations:
- CREATE
resources:
- pods
scope: "Namespaced"
clientConfig:
service:
namespace:
name:
caBundle:
admissionReviewVersions:
- v1beta1
timeoutSeconds: 1
最后我们来总结下 webhook Admission 的优势: