在 Kubernetes 的 Webhook 系統中,我們可以針對不同的資源操作進行精確控制。Webhook 可以監聽並干預各種資源的生命週期事件,包括:

webhook:
  - name: validate-resource-operations.k8s.io
    rules:
    - operations:
      - CREATE
      - UPDATE
      - DELETE
      - CONNECT
      apiGroups:
      - ""  # 核心 API 群組
      apiVersions:
      - "*"  # 所有 API 版本
      resources:
      - pods
      - services
      - configmaps
      - secrets
    failurePolicy: Fail  # 處理失敗時的策略

上面的 YAML 設定了一個定義 Webhook 的規則部分,它指定了:

  1. 操作型別:這個 Webhook 會攔截資源的建立、更新、刪除和連線操作
  2. API 群組:空字串 "" 代表核心 API 群組,包含最基本的 Kubernetes 資源
  3. API 版本"*" 表示所有版本的 API
  4. 資源型別:明確列出了需要攔截的資源型別,包括 Pod、Service、ConfigMap 和 Secret
  5. 失敗策略:設定為 Fail,表示當 Webhook 處理失敗時,相關的 API 請求也會失敗

失敗策略的重要性

failurePolicy 是 Webhook 設定中的關鍵引數,它決定了當 Webhook 服務不可用或發生錯誤時系統的行為:

failurePolicy: Ignore  # 可選值: Fail 或 Ignore

這個設定有兩個可能的值:

  • Fail:當 Webhook 呼叫失敗時,API 請求會被拒絕。這是較為嚴格的設定,確保所有資源變更都必須透過 Webhook 驗證。
  • Ignore:當 Webhook 呼叫失敗時,API 請求會被允許繼續處理。這提供了較高的可用性,但可能會在 Webhook 服務中斷時允許不符合規則的資源進入叢集。

Webhook 型別與應用場景

Kubernetes 支援兩種主要型別的 Webhook:

1. 驗證型 Webhook (Validating Webhook)

apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
  name: pod-policy-validator
webhooks:
- name: validate-pods.example.com
  rules:
  - apiGroups: [""]
    apiVersions: ["v1"]
    resources: ["pods"]
    operations: ["CREATE", "UPDATE"]
  clientConfig:
    service:
      namespace: webhook-system
      name: webhook-service
      path: "/validate-pods"
    caBundle: <base64-encoded-ca-cert>
  failurePolicy: Fail
  sideEffects: None
  admissionReviewVersions: ["v1"]
  timeoutSeconds: 5

這個設定了一個定義驗證型 Webhook,它的主要功能是:

  1. 只針對 Pod 資源的建立和更新操作進行驗證
  2. 指定了 Webhook 服務的位置(namespace、service name 和路徑)
  3. 包含了 CA 憑證,用於 TLS 連線
  4. 設定了 5 秒的超時間
  5. sideEffects: None 表示這個 Webhook 不會產生任何副作用
  6. admissionReviewVersions 指定了支援的 AdmissionReview API 版本

2. 修改型 Webhook (Mutating Webhook)

apiVersion: admissionregistration.k8s.io/v1
kind: MutatingWebhookConfiguration
metadata:
  name: pod-modifier
webhooks:
- name: mutate-pods.example.com
  rules:
  - apiGroups: [""]
    apiVersions: ["v1"]
    resources: ["pods"]
    operations: ["CREATE"]
  clientConfig:
    service:
      namespace: webhook-system
      name: webhook-service
      path: "/mutate-pods"
    caBundle: <base64-encoded-ca-cert>
  failurePolicy: Ignore
  sideEffects: None
  admissionReviewVersions: ["v1"]
  timeoutSeconds: 3
  reinvocationPolicy: Never

這個設定了一個定義修改型 Webhook,它的特點是:

  1. 只針對 Pod 資源的建立操作進行修改
  2. 失敗策略設為 Ignore,表示 Webhook 失敗時不會阻止 Pod 的建立
  3. 超時間設為 3 秒,比驗證型 Webhook 更短
  4. reinvocationPolicy: Never 表示這個 Webhook 在同一個 AdmissionReview 請求中只會被呼叫一次

Webhook 執行順序與優先順序

在 Kubernetes 中,Webhook 的執行順序非常重要,特別是當有多個 Webhook 時:

  1. 所有的修改型 Webhook 先執行,按照它們的 metadata.name 字母順序排序
  2. 然後執行所有的驗證型 Webhook,同樣按照名稱排序

這意味著如果你需要控制 Webhook 的執行順序,可以透過命名約定來實作:

metadata:
  name: "01-inject-sidecars"  # 會先執行
metadata:
  name: "02-validate-security"  # 後執行

高階 Webhook 設定選項

1. 名稱空間選擇器

你可以使用名稱空間選擇器來限制 Webhook 只對特定名稱空間的資源生效:

namespaceSelector:
  matchExpressions:
  - key: environment
    operator: In
    values: ["production", "staging"]

