メインコンテンツにスキップ

Admission Webhook for K8s

1. Casbin K8s-Gatekeeperの概要とドキュメント

Casbin K8s-GateKeeperは、アクセス制御ツールとしてCasbinを統合したKubernetesのアドミッションウェブフックです。 Casbin K8s-GateKeeperを使用すると、コードを一切書かずに、Casbinモデルとポリシーの宣言的な設定のみで、K8sリソースに対する任意の操作を認証またはインターセプトする柔軟なルールを確立できます。これらはCasbin ACL(アクセス制御リスト)言語の一部です。

Casbin K8s-GateKeeperはCasbinコミュニティによって開発・維持されています。 このプロジェクトのリポジトリはこちらで利用できます:https://github.com/casbin/k8s-gatekeeper

0.1 シンプルな例

例えば、コードを一切書かずに、以下の設定行を使用してこの機能を実現できます:「特定のタグを持つイメージが任意のデプロイメントで使用されるのを禁止する」:

Model:

[request_definition]
r = obj

[policy_definition]
p = obj,eft

[policy_effect]
e = !some(where (p.eft == deny))

[matchers]
m = r.obj.Request.Namespace == "default" && r.obj.Request.Resource.Resource =="deployments" && \
contain(split(accessWithWildcard(${OBJECT}.Spec.Template.Spec.Containers , "*", "Image"),":",1) , p.obj)

And Policy:

p, "1.14.1",deny

これらは通常のCasbin ACL言語です。 すでにそれらについての章を読んでいれば、理解するのは非常に簡単です。

Casbin K8s-Gatekeeperには以下のような利点があります:

  • 使いやすい。 たくさんのコードを書くよりも、いくつかのACL行を書く方がはるかに良い。
  • 設定のホットアップデートを許可します。 設定を変更するためにプラグイン全体をシャットダウンする必要はありません。
  • 柔軟性があります。 任意のルールを任意のK8sリソースに作成でき、kubectl gatekeeperで探索できます。
  • 非常に複雑なK8sアドミッションウェブフックの実装を簡素化します。 K8sアドミッションウェブフックが何であるか、またはそれのためのコードをどのように書くかを知る必要はありません。 あなたが制約を置きたいリソースを知り、次にCasbin ACLを書くだけです。 皆さんがK8sが複雑であることを知っていますが、Casbin K8s-Gatekeeperを使用することで、時間を節約できます。
  • Casbinコミュニティによって維持されています。 このプラグインについて何か混乱していることや、これを試しているときに問題に遭遇した場合は、お気軽にお問い合わせください。

1.1 Casbin K8s-Gatekeeperはどのように動作するのか?

K8s-Gatekeeperは、管理者が望まないK8s上の任意の操作を防ぐために、任意のユーザー定義アクセス制御ルールを適用するCasbinを使用するK8sのアドミッションウェブフックです。

Casbinは、強力で効率的なオープンソースのアクセス制御ライブラリです。 さまざまなアクセス制御モデルに基づく認証の強制をサポートしています。 Casbinについての詳細は、概要を参照してください。

K8sのアドミッションウェブフックは、'アドミッションリクエスト'を受け取り、それらを処理するHTTPコールバックです。 特に、K8s-Gatekeeperは特別な種類のアドミッションウェブフックであり、'ValidatingAdmissionWebhook'と呼ばれ、このアドミッションリクエストを受け入れるか否かを決定することができます。 アドミッションリクエストについては、K8sの指定されたリソースに対する操作を記述したHTTPリクエストです(例えば、デプロイメントの作成/削除)。 アドミッションウェブフックについての詳細は、K8s公式ドキュメンテーションを参照してください。

1.2 それがどのように動作するかを示す例

例えば、誰かがnginxを実行するポッドを含むデプロイメントを作成したいと思ったとき(kubectlまたはK8sクライアントを使用して)、K8sはアドミッションリクエストを生成し、それは(YAML形式に変換された場合)次のようなものになる可能性があります:

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

このリクエストは、画像に示されているすべてのミドルウェアのプロセスを経ることになり、その中には私たちのK8s-Gatekeeperも含まれます。 K8s-Gatekeeperは、ユーザーが作成し維持しているK8sのetcdに格納されているすべてのCasbinエンフォーサーを検出することができます(kubectlまたは私たちが提供するGoクライアントを介して)。 各エンフォーサーには、CasbinモデルとCasbinポリシーが含まれています。 アドミッションリクエストは、一つ一つのエンフォーサーによって処理され、すべてのエンフォーサーを通過したリクエストのみがこのK8s-Gatekeeperによって受け入れられます。

