이쿠의 슬기로운 개발생활

함께 성장하기 위한 보안 개발자 EverNote 내용 공유

Kubernetes/Kubernetes 이론

Mutating Admission Controller

이쿠우우 2020. 11. 5. 19:25
반응형

 

 

 

 

 

Mutating Admission Controller 생성해보기


[Admission Controller 관련 글목록]

Kubernetes Version 1.18 에 해당함.

Kubernetes Admission Controller
Mutating Admission Controller 생성해보기
Validating Admission Controller


 

 

 

예제로

iksoon-ns namespace에 Pod가 생성이 되면

mutate: admission-test

labels을 추가하고

 

먄약 첫번째 container가

peksoon/iksoon_tomcat:1.0.6 image를 사용한다면

peksoon/iksoon_mysql:1.0.2

로 변경하는

Mutating Admission Controller를 생성해보겠음.

 

0. MutatingAdmissionWebhook 활성화

 

kubernetes에서는MutatingAdmissionWebhook은 

default로 이미 활성화과 되어있는 상태라 

default 환경이라면 따로 설정하지 않고 진행해도 됨.

 

[활성화된 admission controller 조회 명령어]

kube-apiserver -h | grep enable-admission-plugins

 

만약 목록에 MutatingAdmissionWebhook 항목이 없다면

kube-apiserver container에 들어가서 아래의 명령을 실행

 

[명령어]

kube-apiserver --enable-admission-plugins=MutatingAdmissionWebhook

 

1. iksoon-ns namespace생성

 

iksoon-ns namespace에 pod가 생성됐을 때를 고려해야하니

먼저 namespace를 미리 생성해놓음

 

[명령어]

kubectl create ns iksoon-ns

 

[생성 확인]

kubectl get ns

항목 중 iksoon-ns가 있는지 확인

 

 

 

 

2. Webhook Server Container 생성

 

2.1. Python Flask를 사용해서 Webhook Server를 구현.

 

[iksoon-webhook-mutating.py 예제]

