Giới thiệu về Kubernetes Admission Controllers

Admission Controllers là gì?

  • Admission Controllers được nhúng vào Kubernetes API Server để kiểm tra và xác thực các yêu cầu đến. Chúng được áp dụng vào các thao tác như tạo, cập nhật, hoặc xóa đối tượng. Tuy nhiên, chúng không ảnh hưởng đến các hoạt động chỉ đọc như lấy, xem, hoặc liệt kê để chúng bỏ qua các lớp Admission Controller.
  • Admission Controllers là các thành phần trong Kubernetes API Server can thiệp các yêu cầu đến Kubernetes API Server. Những controllers này hoạt động sau khi yêu cầu đã được xác thực và ủy quyền nhưng trước khi tài nguyên được áp dụng.
  • Admission Controllers có thể phục vụ hai mục đích
    • Validating: Các controllers này đảm bảo yêu cầu tuân theo các quy tắc đã chỉ định nhưng không thể thay đổi resource manifest.
    • Mutating: Các controllers này có thể sửa đổi resource manifest có trong yêu cầu

Các giai đoạn trong Admission Controllers

  • Quá trình Admission Controllers diễn ra theo hai giai đoạn
    • Đầu tiên, mutating admission controllers được thực thi, có thể sửa đổi tài nguyên đang được yêu cầu.
    • Tiếp theo, validating admission controllers chạy và đảm bảo yêu cầu tuân thủ các quy tắc đã được chỉ định.
  • Nếu bất cứ controllers nào ở bất kì giai đoạn nào từ chối yêu cầu, toàn bộ hoạt động sẽ dừng ngay lập tức và thông báo lỗi sẽ được trả về cho người dùng
  • Hơn nữa, trong khi các Admission Controllers đôi khi có thể thay đổi resource manifest, chúng cũng có thể tạo ra các tác dụng phụ bằng cách sửa đổi các resources liên quan trong quá trình xử lí. Ví dụ, cập nhật quota là một tình huống phổ biến khi cần phải làm như vậy. Vì không có gì đảm bảo rằng một request sẽ vượt qua tất cả các admission controllers tiếp theo, nên bất kì tác dụng phụ nào cũng phải đi kèm với một cơ chế thu hồi hoặc đối chiếu để đảm bảo tính nhất quán.
Sequence diagram for kube-apiserver handling requests during the admission phase showing mutation webhooks, followed by validatingadmissionpolicies and finally validating webhooks. It shows that the continue until the first rejection, or being accepted by all of them. It also shows that mutations by mutating webhooks cause all previously called webhooks to be called again.

Ví dụ, khi người dùng áp dụng yêu cầu như tạo một pod, yêu cầu trước tiên sẽ đến Kubernetes API Server để kiểm tra xác thực người dùng và quyền của người dùng. Nếu người dùng được phép tạo yêu cầu, mutating webhooks sẽ nhận được yêu cầu sửa đổi resource manifest. Nếu mutating webhooks thay đổi thành công resource manifest, resource manifest được sửa đổi sẽ đến validating webhooks. Tại đây, resource manifest được xác thực dựa trên quy tắc đã chỉ định trước khi chuyển đến Kubernetes API Server để xử lí.

Mutating admission webhooks 

  • Admission Controllers sẽ gọi bất kì mutating webhooks nào khớp với các yêu cầu đến. Những webhooks này được thực thi tuần tự và mỗi webhook đều có tùy chọn sửa đổi đối tượng. Những controllers này hoạt động độc lập trong giai đoạn thay đổi của quy trình chấp thuận.
  • Nếu một webhook gây ra các tác dụng phụ, chẳng hạn như giảm mức sử dụng quota, thì nó phải bao gồm một cơ chế đối chiếu. Điều này là do không có sự đảm bảo rằng các webhooks tiếp theo hoặc các validating admission controllers sẽ chấp thuận yêu cầu.
  • Nếu bạn muốn vô hiệu hóa MutatingAdmissionWebhook, ban phải vô hiệu hóa đối tượng MutatingWebhookConfiguration trong nhóm admissionregistration.k8s.io/v1 bằng cách sử dụng flag –runtime-config
  • Có một số lưu ý khi sử dụng mutating webhooks
    • Người dùng có thể bị nhầm lẫn nếu resource manifest khác với những gì họ đã áp dụng
    • Việc thêm giá trị vào các trường chưa đặt ít có khả năng gây ra sự cố hơn là ghi đè các trường được đặt rõ ràng trong bản resource manifest gốc

