avatar

Kubernetes 提供了很多确保工作负载安全的功能,其中有一项是准入控制器(Admission Controllers), 在对象持久化之前对 API Server 的请求进行拦截,可以更改请求对象,也可以完全拒绝请求。Kubernetes有两个特殊的准入控制器: MutatingAdmissionWebhook 和 ValidatingAdmissionWebhook,分别作为修改和验证请求对象的准入控制 WebHook。

回到 Karmada 的应用场景中来,笔者之前收到用户反馈,有一些 Workload 和 PVC 并不在 Karmada 的控制面,删除 Namespace 时没有检查或因为 member 集群多检查没有到位,在 Karmada 控制面删除 Namespace 后, 有些 member 集群的 Namespace 下的 Workload 和 PVC 被级联删除了。在生产环境中,这种失误往往能导致灾难级的故障。

对于这个问题,我们可以通过验证(Validating)准入控制来解决,在 Karmada 控制面删除 Namespace 或 CRDs 时,去遍历 member 集群,如果 Namespace 下有 Workload 或 PVC, CRDs 还有资源对象存在就拒绝请求。

方案

以删除 Namespace 为例,当 API Server 收到删除 Namespace 的请求时,ValidatingAdmissionWebhook 就去调用我们自定义的 WebHook 服务,WebHook 服务从 Karmada 的 AA Server 中查找该 Namespace 有没有 Pod ,如果有,则拒绝请求。 avatar

实现

其实实现起来非常简单,通过HTTP Server 接收 AdmissionRequest 响应 AdmissionResponse 就行。我们可以参考 Karmada 社区的 WebHook,通过 WebHook Manager 来管理 WebHook,可以很优雅的复用和封装。

func Run(ctx context.Context, opts *options.Options) error {
 config, err := ctrl.GetConfig()
 if err != nil {
  panic(err)
 }

 hookManager, err := ctrl.NewManager(config, ctrl.Options{
  Scheme: NewSchema(),
  WebhookServer: &webhook.Server{
   Host:          opts.BindAddress,
   Port:          opts.SecurePort,
   CertDir:       opts.CertDir,
   CertName:      opts.CertName,
   KeyName:       opts.KeyName,
   TLSMinVersion: opts.TLSMinVersion,
  },
  LeaderElection:         false,
  MetricsBindAddress:     opts.MetricsBindAddress,
  HealthProbeBindAddress: opts.HealthProbeBindAddress,
 })
 if err != nil {
  klog.Errorf("failed to build webhook server: %v", err)
  return err
 }
 if err := hookManager.AddHealthzCheck(CheckEndpointHealthz, healthz.Ping); err != nil {
  return fmt.Errorf("failed to add %q health check endpoint: %v", CheckEndpointHealthz, err)
 }
 if err := hookManager.AddReadyzCheck(CheckEndpointReadyz, healthz.Ping); err != nil {
  return fmt.Errorf("failed to add %q health check endpoint: %v", CheckEndpointReadyz, err)
 }

 klog.Info("registering webhooks to the webhook server")
 hookServer := hookManager.GetWebhookServer()

 hookServer.Register("/validate-namespace", &webhook.Admission{Handler: namespace.NewValidatingAdmission(hookManager)})

 // blocks until the context is done.
 if err := hookManager.Start(ctx); err != nil {
  klog.Errorf("webhook server exits unexpectedly: %v", err)
  return err
 }

 // never reach here
 return nil
}

同样,我们逻辑也在 Handle 函数中处理。先列出 member 集群,然后再检查 member 集群下的 Namespace 有没有运行 Pod。如果有,则拒绝请求。

func (v *ValidatingAdmission) Handle(ctx context.Context, req admission.Request) admission.Response {
 klog.Infof("%s namespace %q", req.Operation, req.Name)
 clusters, err := v.ClusterList(ctx)
 if err != nil {
  return admission.Denied(err.Error())
 }

 for _, c := range clusters {
  if err := v.PodList(c, req.Name); err != nil {
   return admission.Denied(err.Error())
  }
 }

 return admission.Allowed("")
}

需要注意的是,Pull 模式需要部署 ANP 。感兴趣的读者可以去 Karmada 社区查看这方面的文档。

