Search
Duplicate

Admission Controller

Admission Control?

쿠버네티스 접근제어에는 크게 3단계가 있습니다.
Authentication: 접속한 사람의 신분을 시스템이 인증하는 단계입니다. (신분증 확인)
Authorization: 누가 어떤 권한을 가지고 어떤 행동을 할 수 있는지 확인하는 단계입니다. (view권한, create권한 등)
Admission Control: 인증과 권한확인 이후에 추가적으로 요청 내용에 대한 검증이나 요청 내용을 강제로 변경할 때 사용합니다.
Authorization 단계는 말그대로 사용자가 특정 작업을 수행할 때 어떤 권한이 있는지를 확인하는 단계입니다. 반대로 Admission Control은 권한이 있는 사용자에 한해서 관리자(Admin)가 추가로 특정 행동을 제한(validate) 혹은 변경(mutate)하는 작업입니다.

Admission Controller Plugins

Admission Controller란 관리자의 특정 정책을 수행하는 주체, 다시 말해 Admission Control을 수행하는 주체입니다. 쿠버네티스에는 다양한 Admission Controller들이 존재합니다. 쿠버네티스 버전마다 조금씩 default로 설정된 Admission Controller들이 다릅니다. 대표적인 것이 LimitRange와 ResourceQuota 등이 있습니다. 관리자에 의해 정해진 리소스를 초과하는 경우(관리자의 특정 정책) Pod 생성을 막아버리는 기능(작업 제한)이라 볼 수 있습니다.
Admission Controller는 Compiled-in Admission Controller와 Custom Admission Controller
로 2가지 종류가 있습니다.

Compiled-in Admission Controller(Interneal Admission Webhook)

kube-apiserver에 complie되어서 존재하고 있는 admission controller입니다. 사용자(관리자)가 별도의 webhook server를 구현하여 통신할 필요없이 kube-apiserver에서 admission controller를 활성화하는 것으로 동작이 가능합니다.
ex) DefaultIngressClass, NamespaceExists Admission Controller, ServiceAccount Admission Controller

Custom Admission Controller(Externeal Admission Webhook)

kube-apiserver 외부에서 동작하는 admission controller로 사용자(관리자)가 별도의 webhook server를 구현하여 kube-apiserver가 해당 webhook server와 통신하며 동작합니다.
ex) MutatingAdmissionWebhook, ValidatingAdmissionWebhook, imagePolicyWebhook
kubernetes version 1.7 이전에는 Admission Controller를 추가하기 위해 kube-apiserver 코드를 직접 수정하고 다시 컴파일을 했어야 가능했습니다. 하지만 1.17 이후부터는 이러한 번거롭고 어려운 작업을 대체하기 위해Externeal Admission Webhook 기능을 추가해서 컴파일 없이 Webhook Server를 통해 plugin을 연동하면 Admission Controller 기능이 수행 되도록 Update를 했습니다.
이후 1.18 version 부터는 Externeal Admission Webhook이 세분화 되어 mutating, validating Admission Controller로 나누어서 관리할 수 있게 되었습니다.
이러한 Webhook admission controller를 통해 개발자는 본인이 필요한 webhook server를 만들어서 Admission Controller에 plugin형식으로 적용해서 사용할 수 있습니다.

kube-apiserver follow 알아보기