Cách sử dụng Mutating webhooks trong Kubernetes

Bởi vì Mutating Admission Webhook sử dụng mutual TLS để giao tiếp Kubernetes API server, chúng ta cần tạo PKI sau đó nhúng vào Kubernetes bằng Kubernetes resource có tên là MutatingWebhookConfiguration

Sinh ra CA private key

openssl genrsa -out ca.key 2048

Sinh ra CA certificate

openssl req -x509 -new -nodes -key ca.key -subj "/CN=webhook-ca" -days 3650 -out ca.crt
  • Đảm bảo -subj "/CN=admission-webhook.atc-gpu-sharing.svc"  khớp với K8s service
  • Trong trường hợp này, K8s service có tên  admission-webhook trong namespace atc-gpu-sharing

Kí Server certificate với CA

[req]
req_extensions = v3_req
distinguished_name = req_distinguished_name
  
[req_distinguished_name]
  
[v3_req]
basicConstraints = CA:FALSE
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth
subjectAltName = @alt_names
  
[alt_names]
DNS.1 = admission-webhook.atc-gpu-sharing.svc
  • Tạo file cài đặt csr.conf  cho việc kí
  • Đảm bảo rằng alt_names khớp với K8s service 

Ký server certificate

openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt -days 365 -extensions v3_req -extfile csr.conf

Xác minh CA certificate

openssl x509 -in ca.crt -text -noout

Xác minh Server certificate

openssl x509 -in server.crt -text -noout

Đảm bảo rằng Server Certificate được kí bởi CA

openssl verify -CAfile ca.crt server.crt

Sau khi tạo ra CA và khóa công khai cũng như khóa riêng tư tương ứng, hãy tạo K8s secrets webhook-certs cho các khóa công khai và khóa riêng tư trong cùng một namespace với tên webhook service

kubectl -n {webhook_namespace} create secret tls webhook-certs --cert /path/to/server.crt --key /path/to/server.key

Ví dụ, trong mấu cấu hình Mutating Webhook bên dưới:

  • Chúng tôi loại trừ một số namespace mặc định mà Mutating Webhook không được áp dụng khi các resources trong những namespaces này được tạo, sửa đổi hoặc xóa.
  • Trong các namespaces còn lại, Mutating webhook chỉ có hiệu lực nếu người dùng tạo hoặc cập nhật pods
  • Mutating webhook này được triển khai tại K8s service <serviceName>.<serviceNamespace>/mutate 
  • Hãy nhớ thay thế <caBundle> bằng mã hóa base64 của ca.crt bằng cách sử dụng cat ca.crt | base64 | tr -d '\n' 

apiVersion: admissionregistration.k8s.io/v1

kind: MutatingWebhookConfiguration

metadata:

  name: <MutatingWebhookName>

  namespace: <TargetNamespace>

webhooks:

- name: <MutatingWebhookSubname>

  sideEffects: None

  namespaceSelector:

    matchExpressions:

      - key: kubernetes.io/metadata.name

        operator: NotIn

        values: ["kube-system", "kube-public", "kube-node-lease", "default"]

  rules:

    - apiGroups: [""]

      apiVersions: ["v1"]

      operations: ["CREATE", "UPDATE"]

      resources: ["pods"]

  admissionReviewVersions: ["v1"]

  clientConfig:

    service:

      name: <serviceName>

      namespace: <serviceNamespace>

      path: "/mutate"

    caBundle: <caBundle>

Dưới đây là mẫu Mutating Admission Webhook tại <serviceName>.<serviceNamespace>/mutate được triển khai bằng Go

  • Khởi tạo
    • Đăng kí các loại Kubernetes API (ví dụ: corev1.Pod, admissionv1.AdmissionReview) với lược đồ thời gian chạy
    • Thiết lập trình hủy tuần tự hóa chung để giải mã các yêu cầu AdmissionReview đến
  • Máy chủ HTTP
    • Lắng nghe trên cổng 8443 cho các yêu cầu HTTPS đến
    • Sử dụng chứng chỉ TLS (tls.crt và tls.key ) để giao tiếp an toàn
  • Xử lí yêu cầu
    • Giải mã AdmissionReview
      • Phân tích cú pháp JSON payload đến thành đối tượng AdmissionReview
    • Xử lí yêu cầu
      • Giải mã Pod object từ AdmissionRequest
      • Thêm nhãn (example-mutated: “true”) vào Pod
      • Tạo bản vá JSON để áp dụng sửa đổi
    • Phản hồi với API Server
      • Trả về AdmissionResponse chứa bản vá và đánh dấu yêu cầu là được phép
  • Bản vá JSON
    • Bản vá JSON chỉ định cách sửa đổi resource 
