开发apiserver实现一个容器镜像白名单的 K8S 准入控制器

在这里插入图片描述在这里插入图片描述我们使用go编写validate和mutate

package main

import (
	"context"
	"crypto/tls"
	"flag"
	"fmt"
	"net/http"
	"os"
	"os/signal"
	"strings"
	"syscall"
	"webhook/pkg"

	"k8s.io/klog/v2"
)

func main() {
	var param pkg.WhSvrParam
	flag.IntVar(&param.Port, "port", 443, "webhook server port")
	flag.StringVar(&param.Certfile, "tlsCertfile", "/etc/webhook/certs/tls.crt", "x509 certification file")
	flag.StringVar(&param.KeyFile, "tlsKeyfile", "/etc/webhook/certs/tls.key", "x509 private key file")
	flag.Parse()

	certficate, err := tls.LoadX509KeyPair(param.Certfile, param.KeyFile)
	if err != nil {
		klog.Errorf("Failed to load key pair: &v", err)
		return
	}

	whsrv := pkg.WebhookServer{
		Server: &http.Server{
			Addr: fmt.Sprintf(":%d", param.Port),
			TLSConfig: &tls.Config{
				Certificates: []tls.Certificate{certficate},
			},
		},
		WhiteListRegistries: strings.Split(os.Getenv("WHITELIST_REGISTRIES"), ","),
	}

	mux := http.NewServeMux()
	mux.HandleFunc("/validate", whsrv.Handler)
	mux.HandleFunc("/mutate", whsrv.Handler)
	whsrv.Server.Handler = mux

	go func() {
		if err := whsrv.Server.ListenAndServeTLS("", ""); err != nil {
			klog.Errorf("failed to listen and serve webhook: %v", err)
		}
	}()

	klog.Info("Serevr start")

	signalChan := make(chan os.Signal, 1)
	signal.Notify(signalChan, syscall.SIGINT, syscall.SIGTERM)
	<-signalChan

	klog.Info("get os shuting...")
	if err := whsrv.Server.Shutdown(context.Background()); err != nil {
		klog.Errorf("http server shutdown err: %v", err)
	}
}

package pkg

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"net/http"
	"strings"

	admissionv1 "k8s.io/api/admission/v1"
	appsv1 "k8s.io/api/apps/v1"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/runtime/serializer"
	"k8s.io/klog/v2"
)

type patchOperation struct {
	Op    string      `json:"op"`
	Path  string      `json:"path"`
	Value interface{} `json:"value,omitempty"`
}

const (
	AnnotationMutateKey = "io.ydzs.admission-registry/mutate"
	AnnotationStatusKey = "io.ydzs.admission-registry/status"
)

var (
	runtimeScheme = runtime.NewScheme()
	codeFactory   = serializer.NewCodecFactory(runtimeScheme)
	deserializer  = codeFactory.UniversalDeserializer()
)

type WhSvrParam struct {
	Port     int
	Certfile string
	KeyFile  string
}

type WebhookServer struct {
	Server              *http.Server
	WhiteListRegistries []string
}