[kube-apiserver follow chart]
1. API HTTP handler
Kubernetes 관리자가 kubectl tool로 apply 혹은 create 명령을 통해서 Pod 등 각종 object 생성 요청을 하게되면 바로 kube-apiserver로 해당 요청이 전달되는데 이 요청은 모두 HTTP 형식으로 전달됩니다.
kube-apiserver는 HTTP Request를 받아서 요청을 처리하기 위한 작업을 시작합니다.
2. Authentication Authorization
가장 먼저 인증과 권한을 확인합니다.
kubectl 명령으로 kube-apiserver에 접근하기 위해 kubeconfig 파일에 설정된 인증 방식으로 kube-apiserver에 접근을 시도하는데 이때 사용되는 인증 방식으로 JWT, TLS 등등이 있습니다.
이 것을 검증하고 또 권한확인을 위해 RBAC 즉 clusterRole 혹은 Role을 확인하게 되는데 이런 과정을 검증하는 단계가 바로 Authentication Authorization 단계입니다.
3. Mutating admission (Admission Controller)
Hook point
해당 단계는 Admission Controller에 속하는 단계로 Http Request(yaml 혹은 json 형식)를 검사한뒤 적절한 값으로 변경할 수 있습니다.
Request를 중간에 가로채서 관리자가 생성한 webhook server로 보낸 뒤 webhook server에서 관리자가 원하는 검증을 수행하고 webhook server가 request를 변경해서 respone을 보낼수도 있습니다. 즉 Mutating 단계를 통해 request data를 변경할 수 있습니다.
4. Object Schema Validation
request에 형식이 올바른지 확인하는 단계. 필수 data가 정상적으로 들어가 있는지 오타는 없는지, name에 설정된 값이 규칙에 따라 올바르게 입력됐는지 확인합니다. yaml 파일 형식이 올바른지 확인하는 단계라고 생각하면 됩니다.
5. Validating admission (Admission Controller)
Hook point
해당 단계 또한 Admission Controller에 속하는 단계입니다.
Request(yaml 혹은 json 형식)가 생성될 수 있는지 확인하는 단계. 예를 들어 namespace에 Pod, service등 각종 object를 생성하기 전에 해당 namespace에 동일한 object가 이미 있는지 확인하거나 관리자가 직접 webhook server를 구현해서 체크하고 싶은 부분을 추가할 수 있습니다.
6. Persisted to etcd
상위의 모든 단계를 통화하면 정상적인 Request data로 확인하고 etcd database에 내용을 저장합니다.
etcd에 request data가 저장되어 있으면 이후에 kube-controller, scheduler 모듈이 이를 확인하고 대응합니다.

Input 인터페이스

MutatingWebhook과 ValidatingWebhook의 Input은 다음과 같이 동일합니다.
사용자가 새로운 Pod를 하나 생성하게 되면 아래와 같은 AdmissionReview라는 json 객체가 Dynamic Admission Controller로 전달이 됩니다.
{ "kind": "AdmissionReview", "apiVersion": "admission.k8s.io/v1beta1", "request": { "kind": { "group": "", "version": "v1", "kind": "Pod" }, "resource": { "group": "", "version": "v1", "resource": "pods" }, "namespace": "default", "operation": "CREATE", "userInfo": { "username": "system:serviceaccount:default:default", "uid": "439dea65-3e4e-4fa8-b5f8-8fdc4bc7cf53", "groups": [ "system:serviceaccounts", "system:serviceaccounts:default", "system:authenticated" ] }, "object": { "apiVersion": "v1", "kind": "Pod", "metadata": { "creationTimestamp": "2019-08-13T16:01:54Z", "generateName": "nginx-7bb7cd8db5-", "name": "nginx-7bb7cd8db5-dbplk", "namespace": "default", }, "spec": { "containers": [ { "image": "nginx", "imagePullPolicy": "Always", "name": "nginx", } ] }, "status": { "phase": "Pending", "qosClass": "BestEffort" } }, "oldObject": null } }
YAML
복사
여기서 눈여겨 볼 property는 다음과 같습니다.
request: 사용자가 요청한 전체 스펙
kind: 요청한 쿠버네티스 리소스 종류 (예시에서는 Pod를 요청함)
operation: Create / Read / Update / Delete 등 연산 내용
userInfo: 요청한 사용자의 정보
object: 실제 리소스 스펙 (Pod 상세 스펙)
oldObject: 리소스 변경(Update)이 발생하는 경우 이전 리소스 스펙이 전달됨
사용자는 Webhook의 POST body로 전달되는 AdmissionReview 객체의 전체 요청사항을 파악하여 적절하게 판단(mutate or validate) 합니다. 판단이 완료되면 다시 쿠버네티스쪽으로 Admission Control의 결과를 반환해야 합니다. 리턴값도 동일하게 AdmissionReview 객체를 전달하게 됩니다.

Validating Admission Output

Validating admission의 리턴값은 간단합니다. AdmissionReview 객체를 리턴할 때, 최종적으로 허가를 할지, 반려할지 response.allowed 값을 설정하기만 하면 됩니다.
{ "apiVersion": "admission.k8s.io/v1", "kind": "AdmissionReview", "response": { "allowed": true, } }
YAML
복사
allowed값이 true로 반환이 되면 정상적으로 허용이 되는 것이고 반대로 false면 반려가 됩니다.

Mutating Admission Output

