아이엠 !나이롱맨😎
article thumbnail
반응형

쿠버네티스는 다른 플랫폼처럼 인증(Authentication)/인가(Athorization) 를 제공해줍니다. 따라서 인증받지 못한 사용자라면 401 (UnAuthorized) 를 응답 받고, 인증은 되었지만 권한이 없다면 403(Forbidden) 응답을 받게 되죠.

아주 평범한 인증/인가 프로세스입니다. 그런데 인증/인가가 완료되었다고쿠버네티스 환경에 바로 적용할 순 있는 건 아닙니다. 사용자가 보낸 요청을 적용하기 직전 한 군데를 더 들립니다. 그것이 바로 Adimission Controller 입니다.

 

이번 글에서는 Adimission Controller 에 대한 개념을 간단하게 설명하고, Adimission Controller 를 구현해볼 생각입니다. 😎

 

Admission Controller 가 뭐죠?


공식 문서에 있는 Admission Controller 관련 글을 번역해보자면 이렇습니다.

 

Admission Controller 는 클러스터의 사용 방식을 제어하는 플러그인입니다. 인증/인가된 API 요청을 Admission Controller 이 가로채어 요청 개체를 변경하거나 요청을 모두 거부할 수 있는 게이트키퍼로 생각할 수 있습니다.

 

 

Admission Controller Phases

 

이 그림이 바로 Admission Controller 의 Phases 이죠. 인증(Authentication)/인가(Athorization) 를 거친 후 Mutating Admission 과 Validating Admission 이 API Request 를 가로채게 되는데 이 2개가 Admission Controller 의 컴포넌트입니다.

 

가로챈 API Request 는 WebHook 방식으로 Admission Controller Server 로 보내지게 됩니다. 그럼 여기서 요청을 변경하거나 거부할 수 있는 로직을 구현하게 되죠.

 

Mutating Admission 은 주로 API Request 의 개체를 변경하는 용도이고, Validating Admission 은 주로 API Request 를 거부할 지를 결정합니다.

 

논리적으로는 이 둘의 역할이 각가 다르지만 Validating Admission 에서 꼭 거부만 할 필요 없이 개체를 변경해도 무방합니다.

 

Admission Controller 의 대표적인 예로는 LimitRanger 가 있습니다. 위에서 Admission Controller 은 플러그인이라고 말씀드렸죠? 30개 정도의 플러그인을 쿠버네티스에서 기본적으로 제공해주는데 LimitRanger 은 그 중 하나입니다.

 

LimitRanger 은 쿠버네티스에 파드를 배포할 때 만약 resource 를 지정하지 않았다면 default resource 를 작성하여 자동으로 설정해놓은 resource 를 갖게되거나, resource 를 지정하지 않은 파드는 배포 제한을 할 수 있도록 도와줍니다.

 

적용하는 방법도 어렵지 않습니다. kube-apiserver.yaml 에서 아래와 같이 추가만 해주면 됩니다.

apiVersion: v1
kind: Pod
metadata:
  annotations:
    kubeadm.kubernetes.io/kube-apiserver.advertise-address.endpoint: 10.178.0.2:6443
  creationTimestamp: null
  labels:
    component: kube-apiserver
    tier: control-plane
  name: kube-apiserver
  namespace: kube-system
spec:
  containers:
  - command:
    - kube-apiserver
    - --encryption-provider-config=/etc/kubernetes/etcd/ec.yaml
    - --anonymous-auth=true
    - --advertise-address=10.178.0.2
    - --allow-privileged=true
    - --authorization-mode=Node,RBAC
    - --client-ca-file=/etc/kubernetes/pki/ca.crt
    - --enable-admission-plugins=LimitRanger # 추가
     
    ...

 

LimitRanger 말고 눈여겨볼만한 플러그인은 PodSecurityPolicy 가 있는데요. 이 플러그인 Pod 와 관련된 보안 정책을 지정할 수 있게 도와주는 플러그인입니다.

 

예를 들면 SecurityContext 는 1000 이 아닌 root 로 생성하려고 한다면 파드 생성을 거부할 수 있는 정책을 적용할 수 있죠.

 

하지만 PodSeuciryPolicy 는 쿠버네티스 v1.21 에서 deprecated 되었고, v1.25 에선 완전히 삭제되기 때문에 사용할 수가 없습니다. 그럼 이러한 정책은 어떻게 생성해서 적용해야 할까요?

 

답은 간단합니다. 만들면 되죠 🤟

 

그럼 한번 간단하게 Admission Controller 를 만들어보죠!

 