(Casbinエンフォーサー、モデル、ポリシーが何であるか理解できない場合は、このドキュメントを参照してください:Get Started)。

例えば、何らかの理由で、管理者は'image 'nginx:1.14.1'の出現を禁止しつつ、'nginx:1.3.1'を許可したいと考えています。 以下のルールとポリシーを含むエンフォーサーを作成することができます(エンフォーサーの作成方法、これらのモデルとポリシーが何であるか、それらをどのように書くかについては、次の章で説明します)。

Model:

[request_definition]
r = obj

[policy_definition]
p = obj,eft

[policy_effect]
e = !some(where (p.eft == deny))

[matchers]
m = r.obj.Request.Namespace == "default" && r.obj.Request.Resource.Resource =="deployments" && \
access(r.obj.Request.Object.Object.Spec.Template.Spec.Containers , 0, "Image") == p.obj

Policy:

p, "nginx:1.13.1",allow
p, "nginx:1.14.1",deny

上記のモデルとポリシーを含むエンフォーサーを作成することで、前述のアドミッションリクエストはこのエンフォーサーによって拒否され、これはK8sがこのデプロイメントを作成しないことを意味します。

2 K8s-gatekeeperのインストール

K8s-gatekeeperのインストールには、External webhook、Internal webhook、Helmの3つの方法があります。

メモ

注意:これらの方法は、ユーザーがK8s-gatekeeperを試すためのものであり、セキュアではありません。 生産環境で使用する場合は、Chapter 5. 高度な設定を読み、インストール前に必要な変更を行ってください。

2.1 内部ウェブフック

2.1.1 ステップ1:イメージのビルド

内部ウェブフック方法では、ウェブフック自体がKubernetes内のサービスとして実装されます。 必要なサービスとデプロイメントを作成するためには、K8s-gatekeeperのイメージをビルドする必要があります。 以下のコマンドを実行することで、自分のイメージをビルドすることができます:

docker build --target webhook -t k8s-gatekeeper .

このコマンドは、'k8s-gatekeeper:latest'というローカルイメージを作成します。

メモ

注意:minikubeを使用している場合は、'docker build'を実行する前にeval $(minikube -p minikube docker-env)を実行してください。

2.1.2 ステップ2:K8s-gatekeeperのサービスとデプロイメントの設定

以下のコマンドを実行します:

kubectl apply -f config/rbac.yaml
kubectl apply -f config/webhook_deployment.yaml
kubectl apply -f config/webhook_internal.yaml

これにより、K8s-gatekeeperが実行を開始し、kubectl get podsを実行することでこれを確認することができます。

2.1.3 ステップ3:K8s-gatekeeperのCRDリソースのインストール

以下のコマンドを実行します:

kubectl apply -f config/auth.casbin.org_casbinmodels.yaml 
kubectl apply -f config/auth.casbin.org_casbinpolicies.yaml

2.2 External webhook

For the external webhook method, K8s-gatekeeper will be running outside of Kubernetes, and Kubernetes will access K8s-gatekeeper as it would access a regular website. Kubernetes has a mandatory requirement that the admission webhook must be HTTPS. For the purpose of trying out K8s-gatekeeper, we have provided a set of certificates and a private key (although this is not secure). If you prefer to use your own certificate, please refer to Chapter 5. Advanced settings for instructions on adjusting the certificate and private key.

The certificate we provide is issued for 'webhook.domain.local'. So, modify the host (e.g., /etc/hosts) and point 'webhook.domain.local' to the IP address on which K8s-gatekeeper is running.

Then execute the following command:

go mod tidy
go mod vendor
go run cmd/webhook/main.go
kubectl apply -f config/auth.casbin.org_casbinmodels.yaml
kubectl apply -f config/auth.casbin.org_casbinpolicies.yaml
kubectl apply -f config/webhook_external.yaml

2.3 Install K8s-gatekeeper via Helm

2.3.1 Step 1: Build the image

Please refer to Chapter 2.1.1.

2.3.2 Helm installation

Run the command helm install k8sgatekeeper ./k8sgatekeeper.

3. Try K8s-gatekeeper

3.1 Create Casbin Model and Policy

You have two methods to create a model and policy: via kubectl or via the go-client we provide.