Mutating admission에서는 한가지만 더 추가하면 되는데요. 변경(mutating)하려는 정보를 JSONPath 형식의 json을 base64로 인코딩하여 patch라는 property에 추가하면 됩니다.
{ "apiVersion": "admission.k8s.io/v1", "kind": "AdmissionReview", "response": { "allowed": true, "patchType": "JSONPatch", "patch": "W3tvcDogYWRkLCBwYXRoOiAvbWV0YWRhdGEvYW5ub3RhdGlvbnMvZm9vLCB2YWx1ZTogYmFyfV0=" } }
YAML
복사

Admission Controller(Mutation Webhook) 구현

이번에 테스트할 시나리오는 아래와 같습니다.
kkj-ns namespace에 Pod가 생성이 되면 mutate: admission-test labels을 추가.

Self-signed Certificate 만들기

Dynamic Admission Controller는 서비스 형태로 존재합니다. 그래서 간단한 웹서버를 만들어야 합니다. 한가지 특징은 통신이 HTTPS로 동작해야 합니다. 이를 위해 자체적인 Certificate을 생성해 봅시다.
openssl genrsa -out ca.key 2048 openssl req -x509 -new -nodes -key ca.key -days 100000 -out ca.crt -subj "/CN=admission_ca" # 이것은 Certificate Authority의 CN입니다. # Server Certificate을 만들기 위한 설정 파일을 생성합니다. cat >server.conf <<EOF [req] req_extensions = v3_req distinguished_name = req_distinguished_name prompt = no [req_distinguished_name] CN = webhook.default.svc [ v3_req ] basicConstraints = CA:FALSE keyUsage = nonRepudiation, digitalSignature, keyEncipherment extendedKeyUsage = clientAuth, serverAuth subjectAltName = @alt_names [alt_names] DNS.1 = webhook.default.svc EOF openssl genrsa -out server.key 2048 openssl req -new -key server.key -out server.csr -config server.conf openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt -days 100000 -extensions v3_req -extfile server.conf
Bash
복사

Webhook handler 만들기(python flask)

1. kkj-ns namespace생성

kkj-ns namespace에 pod가 생성됐을 때를 고려해야하니 먼저 namespace를 미리 생성합니다.

2. Webhook Server 만들기

webhook.py
기본조건 : flask, request, jsonpatch 설치
from flask import Flask, request, jsonify import ssl import pprint import requests import logging import copy import base64 import jsonpatch from requests.auth import HTTPBasicAuth logging.basicConfig(filename = "admission_controller.log", level = logging.DEBUG) app = Flask(__name__) @app.route('/kkj/mutate', methods=['POST']) def mutate(): message_text = "" patch = "" admissionReview = "" container_image= "" mutating_result = [] # kubernetes API Server의 Request를 해당 webhook으로 가져옴 kube_api_request= request.json logging.info("\n") logging.info('--Kube-apiserver request---') logging.info(kube_api_request) logging.info("\n") #namespace data를 알아옴 request_namespace = kube_api_request["request"]["namespace"] #namesapce 가 "kkj-ns"과 동일한지 확인 logging.info("----namespace name check result----") logging.info("namespace = %s", request_namespace) #namesapce 가 "kkj-ns"가 아니라면 mutating 하지 않고 return if "kkj-ns" != request_namespace: message_text = "namespace is not kkj-ns (Don't Mutating)" admissionReview = admission_response(True, message_text) logging.info(admissionReview) return jsonify(admissionReview) else: message_text = "namespace is kkj-ns (Mutating start)" logging.info(message_text) # labels 추가 ( mutating: admission-test ) # 먼저 pod 생성 yaml에 labels 항목이 있는지 확인 # (labels 항목이 없이 생성될 수도 있음) if 'labels' in kube_api_request["request"]["object"]["metadata"]: message_text = "labels is exsist" logging.info(message_text) # labels 항목 중에 mutate가 있는지 확인 # mutate key가 있다면 value를 무조건 admission-test로 변경 if 'mutate' in kube_api_request["request"]["object"]["metadata"]["labels"]: message_text = "mutate labels is exsist (labels mutate)" logging.info(message_text) container_mutating={'op': 'replace', 'path': '/metadata/labels/mutate', 'value': 'admission-test'} # labels 항목 중에 mutate가 없다면 labels 추가 else: message_text = "mutate labels is null (labels add)" logging.info(message_text) labelAdd_mutating={'op': 'add', 'path': '/metadata/labels/mutate', 'value': 'admission-test'} mutating_result.append(labelAdd_mutating) logging.info(labelAdd_mutating) else: message_text = "labels is null (Don't Mutating)" logging.info(message_text) if 0 != len(mutating_result): message_text = "Data mutating" logging.info(mutating_result) patch = jsonpatch.JsonPatch(mutating_result) #jsonpath를 base64 인코딩함. base64_patch = base64.b64encode(patch.to_string().encode("utf-8")).decode("utf-8") uid = kube_api_request["request"]["uid"] admissionReview = admission_response_mutating(True, message_text, uid, base64_patch) else: message_text = "nothing to change" admissionReview = admission_response(True, message_text) # Webhook Server에서 Kubernetes API Server로 response logging.info("\n") logging.info('--Kube-apiserver response result---') logging.info(admissionReview) logging.info("\n") return jsonify(admissionReview) def admission_response(result, message_text): admissionReview = { "response": { "allowed": result, "status": {"message": message_text} } } return admissionReview def admission_response_mutating(result, message_text, uid_data, path_data): #Kube-apiserver로 보내는 response 필드로 아래와 같이 data 포맷이 정해져있어서 #해당 규칙대로 return 해줘야함 #return 값은 patch 항목도 base64로 설정되는것에 주의함. #allowed 항목을 보면 #원래 요청 거절을 validate에서 하지만 해당 값이 false면 mutate 단계에서도 요청을 거절할 수 있음 #거절할 경우 deny를 추가해서 거절에 대한 이유를 같이 return 해주는것이 좋음 admissionReview = { "response": { "allowed": True, "uid": uid_data, "patch": path_data, "patchtype": "JSONPatch", "status": {"message": message_text} } } return admissionReview if __name__ == '__main__': context = ssl.SSLContext(ssl.PROTOCOL_TLS) context.load_verify_locations('./ca.crt') context.load_cert_chain('./server.crt', './server.key') app.run(host= '0.0.0.0', port=6000, debug=True, ssl_context=context)
Python
복사