這個選擇器會使 Webhook 只處理帶有 environment: productionenvironment: staging 標籤的名稱空間中的資源。這對於實作環境隔離的策略非常有用。

2. 物件選擇器

物件選擇器允許你根據資源的標籤來決定是否應用 Webhook:

objectSelector:
  matchLabels:
    app: critical-service

這個設定使 Webhook 只處理帶有 app: critical-service 標籤的資源,忽略其他資源。這對於只對特定應用程式實施策略非常有用。

3. 比對條件

在 Kubernetes v1.25+ 中,你可以使用更強大的比對條件:

matchConditions:
- name: "exclude-system-namespaces"
  expression: "!(object.metadata.namespace.startsWith('kube-'))"

這個條件使用 CEL (Common Expression Language) 表示式來排除所有 kube- 開頭的名稱空間中的資源。這提供了比簡單選擇器更靈活的過濾機制。

Webhook 安全性考量

Webhook 在 Kubernetes 中扮演著關鍵的角色,因此安全性至關重要:

  1. TLS 加密:所有 Webhook 通訊都應該使用 TLS 加密,透過 caBundle 欄位提供 CA 證書

  2. 最小許可權原則:Webhook 服務應該只有執行其功能所需的最小許可權

  3. 超時設定:合理設定 timeoutSeconds 以避免 Webhook 延遲影響整個 API 伺服器

  4. 高用性:關鍵的 Webhook 應該佈署為高用性設定,避免單點故障

  5. 稽核日誌:啟用稽核日誌來追蹤 Webhook 的決策和操作

實際應用案例:Pod 安全策略實施

以下是一個完整的例子,展示如何使用 Webhook 來實施 Pod 安全策略:

apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
  name: pod-security-validator
webhooks:
- name: podsecurity.webhook.example.com
  rules:
  - apiGroups: [""]
    apiVersions: ["v1"]
    resources: ["pods"]
    operations: ["CREATE", "UPDATE"]
  clientConfig:
    service:
      namespace: security
      name: pod-security-webhook
      path: "/validate"
    caBundle: <base64-encoded-ca-cert>
  failurePolicy: Fail
  namespaceSelector:
    matchExpressions:
    - key: pod-security.kubernetes.io/enforce
      operator: In
      values: ["baseline", "restricted"]
  sideEffects: None
  admissionReviewVersions: ["v1"]
  timeoutSeconds: 2

這個設定建立了一個驗證型 Webhook,用於實施 Pod 安全策略:

  1. 它只針對 Pod 的建立和更新操作
  2. 只在帶有特定安全標籤的名稱空間中生效
  3. 失敗策略設為 Fail,確保不符合安全要求的 Pod 無法被建立
  4. 超時間設為 2 秒,保證快速回應
  5. 透過服務設定指向實際執行驗證邏輯的 Webhook 服務

Webhook 服務實作範例

下面是一個簡單的 Go 語言 Webhook 服務實作,用於驗證 Pod 安全性:

package main

import (
    "encoding/json"
    "fmt"
    "io/ioutil"
    "net/http"
    
    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"
)

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

// 驗證 Pod 是否符合安全要求
func validatePod(pod *corev1.Pod) (bool, string) {
    // 檢查是否有特權容器
    for _, container := range pod.Spec.Containers {
        if container.SecurityContext != nil && 
           container.SecurityContext.Privileged != nil && 
           *container.SecurityContext.Privileged {
            return false, fmt.Sprintf("特權容器不允許: %s", container.Name)
        }
    }
    
    // 檢查是否掛載了敏感主機路徑
    for _, volume := range pod.Spec.Volumes {
        if volume.HostPath != nil {
            return false, fmt.Sprintf("不允許掛載主機路徑: %s", volume.Name)
        }
    }
    
    return true, ""
}

// 處理 Webhook 請求
func handleValidate(w http.ResponseWriter, r *http.Request) {
    var body []byte
    if r.Body != nil {
        if data, err := ioutil.ReadAll(r.Body); err == nil {
            body = data
        }
    }
    
    // 驗證內容型別
    contentType := r.Header.Get("Content-Type")
    if contentType != "application/json" {
        http.Error(w, "無效的 Content-Type", http.StatusUnsupportedMediaType)
        return
    }
    
    // 解析 AdmissionReview 請求
    var admissionReview admissionv1.AdmissionReview
    if _, _, err := deserializer.Decode(body, nil, &admissionReview); err != nil {
        http.Error(w, fmt.Sprintf("無法解析請求: %v", err), http.StatusBadRequest)
        return
    }
    
    // 確保請求有效
    if admissionReview.Request == nil {
        http.Error(w, "無效的 AdmissionReview 請求", http.StatusBadRequest)
        return
    }
    
    // 解析 Pod 物件
    var pod corev1.Pod
    if err := json.Unmarshal(admissionReview.Request.Object.Raw, &pod); err != nil {
        http.Error(w, fmt.Sprintf("無法解析 Pod: %v", err), http.StatusBadRequest)
        return
    }
    
    // 驗證 Pod
    allowed, reason := validatePod(&pod)
    
    // 準備回應
    response := admissionv1.AdmissionReview{
        TypeMeta: metav1.TypeMeta{
            Kind:       "AdmissionReview",
            APIVersion: "admission.k8s.io/v1",
        },
        Response: &admissionv1.AdmissionResponse{
            UID:     admissionReview.Request.UID,
            Allowed: allowed,
        },
    }
    
    // 如果不允許,增加原因
    if !allowed {
        response.Response.Result = &metav1.Status{
            Message: reason,
        }
    }
    
    // 回傳 JSON 回應
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(response)
}

