kubernetes中的AOP - 准入控制

1.Admission Controllers

在整体的流程中可以理解为过滤器,拦截器甚至是AOP中的切面。准入控制是通过一个个插件实现,每个插件实现特定功能。例如ServiceAccount插件就是为了完成验证工作,MutatingAdmissionWebhook 和ValidatingAdmissionWebhook是为了实现动态注入控制。

2.kubernetes本身有哪些webhook

image.png

其中
NamespaceLifecycle, LimitRanger, ServiceAccount, TaintNodesByCondition, Priority, DefaultTolerationSeconds, DefaultStorageClass, StorageObjectInUseProtection, PersistentVolumeClaimResize, MutatingAdmissionWebhook, ValidatingAdmissionWebhook, ResourceQuota是默认开启的

  • AlwaysAdmit
    使用这个插件自行通过所有的请求。

  • AlwaysDeny
    拒绝所有的请求。用于测试

  • AlwaysPullImages
    不管镜像的拉去策略是那种,都将会被改成Always。在多租户的场景下,即便镜像被拉去到本地,其他租户的pod也使用不了

  • ServiceAccount
    做ServiceAccount的验证

  • NamespaceLifecycle
    这个插件强制不能在一个正在被终止的 Namespace 中创建新对象,和确保使用不存在 Namespace 的请求被拒绝。

  • PodNodeSelector
    通过读取命名空间注释和全局配置,这个插件默认并限制了在一个命名空间中使用什么节点选择器。

  • other
    其他

3.动态准入控制 Dynamic Admission Control

上面所说的准入控制都是kubernetes自身的,并且是编译时织如。对于面向定制化、插件化的kubernetes提供了运行时的动态实现,可以让开发人员实现相关插件,动态织入并生效。

4.如何实现自己的准入控制插件

首先了解下Admission webhooks,它是接收许可请求并对其执行操作的HTTP回调,Admission webhooks有两个两种类型validating admission Webhookmutating admission webhook

  • mutating admission webhook
    mutating 是最先被注入的,并且他可以拦截并修改修改发送给API server的请求。
  • validating admission Webhook
    在mutating 修改完请求并被apiserver验证之后,validating 将被注入。根据校验规则可以拒绝强制执行自定义策略的请求。

注意:为了保证对象的最终状态,都应该有一个validating admission Webhook去验证,因为准入控制产检有后多个,在后续进过其他插件式对象数据可能会被修改。

Admission webhooks属于kubernetes控制面的一部分。

4.1、使用Admission webhooks的条件
  • kubernetes版本>1.9
  • MutatingAdmissionWebhook 和ValidatingAdmissionWebhook被启用,默认启用
  • admissionregistration.k8s.io/v1或者admissionregistration.k8s.io/v1beta1在APiserver中被启用
4.2、kubernetes有哪些API

既然webhook是http的回调,那么首先得了解kubernetes中有哪些API接口

[root@node4 pki]# kubectl get --raw=/api/v1 |jq . |grep name\":
      "name": "bindings",
      "name": "componentstatuses",
      "name": "configmaps",
      "name": "endpoints",
      "name": "events",
      "name": "limitranges",
      "name": "namespaces",
      "name": "namespaces/finalize",
      "name": "namespaces/status",
      "name": "nodes",
      "name": "nodes/proxy",
      "name": "nodes/status",
      "name": "persistentvolumeclaims",
      "name": "persistentvolumeclaims/status",
      "name": "persistentvolumes",
      "name": "persistentvolumes/status",
      "name": "pods",
      "name": "pods/attach",
      "name": "pods/binding",
      "name": "pods/eviction",
      "name": "pods/exec",
      "name": "pods/log",
      "name": "pods/portforward",
      "name": "pods/proxy",
      "name": "pods/status",
      "name": "podtemplates",
      "name": "replicationcontrollers",
      "name": "replicationcontrollers/scale",
      "name": "replicationcontrollers/status",
      "name": "resourcequotas",
      "name": "resourcequotas/status",
      "name": "secrets",
      "name": "serviceaccounts",
      "name": "services",
      "name": "services/proxy",
      "name": "services/status",