3. Webhook Server Container image 만들기

Dockerfile
FROM centos:7 RUN yum install -y net-tools RUN yum install -y gcc openssl-devel bzip2-devel libffi-devel wget RUN yum install -y make RUN yum update -y RUN wget https://www.python.org/ftp/python/3.8.5/Python-3.8.5.tgz RUN tar xvf Python-3.8.5.tgz WORKDIR Python-3.8.5/ RUN ./configure --enable-optimizations RUN make altinstall RUN unlink /bin/python RUN ln -s /usr/local/bin/python3.8 /bin/python RUN pip3.8 install flask RUN pip3.8 install jsonpatch RUN pip3.8 install requests ADD webhook.py . ADD server.key . ADD server.crt . ADD ca.crt . CMD ["python", "webhook.py"]
Docker
복사

4. 이미지 빌드 및 registry push

$ docker build -t kkjwebhook:1.0.0 . $ docker tag kkjwebhook:1.0.0 6d0379ca-kr2-registry.container.nhncloud.com/kkj-registry/kkjwebhook:1.0.0 $ docker push 6d0379ca-kr2-registry.container.nhncloud.com/kkj-registry/kkjwebhook:1.0.0
Bash
복사

5. Webhook Server deployment & Service 생성

webhook.yaml
apiVersion: apps/v1 kind: Deployment metadata: name: kkj-admission-webhook namespace: default labels: app: mutating-webhook spec: replicas: 1 selector: matchLabels: app: admission-pod template: metadata: labels: app: admission-pod spec: imagePullSecrets: - name: registry-credential containers: - name: webhook image: 6d0379ca-kr2-registry.container.nhncloud.com/kkj-registry/kkjwebhook:1.0.0 imagePullPolicy: Always ports: - containerPort: 6000 name: flask-webhook --- apiVersion: v1 kind: Service metadata: name: webhook namespace: default spec: ports: - port: 443 targetPort: flask-webhook selector: app: admission-pod
YAML
복사

6. MutatingWebhookConfiguration 만들기