func (s *WebhookServer) Handler(w http.ResponseWriter, r *http.Request) {
	var body []byte
	if r.Body != nil {
		if data, err := ioutil.ReadAll(r.Body); err == nil {
			body = data
		}
	}

	if len(body) == 0 {
		klog.Errorf("empty data body")
		http.Error(w, "empty data body", http.StatusBadRequest)
		return
	}

	contentType := r.Header.Get("Content-Type")
	if contentType != "application/json" {
		klog.Errorf("content-type id %s, but expect application/json", contentType)
		http.Error(w, "content-type invalid, expect application/json", http.StatusBadRequest)
		return
	}

	var admissionReponse *admissionv1.AdmissionResponse
	requestedAdmissionReview := admissionv1.AdmissionReview{}

	if _, _, err := deserializer.Decode(body, nil, &requestedAdmissionReview); err != nil {
		klog.Errorf("can not decode body: %v", err)
		admissionReponse = &admissionv1.AdmissionResponse{
			Result: &metav1.Status{
				Code:    http.StatusInternalServerError,
				Message: err.Error(),
			},
		}
	} else {
		if r.URL.Path == "/mutate" {
			admissionReponse = s.mutate(&requestedAdmissionReview)
		} else if r.URL.Path == "/validate" {
			admissionReponse = s.validate(&requestedAdmissionReview)
		}
	}

	//构造返回的 admissionreview
	reponseAdmissionReview := admissionv1.AdmissionReview{}
	reponseAdmissionReview.APIVersion = requestedAdmissionReview.APIVersion
	reponseAdmissionReview.Kind = requestedAdmissionReview.Kind

	if admissionReponse != nil {
		reponseAdmissionReview.Response = admissionReponse
		if requestedAdmissionReview.Request != nil {
			reponseAdmissionReview.Response.UID = requestedAdmissionReview.Request.UID
		}
	}

	klog.Info(fmt.Printf("sending reponse: %v", requestedAdmissionReview.Response))

	respBytes, err := json.Marshal(reponseAdmissionReview)
	if err != nil {
		klog.Errorf("can not encode reponse: %v ", err)
		http.Error(w, fmt.Sprintf("could not encode reponse: %v", err), http.StatusInternalServerError)
	}

	klog.Info("read to write reponse...")
	if _, err := w.Write(respBytes); err != nil {
		klog.Errorf("can not write response: %v", err)
		http.Error(w, fmt.Sprintf("could not write reponse: %v", err), http.StatusInternalServerError)
	}

}

// validate pod
func (serv *WebhookServer) validate(ar *admissionv1.AdmissionReview) *admissionv1.AdmissionResponse {
	req := ar.Request
	var (
		allowed = true
		code    = 200
		message = ""
	)

	klog.Infof("AdmissionReview for Kind=%s, Namespace=%s Name=%v UID=%v Operation=%v UserInfo=%v",
		req.Kind.Kind, req.Namespace, req.Name, req.UID, req.Operation, req.UserInfo)

	var pod corev1.Pod
	if err := json.Unmarshal(req.Object.Raw, &pod); err != nil {
		klog.Errorf("Could not unmarshal raw object: %v", err)
		allowed = false
		code = 400
		return &admissionv1.AdmissionResponse{
			Allowed: allowed,
			Result: &metav1.Status{
				Code:    int32(code),
				Message: err.Error(),
			},
		}
	}
	for _, container := range pod.Spec.Containers {
		var whitelisted = false
		for _, reg := range serv.WhiteListRegistries {
			if strings.HasPrefix(container.Image, reg) {
				whitelisted = true
			}
		}
		if !whitelisted {
			allowed = false
			code = 403
			message = fmt.Sprintf("%s image comes from an untrusted registry! Only images from %v are allowed.",
				container.Image, serv.WhiteListRegistries)
			break
		}
	}

	return &admissionv1.AdmissionResponse{
		Allowed: allowed,
		Result: &metav1.Status{
			Code:    int32(code),
			Message: message,
		},
	}
}

func (s *WebhookServer) mutate(ar *admissionv1.AdmissionReview) *admissionv1.AdmissionResponse {
	req := ar.Request

	var (
		objectMeta *metav1.ObjectMeta
	)

	klog.Infof("admissionreview for kind=%s, namespace=%s", req.Kind.Kind, req.Namespace)

	switch req.Kind.Kind {
	case "Deployment":
		var deployment appsv1.Deployment
		if err := json.Unmarshal(req.Object.Raw, &deployment); err != nil {
			klog.Errorf("can not unmarshal %v", err)
			return &admissionv1.AdmissionResponse{
				Result: &metav1.Status{
					Code:    http.StatusBadRequest,
					Message: err.Error(),
				},
			}
		}
		objectMeta = &deployment.ObjectMeta
	case "Service":
		var service corev1.Service
		if err := json.Unmarshal(req.Object.Raw, &service); err != nil {
			klog.Errorf("can not unmarshal %v", err)
			return &admissionv1.AdmissionResponse{
				Result: &metav1.Status{
					Code:    http.StatusBadRequest,
					Message: err.Error(),
				},
			}
		}
		objectMeta = &service.ObjectMeta
	default:
		return &admissionv1.AdmissionResponse{
			Result: &metav1.Status{
				Code:    http.StatusBadRequest,
				Message: fmt.Sprintf("can not handle the kind %s object", req.Kind.Kind),
			},
		}
	}

	if !mutateRequired(objectMeta) {
		return &admissionv1.AdmissionResponse{
			Allowed: true,
		}
	}

	annotations := map[string]string{AnnotationStatusKey: "mutated"}

	var patch []patchOperation

	patch = append(patch, mutateAnnotations(objectMeta.GetAnnotations(), annotations)...)

	fmt.Println("********")
	fmt.Println(patch)
	fmt.Println("********")
	patchBytes, err := json.Marshal(patch)
	if err != nil {
		klog.Errorf("patch marshal error: %v", err)
		return &admissionv1.AdmissionResponse{
			Result: &metav1.Status{
				Code:    http.StatusBadRequest,
				Message: err.Error(),
			},
		}
	}

	return &admissionv1.AdmissionResponse{
		Allowed: true,
		Patch:   patchBytes,
		PatchType: func() *admissionv1.PatchType {
			pt := admissionv1.PatchTypeJSONPatch
			return &pt
		}(),
	}
}