func main() {
    http.HandleFunc("/validate", handleValidate)
    fmt.Println("啟動 Webhook 服務,監聽 8443 連線埠...")
    if err := http.ListenAndServeTLS(":8443", "/certs/tls.crt", "/certs/tls.key", nil); err != nil {
        panic(err)
    }
}

這個 Go 程式實作了一個簡單的驗證型 Webhook 服務:

  1. 它接收 Kubernetes 的 AdmissionReview 請求
  2. 解析請求中的 Pod 物件
  3. 檢查 Pod 是否符合安全要求(不允許特權容器和主機路徑掛載)
  4. 回傳一個 AdmissionResponse,指示是否允許該 Pod
  5. 如果拒絕,提供拒絕的原因
  6. 服務使用 TLS 加密通訊,證書和金鑰從掛載的卷中讀取

Webhook 佈署最佳實踐

為了確保 Webhook 服務的可靠性和安全性,以下是一些佈署最佳實踐:

  1. 使用 Deployment 佈署:確保 Webhook 服務有多個副本,提高用性
apiVersion: apps/v1
kind: Deployment
metadata:
  name: webhook-service
  namespace: webhook-system
spec:
  replicas: 2
  selector:
    matchLabels:
      app: webhook-service
  template:
    metadata:
      labels:
        app: webhook-service
    spec:
      containers:
      - name: webhook
        image: webhook-image:latest
        ports:
        - containerPort: 8443
        volumeMounts:
        - name: webhook-certs
          mountPath: /certs
          readOnly: true
        resources:
          limits:
            cpu: 200m
            memory: 256Mi
          requests:
            cpu: 100m
            memory: 128Mi
        readinessProbe:
          httpGet:
            path: /health
            port: 8080
            scheme: HTTP
      volumes:
      - name: webhook-certs
        secret:
          secretName: webhook-certs
  1. 使用 PodDisruptionBudget:確保在節點維護期間 Webhook 服務仍然可用
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: webhook-pdb
  namespace: webhook-system
spec:
  minAvailable: 1
  selector:
    matchLabels:
      app: webhook-service
  1. 使用 NetworkPolicy:限制對 Webhook 服務的網路存取
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: webhook-network-policy
  namespace: webhook-system
spec:
  podSelector:
    matchLabels:
      app: webhook-service
  ingress:
  - from:
    - namespaceSelector:
        matchLabels:
          kubernetes.io/metadata.name: kube-system
    ports:
    - protocol: TCP
      port: 8443
  1. 自動證書管理:使用 cert-manager 自動管理 Webhook 的 TLS 證書
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: webhook-cert
  namespace: webhook-system
spec:
  secretName: webhook-certs
  duration: 8760h  # 1 年
  renewBefore: 720h  # 30 天
  subject:
    organizations:
    - Example Org
  isCA: false
  privateKey:
    algorithm: RSA
    encoding: PKCS1
    size: 2048
  usages:
    - server auth
  dnsNames:
  - webhook-service.webhook-system.svc
  - webhook-service.webhook-system.svc.cluster.local
  issuerRef:
    name: webhook-issuer
    kind: ClusterIssuer

故障排除與監控

有效監控和故障排除對於維護 Webhook 系統至關重要:

  1. 日誌收集:確保 Webhook 服務的日誌被收集並可搜尋
containers:
- name: webhook
  image: webhook-image:latest
  args:
  - "--log-level=info"
  - "--log-format=json"
  1. 指標監控:暴露 Prometheus 指標以監控 Webhook 效能
import (
    "github.com/prometheus/client_golang/prometheus"
    "github.com/prometheus/client_golang/prometheus/promhttp"
)

var (
    webhookRequests = prometheus.NewCounterVec(
        prometheus.CounterOpts{
            Name: "webhook_requests_total",
            Help: "Webhook 請求總數",
        },
        []string{"operation", "resource", "allowed"},
    )
    
    webhookLatency = prometheus.NewHistogramVec(
        prometheus.HistogramOpts{
            Name:    "webhook_latency_seconds",
            Help:    "Webhook 處理延遲(秒)",
            Buckets: prometheus.DefBuckets,
        },
        []string{"operation", "resource"},
    )
)

