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.
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 namespaceatc-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ụngcat 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
- Giải mã AdmissionReview
- 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.
- Xử lý các yêu cầu
- Giải mã AdmissionReview
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
- Admission Controllers in Kubernetes – https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers/
- How to setup Admission Controllers in Kubernetes – https://dev.to/marocz/mastering-kubernetes-admission-controllers-setup-and-use-cases-2n1