3.1.1 Create/Update Casbin Model and Policy via kubectl

In K8s-gatekeeper, the Casbin model is stored in a CRD resource called 'CasbinModel'. Its definition is located in config/auth.casbin.org_casbinmodels.yaml.

There are examples in example/allowed_repo/model.yaml. Pay attention to the following fields:

  • metadata.name: the name of the model. This name MUST be the same as the name of the CasbinPolicy object related to this model, so that K8s-gatekeeper can pair them and create an enforcer.
  • spec.enable: if this field is set to "false", this model (as well as the CasbinPolicy object related to this model) will be ignored.
  • spec.modelText: a string that contains the model text of a Casbin model.

The Casbin Policy is stored in another CRD resource called 'CasbinPolicy', whose definition can be found in config/auth.casbin.org_casbinpolicies.yaml.

There are examples in example/allowed_repo/policy.yaml. Pay attention to the following fields:

  • metadata.name: ポリシーの名前。 この名前は、このポリシーに関連するCasbinModelオブジェクトの名前と同じでなければならず、K8s-gatekeeperがそれらをペアにしてエンフォーサーを作成できるようにするためです。
  • spec.policyItem: Casbinモデルのポリシーテキストを含む文字列。

自分自身のCasbinModelとCasbinPolicyファイルを作成した後、以下のコマンドを使用してそれらを適用します:

kubectl apply -f <filename>

CasbinModelとCasbinPolicyのペアが作成されると、K8s-gatekeeperは5秒以内にそれを検出できます。

3.1.2 私たちが提供するgo-clientを通じてCasbinモデルとポリシーを作成/更新する

K8sクラスタのノード上で直接コマンドを実行するシェルを使用するのが便利でない場合があることを理解しています。例えば、あなたが自社の自動クラウドプラットフォームを構築しているときなどです。 したがって、私たちはCasbinModelとCasbinPolicyを作成し、維持するためのgo-clientを開発しました。

go-clientライブラリはpkg/clientに位置しています。

client.goでは、クライアントを作成するための関数を提供しています。

func NewK8sGateKeeperClient(externalClient bool) (*K8sGateKeeperClient, error) 

externalClientパラメータは、K8s-gatekeeperがK8sクラスタ内で動作しているかどうかを決定します。

model.goでは、CasbinModelを作成、削除、修正するためのさまざまな関数を提供しています。 これらのインターフェースの使用方法はmodel_test.goで確認できます。

policy.goでは、CasbiPolicyを作成、削除、修正するためのさまざまな関数を提供しています。 これらのインターフェースの使用方法はpolicy_test.goで確認できます。

3.1.2 K8s-gatekeeperが動作するかどうかを試す

あなたがすでにexample/allowed_repoで正確なモデルとポリシーを作成したと仮定します。 次に、以下のコマンドを試してみてください:

kubectl apply -f example/allowed_repo/testcase/reject_1.yaml

K8sはこのリクエストを拒否し、このリクエストが拒否された理由がwebhookであったことを示すはずです。 しかし、example/allowed_repo/testcase/approve_2.yamlを適用しようとすると、それは受け入れられます。

4. K8s-gatekeeperでモデルとポリシーを書く方法

まず最初に、Casbinモデルとポリシーの基本的な文法に精通していることを確認してください。 もし精通していないなら、まずGet Startedセクションを読んでください。 この章では、あなたがすでにCasbinモデルとポリシーが何であるかを理解していると仮定しています。

4.1 モデルのリクエスト定義

K8s-gatekeeperがリクエストを認証するとき、入力は常にオブジェクトです: Admission RequestのGoオブジェクト。 これは、エンフォーサーは常にこのように使用されることを意味します:

ok, err := enforcer.Enforce(admission)

ここでadmissionは、K8sの公式go api"k8s.io/api/admission/v1"で定義されたAdmissionReviewオブジェクトです。 この構造体の定義はこのリポジトリで見つけることができます: https://github.com/kubernetes/api/blob/master/admission/v1/types.go。 詳細については、https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#webhook-request-and-responseも参照してください。

したがって、K8s-gatekeeperによって使用される任意のモデルについては、request_definitionの定義は常にこのようになるべきです:

    [request_definition]
r = obj

名前'obj'は必須ではなく、名前が[matchers]部分で使用される名前と一致していればよいです。

4.2 モデルのマッチャー