func init() {
    prometheus.MustRegister(webhookRequests)
    prometheus.MustRegister(webhookLatency)
}

func main() {
    // ... 其他初始化程式碼 ...
    
    // 暴露 Prometheus 指標
    http.Handle("/metrics", promhttp.Handler())
    
    // ... 其他程式碼 ...
}
  1. 告警設定:設定告警以在 Webhook 失敗時通知管理員
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
  name: webhook-alerts
  namespace: monitoring
spec:
  groups:
  - name: webhook.rules
    rules:
    - alert: WebhookHighErrorRate
      expr: sum(rate(webhook_requests_total{allowed="false"}[5m])) / sum(rate(webhook_requests_total[5m])) > 0.1
      for: 5m
      labels:
        severity: warning
      annotations:
        summary: "Webhook 錯誤率高"
        description: "Webhook 拒絕率超過 10%,請檢查日誌"
    
    - alert: WebhookHighLatency
      expr: histogram_quantile(0.95, sum(rate(webhook_latency_seconds_bucket[5m])) by (le)) > 0.5
      for: 5m
      labels:
        severity: warning
      annotations:
        summary: "Webhook 延遲高"
        description: "Webhook 95% 延遲超過 500ms,可能影響 API 伺服器效能"

進階 Webhook 使用案例

1. 動態資源修改

修改型 Webhook 可以在資源建立時自動注入設定,例如為所有 Pod 增加 sidecar 容器:

func createPatch(pod *corev1.Pod) ([]byte, error) {
    // 建立一個 sidecar 容器
    sidecar := corev1.Container{
        Name:  "logging-sidecar",
        Image: "fluent/fluent-bit:latest",
        Resources: corev1.ResourceRequirements{
            Limits: corev1.ResourceList{
                corev1.ResourceCPU:    resource.MustParse("100m"),
                corev1.ResourceMemory: resource.MustParse("50Mi"),
            },
            Requests: corev1.ResourceList{
                corev1.ResourceCPU:    resource.MustParse("10m"),
                corev1.ResourceMemory: resource.MustParse("25Mi"),
            },
        },
    }
    
    // 建立 JSON 補丁
    var patch []patchOperation
    patch = append(patch, patchOperation{
        Op:    "add",
        Path:  "/spec/containers/-",
        Value: sidecar,
    })
    
    return json.Marshal(patch)
}

2. 自定義策略實施

Webhook 可以實施組織特定的策略,例如強制要求所有資源都有特定標籤:

func validateLabels(obj metav1.Object) (bool, string) {
    labels := obj.GetLabels()
    
    // 檢查必要的標籤
    if _, hasTeam := labels["team"]; !hasTeam {
        return false, "缺少必要的 'team' 標籤"
    }
    
    if _, hasApp := labels["app"]; !hasApp {
        return false, "缺少必要的 'app' 標籤"
    }
    
    if _, hasEnv := labels["environment"]; !hasEnv {
        return false, "缺少必要的 'environment' 標籤"
    }
    
    return true, ""
}

3. 成本控制與資源限制

Webhook 可以用於實施資源限制,控制叢整合本:

func validateResourceLimits(pod *corev1.Pod) (bool, string) {
    namespace := pod.Namespace
    
    // 根據名稱空間確定資源限制
    var maxCPU, maxMemory resource.Quantity
    
    switch {
    case strings.HasPrefix(namespace, "dev-"):
        maxCPU = resource.MustParse("500m")
        maxMemory = resource.MustParse("1Gi")
    case strings.HasPrefix(namespace, "staging-"):
        maxCPU = resource.MustParse("2000m")
        maxMemory = resource.MustParse("4Gi")
    case strings.HasPrefix(namespace, "prod-"):
        maxCPU = resource.MustParse("4000m")
        maxMemory = resource.MustParse("8Gi")
    default:
        maxCPU = resource.MustParse("200m")
        maxMemory = resource.MustParse("512Mi")
    }
    
    // 檢查每個容器的資源請求
    for _, container := range pod.Spec.Containers {
        if container.Resources.Requests == nil {
            return false, fmt.Sprintf("容器 %s 缺少資源請求", container.Name)
        }
        
        cpuRequest := container.Resources.Requests[corev1.ResourceCPU]
        memoryRequest := container.Resources.Requests[corev1.ResourceMemory]
        
        if cpuRequest.Cmp(maxCPU) > 0 {
            return false, fmt.Sprintf("容器 %s CPU 請求 %s 超過限制 %s", 
                container.Name, cpuRequest.String(), maxCPU.String())
        }
        
        if memoryRequest.Cmp(maxMemory) > 0 {
            return false, fmt.Sprintf("容器 %s 記憶體請求 %s 超過限制 %s", 
                container.Name, memoryRequest.String(), maxMemory.String())
        }
    }
    
    return true, ""
}

