第十三课 k8s源码学习和二次开发原理篇-准入控制器

第十三课 k8s源码学习和二次开发原理篇-准入控制器

tags:

  • k8s
  • 源码学习

categories:

  • 源码学习
  • 二次开发
  • 准入控制器

第一节 Admission Webhook介绍

1.1 准入控制器介绍

  1. Kubernetes 提供了需要扩展其内置功能的方法,最常用的可能是自定义资源类型和自定义控制器了,除此之外,Kubernetes 还有一些其他非常有趣的功能,比如 admission webhooks 就可以用于扩展 API,用于修改某些 Kubernetes 资源的基本行为
  2. 准入控制器是在对象持久化之前用于对 Kubernetes API Server 的请求进行拦截的代码段,在请求经过身份验证授权之后放行通过。
  3. 准入控制器可能正在 validatingmutating 或者都在执行,Mutating 控制器可以修改他们处理的资源对象,Validating 控制器不会,如果任何一个阶段中的任何控制器拒绝了请求,则会立即拒绝整个请求,并将错误返回给最终的用户。
  4. 这意味着有一些特殊的控制器可以拦截 Kubernetes API 请求,并根据自定义的逻辑修改或者拒绝它们。Kubernetes 有自己实现的一个控制器列表:https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers/#what-does-each-admission-controller-do,当然你也可以编写自己的控制器,虽然这些控制器听起来功能比较强大,但是这些控制器需要被编译进 kube-apiserver,并且只能在 apiserver 启动时启动。
  5. 由于上面的控制器的限制,我们就需要用到动态的概念了,而不是和 apiserver 耦合在一起,Admission webhooks 就通过一种动态配置方法解决了这个限制问题。
  6. 在 Kubernetes apiserver 中包含两个特殊的准入控制器:MutatingAdmissionWebhookValidatingAdmissionWebhook,这两个控制器将发送准入请求到外部的 HTTP 回调服务并接收一个准入响应。如果启用了这两个准入控制器,Kubernetes 管理员可以在集群中创建和配置一个 admission webhook。
    在这里插入图片描述
  7. 整体的步骤如下所示:
    • 检查集群中是否启用了 admission webhook 控制器,并根据需要进行配置。
    • 编写处理准入请求的 HTTP 回调,回调可以是一个部署在集群中的简单 HTTP 服务,甚至也可以是一个 serverless 函数,例如 https://github.com/kelseyhightower/denyenv-validating-admission-webhook 这个项目。
    • 通过 MutatingWebhookConfigurationValidatingWebhookConfiguration 资源配置 admission webhook。
  8. 这两种类型的 admission webhook 之间的区别是非常明显的:validating webhooks 可以拒绝请求,但是它们却不能修改准入请求中获取的对象,而 mutating webhooks 可以在返回准入响应之前通过创建补丁来修改对象,如果 webhook 拒绝了一个请求,则会向最终用户返回错误。
  9. 现在非常火热的 Service Mesh 应用 istio 就是通过 mutating webhooks 来自动将 Envoy 这个 sidecar 容器注入到 Pod 中去的:https://istio.io/docs/setup/kubernetes/sidecar-injection/

1.2 创建配置一个 Admission Webhook

  1. 上面我们介绍了 Admission Webhook 的理论知识,接下来我们在一个真实的 Kubernetes 集群中来实际测试使用下,我们将创建一个 webhook 的 webserver,将其部署到集群中,然后创建 webhook 配置查看是否生效。
  2. 首先确保在 apiserver 中启用了 MutatingAdmissionWebhookValidatingAdmissionWebhook 这两个控制器,由于我这里集群使用的是 kubeadm 搭建的,可以通过查看 apiserver Pod 的配置:
# kubectl get pods kube-apiserver-ydzs-master -n kube-system -o yaml
apiVersion: v1
kind: Pod
metadata:
  labels:
    component: kube-apiserver
    tier: control-plane
  name: kube-apiserver-ydzs-master
  namespace: kube-system
......
spec:
  containers:
  - command:
    - kube-apiserver
    - --advertise-address=10.151.30.11
    - --allow-privileged=true
    - --authorization-mode=Node,RBAC
    - --client-ca-file=/etc/kubernetes/pki/ca.crt
    - --enable-admission-plugins=NodeRestriction,MutatingAdmissionWebhook,ValidatingAdmissionWebhook
......
# 或者通过cat /etc/kubernetes/manifests/kube-apiserver.yaml --enable-admission-plugins选项查看 如果没开启MutatingAdmissionWebhook,ValidatingAdmissionWebhook 需要手动开启
  1. 上面的 enable-admission-plugins 参数中带上了 MutatingAdmissionWebhookValidatingAdmissionWebhook 两个准入控制插件,如果没有的(当前 v1.19.x 版本是默认开启的),需要添加上这两个参数,然后重启 apiserver。
  2. 然后通过运行下面的命令检查集群中是否启用了准入注册 API
kubectl api-versions |grep admission
admissionregistration.k8s.io/v1
admissionregistration.k8s.io/v1beta1
  1. 接下来我们就去实现下我们自己的准入控制器。

第二节 ValidatingAdmissionWebhook 实现

2.1 准入控制器示例

  1. 通过有两个特殊的“动态”控制器 -ValidatingAdmissionWebhookMutatingAdmissionWebhook 来让开发者自行实现自己的准入逻辑。这两个控制器没有实现任何固定逻辑,相反,它们使我们能够在每次在集群中创建、更新或删除Kubernetes 资源时通过 webhooks 灵活地实现和执行自定义逻辑。
    在这里插入图片描述
  2. 接下来我们将构建一个准入控制器示例,只允许使用来自白名单镜像仓库的资源创建 Pod,拒绝使用不受信任的镜像仓库中进行拉取镜像
    在这里插入图片描述
  3. 比如我们这里之允许使用来自 docker.io 或者 gcr.io 镜像仓库的镜像创建 Pod,其他不受信任的镜像创建的 Pod 将会被拒绝。
  4. 要实现这个需求,我们就需要构建一个 ValidatingAdmissionWebhook,并将其注册到 APIServer。在编写这个 Webhook 之前我们就需要先链接通过注册的 Webhook 从 APIServer 接收到的请求的结构,以及我们对 APIServer 的响应结构。
  5. APIServer 实际上使用的是一个 AdmissionReview 类型的对象来向我们自定义的Webhook 发送请求和接收响应。
  6. 对于每个请求,在 AdmissionReview 结构体内部都有一个 AdmissionRequest 类型的属性,该属性中封装了发送到 APIServer 的原始请求数据,我们主要关心的就是该对象内部包含的正在创建/更新或删除的 Kubernetes 对象(比如 Pod、Deployment 等) JSON payload 数据。下面是用于验证准入控制器的 AdmissionReview 请求对象示例:
{
  "apiVersion": "admission.k8s.io/v1",
  "kind": "AdmissionReview",
  "request": {
    # Random uid uniquely identifying this admission call
    "uid": <random uid>,
    ...
    "object": {"apiVersion":"v1","kind":"Pod",...},
    ...
    }
}
  1. 对于验证准入控制器,我们的应用程序必须接收一个 AdmissionReview 对象,对其进行处理来决定是否允许/不允许该请求,并通过在 AdmissionReview 结构中填充一个类型为 AdmissionResponse 的 response 属性来返回我们的验证结果。在 response 中,我们使用一个名为 allowed 的布尔类型来表示是否允许/不允许,我们还可以选择包含一个 HTTP 状态码和一条 message 消息,将其传递回客户端。下面是用于验证准入控制器的 AdmissionReview 响应对象示例:
{
  "apiVersion": "admission.k8s.io/v1",
  "kind": "AdmissionReview",
  "response": {
    "uid": "<value from request.uid>",
    "allowed": true/false,
    "status": {
      "code": <optional http status code, ex: 200/403>,
      "message": "optional message"
    }
  }
}
  1. 如果我们要构建一个 Mutating 准入控制器,我们将使用一个 JSONPatch 类型的对象作为 AdmissionReview 响应的 response 属性的一部分发送回变更的结果,原始请求将使用此JSON Patch 进行修改。下面是用于 Mutating 准入控制器的 AdmissionReview 响应对象示例:
{
  "apiVersion": "admission.k8s.io/v1",
  "kind": "AdmissionReview",
  "response": {
    "uid": "<value from request.uid>",
    "allowed": true/false,
    "status": {
      "code": <optional http status code, ex: 200/403>,
      "message": "optional message"
    },
    "patchType": "JSONPatch",
    "patch": <base64 encoded JSON patch>
  }
}
  1. 关于 AdmissionReview 的完整结构定义可以查看文档:https://github.com/kubernetes/api/blob/master/admission/v1/types.go