Admission Controller 를 직접 만들어도 되지만, 이미 잘 만들어진 CNCF 프로젝트 일부인 OPA Gatekeeper 라는 것이 있습니다. 쿠버네티스에서 매우 밀고 있는 프로젝트이기도 하고, 개발해야한다는 번거로움 때문에 아마 많은 분들이 OPA Gatekeeper 를 사용하지 않을까 라는 생각은 듭니다.

그래도 원리는 무엇인지 파악하고 싶으니 만들어보죠.

 

 

Admission Controller 를 만들어보자!


매우 간단하게 만들것이기 때문에 Node.js 의 Express 프레임워크를 사용해보도록 하겠습니다!

Node 버전은 18.5 에서 진행됩니다.

 

Express 프로젝트를 생성해줍니다. 이후 package.json 에 스크립트를 추가합니다.

"scripts": {
    "start": "node ./bin/www",
    "dev": "nodemon app start",
    "prod": "pm2-runtime start app.js -i 1"
  }

 

 

프로젝트 관련 npm 라이브러리를 설치하고, 정상적으로 동작하는 지 확인합니다.

npm install -i nodemon pm2

npm run dev

 

Admission Controller Server 는 반드시 TLS 통신을 해야합니다.

즉, http 통신이 아닌 https 로 통신해야합니다.

 

코드로는 다음과 같습니다.

const bodyParser = require('body-parser');
const express = require('express');
const fs = require('fs');
const https = require('https');

const app = express();
app.use(bodyParser.json());

# 서버 8443 리스닝
const port = 8443; 

# tls 통신에 사용할 ca.crt, server.crt, server.key 읽기
const options = {
  ca: fs.readFileSync('ca.crt'),
  cert: fs.readFileSync('server.crt'),
  key: fs.readFileSync('server.key'),
};

# https 통신
const server = https.createServer(options, app);

# 서버 실행
server.listen(port, () => {
  console.log(`Server running on port ${port}/`);
});

 

그럼 이번엔 ca.crt, server.crt, server.key 를 생성해주죠.

# CA Key 와 CA CRT 생성
# X.509는 PKI 기술 중에서 가장 널리 알려진 표준 포맷
openssl req -nodes -new -x509 -keyout ca.key -out ca.crt -subj "/CN=Admission Controller Webhook Demo CA" -sha256

# 서버 Key 생성
openssl genrsa -out server.key 2048

# server.conf 생성
cat >server.conf <<EOF
[req]
req_extensions = v3_req
distinguished_name = req_distinguished_name
prompt = no
[req_distinguished_name]
CN = admission-controller-server.default.svc
[ v3_req ]
basicConstraints = CA:FALSE
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
extendedKeyUsage = clientAuth, serverAuth
subjectAltName = @alt_names
[alt_names]
DNS.1 = admission-controller-server.default.svc
EOF

# CSR 생성
openssl req -new -key server.key -out server.csr -config server.conf

# Server CRT 생성
openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt -extensions v3_req -extfile server.conf -sha256

 

위 명령어대로 진행하는 걸 추천드립니다.

 

그럼 이제 health check 용 엔드포인트와 webhook 용 엔드포인트 코드를 작성합니다.

app.get('/health', (req, res) => {
  res.send('ok');
});

app.post('/', (req, res) => {
  if (req.body.request === undefined) {
    res.status(400).send();
    return;
  }

  console.log(req.body); // DEBUGGING
  
	const { request: { uid } } = req.body;

  res.send({
    apiVersion: "admission.k8s.io/v1",
    kind: "AdmissionReview",
    response: {
      uid,
      allowed: validate(req)
    }
  })

});

// Create Pod 에 대한 제한
function validate(req) {
  if (req.body['request']['object']['kind'] == 'Pod' && req.body['request']['operation'] == 'CREATE') {
    return false
  } else {
    return true
  }
}

 

GET /health 는 health check 용 엔드포인트이고, POST / 는 webhook 용 엔드포인트 입니다.

그리고 간단하게 Create Pod 에 대한 이벤트를 거부합니다.

 

즉, Pod 생성은 제한되지만 Pod 외에 Deploy 나 Service 등은 생성이 가능합니다.

 

참고로 쿠버네티스가 webhook 으로 보내는 Request 객체는 아래와 같습니다. 

자세한 정보는 여기를 참고해주세요.

