KubernetesのDynamic Admission Controlを試してみる

Dynamic Admission Control とは

公式の説明はこちら

KubernetesでPodのようなオブジェクトを操作する際には、kubectl等のクライアントからkube-apiserverへのAPIリクエストが実行されます。この時、リクエストの認証および認可が行われた後の段階で、Admission Controlと呼ばれる追加の検証を行うことが可能です。

例えば、Pod作成時のSpecの中に runAsUser の記述がある場合はリクエストを拒否するといった制御を行えます。使用できるAdmission Controllerの一覧は こちら に列挙されています。

現在これらのAdmission Controllerはkube-apiserverにビルトインで実装されているため、新しくAdmission Controllerを追加したくなった場合には、プラグインを実装してKubernetesのupstreamに取り込んでもらう必要があります。

しかし、自分たちのクラスタ用にカスタマイズしたAdmission Controllerを作りたいといったケースではupstreamに取り込んでもらうことは難しいでしょう。そのような時にDynamic Admission Controlが1つの手段として使用できます。

仕組み

Dynamic Admission Controlを使用するにあたっては、以下の2つのAdmission Controllerを使って設定を行います。

  • ValidatingAdmissionWebhook
  • MutatingAdmissionWebhook

Webhookという名の通り、kube-apiserverがオブジェクトの作成/更新等のリクエストを受け取った際にWebhookがトリガーされ、任意のエンドポイントへPOSTリクエストを送信してくれます。

このWebhookリクエストのBodyには、操作対象であるオブジェクトのManifestがJSON形式で含まれています。リクエストを受け取るHTTPサーバでは、その中身を見て結果をJSONで返すシンプルな処理を行います。

https://d33wubrfki0l68.cloudfront.net/af21ecd38ec67b3d81c1b762221b4ac777fcf02d/7c60e/images/blog/2019-03-21-a-guide-to-kubernetes-admission-controllers/admission-controller-phases.png

引用元: https://kubernetes.io/blog/2019/03/21/a-guide-to-kubernetes-admission-controllers/

とりあえず試す

VolumeにhostPathを使用しているPodは作成を拒否するValidatingAdmissionWebhookのサンプルを以下のリポジトリで公開しています。

github.com

まずは試してみようということで、minikube(もしくはkind)のクラスタで実際に動くところを見てみましょう。サンプルはKubernetesのバージョンが1.17以降でなければ動作しませんので、必要に応じてminikubeのバージョンを上げてください。

次のコマンドを実行するとValidatingAdmissionWebhookの設定が追加されて、kube-public namespaceにWebhookを受け取るHTTPサーバーが立ち上がります。

Podの作成に影響を及ぼすため、Productionのクラスタなどで実行しないようにご注意ください

$ curl -s https://raw.githubusercontent.com/tokibi/sample-admission-controller/master/deploy/{configmap,deployment,webhook-configuration}.yaml | kubectl apply -f -
secret/admission-controller-certs created
deployment.apps/admission-controller created
service/admission-controller created
validatingwebhookconfiguration.admissionregistration.k8s.io/validating-webhook created

$ kubectl get pod -n kube-public
NAME                                    READY   STATUS    RESTARTS   AGE
admission-controller-5c954f94c9-5dnbp   1/1     Running   0          9m12s

HTTPサーバーのPodがRunningになったことを確認したら、hostPathを使用していないPodと使用しているPodの2つを作成してみます。

$ cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
  name: valid-pod
spec:
  containers:
  - name: valid-pod
    image: alpine:latest
    command:
      - "sleep"
      - "1"
---
apiVersion: v1
kind: Pod
metadata:
  name: invalid-pod
spec:
  containers:
  - name: invalid-pod
    image: alpine:latest
    command:
      - "sleep"
      - "1"
  volumes:
  - name: hostpath-volume
    hostPath: # <- invalid
      path: /etc
EOF

実行すると valid-pod は正常に作成されますが、invalid-pod はhostPathを使用しているため、エラーメッセージと共に作成が拒否されることがわかります。

pod/valid-pod created
Error from server: error when creating "STDIN": admission webhook "validating-webhook.example.com" denied the request: pod "invalid-pod" creation denied, hostPath is not allowed

このようにAdmissionWebhookを使用して、Podだけに限らず任意のリソースの操作時に制御を行うことができます。

設定内容

