乐趣区

k8s与Admissionwebhook-admission

前言

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
退出移动版