{
    "kind": "AdmissionReview",
    "apiVersion": "admission.k8s.io/v1",
    "request": {
        "uid": "687a5fd9-0939-4d17-a398-d80582b199f8",
        "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": "hello-pod",
        "namespace": "default",
        "operation": "CREATE",
        "userInfo": {
            "username": "kubernetes-admin",
            "groups": []
        },
        "object": {
            "kind": "Pod",
            "apiVersion": "v1",
            "metadata": [],
            "spec": [],
            "status": []
        },
        "oldObject": null,
        "dryRun": "false",
        "options": {
            "kind": "CreateOptions",
            "apiVersion": "meta.k8s.io/v1",
            "fieldManager": "kubectl-client-side-apply",
            "fieldValidation": "Strict"
        },
        "apiVersion": "v1"
    }
}

 

그럼 이제 이미지로 만들 수 있도록 Dockerfile 을 작성하죠.

FROM node:12.18.2

RUN mkdir /var/node

COPY ./ /var/node

WORKDIR /var/node

RUN npm i

RUN npm i -g pm2

CMD [ "npm", "run", "prod" ]

 

모든 준비가 끝났습니다. 바로 쿠버네티스 환경에서 방금 만든 Adimission Controller Server 를 배포해보죠 😀

 

쿠버네티스에 배포


제 쿠버네티스 클러스터는 kubeadm 으로 만들었으며, v1.25 입니다.

 

admission-controller-server.yaml 을 만듭니다.

apiVersion: v1
kind: Service
metadata:
  name: admission-controller-server
spec:
  ports:
  - port: 443
    protocol: TCP
    targetPort: 8443
  selector:
    run: admission-controller-server
---
apiVersion: v1
kind: Pod
metadata:
  labels:
    run: admission-controller-server
  name: admission-controller-server
spec:
  containers:
  - image: kingbj0429/admission-controller-server
    name: admission-controller-server
    ports:
    - containerPort: 8443
    imagePullPolicy: Always
    livenessProbe:
      httpGet:
        port: 8443
        path: /health
        scheme: HTTPS
    readinessProbe:
      httpGet:
        port: 8443
        path: /health
        scheme: HTTPS

 

여기서 정말정말 중요한게 있는데 Service 의 .metadata.name 은 위 server.crt 를 생성하기 위해 사용했던 server.conf 에 명시한 CN 과 이름이 같아야 합니다.

Service 이름이 admission-controller-server 이고, Namespace 가 default 이니 admission-controller-server.default.svc 가 되어야 합니다.

 

이제 마지막 단계인 WebhookConfiguration 을 생성해주죠. mutate 와 valid 2 종류가 있는데, 여기선 valid 로 생성하겠습니다.

자세한 사항은 여기를 참고해주세요.

apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
  name: "pod-policy.example.com"
webhooks:
- name: "pod-policy.example.com"
  rules:
  - apiGroups:   ["*"]
    apiVersions: ["*"]
    operations:  ["CREATE"]
    resources:   ["*"]
    scope:       "Namespaced"
  clientConfig:
    service:
      namespace: "default"
      name: "admission-controller-server" # CN 과 일치해야함
    caBundle: $(cat ca.crt | base64 | tr -d '\n')
  admissionReviewVersions: ["v1"]
  sideEffects: None
  timeoutSeconds: 5

 

간단히 설명하자면 CREATE 이벤트가 발생했을 경우 clientConfig 에 등록된 Service 도메인(admission-controller-server)으로 webhook 을 보내게 됩니다.

 

모든 준비가 끝났으니 제대로 되었는 지 확인해보죠.

 

Admission Controller Server 검증


아주 심플한 Pod 를 생성해보죠. 제대로 만들었다면 제한이 되겠죠?

apiVersion: v1
kind: Pod
metadata:
  name: nginx
spec:
  containers:
  - name: nginx
    image: nginx:1.14.2
    ports:
    - containerPort: 80

 

Admission Webhook Denied 가 난 걸 보니 문제가 없어 보입니다. 그럼 다른 리소스는 어떨까요? 

Error from server: error when creating "nginx.yaml": admission webhook "pod-policy.example.com" denied the request without explanation

 

Deploy 로 테스트 해보죠.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
  labels:
    app: nginx
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.14.2
        ports:
        - containerPort: 80

 

생성이 아주 잘됩니다.

deployment.apps/nginx-deployment created

 

이번 글에서는 Admission Controller 에 대해 알아보고 직접 Server 를 생성까지 해보았습니다.

긴 글 읽어주셔서 감사합니다 😎

 

코드는 깃헙에 있습니다 

반응형

article prev thumbnail
article next thumbnail
profile on loading

Loading...