とりあえずサンプルを使って動作を確認してみましたが、具体的に何を行ったかを順に解説していきます。

  1. kube-apiserverのオプションでAdmissionWebhookを有効化
  2. WebhookConfigurationを作成
  3. Webhookを受け取るAPIサーバーを実装

kube-apiserverのオプションでAdmissionWebhookを有効化

minikubeやkindのクラスタで起動しているkube-apiserverでは初期状態で有効化されているため、先ほどサンプルを試した際にはこの手順は省略されていました。その他のクラスタでは、必要に応じてkube-apiserverの --enable-admission-plugins オプションに MutatingAdmissionWebhookValidatingAdmissionWebhook を追加してください。

参考までにminikubeのkube-apiserverのオプションでは、次のように最後から2, 3番目で指定されています。

$ minikube ssh "sudo cat /etc/kubernetes/manifests/kube-apiserver.yaml" | grep enable-admission-plugins
    - --enable-admission-plugins=NamespaceLifecycle,LimitRanger,ServiceAccount,DefaultStorageClass,DefaultTolerationSeconds,NodeRestriction,MutatingAdmissionWebhook,ValidatingAdmissionWebhook,ResourceQuota

Admission Controlは先頭から順に実行されるため、この場合は NodeRestriction の検証が実施された後、MutatingAdmissionWebhook, ValidatingAdmissionWebhookの順に検証が実施されることになります。

WebhookConfigurationを作成

kube-apiserverの設定でAdmissionWebhookを有効にしただけでは、オブジェクトの操作時にWebhookは実行されません。AdmissionWebhookを追加できるようになっただけで、設定は別途行う必要があります。

この時に作成するのがWebhookConfigurationで、サンプルでは以下のようなManifestを定義してクラスタにapplyしています。kindは ValidatingWebhookConfiguration になっていますので、検証のみを実施します。MutatingAdmissionWebhookを使用する場合は MutatingWebhookConfiguration を別途作成してください。

---
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
  name: validating-webhook
  namespace: kube-public
webhooks:
- name: validating-webhook.example.com
  admissionReviewVersions:
  - v1beta1
  clientConfig:
    caBundle: LS0tLS1CRU...
    service:
      name: admission-controller
      namespace: kube-public
      path: "/"
  rules:
  - apiGroups: [""]
    apiVersions: ["v1"]
    operations: ["CREATE"]
    resources: ["pods"]
  sideEffects: None
  timeoutSeconds: 5

clientConfig の箇所では、HTTPサーバーのTLS証明書を検証するために必要なCA証明書をbase64エンコードしたものと、HTTPサーバーのPodの前段に設置されたServiceのセレクタを記載しています。

サンプルでは、kube-apiserverからのWebhookリクエストを受け取るサーバーもKubernetesクラスタ内に起動していますのでServiceを指定していますが、以下のようにクラスタ外で起動しているサーバーをURL形式で指定することも可能です。

apiVersion: admissionregistration.k8s.io/v1
kind: MutatingWebhookConfiguration
...
webhooks:
- name: my-webhook.example.com
  clientConfig:
    url: "https://my-webhook.example.com:9443/my-webhook-path"
  ...

引用元: https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#url

また、rules の箇所ではkube-apiserverがリクエストを受け取った時にWebhookをトリガーする条件を記載します。サンプルではPodの作成時のみWebhookリクエストが送信されるようにしています。その他の設定項目については このあたり が参考になると思います。

この設定を行うことでWebhookリクエストが飛ぶようになります。

Webhookを受け取るHTTPサーバーを実装

Request

kube-apiserverからのWebhookリクエストを受け取って結果を返すHTTPサーバーを実装します。リクエストBodyに含まれるJSONデータの構造は 公式 で解説されていて、以下はその引用です。