func mutateAnnotations(target map[string]string, added map[string]string) (patch []patchOperation) {
	for key, value := range added {
		if target == nil || target[key] == "" {
			target = map[string]string{}
			patch = append(patch, patchOperation{
				Op:   "add",
				Path: "/metadata/annotations",
				Value: map[string]string{
					key: value,
				},
			})
		} else {
			patch = append(patch, patchOperation{
				Op:    "replace",
				Path:  "/metadata/annotations/" + key,
				Value: value,
			})
		}
	}
	return
}

func mutateRequired(metadata *metav1.ObjectMeta) bool {
	annotations := metadata.GetAnnotations()

	if annotations == nil {
		annotations = map[string]string{}
	}

	var required bool

	switch strings.ToLower(annotations[AnnotationMutateKey]) {
	case "n", "no", "false", "off":
		required = false
	default:
		required = true
	}

	status := annotations[AnnotationStatusKey]

	if strings.ToLower(status) == "mutated" {
		required = false
	}

	klog.Infof("mutation policy for %s/%s: required: %v", metadata.Name, metadata.Namespace, required)
	return required
}

dockerfile如下

FROM golang:1.18 as builder

RUN apt-get -y update && apt-get -y install upx

WORKDIR /workspace
# Copy the Go Modules manifests
COPY go.mod go.mod
COPY go.sum go.sum

# Copy the go source
COPY main.go main.go
COPY pkg/ pkg/

# Build
ENV CGO_ENABLED=0
ENV GOOS=linux
ENV GOARCH=amd64
ENV GO111MODULE=on
ENV GOPROXY="https://goproxy.cn"

# cache deps before building and copying source so that we don't need to re-download as much
# and so that source changes don't invalidate our downloaded layer
RUN go mod download && \
    go build -a -o admission-registry main.go && \
    upx admission-registry

FROM alpine:3.9.2
COPY --from=builder /workspace/admission-registry .
ENTRYPOINT ["/admission-registry"]

我们把认证作为pod运行在集群中

apiVersion: apps/v1
kind: Deployment
metadata:
  name: admission-deploy
  labels: 
    app: admission-deploy
spec:
  replicas: 1
  selector: 
    matchLabels: 
      app: admission-deploy
  template:
    metadata:
      labels:
        app: admission-deploy 
    spec:
      containers: 
        - name: whitelist
          image: webhook-validate-mutate:v1.0
          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
          secret:
            secretName: admission-registry-tls
---
apiVersion: v1
kind: Service
metadata:
  name: admission-registry
  labels:
    app: admission-registry
spec:
  ports:
  - port: 443
    targetPort: 443
  selector:
    app: admission-deploy

在这一步之前需要创建secret,接着需要创建validatingwebhookconfigurations,mutatingwebhookconfigurations

apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
  name: admission-registry