2.2 准入控制器逻辑实现

  1. 这里我们要实现的是一个简单的带 TLS 认证的 HTTP 服务,用 Deployment 方式部署在我们的集群中。webhook实际上是一个http的服务端。
  2. 首先新建项目:
mkdir admission-registry
cd admission-registry
export GOPROXY=https://goproxy.cn
go mod init gitee.com/k8sdev/admission-registry
  1. 然后在根目录下面新建一个** main.go 的入口文件**,在该文件中定义 webhook server 的入口点,代码如下所示:
package main

import (
	"context"
	"crypto/tls"
	"flag"
	"fmt"
	"gitee.com/k8sdev/admission-registry/pkg"
	"k8s.io/klog"
	"net/http"
	"os"
	"os/signal"
	"strings"
	"syscall"
)

func main(){
	var param pkg.WhSvrParam
	// webhook http server (tls)
	// 命令行参数传递证书
	flag.IntVar(&param.Port, "port", 443, "Webhook Server")
	flag.StringVar(&param.CertFile, "tlsCertFile", "/etc/webhook/certs/tls.crt", "File containing the x509 Certificate for HTTPS.")
	flag.StringVar(&param.KeyFile, "tlsKeyFile", "/etc/webhook/certs/tls.key", "File containing the x509 private key to --tlsCertFile.")
	flag.Parse()

	// 先获取k8s中 日志模块 go get k8s.io/klog
	klog.Info(fmt.Sprintf("port=%d, cert-file=%s, key-file=%s", param.Port, param.CertFile, param.KeyFile))

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

	// 实例化一个webhook server
	whsvr := pkg.WebhookServer{
		Server: &http.Server{
			Addr:      fmt.Sprintf(":%v", param.Port),
			TLSConfig: &tls.Config{Certificates: []tls.Certificate{pair}},
		},
		WhiteListRegistries: strings.Split(os.Getenv("WHITELIST_REGISTRIES"), ","),
	}

	// 定义 http server 和 handler
	mux := http.NewServeMux()
	mux.HandleFunc("/validate", whsvr.Serve)
	mux.HandleFunc("/mutate", whsvr.Serve)
	whsvr.Server.Handler = mux

	// 在一个新的 goroutine 中启动 webhook server
	go func() {
		if err := whsvr.Server.ListenAndServeTLS("", ""); err != nil {
			klog.Errorf("Failed to listen and serve webhook server: %v", err)
		}
	}()

	klog.Info("Server started")

	// 监听 OS shutdown 信号
	signalChan := make(chan os.Signal, 1)
	signal.Notify(signalChan, syscall.SIGINT, syscall.SIGTERM)
	<-signalChan

	klog.Infof("Got OS shutdown signal, shutting down webhook server gracefully...")
	if err := whsvr.Server.Shutdown(context.Background()); err != nil {
		klog.Errorf("HTTP server Shutdown: %v", err)
	}
}
// admission-registry/pkg/webhook.go
package pkg

import "net/http"

type WhSvrParam struct {
	Port int
	CertFile string
	KeyFile string
}

type WebhookServer struct {
	Server *http.Server // http server
	WhiteListRegistries []string //白名单镜像仓库列表
}

func (s *WebhookServer) Serve(writer http.ResponseWriter, request *http.Request){
	
}
  1. 通过 flag 来获取传递的命令行参数,比如 TLS 证书,镜像仓库白名单等。然后使用标准库 http 来定义服务,通过一个 WebhookServer 结构体进行了简单的封装,虽然我们这里主要是实现 validate 校验功能,为了扩展支持 muate,这里我们分别定义两个端点来进行支持:
mux.HandleFunc("/validate", whsvr.Serve)
mux.HandleFunc("/mutate", whsvr.Serve)
  1. 所以这里最重要的就是 serve 函数了,用来处理传入的 mutate 和 validating 函数的 HTTP 请求。该函数从请求中反序列化 AdmissionReview 对象,执行一些基本的内容校验,根据 URL 路径调用相应的 mutate 和 validate 函数,然后序列化 AdmissionReview 对象:
  2. 在上面的 serve 函数中会根据传入的 PATH 来决定调用的逻辑,这里我们主要是实现校验的功能,所以主要是实现 validate 函数的逻辑
// admission-registry/pkg/webhook.go
package pkg

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	admissionV1 "k8s.io/api/admission/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"
	"net/http"
	"strings"
)

var (
	// 反序列化 go get k8s.io/apimachinery
	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 // http server
	WhiteListRegistries []string //白名单镜像仓库列表
}