あなたはCasbinのABAC機能を使用してルールを記述することが想定されています。 しかし、Casbinに組み込まれた式評価器は、マップや配列(スライス)のインデックス付け、または配列の展開をサポートしていません。 したがって、K8s-gatekeeperはこれらの機能を実装するための拡張としてさまざまな'Casbin関数'を提供します。 それでもなお、これらの拡張機能で要求を満たせない場合は、自由に問題を起こすか、プルリクエストを作成してください。

Casbin関数に慣れていない場合は、詳細についてはFunctionを参照してください。

以下に拡張関数を示します:

4.2.1 拡張関数

4.2.1.1 access

Accessは、Casbinがマップや配列のインデックス付けをサポートしていない問題を解決するために使用されます。 この関数の使用例はexample/allowed_repo/model.yamlで示されています:

[matchers]
m = r.obj.Request.Namespace == "default" && r.obj.Request.Resource.Resource =="deployments" && \
access(r.obj.Request.Object.Object.Spec.Template.Spec.Containers , 0, "Image") == p.obj

このマッチャーでは、access(r.obj.Request.Object.Object.Spec.Template.Spec.Containers , 0, "Image")r.obj.Request.Object.Object.Spec.Template.Spec.Containers[0].Imageと同等で、ここでr.obj.Request.Object.Object.Spec.Template.Spec.Containersはスライスです。

Accessは、パラメータを持たず単一の値を返すシンプルな関数も呼び出すことができます。 この例はexample/container_resource_limit/model.yamlで示されています:

[matchers]
m = r.obj.Request.Namespace == "default" && r.obj.Request.Resource.Resource =="deployments" && \
parseFloat(access(r.obj.Request.Object.Object.Spec.Template.Spec.Containers , 0, "Resources","Limits","cpu","Value")) >= parseFloat(p.cpu) && \
parseFloat(access(r.obj.Request.Object.Object.Spec.Template.Spec.Containers , 0, "Resources","Limits","memory","Value")) >= parseFloat(p.memory)

このマッチャーでは、access(r.obj.Request.Object.Object.Spec.Template.Spec.Containers , 0, "Resources","Limits","cpu","Value")r.obj.Request.Object.Object.Spec.Template.Spec.Containers[0].Resources.Limits["cpu"].Value()と同等で、ここでr.obj.Request.Object.Object.Spec.Template.Spec.Containers[0].Resources.Limitsはマップであり、Value()はパラメータを持たず単一の値を返すシンプルな関数です。

4.2.1.2 accessWithWildcard

時々、配列のすべての要素が"aaa"というプレフィックスを持つような要求があるかもしれません。 しかし、Casbinはforループをサポートしていません。 accessWithWildcardと"map/slice expansion"機能を使用すると、このような要求を簡単に実装できます。

例えば、a.b.cが配列[aaa,bbb,ccc,ddd,eee]であると仮定すると、accessWithWildcard(a,"b","c","*")の結果はスライス[aaa,bbb,ccc,ddd,eee]になります。 ワイルドカード*を使用すると、スライスが展開されます。

同様に、ワイルドカードは複数回使用することができます。 例えば、accessWithWildcard(a,"b","c","*","*")の結果は[a.b.c[0][0], a.b.c[0][1], ..., a.b.c[1][0], a.b.c[1][1], ...]になります。

4.2.1.3 可変長引数をサポートする関数

Casbinの式評価器では、パラメータが配列の場合、それは自動的に可変長引数として展開されます。 この機能を利用して配列/スライス/マップの展開をサポートし、配列/スライスをパラメータとして受け入れるいくつかの関数も統合しました:

  • contain(): 複数のパラメータを受け入れ、任意のパラメータ(最後のパラメータを除く)が最後のパラメータと等しいかどうかを返します。
  • split(a,b,c...,sep,index): returns a slice that contains [splits(a,sep)[index], splits(b,sep)[index], splits(a,sep)[index], ...].
  • len(): returns the length of the variable-length argument.
  • matchRegex(a,b,c...,regex): returns whether all of the given parameters (a, b, c, ...) match the given regex.

Here is an example in example/disallowed_tag/model.yaml:

    [matchers]
m = r.obj.Request.Namespace == "default" && r.obj.Request.Resource.Resource =="deployments" && \
contain(split(accessWithWildcard(r.obj.Request.Object.Object.Spec.Template.Spec.Containers , "*", "Image"),":",1) , p.obj)