Kubernetes Webhook 系統提供了強大的擴充套件機制,讓管理員和開發者能夠實施自定義的策略和自動化。透過正確設定 Webhook 的操作範圍、API 群組、資源型別和失敗策略,可以精確控制 Webhook 的行為。在實際佈署中,應該注意 Webhook 的效能、可用性和安全性,確保它們不會成為 Kubernetes API 伺服器的瓶頸。透過遵循本文介紹的最佳實踐,你可以構建強大、可靠的 Webhook 系統,為你的 Kubernetes 叢集增加自定義的控制和自動化能力。

深入解析 Kubernetes 准入控制器:原理、實作與最佳實踐

准入控制器的核心概念

准入控制器是 Kubernetes API 伺服器中的關鍵元件,負責在資源持久化到 etcd 之前對請求進行攔截和檢查。它們就像是 Kubernetes 叢集的守門員,確保只有符合特定規則和政策的資源才能被建立、修改或刪除。

准入控制器的工作流程分為兩個階段:

  1. 變更准入控制(Mutating Admission Control):可以修改請求的資源物件
  2. 驗證准入控制(Validating Admission Control):只能驗證請求的資源物件,不能修改

這種雙階段設計使得 Kubernetes 能夠在保證安全性的同時提供靈活的資源處理機制。

准入控制器的設計體現了 Kubernetes 的防禦縱深策略。透過將控制分為變更和驗證兩個階段,系統能夠先對資源進行必要的修改(如注入 sidecar 容器),再進行嚴格的政策驗證。這種分離設計也符合單一職責原則,使得每個控制器的邏輯更加清晰和可維護。

准入控制器的型別

Kubernetes 中的准入控制器可以分為兩大類別:

  1. 內建准入控制器:直接編譯到 API 伺服器中,透過啟動引數設定
  2. 動態准入控制器:以 Webhook 形式佈署,可以在不重啟 API 伺服器的情況下動態設定

內建准入控制器包括 LimitRangerResourceQuotaServiceAccount 等,而動態准入控制器則透過 MutatingWebhookConfigurationValidatingWebhookConfiguration 資源進行設定。

內建准入控制器雖然功能強大,但缺乏靈活性,每次修改都需要重啟 API 伺服器。而動態准入控制器(Webhook)則彌補了這一缺陷,使得開發者可以根據自身需求實作自定義的准入邏輯,並且能夠在執行時動態調整。這種設計反映了 Kubernetes 的擴充套件性理念,允許使用者在不修改核心程式碼的情況下擴充套件系統功能。

實作自定義准入 Webhook

讓我們來實作一個簡單的驗證准入 Webhook,它將確保所有 Pod 都有特定的標籤。

首先,我們需要建立一個 Webhook 伺服器:

package main

import (
    "encoding/json"
    "fmt"
    "io/ioutil"
    "net/http"
    
    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"
)

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

// 定義 Webhook 伺服器
type WebhookServer struct {
    server *http.Server
}

// 處理准入請求
func (ws *WebhookServer) validate(ar *admissionv1.AdmissionReview) *admissionv1.AdmissionResponse {
    req := ar.Request
    
    // 解析 Pod 物件
    var pod corev1.Pod
    if err := json.Unmarshal(req.Object.Raw, &pod); err != nil {
        return &admissionv1.AdmissionResponse{
            Result: &metav1.Status{
                Message: err.Error(),
            },
        }
    }
    
    // 檢查是否有必要的標籤
    if _, ok := pod.Labels["app"]; !ok {
        return &admissionv1.AdmissionResponse{
            Allowed: false,
            Result: &metav1.Status{
                Message: "Pod 必須包含 'app' 標籤",
            },
        }
    }
    
    // 允許請求
    return &admissionv1.AdmissionResponse{
        Allowed: true,
    }
}

// 處理 HTTP 請求
func (ws *WebhookServer) serveValidate(w http.ResponseWriter, r *http.Request) {
    var body []byte
    if r.Body != nil {
        if data, err := ioutil.ReadAll(r.Body); err == nil {
            body = data
        }
    }
    
    // 驗證 Content-Type
    contentType := r.Header.Get("Content-Type")
    if contentType != "application/json" {
        http.Error(w, "Content-Type 必須是 application/json", http.StatusUnsupportedMediaType)
        return
    }
    
    // 解析 AdmissionReview
    var admissionReview admissionv1.AdmissionReview
    if _, _, err := deserializer.Decode(body, nil, &admissionReview); err != nil {
        http.Error(w, fmt.Sprintf("無法解析請求: %v", err), http.StatusBadRequest)
        return
    }
    
    // 處理請求
    var responseObj runtime.Object
    if admissionReview.Request != nil {
        admissionResponse := ws.validate(&admissionReview)
        admissionReview.Response = admissionResponse
        admissionReview.Response.UID = admissionReview.Request.UID
        responseObj = &admissionReview
    }
    
    // 傳送回應
    resp, err := json.Marshal(responseObj)
    if err != nil {
        http.Error(w, fmt.Sprintf("無法編碼回應: %v", err), http.StatusInternalServerError)
        return
    }
    
    w.Header().Set("Content-Type", "application/json")
    w.Write(resp)
}