func (v *ValidatingAdmission) ClusterList(ctx context.Context) ([]string, error) {
 clusterList := &clusterv1alpha1.ClusterList{}
 if err := v.Client.List(ctx, clusterList); err != nil {
  return nil, fmt.Errorf("failed to list clusters, error: %v", err)
 }

 var clusters []string
 for _, c := range clusterList.Items {
  // TODO Pull 模式可以部署 apiserver-network-proxy(ANP)来访问。这里跳过.
  // https://github.com/karmada-io/karmada/blob/master/docs/userguide/aggregated-api-endpoint.md
  if c.Spec.SyncMode == clusterv1alpha1.Pull {
   continue
  }
  clusters = append(clusters, c.Name)
 }
 return clusters, nil
}

这里通过 Karmada API Server Proxy 获取 Pod

func (v *ValidatingAdmission) PodList(cluster, namespace string) error {
 uil := fmt.Sprintf("%s/apis/cluster.karmada.io/v1alpha1/clusters/%s/proxy/api/v1/namespaces/%s/pods", v.Config.Host, cluster, namespace)
 request := v.Clientset.RESTClient().Get().RequestURI(uil)
 result := request.Do(context.Background())
 if err := result.Error(); err != nil {
  return fmt.Errorf("result err: %v", err)
 }
 pods, err := result.Raw()
 if err != nil {
  return fmt.Errorf("raw err: %v", err)
 }
 podList := &corev1.PodList{}

 if err := json.Unmarshal(pods, podList); err != nil {
  return fmt.Errorf("unmarshal err: %v", err)
 }

 if len(podList.Items) == 0 {
  return nil
 }

 var activityPods []string
 for _, p := range podList.Items {
  fmt.Println(p.Name)
  activityPods = append(activityPods, p.Name)
 }

 return fmt.Errorf("the workload of cluster %q namespace %q is not empty, pods: %s", cluster, namespace, activityPods)
}

部署

容器化部署也行,笔者这里是二进制运行。

生成证书

openssl genrsa -out ca.key 2048
openssl req -x509 -new -nodes -key ca.key -subj "/C=CN/ST=Guangdong/L=Guangzhou/O=karmada/OU=System/CN=karmada" -days 3650 -out ca.crt

openssl genrsa -out tls.key 2048
openssl req -new -nodes -sha256 -subj "/C=CN/ST=Guangdong/L=Guangzhou/O=kubernetes/OU=System/CN=karmada"  -key tls.key -out tls.csr
openssl x509 -req -days 3650 \
  -extfile <(printf "keyUsage=critical,Digital Signature, Key Encipherment\nextendedKeyUsage=serverAuth,clientAuth\nauthorityKeyIdentifier=keyid,issuer\nsubjectAltName=DNS:karmada-custom-webhook.karmada-system.svc.cluster.local,DNS:localhost,IP:172.0.0.1,IP:172.31.6.145") \
  -sha256 -CA ca.crt -CAkey ca.key -set_serial 01 -in tls.csr -out tls.crt

创建 Validating 配置文件,只验证删除请求。Namespace 是集群级别的资源,所以 scope 是 Cluster。

---
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
  name: validating-config
  labels:
    app: validating-config
webhooks:
  - name: "namespace-policy.example.com"
    rules:
      - apiGroups: [""]
        apiVersions: ["v1"]
        operations: ["DELETE"]
        resources: ["namespaces"]
        scope: "Cluster"
    clientConfig:
      url: https://172.31.6.145:8443/validate-namespace
      caBundle: {{caBundle}}
    failurePolicy: Fail
    sideEffects: None
    admissionReviewVersions: ["v1"]
    timeoutSeconds: 3

应用 Validating 配置

ca_string=$(sudo cat ca.crt| base64 | tr "\n" " "|sed s/[[:space:]]//g)
sed -i "s/{{caBundle}}/${ca_string}/g" webhook-configuration.yaml
kubectl --kubeconfig /etc/karmada/karmada-apiserver.config create  -f webhook-configuration.yaml

运行 WebHook 服务

./karmada-custom-webhook \
--kubeconfig /etc/karmada/karmada-apiserver.config \
--cert-dir ./certs

创建测试 Namespace

kubectl --kubeconfig /etc/karmada/karmada-apiserver.config create ns test

在其中一个 member 集群创建一个 Pod

kubectl create -f busybox.yaml -n test

删除 Namespace ,因为有 Pod 在运行,使用拒绝了请求。说明它可以正常的工作。 avatar

最后

当然,这也只是其中一种保障生产安全的一种方式,仅供参考。在 kubernetes 生态中,也有一些工具可以做到。

参考链接: https://github.com/karmada-io/karmada

文章中的源码: https://github.com/prodanlabs/karmada-examples