func (s *WebhookServer) Serve(writer http.ResponseWriter, request *http.Request){
	var body []byte
	if request.Body != nil {
		if data, err := ioutil.ReadAll(request.Body); err == nil {
			body = data
		}
	}
	if len(body) == 0 {
		klog.Error("empty body")
		http.Error(writer, "empty body", http.StatusBadRequest)
		return
	}

	// verify the content type is accurate 校验Content-Type
	contentType := request.Header.Get("Content-Type")
	if contentType != "application/json" {
		klog.Errorf("Content-Type=%s, expect application/json", contentType)
		http.Error(writer, "invalid Content-Type, expect `application/json`", http.StatusUnsupportedMediaType)
		return
	}

	// 数据序列化(validate、mutate)请求的数据都是 AdmissionReview
	// go get k8s.io/api
	var admissionResponse *admissionV1.AdmissionResponse
	requestedAdmissionReview := admissionV1.AdmissionReview{}
	if _, _, err := deserializer.Decode(body, nil, &requestedAdmissionReview); err != nil {
		klog.Errorf("Can't decode body: %v", err)
		admissionResponse = &admissionV1.AdmissionResponse{
			Result: &metav1.Status{
				Message: err.Error(),
			},
		}
	} else {
		// 序列化成功,也就是说获取到了请求的 AdmissionReview的数据
		if request.URL.Path == "/mutate" {
			// TODO
		} else if request.URL.Path == "/validate" {
			admissionResponse = s.validate(&requestedAdmissionReview)
		}
	}

	// 构造返回的 AdmissionReview 结构
	responseAdmissionReview := admissionV1.AdmissionReview{}
	// admission.k8s.io/v1 版本需要指定对应的 APIVersion
	responseAdmissionReview.APIVersion = requestedAdmissionReview.APIVersion
	responseAdmissionReview.Kind = requestedAdmissionReview.Kind
	if admissionResponse != nil {
		// 设置 response 属性
		responseAdmissionReview.Response = admissionResponse
		if requestedAdmissionReview.Request != nil {
			// 返回相同的 UID
			responseAdmissionReview.Response.UID = requestedAdmissionReview.Request.UID
		}
	}

	klog.Info(fmt.Sprintf("sending response: %v", responseAdmissionReview.Response))
	// send response
	respBytes, err := json.Marshal(responseAdmissionReview)
	if err != nil {
		klog.Errorf("Can't encode response: %v", err)
		http.Error(writer, fmt.Sprintf("could not encode response: %v", err), http.StatusInternalServerError)
	}
	klog.Infof("Ready to write response ...")
	if _, err := writer.Write(respBytes); err != nil {
		klog.Errorf("Can't write response: %v", err)
		http.Error(writer, fmt.Sprintf("could not write response: %v", err), http.StatusInternalServerError)
	}
}

func (s *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 s.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, s.WhiteListRegistries)
			break
		}
	}

	return &admissionV1.AdmissionResponse{
		Allowed: allowed,
		Result: &metav1.Status{
			Code:    int32(code),
			Message: message,
		},
	}
} 
  1. 代码实现逻辑也很简单的,就是拿着传入的对象 Pod,循环里面的镜像,判断这些镜像是否都是白名单列表中的镜像,如果是则校验通过,否则校验失败,返回 allowed=false

2.3 证书准备

  1. 上面我们实现了最基本的业务逻辑,由于 webhook 要求是通过 HTTPS 暴露服务,所以我们还需要为其生成相关的证书。为了方便这里我们可以使用 cfssl 来生成相关证书。
  2. 安装 cfssl:
# mac
brew install cfssl
# Linux
wget -q --show-progress --https-only --timestamping \
  https://pkg.cfssl.org/R1.2/cfssl_linux-amd64 \
  https://pkg.cfssl.org/R1.2/cfssljson_linux-amd64
chmod +x cfssl_linux-amd64 cfssljson_linux-amd64
sudo mv cfssl_linux-amd64 /usr/local/bin/cfssl
sudo mv cfssljson_linux-amd64 /usr/local/bin/cfssljson
  1. 在项目目录下创建certs文件夹,然后创建 CA 证书机构,执行下面的命令:
cat > ca-config.json <<EOF
{
  "signing": {
    "default": {
      "expiry": "8760h"
    },
    "profiles": {
      "server": {
        "usages": ["signing", "key encipherment", "server auth", "client auth"],
        "expiry": "8760h"
      }
    }
  }
}
EOF

cat > ca-csr.json <<EOF
{
    "CN": "kubernetes",
    "key": {
        "algo": "rsa",
        "size": 2048
    },
    "names": [
        {
            "C": "CN",
            "L": "BeiJing",
            "ST": "BeiJing",
            "O": "k8s",
            "OU": "System"
        }
    ]
}
EOF
  1. 然后使用下面的命令生成 CA 证书和私钥:
cfssl gencert -initca ca-csr.json | cfssljson -bare ca
ls -la *.pem
  1. 然后接下来就可以创建 Server 端证书了:
cat > server-csr.json <<EOF
{
  "CN": "admission",
  "key": {
    "algo": "rsa",
    "size": 2048
  },
  "names": [
    {
        "C": "CN",
        "L": "BeiJing",
        "ST": "BeiJing",
        "O": "k8s",
        "OU": "System"
    }
  ]
}
EOF

cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=ca-config.json -hostname=admission-registry.default.svc -profile=server server-csr.json | cfssljson -bare server
ls -la *.pem
  1. 其中最重要的就是 -hostname 的值,格式为 {service-name}.{service-namespace}.svc,其中 service-name 代表你 webhook 的 Service 名字,service-namespace 代表你 webhook 的命名空间。
  2. 然后使用生成的 server 证书和私钥创建一个 Secret 对象:
# 创建Secret
kubectl create secret tls admission-registry-tls \
        --key=server-key.pem \
        --cert=server.pem
#secret/admission-registry-tls created
  1. 后面我们通过 Volumes 的形式将 Secret 挂载到 webhook 的容器中指定的位置给 webhook 使用即可。

2.4 Docker 镜像

  1. 我们只需要将 webhook 打包成 Docker 镜像,并使用一个 Deployment 来运行这个容器应用即可,对应的 Dockerfile 文件如下所示:
# Build the webhook binary
FROM golang:1.13 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"]
  1. 这里我们使用了 Docker 的多阶段构建功能,先将项目构建打包成二进制文件,然后在 distrolesss 中运行该应用,执行项目的命令构建推送镜像即可:
docker build -t qnhyn/admission-registry:v0.0.1 .
docker push qnhyn/admission-registry:v0.0.1

2.5 部署webhook

  1. 现在 webhook 的镜像已经准备好了,接下来我们就需要将其部署到 Kubernetes 集群中,这里我们使用 Deployment + Service 来提供服务即可,在 Pod 的规范中配置环境变量 WHITELIST_REGISTRIES 来定义白名单镜像仓库地址,然后将证书通过 Secret 的 Volumes 形式挂载到 Pod 容器中,对应的资源清单文件如下所示:
#  webhook.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: admission-registry
  labels:
    app: admission-registry
spec:
  selector:
    matchLabels:
      app: admission-registry
  template:
    metadata:
      labels:
        app: admission-registry
    spec:
      containers:
        - name: whitelist
          image: qnhyn/admission-registry:v0.0.1
          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-registry
  1. 直接创建上面的资源清单即可:
kubectl get pods -l app=admission-registry
kubectl get svc -l app=admission-registry        

2.6 注册webhook

  1. 上面我们只是单纯将我们实现的 webhook 部署到了 Kubernetes 集群中,但是还并没有和 ValidatingWebhook 对接起来,要将我们上面实现的服务注册到 ValidatingWebhook 中只需要创建一个类型为 ValidatingWebhookConfiguration 的 Kubernetes 资源对象即可,在这个对象中就可以来配置我们的 webhook 这个服务。
  2. 如下所示,我们将 webhook 命名为 io.ydzs.admission-registry ,只需要保证在集群中名称唯一即可。然后在 rules 属性下面就是来指定在什么条件下使用该 webhook 的配置,这里我们只需要在创建 Pod 的时候才调用这个 webhook。此外在 ClientConfig 属性下我们还需要指定 Kubernetes APIServer 如何来找到我们的 webhook 服务,这里我们将通过一个在 default 命名空间下面的名为 admission-registry 的 Service 服务在 /validate 路径下面提供服务,此外还指定了一个 caBundle 的属性,这个属性通过指定一个 PEM 格式的 CA bundle 来表示 APIServer 作为客户端可以使用它来验证我们的 webhook 应用上的服务器证书。对应的注册 webhook 的资源清单如下所示:
# validatingwebhook.yaml
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: CA_BUNDLE
  admissionReviewVersions: ["v1"]
  sideEffects: None
  1. 上面的 CA_BUNDLE 值使用的是上面生成 ca.crt 文件内容的 base64 值:
cat ca.pem | base64 # 把这个值覆盖上面CA_BUNDLE变量
LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUR2akNDQXFhZ0F3SUJBZ0lVRXV6ZE9LTld2cDJZK3FRU2tyay9yRkhNMXFrd0RRWUpLb1pJaHZjTkFRRUwKQlFBd1pURUxNQWtHQTFVRUJoTUNRMDR4RURBT0JnTlZCQWdUQjBKbGFVcHBibWN4RURBT0JnTlZCQWNUQjBKbAphVXBwYm1jeEREQUtCZ05WQkFvVEEyczRjekVQTUEwR0ExVUVDeE1HVTNsemRHVnRNUk13RVFZRFZRUURFd3ByCmRXSmxjbTVsZEdWek1CNFhEVEl4TVRBd05URTBOVEV3TUZvWERUSTJNVEF3TkRFME5URXdNRm93WlRFTE1Ba0cKQTFVRUJoTUNRMDR4RURBT0JnTlZCQWdUQjBKbGFVcHBibWN4RURBT0JnTlZCQWNUQjBKbGFVcHBibWN4RERBSwpCZ05WQkFvVEEyczRjekVQTUEwR0ExVUVDeE1HVTNsemRHVnRNUk13RVFZRFZRUURFd3ByZFdKbGNtNWxkR1Z6Ck1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBN08xSWhMMkI1WEVaR25USXVXZDkKMEhCR2QwNFcvQzJ4d3JBY21RYmc0Um5nK3JGMGc1Qkw5MEliS3NIdk5Ed1FQbkluclgrcmhVSXZiSGNmYnlpVwoyY1NuY055MTJwbWlVR21pMHlsU2dueXhtT21hcVM5cmNkcDVudm5sWGdSa1g3bHpmS0RsMGlzYWVkWGhKVUV0CkI1N2tKckY0czlKakdUbkxjZ1BrRFFFS3lncFVPRlZvRXNQMHY0VnpkeU5FRUpWWFN5dm9Zems0d2lUcE9RNzMKdk1QZDh6aHI3bzhnRFVNQUN3MmhQMFJVRVo5ZGNWb01jWjlxWXpjZHFPek1DV2dSaXlaUW14aytQRUVObStoZApPVFl6LzdOTUZSVnpUNkd6UGZBY0RKbVZObDVOZEs5c1FMNUV6Ym42UU5tWW95a1lGTWFWSG1xMEVnYkw1Rm5kCi9RSURBUUFCbzJZd1pEQU9CZ05WSFE4QkFmOEVCQU1DQVFZd0VnWURWUjBUQVFIL0JBZ3dCZ0VCL3dJQkFqQWQKQmdOVkhRNEVGZ1FVeGdxa3BKRFNyUVVKVnI4TDNhREx2bVJ6OHNZd0h3WURWUjBqQkJnd0ZvQVV4Z3FrcEpEUwpyUVVKVnI4TDNhREx2bVJ6OHNZd0RRWUpLb1pJaHZjTkFRRUxCUUFEZ2dFQkFFZEp6ZVlwQWQ2N241QWltU0tKCmg1Z0NrdnYxZDdOeUJseklxOWdtNU00aEpXZGlNaHJUYXRTK2hKUjFtQkFNbVhtTzcxbTY5empKV2RKZ0JFN28KZ28xdGVndVIvdU9aS0ZTV3NiaTZZWDFmSkFVeG95SnBpSHg0dVlvUkIybERNNkNub3l1L2N1TXhtV0JzelN4eQo1NGZPYVdTUnBxd2wrSFAwYkltbUJUMlhzK0VyNmdldWRiTE1nZjRCcVZ3Q1pNbnd5T2EyVG5ZM25uWklDUnMxCjltN2JDSUh6U09xWlR6N1NrNHUyM0RhdW82S0tnVDZxeE8xTmNSQVV1K2RwNkdZWjBRZ2ZVWUVZeGx2ZDAvSXMKaTYzN21HbkZCWmZPUzlDa1hTaU5KYnZxZzNvNmJGSFFUT2xyK2lYcnhRUGp0aFBCbGVVQ3FqeW5VKzVEYUgyYQo0Wk09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K
  1. 然后将得到的值替换掉 validatingwebhook.yaml 文件中的 CA_BUNDLE ,然后就可以直接部署到集群中:
kubectl apply -f validatingwebhook.yaml
kubectl get validatingwebhookconfiguration       
  1. 到这里我们的镜像白名单校验的 webhook 就部署完成了。

2.7 测试功能

  1. 接下来我们来测试下上面我们 webhook 是否生效了。首先创建一个如下所示的测试 Pod 清单:
# test-pod1.yaml
apiVersion: v1
kind: Pod
metadata:
  name: test-pod1
spec:
  containers:
  - name: nginx
    image: docker.io/nginx:latest
  1. 由于 docker.io 是我们的镜像白名单,所以正常上面的应用是可以正常创建的:
kubectl apply -f test-pod1.yaml
  1. 然后创建另外一个 Pod,这次我们使用一个 ydzs.io 的镜像仓库的镜像:
# test-pod2.yaml
apiVersion: v1
kind: Pod
metadata:
  name: test-pod2
spec:
  containers:
    - name: nginx
      image: ydzs.io/nginx:latest
  1. 由于 ydzs.io 并不在我们的镜像白名单中,所以正常部署后会被拒绝:
kubectl apply -f test-pod2.yaml
kubectl apply -f manifests/test-pod2.yaml                           
Error from server: error when creating "manifests/test-pod2.yaml": admission webhook "io.ydzs.admission-registry" denied the request: ydzs.io/nginx:latest image comes from an untrusted registry! Only images from [docker.io gcr.io] are allowed.
  1. 可以看到上面的 Pod 部署失败了,因为不在镜像白名单中,证明我们的校验准入控制器逻辑是正确的。

2.8 清理环境

  1. 要移除这个校验准入控制器比较简单,只需要移除上面的几个资源对象即可:
kubectl delete -f validatingwebhook.yaml
kubectl delete -f webhook.yaml
  1. 这里我们只是通过一个简单的示例来说明我们应该如何去开发一个校验的准入控制器,对于 Mutate 类型的控制器实现方式也基本一致。当然如果我们只是简单的想现在下镜像仓库,我们也可以不需要自己去编写代码来实现,毕竟这样效率并不是很高,我们可以通过 Open Policy Agent Gatekeeper 项目来实现,它提供了一种通过策略配置而不是编写代码来实现类似用例的方法。

第三节 MutatingAdmissionWebhook实现

3.1 MutatingAdmissionWebhook的需求

  1. 上面我们已经实现了一个校验的准入控制器,我们来尝试开发一个用于 Mutate 的准入控制器,这两个控制器其实都是我们自己通过 Webhook 去实现,而且他们接收和返回的响应数据结构都是 AdmissionReview,唯一不同的是对于 Mutating 的 Webhook 在处理了资源对象后返回的时候需要我们拼接一个 JSONPatch 的数据