kubernetes中资源是有类型和版本的,不同的资源类型和版本API也不同,其他可自行查看,常用的如下

[root@node4 pki]# kubectl get --raw=/apis/extensions/v1beta1 |jq . |grep name\":
[root@node4 pki]# kubectl get --raw=/apis/batch/v1 |jq . |grep name\":
[root@node4 pki]# kubectl get --raw=/apis/apps/v1 |jq . |grep name\":
4.3、针对特定的接口添加回调函数

以下两个场景来举例说明。创建namespaces时通过webhook添加label,创建pod是为其注入新的容器(istio中的sidecar实现类似)。

注册回调事件

http.HandleFunc("/namespaces", serveNamespaces)
http.HandleFunc("/pods", servePods)
server := &http.Server{
    Addr:      ":443",
    TLSConfig: configTLS(config),
}

将请求报文体转换为AdmissionReview

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)
    }

给pod注入label和sidecar 容器

func mutatePods(ar v1beta1.AdmissionReview) *v1beta1.AdmissionResponse {
    data,_ := json.Marshal(ar.Request.Object)
    fmt.Println(string(data))
    klog.V(2).Info("mutating pods")
    podResource := metav1.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"}
    if ar.Request.Resource != podResource {
        klog.Errorf("expect resource to be %s", podResource)
        return nil
    }

    raw := ar.Request.Object.Raw
    pod := corev1.Pod{}
    deserializer := codecs.UniversalDeserializer()
    if _, _, err := deserializer.Decode(raw, nil, &pod); err != nil {
        klog.Error(err)
        return toAdmissionResponse(err)
    }
    reviewResponse := v1beta1.AdmissionResponse{}
    reviewResponse.Allowed = true
    fmt.Println("为了测试,此处指定只有pod name=demo时才注入")
    if pod.Name == "demo" {
        saMountName,saMount := getSAMount(pod.Spec.Containers)
        fmt.Println("原pod的secretMount的挂载名称:"+saMountName)

        needAddedContainers := injectPod(saMount);
        patch := injectLabels()
        for _, container := range needAddedContainers{
            patch = append(patch, PatchOperation{
                Op:    "add",
                Path:  "/spec/containers/-",//此处虚注意,如果是数组类型,非第一个需要加上“/-”
                Value: container,
            })
        }
        patchBytes ,_ := json.Marshal(patch)
        reviewResponse.Patch = patchBytes
        fmt.Println("==========" + string(reviewResponse.Patch))
        pt := v1beta1.PatchTypeJSONPatch
        reviewResponse.PatchType = &pt
    }
    return &reviewResponse
}

func injectPod(secretMount corev1.VolumeMount) []corev1.Container{
    return []corev1.Container{
        {
            Name: "agent",
            Image: "prima/filebeat:6",
            ImagePullPolicy: corev1.PullIfNotPresent,
            VolumeMounts: []corev1.VolumeMount{secretMount},
        },
    }
}


//获取原容器中serviceaccount的挂载目录
func getSAMount(originContainer []corev1.Container)(string,corev1.VolumeMount){
    mountName := ""
    var mount corev1.VolumeMount
    // find service account secret volume mount(/var/run/secrets/kubernetes.io/serviceaccount,
    // https://kubernetes.io/docs/reference/access-authn-authz/service-accounts-admin/#service-account-automation) from app container
    for _, add := range originContainer {
        for _, vmount := range add.VolumeMounts {
            if vmount.MountPath == "/var/run/secrets/kubernetes.io/serviceaccount" {
                mountName = vmount.Name
                mount = vmount
            }
        }
    }
    return mountName,mount
}

func injectLabels() []PatchOperation{
    return []PatchOperation{
        {
            Op: "add",
            Path: "/metadata/labels",
            Value: map[string]string{
                "webhook-label": "HelloWebhook",
            },
        },
    }
}
4.4、如何部署admission webhook服务