from flask import Flask, request, jsonify
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('/iksoon/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 가 "iksoon-ns"과 동일한지 확인
        logging.info("----namespace name check result----")
        logging.info("namespace = %s", request_namespace)
 
        #namesapce 가 "iksoon-ns"가 아니라면 mutating 하지 않고 return
        if "iksoon-ns" != request_namespace:
           message_text = "namespace is not iksoon-ns (Don't Mutating)"
           admissionReview = admission_response(True, message_text)
           logging.info(admissionReview)
           return jsonify(admissionReview)
        else:   
           message_text = "namespace is iksoon-ns (Mutating start)"
           logging.info(message_text)
        
        # 첫번째 container image정보를 확인
        container_image = kube_api_request["request"]["object"]["spec"]["containers"][0]["image"]
        logging.info("container image = %s", container_image)
   
        # 첫번째 container image가 "peksoon/iksoon_tomcat:1.0.6"과 동일한지 확인
        if "peksoon/iksoon_tomcat:1.0.6" == container_image:
           container_mutating={'op': 'replace', 'path': '/spec/containers/0/image', 'value': 'peksoon/iksoon_mysql:1.0.2'}
           mutating_result.append(container_mutating)


        # 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__':
        app.run(host= '0.0.0.0', port=6000, debug=True, ssl_context=('/run/secrets/tls/tls.crt', '/run/secrets/tls/tls.key'))

[jsonpatch 참고]

http://jsonpatch.com/

 

[response 참고]

https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/

 

 

log 확인을 위해 logging을 사용함.

ssl_context=('/run/secrets/tls/tls.crt', '/run/secrets/tls/tls.key') 은

해당 flask webhook에 HTTPS로 접근하기 위한 인증서을 설정하는 부분으로

아래에서 인증서를 생성하고

deployment 시 해당 경로에 volume 형태로 mount할 것임.

 

 

[python flask test 쿼리 예제]

curl -d '{"request": {"uid":"de47", "namespace":"iksoon-ns", "object": {"spec":{"containers": [{"image": "peksoon/iksoon_tomcat:1.0.6"}] }, "metadata": { "labels": {"mutate": "admission"}} }}}' -H "Content-Type: application/json" -X POST http://10.0.2.5:6000/iksoon/mutate

 

[kubectl patch 명령으로 jsonpath test]

kubectl patch --type json --local --filename test.yaml --patch "[{"op": "replace", "path": "/namespace", "value": "iksoon-ns"}]" --output yaml

 

이 명령으로 patch 시 정상적으로 적용되는지 test해보고 webhook code를 수정하는것을 추천

 

 

[참고 : Pod 생성 kube_api_request변수에 저장되어있는 data상태 예시]

추후에 구성이 완료되고 Mutating Admission Controller를 통해 request를 받았을 경우

최초의 data상태를 미리 보여드리겠음.

더보기

{

   "kind":"AdmissionReview",

   "apiVersion":"admission.k8s.io/v1beta1",

   "request":{

      "uid":"6b876f8e-0046-4d81-b59e-c06b1b03f876",

      "kind":{

         "group":"",

         "version":"v1",

         "kind":"Pod"

      },

      "resource":{

         "group":"",

         "version":"v1",

         "resource":"pods"

      },

      "requestKind":{

         "group":"",

         "version":"v1",

         "kind":"Pod"

      },

      "requestResource":{

         "group":"",

         "version":"v1",

         "resource":"pods"

      },

      "name":"iksoon-pod",

      "namespace":"iksoon-ns",

      "operation":"CREATE",

      "userInfo":{

         "username":"kubernetes-admin",

         "groups":[

            "system:masters",

            "system:authenticated"

         ]

      },

      "object":{

         "kind":"Pod",

         "apiVersion":"v1",

         "metadata":{

            "name":"iksoon-pod",

            "namespace":"iksoon-ns",

            "creationTimestamp":"None",

            "labels":{

               "app":"test"

            },

            "annotations":{

               "kubectl.kubernetes.io/last-applied-configuration":"{\"apiVersion\":\"v1\",\"kind\":\"Pod\",\"metadata\":{\"annotations\":{},\"labels\":{\"app\":\"test\"},\"name\":\"iksoon-pod\",\"namespace\":\"iksoon-ns\"},\"spec\":{\"containers\":[{\"image\":\"peksoon/iksoon_tomcat:1.0.6\",\"name\":\"iksoon-tomcat\",\"ports\":[{\"containerPort\":8080}]}]}}\n"

            },

            "managedFields":[

               {

                  "manager":"kubectl",

                  "operation":"Update",

                  "apiVersion":"v1",

                  "time":"2020-09-18T06:59:07Z",

                  "fieldsType":"FieldsV1",

                  "fieldsV1":{

                     "f:metadata":{

                        "f:annotations":{

                           ".":{

                              

                           },

                           "f:kubectl.kubernetes.io/last-applied-configuration":{

                              

                           }

                        },

                        "f:labels":{

                           ".":{

                              

                           },

                           "f:app":{

                              

                           }

                        }

                     },

                     "f:spec":{

                        "f:containers":{

                           "k:{\"name\":\"iksoon-tomcat\"}":{

                              ".":{

                                 

                              },

                              "f:image":{

                                 

                              },

                              "f:imagePullPolicy":{

                                 

                              },

                              "f:name":{

                                 

                              },

                              "f:ports":{

                                 ".":{

                                    

                                 },

                                 "k:{\"containerPort\":8080,\"protocol\":\"TCP\"}":{

                                    ".":{

                                       

                                    },

                                    "f:containerPort":{

                                       

                                    },

                                    "f:protocol":{

                                       

                                    }

                                 }

                              },

                              "f:resources":{

                                 

                              },

                              "f:terminationMessagePath":{

                                 

                              },

                              "f:terminationMessagePolicy":{

                                 

                              }

                           }

                        },

                        "f:dnsPolicy":{

                           

                        },

                        "f:enableServiceLinks":{

                           

                        },

                        "f:restartPolicy":{

                           

                        },

                        "f:schedulerName":{

                           

                        },

                        "f:securityContext":{

                           

                        },

                        "f:terminationGracePeriodSeconds":{

                           

                        }

                     }

                  }

               }

            ]

         },

         "spec":{

            "volumes":[

               {

                  "name":"default-token-stkjw",

                  "secret":{

                     "secretName":"default-token-stkjw"

                  }

               }

            ],

            "containers":[

               {

                  "name":"iksoon-tomcat",

                  "image":"peksoon/iksoon_tomcat:1.0.6",

                  "ports":[

                     {

                        "containerPort":8080,

                        "protocol":"TCP"

                     }

                  ],

                  "resources":{

                     

                  },

                  "volumeMounts":[

                     {

                        "name":"default-token-stkjw",

                        "readOnly":True,

                        "mountPath":"/var/run/secrets/kubernetes.io/serviceaccount"

                     }

                  ],

                  "terminationMessagePath":"/dev/termination-log",

                  "terminationMessagePolicy":"File",

                  "imagePullPolicy":"IfNotPresent"

               }

            ],

            "restartPolicy":"Always",

            "terminationGracePeriodSeconds":30,

            "dnsPolicy":"ClusterFirst",

            "serviceAccountName":"default",

            "serviceAccount":"default",

            "securityContext":{

               

            },

            "schedulerName":"default-scheduler",

            "tolerations":[

               {

                  "key":"node.kubernetes.io/not-ready",

                  "operator":"Exists",

                  "effect":"NoExecute",

                  "tolerationSeconds":300

               },

               {

                  "key":"node.kubernetes.io/unreachable",

                  "operator":"Exists",

                  "effect":"NoExecute",

                  "tolerationSeconds":300

               }

            ],

            "priority":0,

            "enableServiceLinks":True

         },

         "status":{

            

         }

      },

      "oldObject":"None",

      "dryRun":False,

      "options":{

         "kind":"CreateOptions",

         "apiVersion":"meta.k8s.io/v1"

      }

   }

}

 

 

 

 

 

2.2. 상위 Webhook Server가 실행되는 Container image 생성

 

DockerFile을 사용해서 생성

 

[Dockerfile 생성]

원활한 디버깅을 위해 필요한 tool을 전부 설치한 이미지를 생성

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
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
 
RUN mkdir /admission_test
WORKDIR /admission_test
ADD ./iksoon-webhook-mutating.py .


CMD ["python", "/admission_test/iksoon-webhook-mutating.py"]

 

[Dockerfile 빌드 명령어]

docker build --tag mutatingadmission:1.0.0 .

 

[Docker hub에 push 해놓음]

docker tag [생성된 image id] peksoon/admission_test:1.0.0

docker login

docker push peksoon/admission_test:1.0.0

 

 

 

3. Flask Webhook HTTPS 접속용 인증서 생성

 

[명령어]

webhook-server-tls.crt 생성 시 CN을 iksoon-admission-service.default.svc로 설정했는데

이는 아래에서 생성할 webhook deployment 접속 service object의 domain 을 넣어줘야함.

openssl req -nodes -new -x509 -keyout ca.key -out ca.crt -subj "/CN=iksoon admission controller"


openssl genrsa -out webhook-server-tls.key 2048


openssl req -new -key webhook-server-tls.key -subj "/CN=iksoon-admission-service.default.svc" \
    | openssl x509 -req -CA ca.crt -CAkey ca.key -CAcreateserial -out webhook-server-tls.crt

 

 

[결과]

 

 

4. TLS Secret 생성

 

Secret 을 생성해서 deployment에서 해당 secret 를

volume 행태로 mount해서 

flask webhook container에 인증서를 전달할 것임.

 

[명령어]

kubectl -n default create secret tls webhook-certs \

    --cert "webhook-server-tls.crt" \

    --key "webhook-server-tls.key"

 

[결과]

 

 

5. Webhook Server deployment 와 Service 생성

 

[Deployment, Servcie yaml 예시]

apiVersion: apps/v1
kind: Deployment
metadata:
  name: iksoon-admission-webhook
  namespace: default
  labels:
    app: mutating-webhook
spec:
  replicas: 1
  selector:
    matchLabels:
      app: admission-pod
  template:
    metadata:
      labels:
        app: admission-pod
    spec:
      containers:
      - name: webhook
        image: peksoon/admission_test:1.0.0
        imagePullPolicy: Always
        ports:
        - containerPort: 6000
          name: flask-webhook
        volumeMounts:
        - name: webhook-tls
          mountPath: /run/secrets/tls
          readOnly: true
      volumes:
      - name: webhook-tls
        secret:
          secretName: webhook-certs
 
---


apiVersion: v1
kind: Service
metadata:
  name: iksoon-admission-service
  namespace: default
spec:
  ports:
    - port: 443
      targetPort: flask-webhook
  selector:
    app: admission-pod

 

 

 

6. ca.crt 파일 base64 인코딩

[명령어]

openssl base64 -A <"ca.crt"

 

결과 복사 해놓음

 

 

 

7. Webhook Server deployment 와 Service 생성

 

MutatingWebhookConfiguration name 은 

상위 webhook deployment와 연결되어있는 service의 domain이여야함

service name = iksoon-admission-service

namespace name = default

임으로 생성되는 domain = iksoon-admission-service.default.svc

 

clientConfig의 service name과 namespace는

상위 webhook deployment와 연결되어있는 service의

service name = iksoon-admission-service

namespace name = default

을 입력함.

 

path: "/iksoontest" 

에서 경로를 "/iksoontest" 로 설정한 이유는 

상위 webhook python 코드의

@app.route('/iksoontest', methods=['POST'])

부분에서 경로를 "/iksoontest"로 설정했기 때문.

 

[mutating_admission_controller.yaml]

apiVersion: admissionregistration.k8s.io/v1beta1
kind: MutatingWebhookConfiguration
metadata:
  name: mutating-admission-webhook
webhooks:
  - name: iksoon-admission-service.default.svc
    clientConfig:
      service:
        name: iksoon-admission-service
        namespace: default
        path: "/iksoon/mutate"   
      caBundle: [여기에 상위에서 복사한 base64 encoding ca.crt 값이 들어감.]
    rules:  # Pod를 생성하는 경우에만 해당 mutating admission이 적용됨. 예) deployment로 생성하면 안됨.
      - operations: [ "CREATE" ]
        apiGroups: [""]
        apiVersions: ["v1"]
        resources: ["pods"]

 

 

 

 

 

8. 결과 확인 pod 생성 test

 

[test.yaml]

apiVersion: v1
kind: Pod
metadata:
  name: iksoon-pod
  namespace: iksoon-ns
  labels:
    app: test
spec:
  containers:
  - name: iksoon-tomcat
    image: peksoon/iksoon_tomcat:1.0.6
    ports:
    - containerPort: 8080

 

 

[Pod생성 결과]

상위 예제 yaml로 pod 배포 시 

labels 에

mutate: admission-test

항목이 추가되어있고

peksoon/iksoon_tomcat:1.0.6 image를 명시했는데

peksoon/iksoon_mysql:1.0.2로 생성되어있는것을 확인할 수 있음.

 

 

 

 

 


제 글을 복사할 시 출처를 명시해주세요.
글에 오타, 오류가 있다면 댓글로 알려주세요! 바로 수정하겠습니다!


 

 

 

참고

[mutatingwebhookconfiguration]

https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/

 

[mutating webhook]

https://github.com/alicek106/k8s-admission-controller-python

https://github.com/morvencao/kube-mutating-webhook-tutorial

 

 

 

 

반응형

'Kubernetes > Kubernetes 이론' 카테고리의 다른 글

Kubernetes Custom Resource 개념  (0) 2020.11.05
Validating Admission Controller  (0) 2020.11.05
Admission Controller  (0) 2020.11.05
Pod Security Policy (PSP)  (2) 2020.09.20
Security Context  (0) 2020.09.20