https://bxdc-static.oss-cn-beijing.aliyuncs.com/images/20210123111237.png
2. 接下来我们在前面的镜像白名单的 Webhook 基础之上新增 mutate 的支持,该项目中我们也预留了 mutate 的入口,通过 /mutate 路径的请求进行 mutate 操作。
3. 比如我们的需求是当我们的资源对象(Deployment 或 Service)中包含一个需要 mutate 的 annotation 注解后,通过这个 Webhook 后我们就给这个对象添加上一个执行了 mutate 操作的注解

3.2 MutatingAdmissionWebhook逻辑实现

  1. 首先在 WebhookServ 的 Serve 函数中新增 mutate 的逻辑入口函数:
// 序列化成功,也就是说获取到了请求的 AdmissionReview 的数据
if request.URL.Path == "/mutate" {
	admissionResponse = s.mutate(&requestedAdmissionReview)
} else if request.URL.Path == "/validate" {
	admissionResponse = s.validate(&requestedAdmissionReview)
}
  1. 然后我们的 mutate 逻辑就在下面的函数中去实现了:
func (s *WebhookServer) mutate(ar *admissionV1.AdmissionReview) *admissionV1.AdmissionResponse {
	//Deployment、Service - annotations: AnnotationMutateKey,AnnotationStatusKey
	req := ar.Request
	var (
		objectMeta                      *metav1.ObjectMeta
		resourceNamespace, resourceName string
	)

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

	switch req.Kind.Kind {
	case "Deployment":
		var deployment appsv1.Deployment
		if err := json.Unmarshal(req.Object.Raw, &deployment); err != nil {
			klog.Errorf("Could not unmarshal raw object: %v", err)
			return &admissionV1.AdmissionResponse{
				Result: &metav1.Status{
					Code:    http.StatusBadRequest,
					Message: err.Error(),
				},
			}
		}
		resourceName, resourceNamespace, objectMeta = deployment.Name, deployment.Namespace, &deployment.ObjectMeta
	case "Service":
		var service corev1.Service
		if err := json.Unmarshal(req.Object.Raw, &service); err != nil {
			klog.Errorf("Could not unmarshal raw object: %v", err)
			return &admissionV1.AdmissionResponse{
				Result: &metav1.Status{
					Code:    http.StatusBadRequest,
					Message: err.Error(),
				},
			}
		}
		resourceName, resourceNamespace, objectMeta = service.Name, service.Namespace, &service.ObjectMeta
	default:
		return &admissionV1.AdmissionResponse{
			Result: &metav1.Status{
				Code:    http.StatusBadRequest,
				Message: fmt.Sprintf("Can't handle this kind(%s) object", req.Kind.Kind),
			},
		}
	}

	if !mutationRequired(objectMeta) {
		klog.Infof("Skipping validation for %s/%s due to policy check", resourceNamespace, resourceName)
		return &admissionV1.AdmissionResponse{
			Allowed: true,
		}
	}

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

	var patch []patchOperation
	patch = append(patch, mutateAnnotation(objectMeta.GetAnnotations(), annotations)...)

	patchBytes, err := json.Marshal(patch)
	if err != nil {
		return &admissionV1.AdmissionResponse{
			Result: &metav1.Status{
				Message: err.Error(),
			},
		}
	}

	klog.Infof("AdmissionResponse: patch=%v\n", string(patchBytes))
	return &admissionV1.AdmissionResponse{
		Allowed: true,
		Patch:   patchBytes,
		PatchType: func() *admissionV1.PatchType {
			pt := admissionV1.PatchTypeJSONPatch
			return &pt
		}(),
	}
}
  1. 在这个函数中我们针对 Deployment 和 Service 两种资源类型进行处理,首先通过 mutationRequired 函数来判断当前资源对象是否需要执行 mutate 操作。
func mutationRequired(metadata *metav1.ObjectMeta) bool {
	annotations := metadata.GetAnnotations()
	if annotations == nil {
		annotations = map[string]string{}
	}

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

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

	klog.Infof("Mutation policy for %v/%v: required:%v", metadata.Namespace, metadata.Name, required)
	return required
}
  1. 如果资源对象中包含的 AnnotationMutateKey 这个 annotation 对应的值为 “n”、“no”、“false”、“off” 中的任何一个则不需要执行 mutate 操作,或者AnnotationStatusKey 这个 annotation 对应的值已经是 mutated 了则也不需要,否则就需要执行 mutate 操作。
  2. 如果需要执行 mutate 操作,则需要我们自己创建 Patch 操作,将 {AnnotationStatusKey: “mutated”} 这个 annotation Patch 到资源中去:
annotations := map[string]string{AnnotationStatusKey: "mutated"}

var patch []patchOperation
patch = append(patch, updateAnnotation(objectMeta.GetAnnotations(), annotations)...)
  1. 这里要执行 Patch 操作,需要定义一个如下所示的 patchOperation 的结构体:
type patchOperation struct {
	Op    string      `json:"op"`
	Path  string      `json:"path"`
	Value interface{} `json:"value,omitempty"`
}
  1. 然后在 updateAnnotation 函数中来创建更新 annotation 的 Patch:
func mutateAnnotation(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 patch
}
  1. 最后需要将创建的 Patch 进行序列化,通过 AdmissionResponse 返回给 APIServer 即可:
patchBytes, err := json.Marshal(patch)
	if err != nil {
		return &admissionv1.AdmissionResponse{
			Result: &metav1.Status{
				Message: err.Error(),
			},
		}
	}

	klog.Infof("AdmissionResponse: patch=%v\n", string(patchBytes))
	return &admissionv1.AdmissionResponse{
		Allowed: true,
		Patch:   patchBytes,
		PatchType: func() *admissionv1.PatchType {
			pt := admissionv1.PatchTypeJSONPatch
			return &pt
		}(),
	}
  1. 业务逻辑实现完成后,当然同样还是重新构建打包镜像,重新部署 Webhook 服务,然后同样还需要将这个 Webhook 进行注册,创建一个如下所示的资源对象:
apiVersion: admissionregistration.k8s.io/v1
kind: MutatingWebhookConfiguration
metadata:
  name: admission-registry-mutate
webhooks:
- name: io.ydzs.admission-registry-mutate
  clientConfig:
    service:
      namespace: default
      name: admission-registry
      path: "/mutate"
    caBundle: CA_BUNDLE
  rules:
    - operations: [ "CREATE" ]
      apiGroups: ["apps", ""]
      apiVersions: ["v1"]
      resources: ["deployments","services"]
  admissionReviewVersions: [ "v1" ]
  sideEffects: None

3.3 MutatingAdmissionWebhook功能测试

  1. 接下来我们首先创建如下所示的两个资源对象:
# test-mutating-deploy1.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: test-deploy1
spec:
  selector:
    matchLabels:
      app: test1-mutate
  template:
    metadata:
      labels:
        app: test1-mutate
    spec:
      containers:
        - name: mutate
          image: docker.io/nginx:1.7.9
          imagePullPolicy: IfNotPresent
          ports:
            - containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: test-svc1
spec:
  selector:
    app: test1-mutate
  ports:
    - port: 80
      targetPort: 80
  type: ClusterIP
  1. 由于上面的 Deployment 和 Service 没有添加任何的 annotation,所以正常通过上面我们的 mutate 这个准入控制器过后会被添加上一个 annotion:
kubectl apply -f test-deploy1.yaml
kubectl get deploy test-deploy1 -o yaml
############
apiVersion: apps/v1
kind: Deployment
metadata:
  annotations:
    deployment.kubernetes.io/revision: "1"
    io.ydzs.admission-registry/status: mutated
  ......
kubectl get svc test-svc1 -o yaml
apiVersion: v1
kind: Service
metadata:
  annotations:
    io.ydzs.admission-registry/status: mutated
......
  1. 可以看到创建的 Deployment 和 Service 都被添加了一个 io.ydzs.admission-registry/status: mutated 的 annotation。接下来再创建一个如下所示的资源对象:
apiVersion: apps/v1
kind: Deployment
metadata:
  name: test-deploy2
  annotations:
    io.ydzs.admission-registry/mutate: "no"
spec:
  selector:
    matchLabels:
      app: test2-mutate
  template:
    metadata:
      labels:
        app: test2-mutate
    spec:
      containers:
        - name: mutate
          image: docker.io/nginx:1.7.9
          imagePullPolicy: IfNotPresent
          ports:
            - containerPort: 80
  1. 由于该资源对象中本身就包含一个 io.ydzs.admission-registry/mutate: "no" 的 annotation,所以正常创建后不会被添加新的 annotation 了:
kubectl apply -f test-deploy2.yaml
kubectl get deploy test-deploy2 -o yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  annotations:
    deployment.kubernetes.io/revision: "1"
    io.ydzs.admission-registry/mutate: "no"
    kubectl.kubernetes.io/last-applied-configuration: |
      {"apiVersion":"apps/v1","kind":"Deployment","metadata":{"annotations":{"io.ydzs.admission-registry/mutate":"no"},"name":"test-deploy2","namespace":"default"},"spec":{"selector":{"matchLabels":{"app":"test2-mutate"}},"template":{"metadata":{"labels":{"app":"test2-mutate"}},"spec":{"containers":[{"image":"docker.io/nginx:1.7.9","imagePullPolicy":"IfNotPresent","name":"mutate","ports":[{"containerPort":80}]}]}}}}
  ......
  1. 到这里就成功验证了 Mutate 这个准入控制器,很多时候可能不只是单纯的添加一个 annotation,很有可能是添加一个容器,添加一个环境变量,或者 volumes,这些实现方式都是一样的了。

第四节 管理 Admission Webhook 的 TLS 证书

4.1 Admission Webhook的TLS证书管理需求

  1. 我们开发自己的准入控制器 Webhook,这些准入 Webhook 控制器调用自定义配置的 HTTP 回调服务来进行其他检查。但是,APIServer 仅通过 HTTPS 与 Webhook 服务进行通信,并且需要 TLS 证书的 CA 信息。所以对于如何处理该 Webhook 服务证书以及如何将 CA 信息自动传递给 APIServer 带来了一些麻烦。
  2. 前面我们是通过 openssl(cfssl)来手动生成的相关证书,然后手动配置给 Webhook 服务的,除此之外,我们也可以使用cert-manager 来处理这些 TLS 证书和 CA。但是,cert-manager 本身是一个比较大的应用程序,由许多 CRD 组成来处理其操作。仅安装 cert-manager 来处理准入 webhook TLS 证书和 CA 不是一个很好的做法
  3. 另外一种做法就是我们可以使用自签名证书,然后通过使用 Init 容器来自行处理 CA,这就消除了对其他应用程序(如 cert-manager)的依赖。接下来我们就来重点介绍下如何使用这种方式来管理相关证书。

4.2 初始化容器

  1. 这个初始化容器的主要功能是创建一个自签名的 Webhook 服务证书,并通过 mutate/验证配置将 caBundle 提供给 APIServer。Webhook 服务如何使用该证书(通过 Secret Volumes 或 emptyDir),取决于实际情况。这里我们这个初始化容器将运行一个简单的 Go 二进制文件来执行这些功能。核心代码如下所示:
package main

import (
	"bytes"
	cryptorand "crypto/rand"
	"crypto/rsa"
	"crypto/x509"
	"crypto/x509/pkix"
	"encoding/pem"
	"fmt"
	log "github.com/sirupsen/logrus"
	"math/big"
	"os"
	"time"
)

func main() {
	var caPEM, serverCertPEM, serverPrivKeyPEM *bytes.Buffer
	// CA config ca 配置
	ca := &x509.Certificate{
		SerialNumber: big.NewInt(2021),
		Subject: pkix.Name{
			Country: []string{"CN"},
			Province: []string{"Beijing"},
			Locality: []string{"Beijing"},
			Organization: []string{"ydzs.io"},
			OrganizationalUnit: []string{"ydzs.io"},
		},
		NotBefore:             time.Now(), // 有效期
		NotAfter:              time.Now().AddDate(10, 0, 0),
		IsCA:                  true, // 根证书
		ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
		KeyUsage:              x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
		BasicConstraintsValid: true,
	}

	// CA private key 生成ca私钥
	caPrivKey, err := rsa.GenerateKey(cryptorand.Reader, 4096)
	if err != nil {
		fmt.Println(err)
	}

	// Self signed CA certificate 创建自签名的CA证书
	caBytes, err := x509.CreateCertificate(cryptorand.Reader, ca, ca, &caPrivKey.PublicKey, caPrivKey)
	if err != nil {
		fmt.Println(err)
	}

	// PEM encode CA cert 编码证书文件
	caPEM = new(bytes.Buffer)
	_ = pem.Encode(caPEM, &pem.Block{
		Type:  "CERTIFICATE",
		Bytes: caBytes,
	})
	// 对哪些域名签名
	dnsNames := []string{"admission-registry",
		"admission-registry.default", "admission-registry.default.svc",
		"admission-registry.default.svc.cluster.local"}
	commonName := "admission-registry.default.svc"

	// server cert config 服务端证书配置
	cert := &x509.Certificate{
		DNSNames:     dnsNames,
		SerialNumber: big.NewInt(1658),
		Subject: pkix.Name{
			CommonName:   commonName,
			Country: []string{"CN"},
			Province: []string{"Beijing"},
			Locality: []string{"Beijing"},
			Organization: []string{"ydzs.io"},
			OrganizationalUnit: []string{"ydzs.io"},
		},
		NotBefore:    time.Now(),
		NotAfter:     time.Now().AddDate(1, 0, 0),
		SubjectKeyId: []byte{1, 2, 3, 4, 6},
		ExtKeyUsage:  []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
		KeyUsage:     x509.KeyUsageDigitalSignature,
	}

	// server private key 生成服务器私钥
	serverPrivKey, err := rsa.GenerateKey(cryptorand.Reader, 4096)
	if err != nil {
		fmt.Println(err)
	}

	// sign the server cert 服务端私钥签名
	serverCertBytes, err := x509.CreateCertificate(cryptorand.Reader, cert, ca, &serverPrivKey.PublicKey, caPrivKey)
	if err != nil {
		fmt.Println(err)
	}

	// PEM encode the server cert and key 服务端编码证书文件
	serverCertPEM = new(bytes.Buffer)
	if err := pem.Encode(serverCertPEM, &pem.Block{
		Type:  "CERTIFICATE",
		Bytes: serverCertBytes,
	}); err != nil{
		log.Panic(err)
	}

	serverPrivKeyPEM = new(bytes.Buffer)
	if err := pem.Encode(serverPrivKeyPEM, &pem.Block{
		Type:  "RSA PRIVATE KEY",
		Bytes: x509.MarshalPKCS1PrivateKey(serverPrivKey),
	}); err != nil{
		log.Panic(err)
	}

	// 已经生成了CA server.pem server-key. pem
	if err := os.MkdirAll("/etc/webhook/certs/", 0666); err != nil {
		log.Panic(err)
	}
	if err := WriteFile("/etc/webhook/certs/tls.crt", serverCertPEM); err != nil{
		log.Panic(err)
	}

	if err := WriteFile("/etc/webhook/certs/tls.key", serverPrivKeyPEM); err != nil{
		log.Panic(err)
	}
}