部署admission webhook需要创建MutatingWebhookConfigurationValidatingWebhookConfiguration两个配置文件。

一个配置文件中可以包含多个webhook的配置,多个配置通过名称区分。Configuration的版本目前有v1和v1beta1,相比v1,v1beta1功能更加健壮,同时审核日志和度量更易于与激活的配置匹配。

①为webhook生成证书,此处使用cfssl

准备生成证书的配置文件
ca-csr.json

{
    "CN": "etcd",
    "key": {
        "algo": "rsa",
        "size": 2048
    },
    "names": [{
        "C": "CN",
        "ST": "NanJing",
        "L": "NanJing",
        "O": "Kubernetes",
        "OU": "Kubernetes-manual"
    }]
}

webhook-csr.json

{
    "CN": "webhook",
        "hosts": [
      "webhook.default.svc",
      "10.48.17.175",
          "localhost",
          "kubernetes",
          "kubernetes.default",
          "kubernetes.default.svc",
          "kubernetes.default.svc.cluster",
          "kubernetes.default.svc.cluster.local"
        ],

    "key": {
        "algo": "rsa",
        "size": 2048
    },
    "names": [{
        "C": "CN",
        "ST": "NanJing",
        "L": "NanJing",
        "O": "Kubernetes",
        "OU": "Kubernetes-manual"
    }]
}

生成配置证书文件

# 产生根证书
[root@node4 pki]# cfssl gencert -initca ca-csr.json | cfssljson -bare ca
[root@node4 pki]# cfssl gencert \
  -ca=ca.pem \
  -ca-key=ca-key.pem \
  -config=ca-config.json \
  -profile=kubernetes \
  webhook-csr.json | cfssljson -bare webhook
[root@node4 pki]# ll
total 36
-rw-r--r-- 1 root root  342 Nov 20 11:31 ca-config.json
-rw-r--r-- 1 root root 1017 Nov 20 11:27 ca.csr
-rw-r--r-- 1 root root  250 Nov 20 11:31 ca-csr.json
-rw------- 1 root root 1675 Nov 20 11:31 ca-key.pem
-rw-r--r-- 1 root root 1391 Nov 20 11:27 ca.pem
-rw-r--r-- 1 root root 1289 Nov 20 11:31 webhook.csr
-rw-r--r-- 1 root root  550 Nov 20 11:31 webhook-csr.json
-rw------- 1 root root 1675 Nov 20 11:31 webhook-key.pem
-rw-r--r-- 1 root root 1675 Nov 20 11:31 webhook.pem

②部署为webhook server

webhook server的部署可以通过deployment部署在集群内也可以部署在集群外,此处为了调试,直接在外部启动(注意证书生成时要指定外部的IP),可以通过tls-cert-file和tls-private-key-file指定证书所在位置。

    var config Config
    config.CertFile ="F:/pki/webhook/webhook.pem"
    config.KeyFile="F:/pki/webhook/webhook-key.pem"
    config.addFlags()

    flag.Parse()
    http.HandleFunc("/namespaces", serveNamespaces)
    http.HandleFunc("/pods", servePods)
    http.HandleFunc("/ping", pong)
    server := &http.Server{
        Addr:      ":443",
        TLSConfig: configTLS(config),
    }
    fmt.Println("start ......")
    server.ListenAndServeTLS("", "")

③配置ValidatingWebhookConfiguration和MutatingWebhookConfiguration规则

MutatingWebhookConfiguration.yaml

kind: MutatingWebhookConfiguration
metadata:
  name: pod-webhook
webhooks:
  - name: pod.cloud.org
    failurePolicy: Fail
    clientConfig:
      url: "https://10.48.17.175:443/pods"
      caBundle: "Ci0tLS0tQk......tLS0K"
    rules:
      - operations: [ "CREATE" ]
        apiGroups: [""]
        apiVersions: ["v1"]
        resources: ["pods"]
        scope: "*"

ValidatingWebhookConfiguration.yaml