Assuming that accessWithWildcard(r.obj.Request.Object.Object.Spec.Template.Spec.Containers , "*", "Image") returns ["a:b", "c:d", "e:f", "g:h"], because splits supports variable-length arguments and performs the splits operation on each element, the element at index 1 will be selected and returned. Therefore, split(accessWithWildcard(r.obj.Request.Object.Object.Spec.Template.Spec.Containers , "*", "Image"),":",1) returns ["b","d","f","h"]. And contain(split(accessWithWildcard(r.obj.Request.Object.Object.Spec.Template.Spec.Containers , "*", "Image"),":",1) , p.obj) returns whether p.obj is contained in ["b","d","f","h"].

4.2.1.2 Type Conversion Functions

  • ParseFloat(): Parses an integer to a float (this is necessary because any number used in comparison must be converted into a float).
  • ToString(): Converts an object to a string. This object must have a basic type of string (for example, an object of type XXX when there is a statement type XXX string).
  • IsNil(): Returns whether the parameter is nil.

5. Advanced Settings

5.1 About Certificates

In Kubernetes (k8s), it is mandatory that a webhook should use HTTPS. There are two approaches to achieve this:

  • Use self-signed certificates (examples in this repository use this method)
  • Use a normal certificate

5.1.1 Self-signed certificates

Using a self-signed certificate means that the Certificate Authority (CA) issuing the certificate is not one of the well-known CAs. Therefore, you must let k8s know about this CA.

Currently, the example in this repository uses a self-made CA, whose private key and certificate are stored in config/certificate/ca.crt and config/certificate/ca.key respectively. The certificate for the webhook is config/certificate/server.crt, which is issued by the self-made CA. The domains of this certificate are "webhook.domain.local" (for external webhook) and "casbin-webhook-svc.default.svc" (for internal webhook).

Information about the CA is passed to k8s via webhook configuration files. Both config/webhook_external.yaml and config/webhook_internal.yaml have a field called "CABundle", which contains a base64 encoded string of the CA's certificate.

In case you need to change the certificate/domain (for example, if you want to put this webhook into another namespace of k8s while using an internal webhook, or if you want to change the domain while using an external webhook), the following procedures should be followed:

  1. Generate a new CA:

    • 偽のCAのための秘密鍵を生成します:

      openssl genrsa -des3 -out ca.key 2048
    • 秘密鍵のパスワード保護を解除します:

      openssl rsa -in ca.key -out ca.key
  2. Webhookサーバーのための秘密鍵を生成します:

    openssl genrsa -des3 -out server.key 2048
    openssl rsa -in server.key -out server.key
  3. 自己生成したCAを使用して、Webhookの証明書に署名します:

    • 一時的に使用するために、システムのopenssl設定ファイルをコピーします。 openssl version -aを実行することで設定ファイルの場所を確認できます。通常はopenssl.cnfと呼ばれます。

    • 設定ファイル内で:

      • [req]の段落を見つけて、次の行を追加します:req_extensions = v3_req

      • [v3_req]の段落を見つけて、次の行を追加します:subjectAltName = @alt_names

      • 次の行をファイルに追加します:

        [alt_names]
        DNS.2=<The domain you want>

        注:サービス名を変更することにした場合、'casbin-webhook-svc.default.svc'を自分のサービスの実際のサービス名に置き換えてください。

    • 変更した設定ファイルを使用して証明書リクエストファイルを生成します:

      openssl req -new -nodes -keyout server.key -out server.csr -config openssl.cnf
    • 自作のCAを使用してリクエストに応答し、証明書に署名します:

      openssl x509 -req -days 3650 -in server.csr -out server.crt -CA ca.crt -CAkey ca.key -CAcreateserial -extensions v3_req -extensions SAN -extfile openssl.cnf
  4. 'CABundle'フィールドを置き換えます:config/webhook_external.yamlconfig/webhook_internal.yamlの両方には"CABundle"というフィールドがあり、これはCAの証明書のbase64エンコードされた文字列を含んでいます。 このフィールドを新しい証明書で更新します。

  5. helmを使用している場合、同様の変更をhelmチャートに適用する必要があります。

5.1.2 合法的な証明書

合法的な証明書を使用する場合、これらの手順をすべて経る必要はありません。 config/webhook_external.yamlconfig/webhook_internal.yamlの"CABundle"フィールドを削除し、これらのファイル内のドメインを所有しているドメインに変更します。