webhooks:
- name: io.ydzs.admission-registry
  rules:
  - apiGroups:   [""]
    apiVersions: ["v1"]
    operations:  ["CREATE"]
    resources:   ["pods"]
  clientConfig:
    service:
      namespace: default
      name: admission-registry
      path: "/validate"
    caBundle: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUR2akNDQXFhZ0F3SUJBZ0lVUkZkVTlRNTZXenA0UFNwUHQ0U05sdm9LOVBZd0RRWUpLb1pJaHZjTkFRRUwKQlFBd1pURUxNQWtHQTFVRUJoTUNRMDR4RURBT0JnTlZCQWdUQjBKbGFVcHBibWN4RURBT0JnTlZCQWNUQjBKbAphVXBwYm1jeEREQUtCZ05WQkFvVEEyczRjekVQTUEwR0ExVUVDeE1HVTNsemRHVnRNUk13RVFZRFZRUURFd3ByCmRXSmxjbTVsZEdWek1CNFhEVEl6TURZd016QXlNVEV3TUZvWERUSTRNRFl3TVRBeU1URXdNRm93WlRFTE1Ba0cKQTFVRUJoTUNRMDR4RURBT0JnTlZCQWdUQjBKbGFVcHBibWN4RURBT0JnTlZCQWNUQjBKbGFVcHBibWN4RERBSwpCZ05WQkFvVEEyczRjekVQTUEwR0ExVUVDeE1HVTNsemRHVnRNUk13RVFZRFZRUURFd3ByZFdKbGNtNWxkR1Z6Ck1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMXNKdWwzR3doYXM1NURiVU45VzMKMFpKSEN0TCszZ2tYdzFGT1ZRZ1ZvSkVQcHFhV3pMN2hTTElHenNpQkRyQm5sWVIxVWR0TzNieU52dmVjcnZ5Mgo1ZVh1MmlVcFJkc2Q2NVpGbmZQUVZYNkFWditUSVNVMGg1a0RrU0VmdFpJNm5jNDNkaU9xMVhpVE5vWS82RG9hClZVV0tHemVCcHp0NG1zY2RLYXdvUENvMFZibGdmUytvT1FEQTVPdGl3NTVPclNVYUlrb3NHazlnVkhsU1V1SVMKUUNhaG4zTkxHMG9oblhLWkVWSlpwQ25wZDgwejJPWFRXb3FwSEZpdXVoTjZtd3Z6RUJYaXY3ay96dG9zKzFwVApkYXd4WWMyUVFBVURTK0NubTdwWGE2Sk82eUhjV1d4ZWxwUENMRnBjQm0yN0tJM2tESW1OMVB6SEZaUHFDYlVHCitRSURBUUFCbzJZd1pEQU9CZ05WSFE4QkFmOEVCQU1DQVFZd0VnWURWUjBUQVFIL0JBZ3dCZ0VCL3dJQkFqQWQKQmdOVkhRNEVGZ1FVMEQzZ2RPUERwVzc5Q0ZFZVA1M1ZTK1JnNEVFd0h3WURWUjBqQkJnd0ZvQVUwRDNnZE9QRApwVzc5Q0ZFZVA1M1ZTK1JnNEVFd0RRWUpLb1pJaHZjTkFRRUxCUUFEZ2dFQkFLUUlmQUVVdUEzQmgzOTJ6VkNMCmxHY0NpMzJpeGRBdmpVeXRNaU5pNHpFRHZDNVJoU05CT3lKWmVqemswRmRzZGNXcU9LTEtJT1J2NEpXV1NIQ1EKaDUwM28rUUNBMGQ2RkJmSDNHMkV3ak5iWUJYOWphYkM5K05rSVpDT1U3b3RqSTd4NUxJSGVWanpGZWJXR0FnOQpYejJGQVd2eXlTK3FPVzRzc3N1djAyQW1KMjJqTWlXSHd2elBWTlFDdHZIVWpZNDFrTXlnUmEyYzlYRmN4Wms5CkY2em56ZWdpamlzUXJBVm9jR09KbXNwYVd6N3JlZmFKalYzVU1ZcnZLWGlDRVhHS1JBZ05KRUthTzN5dlBZN3cKaVVHODFTbGdwY1hJNEFjci9QN0FUMGVzc1AwU1NPU1E1aU44UVhqcWhNN0FhRHlKRmNNYU53QzJUcWk1bEpETgprZUE9Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K

  admissionReviewVersions: ["v1"]
  sideEffects: None
---
apiVersion: admissionregistration.k8s.io/v1
kind: MutatingWebhookConfiguration
metadata:
  name: admission-registry-mute