// WriteFile writes data in the file at the given path
func WriteFile(filepath string, sCert *bytes.Buffer) error {
	f, err := os.Create(filepath)
	if err != nil {
		return err
	}
	defer f.Close()

	_, err = f.Write(sCert.Bytes())
	if err != nil {
		return err
	}
	return nil
}
  1. 的代码中我们通过生成自签名的 CA 并签署 Webhook 服务证书来提供服务:
    • 首先为 CA 创建一个配置 ca
    • 为该 CA 创建一个 RSA 私钥 caPrivKey
    • 生成一个自签名的 CA、caByte 和 caPEM,在这里,caPEM 是 PEM 编码的 caBytes,将是提供给 APIServer 的 CA_BUNDLE 数据
    • 创建 webhook 服务证书的配置,即上面代码中的 cert。该配置中的重要属性是 DNSNames 和 commonName,要注意的是该名称必须是到达 Webhook 服务的完整地址名称
    • 然后为 Webhook 服务创建一个 RS 私钥 serverPrivKey
    • 使用上面代码中的 ca 和 caPrivKey 创建服务端证书 serverCertBytes
    • 然后用 PEM 对 serverPrivKey 和 serverCertBytes 进行编码,这个 serverPrivKeyPEM 和 serverCertPEM 就是 TLS 证书和密钥了,将由 Webhook 服务使用。
  2. 我们就可以生成所需的证书,密钥和 CA_BUNDLE 数据了。然后我们将与同一 Pod 中的实际 Webhook 服务容器共享该服务器证书和密钥。
    • 一种方法是事先创建一个空的 Secret 资源,通过将该 Secret 作为环境变量传递来创建 Webhook 服务,初始化容器将生成服务器证书和密钥,并用证书和密钥信息来填充该 Secret。此 Secret 将安装到 Webhook 服务容器上,以使用 TLS 来启动 HTTP 服务器。
    • 第二种方法(在上面的代码中使用)是使用 Kubernete 的本地 Pod 特定的 emptyDir 卷。该数据卷将在两个容器之间共享,在上面的代码中,我们可以看到 init 容器将这些证书和密钥信息写入特定路径的文件中,该路径就是其中的一个 emptyDir 卷,并且 Webhook 服务容器将从该路径读取用于 TLS 配置的证书和密钥,并启动 HTTP Webhook 服务器。请参考下图:
      在这里插入图片描述
  3. hook 的 Pod 规范如下所示:
spec:
  initContainers:
    - image: <webhook init-image name>
      imagePullPolicy: IfNotPresent
      name: webhook-init
      volumeMounts:
        - mountPath: /etc/webhook/certs
          name: webhook-certs
  containers:
    - image: <webhook server image name>
      imagePullPolicy: IfNotPresent
      name: webhook-server
      volumeMounts:
        - mountPath: /etc/webhook/certs
          name: webhook-certs
          readOnly: true
  volumes:
    - name: webhook-certs
      emptyDir: {}

4.3 处理 CA Bundle

  1. 然后剩下的就只有使用 mutate/验证配置将 CA_BUNDLE 信息提供给 APIServer,这可以通过两种方式完成:
    • 使用 init 容器中的 client-go 在现有 MutatingWebhookConfigurationValidatingWebhookConfiguration 中来修补 CA_BUNDLE 数据。
    • 另一种方式使用配置中的 CA_BUNDLE 数据在 init 容器本身中直接创建 MutatingWebhookConfigurationValidatingWebhookConfiguration 即可。
  2. 在这里,我们将通过 init 容器来创建配置,通过动态获取某些参数,例如 mutate 配置名称,Webhook 服务名称和 Webhook 命名空间,我们都可以直接从 init 容器的环境变量中来获取这些值:
initContainers:
- image: <webhook init-image name>
  imagePullPolicy: IfNotPresent
  name: webhook-init
  volumeMounts:
    - mountPath: /etc/webhook/certs
      name: webhook-certs
  env:
    - name: MUTATE_CONFIG
      value: admission-registry-mutate
    - name: VALIDATE_CONFIG
      value: admission-registry
    - name: WEBHOOK_SERVICE
      value: admission-registry
    - name: WEBHOOK_NAMESPACE
      value:  default
  1. 为了创建 MutatingWebhookConfiguration 或者 ValidatingWebhookConfiguration 资源对象,我们将以下代码添加到上面的 init 容器代码中。
package main

import (
	"bytes"
	"context"
	"os"

	admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/client-go/kubernetes"
	"k8s.io/client-go/rest"
)

func initKubeClient() (*kubernetes.Clientset, error) {
	var (
		err error
		config *rest.Config
	)
	if config, err = rest.InClusterConfig(); err != nil {
		return nil, err
	}

	// 创建 Clientset 对象 go get k8s.io/client-go v0.22.2
	clientset, err := kubernetes.NewForConfig(config)
	if err != nil {
		return nil, err
	}
	return clientset, nil
}