쿠버네티스로 들어오는 요청들을 방금 생성한 Service로 전달해주기 위해 WebhookConfiguration을 설정합니다. 크게 4가지 정보를 합니다.
name: webhook의 이름을 정의합니다.
namespaceSelector: 특정 네임스페이스에 대해서는 validation을 수행하지 않게 만듭니다.
rules: 어떤 리소스에 대해서 validation할지 정의합니다.
clientConfig: 어떤 webhook 서버로 보낼지 지정합니다. 자체 인증한(self-signed) 인증서이기 때문에 CA(Certificate Authority) certificate을 지정해 줍니다.
mutating_admission_controller.yaml
apiVersion: admissionregistration.k8s.io/v1 kind: MutatingWebhookConfiguration metadata: name: mutating-admission-webhook webhooks: - name: webhook.default.svc sideEffects: None admissionReviewVersions: ["v1"] timeoutSeconds: 5 clientConfig: service: name: webhook namespace: default path: "/kkj/mutate" caBundle: <ca_bundle> rules: # Pod를 생성하는 경우에만 해당 mutating admission이 적용됨. 예) deployment로 생성하면 안됨. - operations: [ "CREATE" ] apiGroups: [""] apiVersions: ["v1"] resources: ["pods"]
YAML
복사
MutatingWebhookConfiguration name 은 상위 webhook deployment와 연결되어있는 service의 domain이어야 합니다. 따라서 Service에서 지정한
service name = webhook
namespace name = default
를 조합한 webhook.default.svc 값을 넣습니다. ClientConfig도 동일합니다.
CABundle은 위에서 생성한 SelfSigned Certificate 값을 이용합니다. Flask Webhook Server가 https로 동작하여 요청을 받을 때 인증을 거치도록 되어있는데, 이전에 ca.crt를 사용하여 인증하도록 해놓았습니다.
따라서 ca.crt 정보를 caBundle 항목에 넣어주어 Mutation handler에 갈때 인증을 거치도록 해야합니다.
$ openssl base64 -A < "ca.crt" LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURFVENDQWZtZ0F3SUJBZ 0lVU2hoSzlkbnplemxJYXJTdWRkaUZnR3VpRDZrd0RRWUpLb1pJaHZjTkFRRU wKQlFBd0Z6RVZNQk1HQTFVRUF3d01ZV1J0YVhOemFXOXVYMk5oTUNBWERUSXl NVEV3TWpBNU16a3lOVm9ZRHpJeQpPVFl3T0RFM01Ea3pPVEkxV2pBWE1SVXdF d1lEVlFRRERBeGhaRzFwYzNOcGIyNWZZMkV3Z2dFaU1BMEdDU3FHClNJYjNEU UVCQVFVQUE0SUJEd0F3Z2dFS0FvSUJBUURYWnRJWTJBV2FTVWVENUdZOTBWN3 R1Rk9TcVp1WC81bUYKQmViL1VtbEhKVmdsWmIyQkZQV2g5V1Qyd0RPRzVXamY yR0pFRWcwc1BrSk9FLytrUDF3MjdaRzNmM0RBTndEcwpYaXJEL0RKTlpzR3Zi bXRqTkhSbk1RNWxyc1NnVG5sMkx5WmFGQkpwb1RYek5CaWx4Z0lxMFo3QmxGV UoxUXBrCkYxQVVpNm42MC9iZkN0eE5aUVZQYUJqNTIrS2dlKyt5bVhNUWpzbG NMbnJ0bFk2NTB3VVFaZjBicHYrekJnTUcKZHBSRkZ5UEhCU1JOTE9ZY2hjcFJ ...
Bash
복사
이 값을 복사하여 caBundle에 넣습니다.

7. 결과 확인 Pod 생성

apiVersion: v1 kind: Pod metadata: name: kkj-pod namespace: kkj-ns labels: app: test spec: containers: - name: kkj-nginx image: nginx ports: - containerPort: 80
YAML
복사
테스트 실패… 참고한 예제가 apiVersion: admissionregistration.k8s.io/v1 이 아닌, apiVersion: admissionregistration.k8s.io/v1beta1 기준으로 작성된 코드여서 수정하는데 시간이 좀 걸리고 완성을 못했습니다..
$ kubectl apply -f test.yaml Error from server (InternalError): error when creating "test.yaml": Internal error occurred: failed calling webhook "webhook.default.svc": received invalid webhook response: expected webhook response of admission.k8s.io/v1, Kind=AdmissionReview, got /, Kind=
Bash
복사
[정상적으로 실행될 경우 결과물]
[참고]