func main() {
    ws := &WebhookServer{
        server: &http.Server{
            Addr: ":8443",
        },
    }
    
    // 設定路由
    mux := http.NewServeMux()
    mux.HandleFunc("/validate", ws.serveValidate)
    ws.server.Handler = mux
    
    // 啟動伺服器
    fmt.Println("啟動伺服器...")
    if err := ws.server.ListenAndServeTLS("/etc/webhook/certs/tls.crt", "/etc/webhook/certs/tls.key"); err != nil {
        fmt.Printf("啟動伺服器失敗: %v", err)
    }
}

這段程式碼實作了一個基本的驗證准入 Webhook 伺服器。它接收來自 Kubernetes API 伺服器的 AdmissionReview 請求,解析出 Pod 物件,然後檢查該 Pod 是否包含 “app” 標籤。如果沒有,則拒絕請求並回傳錯誤訊息。

關鍵部分包括:

  1. 使用 deserializer.Decode() 解析 AdmissionReview 物件
  2. 使用 json.Unmarshal() 將原始 Pod 資料轉換為結構化物件
  3. 檢查 Pod 標籤並回傳相應的 AdmissionResponse
  4. 設定 HTTPS 伺服器以接收 Webhook 請求

這個實作展示了准入 Webhook 的基本工作流程,但在生產環境中還需要考慮錯誤處理、日誌記錄、監控等方面。

設定 Webhook

接下來,我們需要在 Kubernetes 中設定 ValidatingWebhookConfiguration 來啟用我們的 Webhook:

apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
  name: pod-label-validator
webhooks:
- name: pod-label.example.com
  clientConfig:
    service:
      name: pod-label-validator
      namespace: default
      path: "/validate"
    caBundle: ${CA_BUNDLE}
  rules:
  - apiGroups: [""]
    apiVersions: ["v1"]
    operations: ["CREATE", "UPDATE"]
    resources: ["pods"]
    scope: "Namespaced"
  sideEffects: None
  admissionReviewVersions: ["v1"]
  failurePolicy: Fail
  timeoutSeconds: 5

這個 YAML 設定了一個定義 ValidatingWebhookConfiguration 資源,它告訴 Kubernetes API 伺服器在建立或更新 Pod 時呼叫我們的 Webhook。

關鍵設定包括:

  • clientConfig:指定 Webhook 伺服器的位置,這裡使用 Service 參照
  • rules:定義哪些資源和操作會觸發 Webhook
  • sideEffects:宣告 Webhook 沒有副作用,這是 v1 版本的必要欄位
  • failurePolicy:定義當 Webhook 不可用時的行為,這裡設定為 Fail(拒絕請求)
  • timeoutSeconds:設定 API 伺服器等待 Webhook 回應的最長時間

${CA_BUNDLE} 需要替換為 Webhook 伺服器 TLS 證書的 CA 證書,通常透過 base64 編碼後注入。

佈署 Webhook 伺服器

為了佈署我們的 Webhook 伺服器,我們需要建立以下 Kubernetes 資源:

apiVersion: v1
kind: Service
metadata:
  name: pod-label-validator
  namespace: default
spec:
  selector:
    app: pod-label-validator
  ports:
  - port: 443
    targetPort: 8443
apiVersion: v1
kind: ConfigMap
metadata:
  name: opa-policies
  namespace: opa
data:
  pod-security.rego: |
    package kubernetes.admission
    
    deny[msg] {
      input.request.kind.kind == "Pod"
      input.request.operation == "CREATE"
      container := input.request.object.spec.containers[_]
      container.securityContext.privileged == true
      msg := sprintf("privileged container is not allowed: %v", [container.name])
    }
    
    deny[msg] {
      input.request.kind.kind == "Pod"
      input.request.operation == "CREATE"
      not input.request.object.metadata.labels.app
      msg := "pod must have app label"
    }
---
apiVersion: v1
kind: Service
metadata:
  name: opa
  namespace: opa
spec:
  selector:
    app: opa
  ports:
  - port: 443
    targetPort: 8181
---
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
  name: opa-validating-webhook
webhooks:
- name: validating-webhook.openpolicyagent.org
  clientConfig:
    service:
      name: opa
      namespace: opa
      path: "/v1/data/kubernetes/admission/deny"
    caBundle: ${CA_BUNDLE}
  rules:
  - apiGroups: [""]
    apiVersions: ["v1"]
    operations: ["CREATE", "UPDATE"]
    resources: ["pods"]
    scope: "Namespaced"
  sideEffects: None
  admissionReviewVersions: ["v1"]
  failurePolicy: Fail