func CreateAdmissionConfig(caCert *bytes.Buffer) error {
	var (
		webhookNamespace, _ = os.LookupEnv("WEBHOOK_NAMESPACE")
		mutationCfgName, _  = os.LookupEnv("MUTATE_CONFIG")
		validateCfgName, _ = os.LookupEnv("VALIDATE_CONFIG")
		webhookService, _ = os.LookupEnv("WEBHOOK_SERVICE")
		validatePath, _ = os.LookupEnv("VALIDATE_PATH")
		mutationPath, _ = os.LookupEnv("MUTATE_PATH")
	)

	clientset, err := initKubeClient()
	if err != nil {
		return err
	}

	ctx := context.Background()

	if validateCfgName != "" {
		validateConfig := &admissionregistrationv1.ValidatingWebhookConfiguration{
			ObjectMeta: metav1.ObjectMeta{
				Name: validateCfgName,
			},
			Webhooks: []admissionregistrationv1.ValidatingWebhook{
				{
					Name: "io.ydzs.admission-registry",
					ClientConfig: admissionregistrationv1.WebhookClientConfig{
						CABundle: caCert.Bytes(),
						Service: &admissionregistrationv1.ServiceReference{
							Name: webhookService,
							Namespace: webhookNamespace,
							Path: &validatePath,
						},
					},
					Rules: []admissionregistrationv1.RuleWithOperations{
						{
							Operations: []admissionregistrationv1.OperationType{admissionregistrationv1.Create},
							Rule: admissionregistrationv1.Rule{
								APIGroups:   []string{""},
								APIVersions: []string{"v1"},
								Resources:   []string{"pods"},
							},
						},
					},
					FailurePolicy: func() *admissionregistrationv1.FailurePolicyType{
						pt := admissionregistrationv1.Fail
						return &pt
					}(),
					AdmissionReviewVersions: []string{"v1"},
					SideEffects: func() *admissionregistrationv1.SideEffectClass {
						se := admissionregistrationv1.SideEffectClassNone
						return &se
					}(),
				},
			},
		}

		validateAdmissionClient := clientset.AdmissionregistrationV1().ValidatingWebhookConfigurations()
		_, err := validateAdmissionClient.Get(ctx, validateCfgName, metav1.GetOptions{})
		if err != nil {
			if errors.IsNotFound(err) {
				if _, err = validateAdmissionClient.Create(ctx, validateConfig, metav1.CreateOptions{}); err != nil {
					return err
				}
			} else {
				return err
			}
		} else {
			if _, err = validateAdmissionClient.Update(ctx, validateConfig, metav1.UpdateOptions{}); err != nil {
				return err
			}
		}

	}

	if mutationCfgName != "" {
		mutateConfig := &admissionregistrationv1.MutatingWebhookConfiguration{
			ObjectMeta: metav1.ObjectMeta{
				Name: mutationCfgName,
			},
			Webhooks: []admissionregistrationv1.MutatingWebhook{{
				Name: "io.ydzs.admission-registry-mutate",
				ClientConfig: admissionregistrationv1.WebhookClientConfig{
					CABundle: caCert.Bytes(), // CA bundle created earlier
					Service: &admissionregistrationv1.ServiceReference{
						Name:      webhookService,
						Namespace: webhookNamespace,
						Path:      &mutationPath,
					},
				},
				Rules: []admissionregistrationv1.RuleWithOperations{{Operations: []admissionregistrationv1.OperationType{
					admissionregistrationv1.Create},
					Rule: admissionregistrationv1.Rule{
						APIGroups:   []string{"apps", ""},
						APIVersions: []string{"v1"},
						Resources:   []string{"deployments", "services"},
					},
				}},
				FailurePolicy: func() *admissionregistrationv1.FailurePolicyType{
					pt := admissionregistrationv1.Fail
					return &pt
				}(),
				AdmissionReviewVersions: []string{"v1"},
				SideEffects: func() *admissionregistrationv1.SideEffectClass {
					se := admissionregistrationv1.SideEffectClassNone
					return &se
				}(),
			}},
		}

		mutateAdmissionClient := clientset.AdmissionregistrationV1().MutatingWebhookConfigurations()
		_, err := mutateAdmissionClient.Get(ctx, mutationCfgName, metav1.GetOptions{})
		if err != nil {
			if errors.IsNotFound(err) {
				if _, err = mutateAdmissionClient.Create(ctx, mutateConfig, metav1.CreateOptions{}); err != nil {
					return err
				}
			} else {
				return err
			}
		} else {
			if _, err = mutateAdmissionClient.Update(ctx, mutateConfig, metav1.UpdateOptions{}); err != nil {
				return err
			}
		}

	}

	return nil
}
  1. 这里首先我们读取环境变量,例如 webhookNamespace,接下来,我们将使用 CA bundle 信息(先前创建)和其他必需信息来定义配置的资源对象结构。最后,我们使用 client-go 来创建配置资源对象。对于 Pod 重新启动或删除的情况,我们可以在 init 容器中添加额外的逻辑,例如首先删除现有配置,然后再仅在创建或更新 CA bundle(如果配置已存在)之前删除它们。
  2. 对于证书轮换的情况,对于向服务器容器提供此证书所采用的每种方法,方法将有所不同:
    • 如果我们使用的是 emptyDir 卷,则方法将是仅重新启动 Webhook Pod。由于 emptyDir 卷是临时的,并且绑定到 Pod 的生命周期,因此在重新启动时,将生成一个新证书并将其提供给服务器容器。如果已经存在配置,则将在配置中添加新的 CA bundle。
    • 如果我们正在使用 Secret 卷,则在重新启动 Webhook Pod 时,可以检查 Secret 中现有证书的有效期,以决定是将现有证书用于服务器还是创建新证书。
  3. 在这两种情况下,都需要重新启动 Webhook Pod 才能触发证书轮换/续订过程。何时需要重新启动 Webhook 容器以及如何重新启动 Webhook 容器,将取决于实际情况。可能的几种方法可以使用Cronjob、controller 等来实现。
  4. 到这里我们的自定义 Webhook 已注册,APIServer 可以通过 config 读取到 CA bundle 信息,并且Webhook 服务已准备好按照 configs 中定义的规则处理 mutate/验证请求。

4.4 部署

  1. 最后将上面的证书生成应用打包成一个 Docker 镜像,将之前部署的 Webhook 服务删除,重新使用如下所示的资源对象进行部署即可:
apiVersion: v1
kind: ServiceAccount
metadata:
  name: admission-registry-sa
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: admission-registry-role
rules:
- verbs: ["*"]
  resources: ["validatingwebhookconfigurations", "mutatingwebhookconfigurations"]
  apiGroups: ["admissionregistration.k8s.io"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: admission-registry-rolebinding
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: admission-registry-role
subjects:
- kind: ServiceAccount
  name: admission-registry-sa
  namespace: default
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: admission-registry
  labels:
    app: admission-registry
spec:
  selector:
    matchLabels:
      app: admission-registry
  template:
    metadata:
      labels:
        app: admission-registry
    spec:
      serviceAccountName: admission-registry-sa
      initContainers:
      - image: qnhyn/admission-registry-tls:v0.0.3
        imagePullPolicy: IfNotPresent
        name: webhook-init
        env:
        - name: WEBHOOK_NAMESPACE
          value: default
        - name: MUTATE_CONFIG
          value: admission-registry-mutate
        - name: VALIDATE_CONFIG
          value: admission-registry
        - name: WEBHOOK_SERVICE
          value: admission-registry
        - name: VALIDATE_PATH
          value: /validate
        - name: MUTATE_PATH
          value: /mutate
        volumeMounts:
          - mountPath: /etc/webhook/certs
            name: webhook-certs
      containers:
      - name: webhook
        image: qnhyn/admission-registry:v0.1.4
        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
          emptyDir: {}
---
apiVersion: v1
kind: Service
metadata:
  name: admission-registry
  labels:
    app: admission-registry
spec:
  ports:
    - port: 443
      targetPort: 443
  selector:
    app: admission-registry
  1. 现在我们就不需要自己手动去创建包含证书的 Secret 资源对象了,也不需要手动去替换准入控制器配置对象中的 CA bundle 信息了,这些都将通过 Init 初始化容器来帮我们自动完成。
  2. 由于初始化容器需要访问 MutatingWebhookConfigurationValidatingWebhookConfiguration 这两个资源对象,所以我们需要声明对应的 RBAC 权限。创建完成后的资源对象如下所示:
kubectl get pods -l app=admission-registry
kubectl exec -it admission-registry-64f6b46cdc-vqbrl -- ls 
kubectl get validatingwebhookconfiguration       
kubectl get mutatingwebhookconfigurations        
  1. 然后同样再去测试一次即可,到这里我们就完成了使用初始化容器来管理 Admission Webhook 的 TLS 证书的功能。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值