package main
 
import (
    "context"
    "encoding/json"
    "fmt"
    "log"
    "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()
)
 
func init() {
    _ = corev1.AddToScheme(runtimeScheme)
    _ = admissionv1.AddToScheme(runtimeScheme)
}
 
func main() {
    http.HandleFunc("/mutate", handleMutate)
    log.Println("Starting webhook server on :8443...")
    err := http.ListenAndServeTLS(":8443", "/path/to/tls.crt", "/path/to/tls.key", nil)
    if err != nil {
        log.Fatalf("Failed to start server: %v", err)
    }
}
 
func handleMutate(w http.ResponseWriter, r *http.Request) {
    // Step 1: Parse the AdmissionReview request
    var body []byte
    if r.Body != nil {
        if data, err := io.ReadAll(r.Body); err == nil {
            body = data
        }
    }
    if len(body) == 0 {
        http.Error(w, "Empty body", http.StatusBadRequest)
        return
    }
 
    // Verify the content type
    contentType := r.Header.Get("Content-Type")
    if contentType != "application/json" {
        http.Error(w, fmt.Sprintf("Invalid Content-Type: %s", contentType), http.StatusUnsupportedMediaType)
        return
    }
 
    // Decode the AdmissionReview
    admissionReview := admissionv1.AdmissionReview{}
    if _, _, err := deserializer.Decode(body, nil, &admissionReview); err != nil {
        log.Printf("Failed to decode AdmissionReview: %v", err)
        http.Error(w, "Failed to decode AdmissionReview", http.StatusBadRequest)
        return
    }
 
    // Step 2: Process the request
    response := mutatePod(admissionReview.Request)
 
    // Step 3: Construct the AdmissionReview response
    admissionReview.Response = response
    admissionReview.Response.UID = admissionReview.Request.UID
 
    // Send the response
    respBytes, err := json.Marshal(admissionReview)
    if err != nil {
        log.Printf("Failed to marshal AdmissionReview response: %v", err)
        http.Error(w, "Failed to marshal response", http.StatusInternalServerError)
        return
    }
    w.Header().Set("Content-Type", "application/json")
    w.Write(respBytes)
}
 
func mutatePod(req *admissionv1.AdmissionRequest) *admissionv1.AdmissionResponse {
    // Step 1: Decode the Pod object
    var pod corev1.Pod
    if err := json.Unmarshal(req.Object.Raw, &pod); err != nil {
        log.Printf("Failed to unmarshal Pod: %v", err)
        return &admissionv1.AdmissionResponse{
            Result: &metav1.Status{
                Message: fmt.Sprintf("Failed to unmarshal Pod: %v", err),
            },
        }
    }
 
    // Step 2: Modify the Pod (add a label)
    if pod.Labels == nil {
        pod.Labels = make(map[string]string)
    }
    pod.Labels["example-mutated"] = "true"
 
    // Step 3: Create a JSON patch
    patch := []map[string]interface{}{
        {
            "op":    "add",
            "path":  "/metadata/labels/example-mutated",
            "value": "true",
        },
    }
    patchBytes, err := json.Marshal(patch)
    if err != nil {
        log.Printf("Failed to marshal patch: %v", err)
        return &admissionv1.AdmissionResponse{
            Result: &metav1.Status{
                Message: fmt.Sprintf("Failed to marshal patch: %v", err),
            },
        }
    }
 
    // Step 4: Return the AdmissionResponse
    return &admissionv1.AdmissionResponse{
        Allowed: true,
        Patch:   patchBytes,
        PatchType: func() *admissionv1.PatchType {
            pt := admissionv1.PatchTypeJSONPatch
            return &pt
        }(),
    }
}