kind: ValidatingWebhookConfiguration
metadata:
  name: pod-webhook
webhooks:
  - name: pod.cloud.org
    failurePolicy: Fail
    clientConfig:
      url: "https://10.48.17.175:443/pods"
      caBundle: "Ci0tLS0tQk......tLS0K"
    rules:
      - operations: [ "CREATE" ]
        apiGroups: [""]
        apiVersions: ["v1"]
        resources: ["pods"]
        scope: "*"
  • clientConfig
    求达到API server之后规则匹配后,就需要知道发给哪些webhook进行处理。clientConfig字段就是来指定的webhook,主要有urlservice两种方式。

    URL方式直接指定webhook服务地址,URL方式必须是https协议。如果有DNS可以可以通过域名访问,另外不建议使用localhost或127.0.0.1

     clientConfig:
      url: "https://10.48.17.175:443/pods"
      caBundle: "Ci0tLS0tQk......tLS0K"
    

    Service方式主要借助于内部域名的方式

    clientConfig:
      caBundle: "Ci0tLS0tQk......tLS0K"
      service:
        namespace: my-service-namespace
        name: my-service-name
        path: /my-path
        port: 1234
    

    注意: caBundle是对证书进行base64处理的结果,其中ca.pem是生成证书是产生的文件

     cat  ca.pem | base64 | tr -d '\n'
    
  • operations
    指一系列的方法(操作)类型,可选值CREATE, UPDATE, DELETE, CONNECT, *

  • apiGroups
    资源所属组,kubernetes中资源组分为核心组(core)和其他类型。核心组路径为/api/v1,组为空。其他组一般是指扩展组,路劲/apis/$GROUP_NAME,如extensions,apps,batch,autoscaling,policy等。*表示匹配所有资源组。

  • apiVersions
    指资源的版本,如v1, v1beta1,*表示匹配所有版本

  • resources
    指具体的资源类型,支持简单的表达式配置,如下:
    * 匹配所有资源,但不包括子资源
    */* 匹配所有资源,并且包括子资源
    pods/* 匹配pods下的所有子资源.
    */status 匹配pods下的所有资源下的子资源status

  • scope
    webhook的作用域,可选值如下:
    Cluster:集群级别的资源都匹配这个规则
    Namespaced:Namespace级别匹配
    *: 匹配所有,无限制

除此之外,在1.15+版本中,可以规则还可以增加对象标签匹配、命名空间匹配,策略匹配(matchPolicy)。

apiVersion: admissionregistration.k8s.io/v1beta1
kind: MutatingWebhookConfiguration
webhooks:
- name: my-webhook.example.com
  objectSelector:
    matchLabels:
      foo: bar
  namespaceSelector:
     matchExpressions:
     - key: runlevel
       operator: NotIn
       values: ["0","1"]
  rules:
  - operations: ["CREATE"]
    apiGroups: ["*"]
    apiVersions: ["*"]
    resources: ["*"]
    scope: "*"

④部署配置

[root@node4 webhook]# kubectl apply -f MutatingWebhookConfiguration.yaml
[root@node4 webhook]# kubectl apply -f ValidatingWebhookConfiguration.yaml

只有匹配上述规则的请求才会被发送到对应的webhook

5.webhook测试

5.1测试名称为demo的pod

编写pod编排文件

apiVersion: v1
kind: Pod
metadata:
  name: demo
  namespace: webhook
spec:
  containers:
  - name: demo
    image: nginx:1.11.7
    imagePullPolicy: IfNotPresent

部署pod测试

[root@node4 webhook]# kubectl create ns webhook
namespace/webhook created
[root@node4 webhook]# kubectl apply -f nginx-pod.yaml 
pod/demo created

查看webhook打印的日志

image.png

查看标签和容器是否被注入

标签已被注入

image.png

容器已被注入

image.png

5.1测试名称为demo-webhook的pod

标签未注入


image.png

demo源码地址

你可能感兴趣的:(kubernetes中的AOP - 准入控制)