[CKS] Minimize Microservice Vulnerabilities (2) - Validating and Mutating Admission Controllers

쿠버네티스 Admission Controller를 통해 API 요청을 검증하거나 수정하는 방식을 알아봅니다. Validating과 Mutating 컨트롤러, 그리고 Webhook을 활용한 사용자 정의 승인 로직 구현 방법을 설명합니다.

[CKS] Minimize Microservice Vulnerabilities (2) - Validating and Mutating Admission Controllers
Photo by Indra Projects / Unsplash

개요

CKS 자격증을 위해 아래 내용을 정리했습니다.

Validating and Mutating Admission Controllers

Kubernetes상에선 2가지 Admission Controllers가 있습니다.

해당하는 Admission Controller의 특징을 정리했습니다.

Validating Admission controller

들어오는 요청을 확인하고 미리 정의된 규칙에 따라 허용하거나 거부합니다. 예시로 namespace lifecycle가 있습니다. 요청을 허용하기 전 네임스페이스가 존재하는지 확인하고 없다면 거부합니다.

Mutating Admission Controller

default storage class admission controller 는 PVC 생성 요청시 default PVC를 추가합니다.

요청을 거부하는 게 아니라 요청 내용을 변경해서 기본 값을 채워넣습니다.

요청본

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: myclaim
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 500Mi

수정본

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: myclaim
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 500Mi
  storageClassName: default

경우에 따라 검사 & 변경 승인 작업을 모두 수행하는 controller가 존재합니다. 일반적으로는 Mutating 컨트롤러가 Validating 보다 먼저 수행되어 수정 사항을 검증합니다. 처리 과정의 어느 단계든 Admission Controllers가 거부하면 전체 작업이 실패하며 사용자에게 오류 메시지가 반환됩니다.

Extending Admission Controllers with Webhooks

내장된 Admission Controllers 제외, 두 가지 유형의 외부 웹훅으로 사용자 지정 로직을 수행할 수 있습니다.

  • Mutating Admission Webhook
  • Validating Admission Webhook

해당 요청을 통해 외부 사용자 지정 서버로 승인 검토를 수행할 수 있습니다. 웹훅 서버는 사용자, 요청된 작업, 관련 객체 등의 세부 정보가 포함된 JSON 형식의 객체를 수신합니다.

{
  "apiVersion": "admission.k8s.io/v1",
  "kind": "AdmissionReview",
  "request": {
    "uid": "705ab4f5-6393-11e8-b7cc-4201aa800002",
    "kind": {"group": "autoscaling", "version": "v1", "kind": "Scale"},
    "resource": {"group": "apps", "version": "v1", "resource": "deployments"},
    "subResource": "scale",
    "requestKind": {"group": "autoscaling", "version": "v1", "kind": "Scale"},
    "requestResource": {"group": "apps", "version": "v1", "resource": "deployments"}
  }
}

웹훅 서버는 이를 처리하고 응답을 전송합니다. 승인 응답은 다음과 같습니다.

  • allowed: true 항목에서 승인처리 됩니다.
{
  "apiVersion": "admission.k8s.io/v1",
  "kind": "AdmissionReview",
  "request": {
    "uid": "705ab4f5-6393-11e8-b7cc-42010aa80002",
    "kind": {"group": "autoscaling", "version": "v1", "kind": "Scale"},
    "resource": {"group": "apps", "version": "v1", "resource": "deployments"},
    "subResource": "scale",
    "requestKind": {"group": "autoscaling", "version": "v1", "kind": "Scale"},
    "requestResource": {"group": "apps", "version": "v1", "resource": "deployments"}
  },
  "response": {
    "uid": "value_from_request.uid",
    "allowed": true
  }
}

Deploying an Admission Webhook Server

사용자 지정 Admission Controller는 사용자 지정 로직이 포함된 웹훅 서버를 배포하여 구성합니다.

HTTPS를 지원하는 모든 프로그래밍 언어로 가능하며 API Server와 통신하기 위한 HTTPS 구성이 필요합니다.

  • 참고용 Python Code 입니다.
json["request"]["object"]["metadata"]["name"]
    user_name = request.json["request"]["userInfo"]["name"]
    status = True
    message = ""
    if object_name == user_name:
        message = "You can't create objects with your own name"
        status = False
    return jsonify(
        {
            "response": {
                "allowed": status,
                "uid": request.json["request"]["uid"],
                "status": {"message": message},
            }
        }
    )


@app.route("/mutate", methods=["POST"])
def mutate():
    user_name = request.json["request"]["userInfo"]["name"]
    patch = [{"op": "add", "path": "/metadata/labels/users", "value": user_name}]
    # Encode the patch using base64
    patch_encoded = base64.b64encode(str(patch).encode()).decode()
    return jsonify(
        {
            "response": {
                "allowed": True,
                "uid": request.json["request"]["uid"],
                "patch": patch_encoded,
                "patchType": "JSONPatch",
            }
        }
    )


if __name__ == "__main__":
    app.run(debug=True, port=443)

kuberenetes에서 Webhook 등록하기

apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
  name: "pod-policy.example.com"
webhooks:
  - name: "pod-policy.example.com"
    clientConfig:
      service:
        namespace: "webhook-namespace"
        name: "webhook-service"
      caBundle: "CiOtLS0tQk......tLS0K"
    rules:
      - apiGroups: [""]
        apiVersions: ["v1"]
        operations: ["CREATE"]
        resources: ["pods"]
        scope: "Namespaced"
  • 위 예시에서 Webhook은 pod 생성 중에 트리거 됩니다.
  • TLS 통신을 위해 caBundle 을 사용합니다.
  • API Server는 Webhook을 NameNamespace 기반으로 참조합니다.
  • MutatingWebhookConfiguration Mutating의 경우.
securityContext:
  runAsNonRoot: true    
  runAsUser: 0
  
# 에러 메시지를 확인할 수 있습니다.
Error from server: error when creating "/root/pod-with-conflict.yaml": admission webhook "webhook-server.webhook-demo.svc" denied the request: runAsNonRoot specified, but runAsUser set to 0 (the root user)