這個 YAML 設定展示瞭如何佈署 OPA 作為准入控制器:

  1. 建立一個執行 OPA 的 Deployment
  2. 使用 ConfigMap 儲存 Rego 策略
  3. 建立一個 Service 暴露 OPA API
  4. 設定 ValidatingWebhookConfiguration 將准入請求傳送到 OPA

Rego 策略定義了兩個規則:

  • 禁止使用特權容器
  • 要求 Pod 必須有 app 標籤

使用 OPA 的優勢在於策略可以與程式碼分離,並使用宣告式語言定義,使得非開發人員也能理解和修改策略。

與 Kyverno 整合

Kyverno 是一個專為 Kubernetes 設計的策略引擎,提供了更簡單的策略定義方式:

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: require-labels
spec:
  validationFailureAction: enforce
  rules:
  - name: require-app-label
    match:
      resources:
        kinds:
        - Pod
    validate:
      message: "Pod 必須包含 app 標籤"
      pattern:
        metadata:
          labels:
            app: "?*"
---
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: restrict-privileged
spec:
  validationFailureAction: enforce
  rules:
  - name: no-privileged-containers
    match:
      resources:
        kinds:
        - Pod
    validate:
      message: "不允許使用特權容器"
      pattern:
        spec:
          containers:
          - name: "*"
            securityContext:
              privileged: "false"

這個 YAML 設定展示瞭如何使用 Kyverno 定義策略:

  1. 建立一個要求 Pod 包含 app 標籤的 ClusterPolicy
  2. 建立一個禁止使用特權容器的 ClusterPolicy

與 OPA 相比,Kyverno 的策略定義更接近 Kubernetes 資源的結構,使用模式比對而非程式設計語言,更容易上手。Kyverno 還提供了變更策略、生成策略等功能,可以實作更複雜的自動化。

准入控制器的未來發展

隨著 Kubernetes 的不斷發展,准入控制器也在不斷演進:

  1. CEL 表示式支援:Kubernetes 1.23 引入了對 CEL(Common Expression Language)的支援,使得可以在 ValidatingAdmissionPolicy 中使用表示式進行驗證,無需編寫 Webhook
  2. 策略引擎整合:更多的策略引擎(如 OPA、Kyverno)與 Kubernetes 的整合,提供更豐富的策略管理功能
  3. 安全性增強:更多關注於安全性的准入控制器,如 Pod Security Admission
  4. 自動化工具:更多工具用於生成和管理准入控制器設定

准入控制器作為 Kubernetes 安全和治理的關鍵元件,將繼續在雲原生態系統中發揮重要作用。透過深入理解和靈活運用准入控制器,我們可以構建更安全、更可靠的 Kubernetes 環境。

在實際應用中,准入控制器應該是多層防禦策略的一部分,與網路政策、RBAC、Pod 安全標準等機制結合使用,共同構建完整的安全架構。同時,應該根據組織的需求和安全策略,選擇適當的准入控制器實作方式,並確保其高用性和可靠性。

透過本文的介紹,玄貓希望能幫助讀者理解 Kubernetes 准入控制器的工作原理、實作方法和最佳實踐,從而更好地利用這一強大機制來保護和管理 Kubernetes 環境。

物件被接受

當 Kubernetes 的 Webhook 驗證透過時,物件會被接受並進入系統。這是 Webhook 處理流程中的關鍵環節,也是確保系統安全性和一致性的重要步驟。

物件接受後的處理流程

當一個物件透過 Webhook 驗證並被接受後,Kubernetes 會繼續進行以下處理:

  1. 將物件持久化到 etcd 資料函式庫
  2. 通知相關控制器進行後續處理
  3. 更新系統狀態以反映新物件的存在
  4. 觸發任何相關的事件和通知

這個過程確保了只有符合系統要求的物件才能進入 Kubernetes 生態系統,維護了整體系統的穩定性和安全性。

副作用(Side Effects)標記

sideEffects 欄位用於標記 Webhook 是否可能產生副作用。這是一個重要的設定引數,它告訴 Kubernetes API 伺服器如何處理對 Webhook 的呼叫。

sideEffects: None

副作用值的選項

Webhook 的 sideEffects 欄位可以設定為以下值:

  1. none:表示 Webhook 不會產生任何副作用,API 伺服器可以安全地多次呼叫它
  2. NoneOnDryRun:表示當請求包含 dryRun 引數時,Webhook 不會產生副作用
  3. Some:表示 Webhook 可能會產生副作用,即使是在 dryRun 模式下

這個設定非常重要,因為它決定了 API 伺服器如何處理 Webhook 呼叫。如果設定為 None,API 伺服器知道可以安全地多次呼叫 Webhook,這在高用性環境中特別有用。而 NoneOnDryRun 則允許在正常操作中有副作用,但在 dryRun 模式下不會產生副作用,這對於測試和驗證非常有用。