{
  "apiVersion": "admission.k8s.io/v1",
  "kind": "AdmissionReview",
  "request": {
    # Random uid uniquely identifying this admission call
    "uid": "705ab4f5-6393-11e8-b7cc-42010a800002",

    # Fully-qualified group/version/kind of the incoming object
    "kind": {"group":"autoscaling","version":"v1","kind":"Scale"},
    # Fully-qualified group/version/kind of the resource being modified
    "resource": {"group":"apps","version":"v1","resource":"deployments"},
    # subresource, if the request is to a subresource
    "subResource": "scale",

    # Fully-qualified group/version/kind of the incoming object in the original request to the API server.
    # This only differs from `kind` if the webhook specified `matchPolicy: Equivalent` and the
    # original request to the API server was converted to a version the webhook registered for.
    "requestKind": {"group":"autoscaling","version":"v1","kind":"Scale"},
    # Fully-qualified group/version/kind of the resource being modified in the original request to the API server.
    # This only differs from `resource` if the webhook specified `matchPolicy: Equivalent` and the
    # original request to the API server was converted to a version the webhook registered for.
    "requestResource": {"group":"apps","version":"v1","resource":"deployments"},
    # subresource, if the request is to a subresource
    # This only differs from `subResource` if the webhook specified `matchPolicy: Equivalent` and the
    # original request to the API server was converted to a version the webhook registered for.
    "requestSubResource": "scale",

    # Name of the resource being modified
    "name": "my-deployment",
    # Namespace of the resource being modified, if the resource is namespaced (or is a Namespace object)
    "namespace": "my-namespace",

    # operation can be CREATE, UPDATE, DELETE, or CONNECT
    "operation": "UPDATE",

    "userInfo": {
      # Username of the authenticated user making the request to the API server
      "username": "admin",
      # UID of the authenticated user making the request to the API server
      "uid": "014fbff9a07c",
      # Group memberships of the authenticated user making the request to the API server
      "groups": ["system:authenticated","my-admin-group"],
      # Arbitrary extra info associated with the user making the request to the API server.
      # This is populated by the API server authentication layer and should be included
      # if any SubjectAccessReview checks are performed by the webhook.
      "extra": {
        "some-key":["some-value1", "some-value2"]
      }
    },

    # object is the new object being admitted.
    # It is null for DELETE operations.
    "object": {"apiVersion":"autoscaling/v1","kind":"Scale",...},
    # oldObject is the existing object.
    # It is null for CREATE and CONNECT operations.
    "oldObject": {"apiVersion":"autoscaling/v1","kind":"Scale",...},
    # options contains the options for the operation being admitted, like meta.k8s.io/v1 CreateOptions, UpdateOptions, or DeleteOptions.
    # It is null for CONNECT operations.
    "options": {"apiVersion":"meta.k8s.io/v1","kind":"UpdateOptions",...},

    # dryRun indicates the API request is running in dry run mode and will not be persisted.
    # Webhooks with side effects should avoid actuating those side effects when dryRun is true.
    # See http://k8s.io/docs/reference/using-api/api-concepts/#make-a-dry-run-request for more details.
    "dryRun": false
  }
}

HTTPサーバーは基本的にこのJSONの中身を見て処理を実施していくことになります。サンプルではPodのspecにhostPathが使用されているか否かによって結果を変えていましたが、それは request.object の中を見て判断しています。

Response

ValidatingAdmissionWebhookに対するレスポンスでは request.allowed のbool値によってkube-apiserverへのリクエストを許可するかどうかを決定します。falseの場合は response.status.message でオブジェクトを操作しようとしたクライアントにエラーメッセージを通知することができます。

{
  "apiVersion": "admission.k8s.io/v1",
  "kind": "AdmissionReview",
  "response": {
    "uid": "<value from request.uid>",
    "allowed": false,
    "status": {
      "code": 403,
      "message": "You cannot do this because it is Tuesday and your name starts with A"
    }
  }
}

引用元: https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#response

また、MutatingAdmissionWebhookに対するレスポンスでは patchTypepatch によってManifestの変更内容を指定します。patch の値には [{"op": "add", "path": "/spec/replicas", "value": 3}] のように操作内容, 対象, 変更後の値をbase64エンコードしたものを入力します。

{
  "apiVersion": "admission.k8s.io/v1",
  "kind": "AdmissionReview",
  "response": {
    "uid": "<value from request.uid>",
    "allowed": true,
    "patchType": "JSONPatch",
    "patch": "W3sib3AiOiAiYWRkIiwgInBhdGgiOiAiL3NwZWMvcmVwbGljYXMiLCAidmFsdWUiOiAzfV0="
  }
}

引用元: https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#response

HTTPサーバーの行う処理はJSONを受け取ってJSONを返すだけですので、一から作成するのもそこまで大変ではないと思いますが、サンプルではAdmissionWebhook用のフレームワークである slok/kubewebhook を利用させていただきました。

github.com

まとめ

kube-apiserverに組み込まれるAdmission Controllerを新しく追加するのは非常に大変ですが、AdmissionWebhookによって簡単に色々なカスタマイズができそうです。