Validating Admission Webhook 

  • Admission Controller kích hoạt bất kì validating webhooks nào khớp với yêu cầu đến. Các webhooks khớp này được thực thi song song và nếu bất kì webhook nào trong số chúng từ chối yêu cầu, toàn bộ yêu cầu sẽ không thành công. Controller này hoạt động trong giai đoạn xác thực và các webhook mà nó gọi không được phép sửa đổi đối tượng, không giống như các webhook được gọi bởi ValidatingAdmissionWebhook.
  • Nếu một webhook do controller này có tác dụng phụ, chẳng hạn như giảm mức sử dụng quota thì nó phải bao gồm một cơ chế đối chiếu. Điều này là do không có gì đảm bảo rằng các webhooks tiếp theo hoặc các validating admission controllers khác sẽ cháp thuận yêu cầu.
  • Nếu bạn quyết định vô hiệu hóa ValidatingAdmissionWebhook, bạn cũng phải vô hiệu hóa đối tượng ValidatingWebhookConfiguration trong nhóm admisstionregistration.k8s.io/v1 bằng cách sử dụng flag –runtime-config

Cách sử dụng Validating Admission Webhook trong Kubernetes

  • Vì Validating Admission Webhook sử dụng mutual TLS để giao tiếp với Kubernetes API Server, chúng ta cần tạo PKI sau đó nhúng vào Kubernetes bằng Kubernetes resource có tên là ValidatingWebhookConfiguration
    • Thực hiện như chúng ta đã làm trong Mutating Admission Webhook
  • Sau khi tạo CA và khóa công khai cũng như khóa riêng tư tương ứng, hãy tạo K8s secret webhook-certs cho các khóa công khai và tiên tư này trong cùng một namespace với webhook service
kubectl -n {webhook_namespace} create secret tls webhook-certs --cert /path/to/server.crt --key /path/to/server.key

Ví dụ, trong mẫu cấu hình Validating Webhook bên dưới

  • Chúng ta loại trừ một số namespace mặc định mà Validating Webhook này không được áp dụng khi các resources này trong những namespaces này được tạo, sửa đổi hoặc xóa
  • Trong các namespaces khác, Validating webhook chỉ có hiệu lực nếu người dùng tạo hoặc cập nhật Pod
  • Validating webhook này được triển khai tại service <serviceName>.<serviceNamespace>/validate
  • Nhớ thay thế <caBundle> bằng mã hóa base64 của ca.crt bằng cách sử dụng cat ca.crt | base64 | tr -d '\n'
apiVersion:  admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
  name: <ValidatingWebhookName>
  namespace: <TargetNamespace>
webhooks:
  - name: <ValidatingWebhookSubname>
    sideEffects: None
    admissionReviewVersions: ["v1"]
    namespaceSelector:
      matchExpressions:
        - key: kubernetes.io/metadata.name
          operator: NotIn
          values: ["kube-system", "kube-public", "kube-node-lease", "atc-gpu-sharing", "default"]
    rules:
      - apiGroups: [""]
        apiVersions: ["v1"]
        operations: ["CREATE", "UPDATE"]
        resources: ["pods"]
    clientConfig:
      caBundle: <caBundle>
      service:
        name: <serviceName>
        namespace: <serviceNamespace>
        path: /validate

Bên dưới là mẫu Validating Admission Webhook tại service <serviceName>.<serviceNamespace>/validate được triển khai bằng Go

  • Khởi tạo
    • Đăng kí các loại Kubernetes API cần thiết (ví dụ: corev1.Pod, admissionv1.AdmissionReview) với lược đồ thời gian chạy.
    • Thiết lập trình hủy tuần tự hóa chung để giải mã các yêu cầu AdmissionReview đến.
  • Máy chủ HTTP
    • Lắng nghe trên cổng 8443 cho các yêu cầu HTTPS đến.
    • Sử dụng chứng chỉ TLS (tls.crt và tls.key) để giao tiếp an toàn.
  • Xử lý các yêu cầu
    • Giải mã AdmissionReview
      • Xử lý các yêu cầu
        • Giải mã AdmissionReview
        • Phân tích cú pháp JSON payload đến thành một đối tượng AdmissionReview.
      • Xử lý yêu cầu
        • Giải mã đối tượng Pod từ AdmissionRequest.
        • Xác thực rằng Pod có nhãn bắt buộc (example-validation: “true”).
        • Nếu xác thực không thành công, trả về phản hồi từ chối yêu cầu.
        • Nếu xác thực thành công, cho phép yêu cầu.
      • Phản hồi từ API Server
        • Trả về AdmissionResponse cho biết yêu cầu có được phép hay không.