實際應用中的副作用處理

在實際應用中,副作用處理是一個重要的考量因素。以下是一個範例,說明如何在 ValidatingWebhookConfiguration 中正確設定副作用:

apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
  name: validation-webhook
webhooks:
- name: validation.example.com
  clientConfig:
    service:
      namespace: webhook-namespace
      name: webhook-service
      path: "/validate"
    caBundle: <base64-encoded-ca-cert>
  rules:
  - apiGroups: ["apps"]
    apiVersions: ["v1"]
    operations: ["CREATE", "UPDATE"]
    resources: ["deployments"]
    scope: "Namespaced"
  sideEffects: None
  admissionReviewVersions: ["v1", "v1beta1"]
  timeoutSeconds: 5

在這個設定中,我們明確指定了 sideEffects: None,這告訴 Kubernetes API 伺服器這個 Webhook 不會產生任何副作用。這意味著 API 伺服器可以安全地多次呼叫這個 Webhook,例如在重試或高用性場景中。這對於保持系統的可靠性和一致性非常重要。

副作用與系統穩定性

正確處理副作用對於維護 Kubernetes 系統的穩定性至關重要。如果 Webhook 可能產生副作用(如更新外部系統或傳送通知),但被標記為 None,可能會導致意外的行為,因為 API 伺服器可能會多次呼叫 Webhook。

相反,如果 Webhook 實際上沒有副作用,但被標記為 Some,則可能會導致不必要的限制,影響系統的效能和可用性。

最佳實踐

在設計和實作 Webhook 時,應遵循以下最佳實踐:

  1. 明確定義副作用:清楚瞭解你的 Webhook 是否會產生副作用
  2. 正確設定 sideEffects:根據 Webhook 的實際行為正確設定 sideEffects 欄位
  3. 設計無副作用的 Webhook:盡可能設計無副作用的 Webhook,以提高系統的可靠性和可用性
  4. 處理重複呼叫:如果 Webhook 可能被多次呼叫,確保它能夠正確處理重複呼叫

超時設定與失敗策略

在 Webhook 設定中,超時設定和失敗策略是確保系統穩定性的關鍵因素。

超時設定

timeoutSeconds 欄位定義了 API 伺服器等待 Webhook 回應的最長時間。如果 Webhook 在指定時間內沒有回應,API 伺服器會根據 failurePolicy 的設定來決定如何處理請求。

timeoutSeconds: 5

這個設定表示 API 伺服器會等待最多 5 秒鐘來取得 Webhook 的回應。

超時設定是一個關鍵的效能和可靠性引數。設定太短可能導致正常的 Webhook 處理被錯誤地視為超時;設定太長則可能在 Webhook 出現問題時導致 API 伺服器等待過長時間,影響整體系統效能。通常建議設定一個合理的值,如 5 秒,這在大多數情況下足夠 Webhook 完成處理,同時又不會在出現問題時導致過長的等待。

失敗策略

failurePolicy 欄位定義了當 Webhook 呼叫失敗時(例如網路問題或超時)API 伺服器應該採取的行動。

failurePolicy: Fail

可選的值包括:

  1. Fail:如果 Webhook 呼叫失敗,則拒絕請求
  2. Ignore:如果 Webhook 呼叫失敗,則忽略 Webhook 並允許請求繼續

失敗策略的選擇取決於 Webhook 在系統中的重要性。如果 Webhook 執行的是關鍵的安全檢查,那麼 Fail 可能是更安全的選擇,因為它確保只有經過 Webhook 驗證的請求才能被處理。然而,這也意味著如果 Webhook 服務出現問題,可能會影響整個系統的可用性。

相反,如果 Webhook 執行的是非關鍵的檢查,或者在某些情況下可以被安全地跳過,那麼 Ignore 可能是更好的選擇,因為它提高了系統的可用性。

完整設定範例

以下是一個包含超時設定和失敗策略的完整 ValidatingWebhookConfiguration 範例:

apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
  name: validation-webhook
webhooks:
- name: validation.example.com
  clientConfig:
    service:
      namespace: webhook-namespace
      name: webhook-service
      path: "/validate"
    caBundle: <base64-encoded-ca-cert>
  rules:
  - apiGroups: ["apps"]
    apiVersions: ["v1"]
    operations: ["CREATE", "UPDATE"]
    resources: ["deployments"]
    scope: "Namespaced"
  sideEffects: None
  admissionReviewVersions: ["v1", "v1beta1"]
  timeoutSeconds: 5
  failurePolicy: Fail

這個設定設定了一個驗證 Webhook,它會檢查 apps/v1 API 組中的 Deployment 資源的建立和更新操作。Webhook 沒有副作用,API 伺服器會等待最多 5 秒鐘來取得回應,如果呼叫失敗,請求將被拒絕。這種設定適合執行關鍵安全檢查的 Webhook,因為它確保只有經過 Webhook 驗證的請求才能被處理。