前言
Kubernetes 对 API 访问提供了三种安全访问控制措施:认证、授权和 Admission Control。认证解决用户是谁的问题,授权解决用户能做什么的问题,Admission Control 则是资源管理方面的作用。通过合理的权限管理,能够保证系统的安全可靠。
本文主要讲讲 Admission 中 ValidatingAdmissionWebhook 和 MutatingAdmissionWebhook。
AdmissionWebhook
我们知道 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。
如何实现自己的 AdmissionWebhook Server
前提条件
- k8s 版本需至少 v1.9
- 确保启用了 MutatingAdmissionWebhook and ValidatingAdmissionWebhook admission controllers
- 确定 启用了 admissionregistration.k8s.io/v1beta1
写一个 admission webhook server
官方提供了有个 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("","")
}
动态配置 admission webhooks
您可以通过 ValidatingWebhookConfiguration 或 MutatingWebhookConfiguration 动态配置哪些资源受入口 webhooks 的限制。
具体示例如下:
apiVersion: admissionregistration.k8s.io/v1beta1
kind: ValidatingWebhookConfiguration
metadata:
name: <name of this configuration object>
webhooks:
- name: <webhook name, e.g., pod-policy.example.io>
rules:
- apiGroups:
- ""
apiVersions:
- v1
operations:
- CREATE
resources:
- pods
scope: "Namespaced"
clientConfig:
service:
namespace: <namespace of the front-end service>
name: <name of the front-end service>
caBundle: <pem encoded ca cert that signs the server cert used by the webhook>
admissionReviewVersions:
- v1beta1
timeoutSeconds: 1