TCE で Gatekeeper をサクッとやってみる


どうも！ @hirosat です。
本日は、[TUNA-JP Advent Calendar 2021](https://qiita.com/advent-calendar/2021/tuna-jp) の中で、まだ紹介されてない、[Gatekeeper](https://github.com/open-policy-agent/gatekeeper) をやろうかなと思って、エントリしました。

# Gatekeeper とは？
Gatekeeper を語るためには、その前にいくつか紹介しなければ行けない背景があります。

## K8s のアクセス制御の仕組みを知ってる？
(参考: [Kubernetes Documentation](https://kubernetes.io/docs/concepts/security/controlling-access/))

![k8s control](https://d33wubrfki0l68.cloudfront.net/673dbafd771491a080c02c6de3fdd41b09623c90/50100/images/docs/admin/access-control-overview.svg)
「K8sクラスタ のセキュリティをなんか強化したい！」と思った時には、必ずこのアクセス制御の仕組みを使うことになります。K8s クラスタへの全てのリクエストは、必ずこの3ステップで処理されます。

1. Authentication (AuthN。認証)
2. Authorization (AuthZ。認可)
3. Admission Control

「えっ？そんなの気にしたことないよ！」という人は、実はadmin権限を持つK8s contextでクラスタを利用しているパターンだと思います。ちゃんと制御したいなら、AuthNで `お前は誰？` を特定し、AuthZで `お前はxxする権限を持っているよ！` を管理していきます。（ちなみにTCEでは、`Pinniped` というOSSを導入することで、外部IDリソースと連携した認証/認可が可能になります。）

そこまでは分かりやすいと思いますが、では、Admission Controlとはなんでしょうか？

Admission Control は、その通過してきたリクエストに対し、`修正したり拒否したりする改変` を加えることができます。モジュール式の `Admissission Controller` によって、`様々なきめ細かい制御を、後付けで与える` ことが出来る訳です。以下は、その一例です。

- `DefaultStorageClass` : PV作成時に特に指定がない場合、特定のStorageClass設定を利用する指示を与える
- `AlwaysPullImages` : Pod作成時、毎回ImageをPullする設定を矯正させる
- `NamespaceExists` : リクエストされたNamespaceが存在しない場合は、リクエストを拒否
- `PodNodeSelector` : 特定のNamespaceで利用可能な対象ノードを制限

他にもいっぱいAdmission Controllerはあり、単なるリクエスト修正/拒否だけではなく、`あいまいなリクエストを誘導する`側面も、持ち合わせてます。

(参考: [Using Admission Controllers](https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers/))

## PSPが非推奨になったよ :scream:

(参考： [Kubernetes Blog](https://kubernetes.io/blog/2021/04/06/podsecuritypolicy-deprecation-past-present-and-future/#why-is-podsecuritypolicy-going-away))

`PodSecurityPolicy (PSP)` とは、クラスタ管理者がPod仕様のセキュリティに関する制御を行うための、`Admission Controller` の1つです。PodSecurityPolicyリソースをクラスタ内に作成すると、その定義内容の条件を満たしていないと、クラスタ内でPodが動作できなくなります。

それが、Kubernetes v1.21 から Deprecated(非推奨) という扱いに変更になりました。Kubernetes v1.25 で完全に削除される予定です。

では、何故非推奨になったのでしょうか？要は、PSP が「**かゆいところに手が届かない**」仕様のため、以下のような問題が起こりえました。

- 意図したよりも広い範囲の権限を誤って与えてしまう
- どのPSP設定が適用されているかの把握が困難
- 設定出来る内容が限られている
- 自分のPodが適用対象なのかが不明
  - dryrun や 監査モードがない (動かそうとして初めて気づく)

## OPA: PSP代替の最有力候補！
（参考: [Open Policy Agent](https://www.openpolicyagent.org/)）

PSPの代替となりそうなOSSはいくつかあるのですが、その中で`Open Policy Agent (OPA)` が、非常に筋が良さそうなツールとして注目されています。

OPA自体は、ポリシーを定義するためのフレームワークであり、K8sだけではなく、EnvoyやAPP等に対しても定義することができます。

rego言語という古くからある言語を用いることで、ポリシーに関するきめ細やかな制御を実現できます。宣言型なので、Podに限らず、様々なポリシーを定義できます。

## Gatekeeper: OPAのK8s実装
（参考: [Github](https://github.com/open-policy-agent/gatekeeper/)）

ここでついに、Gatekeeperの話となります！以下の特徴により、OPAをK8sで使用するために最適化されています。

- `constraint templates` と、 `constraints` という2つのCRDにより、拡張可能で柔軟なポリシーライブラリを実現
- Audit機能

# サクッとやってみる。
さて、前置きが長くなりましたが、早速試してみます。
> 前提条件：TCEがインストールされていて、TKCが建っていること。

ということで、TKCを作成して、K8sのcontextをそこに向けた直後の状態です。

## Gatekeeperのインストール

まずは、現在のTKC上で、plugin郡を使えるようにするオマジナイ。

``` shell
$ tanzu package repository add tce-repo \
>   --url projects.registry.vmware.com/tce/main:0.9.1 \
>   --namespace tanzu-package-repo-global
```

これにより、そのクラスタで利用可能なパッケージが一気に増えます。

``` shell
$ tanzu package available list
/ Retrieving available packages...
  NAME                                           DISPLAY-NAME        SHORT-DESCRIPTION
  cert-manager.community.tanzu.vmware.com        cert-manager        Certificate management
  contour.community.tanzu.vmware.com             Contour             An ingress controller
  external-dns.community.tanzu.vmware.com        external-dns        This package provides DNS synchronization functionality.
  fluent-bit.community.tanzu.vmware.com          fluent-bit          Fluent Bit is a fast Log Processor and Forwarder
  gatekeeper.community.tanzu.vmware.com          gatekeeper          policy management
  grafana.community.tanzu.vmware.com             grafana             Visualization and analytics software
  harbor.community.tanzu.vmware.com              Harbor              OCI Registry
  knative-serving.community.tanzu.vmware.com     knative-serving     Knative Serving builds on Kubernetes to support deploying and serving of applications and functions as serverless containers
  local-path-storage.community.tanzu.vmware.com  local-path-storage  This package provides local path node storage and primarily supports RWO AccessMode.
  multus-cni.community.tanzu.vmware.com          multus-cni          This package provides the ability for enabling attaching multiple network interfaces to pods in Kubernetes
  prometheus.community.tanzu.vmware.com          prometheus          A time series database for your metrics
  velero.community.tanzu.vmware.com              velero              Disaster recovery capabilities
```

この中で、`NAME`欄が重要になってきます。下記のようにうつと、対象のパッケージで利用可能なバージョンが分かります。

``` shell
$ tanzu package available list gatekeeper.community.tanzu.vmware.com
/ Retrieving package versions for gatekeeper.community.tanzu.vmware.com...
  NAME                                   VERSION  RELEASED-AT
  gatekeeper.community.tanzu.vmware.com  1.0.0
```
おっ、バージョンは`1.0.0` なのね。

おや？[TCEのドキュメント](https://tanzucommunityedition.io/docs/latest/package-readme-gatekeeper-3.7.0/)で紹介されているバージョンは、`3.7.0`だぞ？

どうも、[OSSのGatekeeper](https://github.com/open-policy-agent/gatekeeper)の現在の最新バージョンは`3.7.0` なのだが、TCEのレポジトリでは、別の粒度でバージョン管理されてるらしい。ややこしいな。。

ともかく、今必要な情報は、`1.0.0`の方だ。

次に、設定可能な変数の確認。

``` shell
$ tanzu package available get gatekeeper.community.tanzu.vmware.com/1.0.0 --values-schema
| Retrieving package details for gatekeeper.community.tanzu.vmware.com/1.0.0...
  KEY        DEFAULT            TYPE    DESCRIPTION
  namespace  gatekeeper-system  string  The namespace in which to deploy gatekeeper.
```
どうやら Gatekeeper では、`namespace`しかいじれないらしい。逆に言うと、インストール難易度は低いと言える。

せっかくなので、`gatekeeper`というnamespace に、入れてみる。さて、どうなることやら。

``` shell
$ tanzu package install gatekeeper --package-name gatekeeper.community.tanzu.vmware.com --version 1.0.0 --namespace gatekeeper
/ Installing package 'gatekeeper.community.tanzu.vmware.com'
| Getting namespace 'gatekeeper'
/ Getting package metadata for 'gatekeeper.community.tanzu.vmware.com'
| Creating service account 'gatekeeper-gatekeeper-sa'
| Creating cluster admin role 'gatekeeper-gatekeeper-cluster-role'
| Creating cluster role binding 'gatekeeper-gatekeeper-cluster-rolebinding'
- Creating package resource
| Package install status: Reconciling

 Added installed package 'gatekeeper' in namespace 'gatekeeper'
```
おっ、あっさり入った。

## リソースの確認
インストールされたリソースを探してみる。

``` shell
$ kubectl get all -A
NAMESPACE           NAME                                                         READY   STATUS    RESTARTS   AGE
gatekeeper-system   pod/gatekeeper-audit-5fd6d7bf7b-jtb9h                        1/1     Running   0          3m45s
gatekeeper-system   pod/gatekeeper-controller-manager-6b6bdd8df8-68fc6           1/1     Running   0          3m45s
(省略)

NAMESPACE           NAME                                 TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)                  AGE
gatekeeper-system   service/gatekeeper-webhook-service   ClusterIP   100.71.252.11    <none>        443/TCP                  3m45s
(省略)

NAMESPACE           NAME                                                    READY   UP-TO-DATE   AVAILABLE   AGE
gatekeeper-system   deployment.apps/gatekeeper-audit                        1/1     1            1           3m45s
gatekeeper-system   deployment.apps/gatekeeper-controller-manager           1/1     1            1           3m45s
(省略)

NAMESPACE           NAME                                                               DESIRED   CURRENT   READY   AGE
gatekeeper-system   replicaset.apps/gatekeeper-audit-5fd6d7bf7b                        1         1         1       3m45s
gatekeeper-system   replicaset.apps/gatekeeper-controller-manager-6b6bdd8df8           1         1         1       3m45s
(省略)

ubuntu@step01:~$ kubectl get secrets -A
NAMESPACE                   NAME                                             TYPE                                  DATA   AGE
gatekeeper-system           default-token-mcqpx                              kubernetes.io/service-account-token   3      5m16s
gatekeeper-system           gatekeeper-admin-token-c75v7                     kubernetes.io/service-account-token   3      5m15s
gatekeeper-system           gatekeeper-webhook-server-cert                   Opaque                                4      5m14s
gatekeeper                  default-token-llkfz                              kubernetes.io/service-account-token   3      5m26s
gatekeeper                  gatekeeper-gatekeeper-sa-token-d44w8             kubernetes.io/service-account-token   3      5m19s
(省略)
```
・・なるほど。まず、自分の指定したnamespaceは何であれ、`gatekeeper-system`という場所に、基本的なリソースがインストールされる訳ね。

そのリソースは主に、`gatekeeper-audit`と`gatekeeper-controller`という、2つのDeploymentが入る。

で、私が指定した`gatekeeper`namespaceには、、、tokenが入っているだけのようだ。あまり指定する意味はないかもしれない・・・orz

### (2021年12月12日 追記)

`--namespace` オプションで指定したものについては、上記の結果の通りとなるのだが、`--values-schema`オプションで確認したものは、`--values-file`で指定する必要があった。

例えば、以下のようなファイルを用意。

``` values.yaml
namespace: custom-namespace
```

その後、以下のように適用することで、CRDのインストール場所も変えられた、はず。。

``` shell
$ tanzu package install gatekeeper --package-name gatekeeper.community.tanzu.vmware.com --version 1.0.0 --namespace gatekeeper --values-file values.yaml
```

## 使ってみる
ちょうど、おあつらえ向きのサンプルが、[TCEのGatekeeperのページ](https://tanzucommunityedition.io/docs/latest/package-readme-gatekeeper-3.7.0/#usage-example)に掲載されているので、これを試してみる。

### 1. Constraint Templateの適用

上記のページに掲載している雛形をyamlに保存して適用してみた。

``` shell
$ vi ct.yaml
(↓↓ここからコピー)
apiVersion: templates.gatekeeper.sh/v1beta1
kind: ConstraintTemplate
metadata:
  name: k8srequiredlabels
  annotations:
    description: Requires all resources to contain a specified label with a value
      matching a provided regular expression.
spec:
  crd:
    spec:
      names:
        kind: K8sRequiredLabels
      validation:
        # Schema for the `parameters` field
        openAPIV3Schema:
          properties:
            message:
              type: string
            labels:
              type: array
              items:
                type: object
                properties:
                  key:
                    type: string
                  allowedRegex:
                    type: string
  targets:
    - target: admission.k8s.gatekeeper.sh
      rego: |
        package k8srequiredlabels
        get_message(parameters, _default) = msg {
          not parameters.message
          msg := _default
        }
        get_message(parameters, _default) = msg {
          msg := parameters.message
        }
        violation[{"msg": msg, "details": {"missing_labels": missing}}] {
          provided := {label | input.review.object.metadata.labels[label]}
          required := {label | label := input.parameters.labels[_].key}
          missing := required - provided
          count(missing) > 0
          def_msg := sprintf("you must provide labels: %v", [missing])
          msg := get_message(input.parameters, def_msg)
        }
        violation[{"msg": msg}] {
          value := input.review.object.metadata.labels[key]
          expected := input.parameters.labels[_]
          expected.key == key
          # do not match if allowedRegex is not defined, or is an empty string
          expected.allowedRegex != ""
          not re_match(expected.allowedRegex, value)
          def_msg := sprintf("Label <%v: %v> does not satisfy allowed regex: %v", [key, value, expected.allowedRegex])
          msg := get_message(input.parameters, def_msg)
        }
(↑↑ここまでコピー)

$ kubectl apply -f ct.yaml
```
今適用したものは、制約の雛形ファイルなので、これ単体を宛てただけでは何も起こりません。

この中で注目すべきは、`rego:`と書かれている行より下の部分です。ここには、[Rego言語](https://www.openpolicyagent.org/docs/latest/#rego)と呼ばれるOepn Poliicy Agentで採用されたセキュリティルールを記述するための言語であり、従来のK8sのAdmission Controlより、木目細かいルールが書けるようになります。

ちなみにここに書かれているルール内容を一部紹介すると、labelのkeyに関する`required`と`provided`を比較して、条件がマッチないと、ルール違反！みたいな記載をしています。

### 2. Constraintsの適用

次は、先ほどの雛形に関する、実際の条件を適用します。

``` shell
$ vi const.yaml
(↓↓ここからコピー)
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequiredLabels
metadata:
  name: all-must-have-owner
spec:
  match:
    kinds:
      - apiGroups: [""]
        kinds: ["Namespace"]
  parameters:
    message: "All namespaces must have an `owner` label"
    labels:
      - key: owner
(↑↑ここまでコピー)

$ kubectl apply -f const.yaml
```

これを適用することにより、`Namespace`に`owner`というキーがないとルール違反！というポリシーが発動されます。

### 3. 実際に怒られてみる

``` shell
$ kubectl create ns test
Error from server ([denied by all-must-have-owner] All namespaces must have an `owner` label): admission webhook "validation.gatekeeper.sh" denied the request: [denied by all-must-have-owner] All namespaces must have an `owner` label
```

よしよし、ちゃんと怒られたｗ

`test`namespaceも作られてなかったので、Gatekeeperが正しく機能していることが分かります。

### 4. ルールに沿ってnamespaceを作ってみる
``` shell
$ vi ns.yaml
(↓↓ここからコピー)
apiVersion: v1
kind: Namespace
metadata:
  name: test
  labels:
    owner: hirosat
(↑↑ここまでコピー)

$ kubectl apply -f ns.yaml
$ kubectl get ns test --show-labels
NAME   STATUS   AGE   LABELS
test   Active   78s   kubernetes.io/metadata.name=test,owner=hirosat
```

ルールは`owner`というkeyに関するものだったので、valueは元のサンプルから、`hirosat`に変えてみましたｗ

ちゃんと怒られずにNamespaceを作ることが出来ました。
`test`namespaceは作成され、`owner=hirosat`のLabelが割り当てられていることが分かります。

# 最後に
[OPA Gatekeeper Library](https://github.com/open-policy-agent/gatekeeper-library) には、様々なルールの雛形が置いてあるので、活用してみると良いでしょう。

それではよいセキュリティライフを！