package main
 
import (
    "context"
    "encoding/json"
    "fmt"
    "io"
    "log"
    "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()
)
 
func init() {
    _ = corev1.AddToScheme(runtimeScheme)
    _ = admissionv1.AddToScheme(runtimeScheme)
}
 
func main() {
    http.HandleFunc("/validate", handleValidate)
    log.Println("Starting webhook server on :8443...")
    err := http.ListenAndServeTLS(":8443", "/path/to/tls.crt", "/path/to/tls.key", nil)
    if err != nil {
        log.Fatalf("Failed to start server: %v", err)
    }
}
 
func handleValidate(w http.ResponseWriter, r *http.Request) {
    // Step 1: Parse the AdmissionReview request
    var body []byte
    if r.Body != nil {
        if data, err := io.ReadAll(r.Body); err == nil {
            body = data
        }
    }
    if len(body) == 0 {
        http.Error(w, "Empty body", http.StatusBadRequest)
        return
    }
 
    // Verify the content type
    contentType := r.Header.Get("Content-Type")
    if contentType != "application/json" {
        http.Error(w, fmt.Sprintf("Invalid Content-Type: %s", contentType), http.StatusUnsupportedMediaType)
        return
    }
 
    // Decode the AdmissionReview
    admissionReview := admissionv1.AdmissionReview{}
    if _, _, err := deserializer.Decode(body, nil, &admissionReview); err != nil {
        log.Printf("Failed to decode AdmissionReview: %v", err)
        http.Error(w, "Failed to decode AdmissionReview", http.StatusBadRequest)
        return
    }
 
    // Step 2: Process the request
    response := validatePod(admissionReview.Request)
 
    // Step 3: Construct the AdmissionReview response
    admissionReview.Response = response
    admissionReview.Response.UID = admissionReview.Request.UID
 
    // Send the response
    respBytes, err := json.Marshal(admissionReview)
    if err != nil {
        log.Printf("Failed to marshal AdmissionReview response: %v", err)
        http.Error(w, "Failed to marshal response", http.StatusInternalServerError)
        return
    }
    w.Header().Set("Content-Type", "application/json")
    w.Write(respBytes)
}
 
func validatePod(req *admissionv1.AdmissionRequest) *admissionv1.AdmissionResponse {
    // Step 1: Decode the Pod object
    var pod corev1.Pod
    if err := json.Unmarshal(req.Object.Raw, &pod); err != nil {
        log.Printf("Failed to unmarshal Pod: %v", err)
        return &admissionv1.AdmissionResponse{
            Result: &metav1.Status{
                Message: fmt.Sprintf("Failed to unmarshal Pod: %v", err),
            },
        }
    }
 
    // Step 2: Validate the Pod
    requiredLabel := "example-validation"
    requiredValue := "true"
 
    if pod.Labels[requiredLabel] != requiredValue {
        return &admissionv1.AdmissionResponse{
            Allowed: false,
            Result: &metav1.Status{
                Status:  "Failure",
                Message: fmt.Sprintf("Pod must have label '%s=%s'", requiredLabel, requiredValue),
                Code:    http.StatusForbidden,
            },
        }
    }
 
    // Step 3: Allow the request
    return &admissionv1.AdmissionResponse{
        Allowed: true,
    }
}

Kết luận

  • Admission Controllers đóng vai trò then chốt trong việc tăng cường bảo mật, tuân thủ và hiệu quả hoạt động củ Kubernetes cluster. Bằng cách chặn và xác thực requests đến máy chủ API Kubernetes, chúng đảm bảo rằng chỉ các hoạt động tuân thủ chính sách mới được thực hiện trong Kubernetes cluster.
  • Tính linh hoạt do validating and mutating admission controllers mang lại cho phép chúng tôi tùy chỉnh môi trường Kubernetes của họ để đáp ứng các yêu cầu hoạt động và quy định cụ thể.

Nguồn tham khảo

You may also like...

0 0 đánh giá
Đánh giá bài viết
Theo dõi
Thông báo của
guest
0 Góp ý
Cũ nhất
Mới nhất Được bỏ phiếu nhiều nhất
Phản hồi nội tuyến
Xem tất cả bình luận
0
Rất thích suy nghĩ của bạn, hãy bình luận.x