webhooks:
  - name: io.ydzs.admission-registry-mutate
    clientConfig:
      service:
        namespace: default
        name: admission-registry
        path: "/mutate"
      caBundle: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUR2akNDQXFhZ0F3SUJBZ0lVUkZkVTlRNTZXenA0UFNwUHQ0U05sdm9LOVBZd0RRWUpLb1pJaHZjTkFRRUwKQlFBd1pURUxNQWtHQTFVRUJoTUNRMDR4RURBT0JnTlZCQWdUQjBKbGFVcHBibWN4RURBT0JnTlZCQWNUQjBKbAphVXBwYm1jeEREQUtCZ05WQkFvVEEyczRjekVQTUEwR0ExVUVDeE1HVTNsemRHVnRNUk13RVFZRFZRUURFd3ByCmRXSmxjbTVsZEdWek1CNFhEVEl6TURZd016QXlNVEV3TUZvWERUSTRNRFl3TVRBeU1URXdNRm93WlRFTE1Ba0cKQTFVRUJoTUNRMDR4RURBT0JnTlZCQWdUQjBKbGFVcHBibWN4RURBT0JnTlZCQWNUQjBKbGFVcHBibWN4RERBSwpCZ05WQkFvVEEyczRjekVQTUEwR0ExVUVDeE1HVTNsemRHVnRNUk13RVFZRFZRUURFd3ByZFdKbGNtNWxkR1Z6Ck1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMXNKdWwzR3doYXM1NURiVU45VzMKMFpKSEN0TCszZ2tYdzFGT1ZRZ1ZvSkVQcHFhV3pMN2hTTElHenNpQkRyQm5sWVIxVWR0TzNieU52dmVjcnZ5Mgo1ZVh1MmlVcFJkc2Q2NVpGbmZQUVZYNkFWditUSVNVMGg1a0RrU0VmdFpJNm5jNDNkaU9xMVhpVE5vWS82RG9hClZVV0tHemVCcHp0NG1zY2RLYXdvUENvMFZibGdmUytvT1FEQTVPdGl3NTVPclNVYUlrb3NHazlnVkhsU1V1SVMKUUNhaG4zTkxHMG9oblhLWkVWSlpwQ25wZDgwejJPWFRXb3FwSEZpdXVoTjZtd3Z6RUJYaXY3ay96dG9zKzFwVApkYXd4WWMyUVFBVURTK0NubTdwWGE2Sk82eUhjV1d4ZWxwUENMRnBjQm0yN0tJM2tESW1OMVB6SEZaUHFDYlVHCitRSURBUUFCbzJZd1pEQU9CZ05WSFE4QkFmOEVCQU1DQVFZd0VnWURWUjBUQVFIL0JBZ3dCZ0VCL3dJQkFqQWQKQmdOVkhRNEVGZ1FVMEQzZ2RPUERwVzc5Q0ZFZVA1M1ZTK1JnNEVFd0h3WURWUjBqQkJnd0ZvQVUwRDNnZE9QRApwVzc5Q0ZFZVA1M1ZTK1JnNEVFd0RRWUpLb1pJaHZjTkFRRUxCUUFEZ2dFQkFLUUlmQUVVdUEzQmgzOTJ6VkNMCmxHY0NpMzJpeGRBdmpVeXRNaU5pNHpFRHZDNVJoU05CT3lKWmVqemswRmRzZGNXcU9LTEtJT1J2NEpXV1NIQ1EKaDUwM28rUUNBMGQ2RkJmSDNHMkV3ak5iWUJYOWphYkM5K05rSVpDT1U3b3RqSTd4NUxJSGVWanpGZWJXR0FnOQpYejJGQVd2eXlTK3FPVzRzc3N1djAyQW1KMjJqTWlXSHd2elBWTlFDdHZIVWpZNDFrTXlnUmEyYzlYRmN4Wms5CkY2em56ZWdpamlzUXJBVm9jR09KbXNwYVd6N3JlZmFKalYzVU1ZcnZLWGlDRVhHS1JBZ05KRUthTzN5dlBZN3cKaVVHODFTbGdwY1hJNEFjci9QN0FUMGVzc1AwU1NPU1E1aU44UVhqcWhNN0FhRHlKRmNNYU53QzJUcWk1bEpETgprZUE9Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K

    rules: 
      - operations: ["CREATE"]
        apiGroups: ["apps",""]
        apiVersions: ["v1"]
        resources: ["deployments", "services"]
    admissionReviewVersions: ["v1"]
    sideEffects: None  

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值