Config Connectorを使用し作成したGoogle Cloudリソースを監査するOpen Policy Agent Gatekeeperポリシーの書き方

#はじめに

Config Connectorを使用し作成したGoogle Cloudリソースを監査するOpen Policy Agent Gatekeeperポリシーの書き方について記載します。
以下のチュートリアルが参考になります。
https://cloud.google.com/solutions/policy-compliant-resources

Config ConnectorとOpen Policy Agent Gatekeeperを組み合わせると、GCPリソースに対して制約を独自で実装することが可能です。GCPの組織ポリシーでも制約を実装することができますが、組織ポリシーはユーザが独自に作成したり、カスタマイズすることができません。

Config Connectorは、GKEクラスタが前提となります。サービスをホストするComputeサービスとしてGKEを選択していない場合も、GCPリソースをIaCを使って管理するため仕組みとして、GKEクラスタを構築しConfig Connectorを使という選択肢もあります。Config Connectorは前述の通りOpen Policy Agent Gatekeeperを使ってセキュリティルールを設けることや、Kubernetesのリソース同様にgitopsによる管理が可能になるといったメリットがあります。

#構成要素の説明

Open Policy Agent Gatekeeperポリシーの書き方を説明する前に、必要となる構成要素について記載したいと思います。具体的には以下の機能を使用します。

・Config Connector
・Open Policy Agent (OPA) Gatekeeper （または、 Anthos Config Management Policy Controller) 
・Constraint templateとConstraint（OPA Gatekeeperで制約を記述する時に使用します）
・Rego（Constraint templateで制約を記述するための言語）

これらの関係性のイメージは以下になります。1つずつ説明していきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/78184/1ab82ebd-821c-1d12-b855-de6d1fe24863.png)

##Config Connector
Config Connectorは、SpannerやGCSなどのGoogle CloudリソースをKubernetesのリリースとして作成・管理出来る機能です。
https://cloud.google.com/config-connector/docs/overview

##Open Policy Agent Gatekeeper 
Open Policy Agent Gatekeeperは、kubernetesのリソースに対してセキュリティ、規制に関するポリシーを定義し、監査を行うことが出来る機能です。
https://github.com/open-policy-agent/gatekeeper

Open Policy Agent Gatekeeperは、OSSとして開発されており、GKEでは、Open Policy Agent GatekeeperのManaged機能であるAnthos Config Management Policy Controllerが使用できます。
https://cloud.google.com/anthos-config-management/docs/concepts/policy-controller

###Constraint
Constraintは、作成するGoogle Cloud リソースに対するルールを設定します。
https://github.com/open-policy-agent/frameworks/tree/master/constraint#what-is-a-constraint

###Constraint templates
Constraint templatesを作成することによって、新規に制約(Constraint)を定義することができます。
https://github.com/open-policy-agent/frameworks/tree/master/constraint#what-is-a-constraint-template
制約のロジックは、Regoと呼ばれる言語を使用して記述します。

####Rego
Open Policy Agent Gatekeeperでセキュリティポリシーを記述する方法として、「Rego」というプログラミング言語を使用します。
https://www.openpolicyagent.org/docs/latest/policy-language/

よく使用されるポリシーのサンプルは以下で確認できます。
https://github.com/open-policy-agent/gatekeeper-library

Config Connectorで作成したGoogle Cloud リソースに対するポリシーは、現状まだ多くないため、この記事ではGoogle Cloudリソースに対するポリシー記述方法について説明します。

#Config Connectorの具体例
##試したバージョン
GKE version: 1.19.7-gke.1500

Config connectorをインストールすることで、GCPのリソースをCustom Resourceとして作成、管理することが可能です。
Custom Resourceの定義はGithubレポジトリ（https://github.com/GoogleCloudPlatform/k8s-config-connector/tree/master/crds）を参照するか、describeコマンドで確認することが可能です。

```
kubectl describe crd storagebuckets.storage.cnrm.cloud.google.com
```

仕組みとしては、任意のGCPリソースを作成・管理するために、Config Connectorのnrm-controller-manager PodにWorkload Identity経由でGCPリソースに対して権限を付与することで、GCPリソースをCustom Resourceとして記述しApplyすることで、任意のGCPリソースを作成するとができます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/78184/33a304af-83c6-2e6d-affe-50812d4ddbcc.png)

GCPリソースを作成するプロジェクトの指定方法は、Custom Resourceを作成するnamespaceにAnnotationとしてGCPプロジェクトのIDを設定します。

```
$ k describe  ns tutorial
Name:         tutorial
Labels:       <none>
Annotations:  cnrm.cloud.google.com/project-id: <PROJECT_ID>
Status:       Active

Resource Quotas
 Name:                              gke-resource-quotas
 Resource                           Used  Hard
 --------                           ---   ---
 count/ingresses.extensions         0     100
 count/ingresses.networking.k8s.io  0     100
 count/jobs.batch                   0     5k
 pods                               0     1500
 services                           0     500

No LimitRange resource.
```

StorageBucketを作成するためのCustom Resourceの例としては以下になります。

```
apiVersion: storage.cnrm.cloud.google.com/v1beta1
kind: StorageBucket
metadata:
  name: my-bucket
spec:
  location: us-east1
```

#Open Policy Agent Gatekeeperの具体例
##試したバージョン
GKE version: 1.19.7-gke.1500
GATEKEEPER_VERSION=v3.1.3

##Open Policy Agent Gatekeeperで作成するもの
クラスタのコンプライアンスをConstraint(制約)と呼ばれるポリシーに適用します。制約のロジックは、Constraint Template(制約テンプレート)で定義します。

###チュートリアルにおけるConstraintの例

```
kind: GCPStorageLocationConstraintV1
metadata:
  name: singapore-and-jakarta-only
spec:
  enforcementAction: deny
  match:
    kinds:
    - apiGroups:
      - storage.cnrm.cloud.google.com
      kinds:
      - StorageBucket
    namespaces:
    - NAMESPACE
  parameters:
    locations:
    - asia-southeast1
    - asia-southeast2
    exemptions:
    - ${GOOGLE_CLOUD_PROJECT}_cloudbuild
```

Constraintでは、制約として拒否したい条件値を定義します。
上記の例は、asia-southeast1とasia-southeast2以外のリージョンを禁止するポリシーになります。
exemptionsは、制約の確認を除外するバケットを指定しています。

###チュートリアルにおける制約テンプレートの例

```
apiVersion: templates.gatekeeper.sh/v1beta1
kind: ConstraintTemplate
metadata:
  name: gcpstoragelocationconstraintv1
spec:
  crd:
    spec:
      names:
        kind: GCPStorageLocationConstraintV1
      validation:
        openAPIV3Schema:
          properties:
            locations:
              type: array
              items:
                type: string
            exemptions:
              type: array
              items:
                type: string
  targets:
  - target: admission.k8s.gatekeeper.sh
    rego: |
      package gcpstoragelocationconstraintv1

      allowedLocation(reviewLocation) {
          locations := input.parameters.locations
          satisfied := [ good | location = locations[_]
                                good = lower(location) == lower(reviewLocation)]
          any(satisfied)
      }

      exempt(reviewName) {
          input.parameters.exemptions[_] == reviewName
      }

      violation[{"msg": msg}] {
          bucketName := input.review.object.metadata.name
          bucketLocation := input.review.object.spec.location
          not allowedLocation(bucketLocation)
          not exempt(bucketName)
          msg := sprintf("Cloud Storage bucket <%v> uses a disallowed location <%v>, allowed locations are %v", [bucketName, bucketLocation, input.parameters.locations])
      }

      violation[{"msg": msg}] {
          not input.parameters.locations
          bucketName := input.review.object.metadata.name
          msg := sprintf("No permitted locations provided in constraint for Cloud Storage bucket <%v>", [bucketName])
      }
```

crd.spec.crdで制約を実施する値を定義しています。また「rego: |」以下で、制約のロジックを記載します。
Regoは、昔からあったDatalogというプログラミング言語を参考に開発されたクエリ言語です。詳細な解説は以下にあります。
https://www.openpolicyagent.org/docs/latest/policy-language/

#制約の書き方
ここから具体的にConfig connectorに対する制約の記載方法について記載します。

まずは、制約を適用したいGCPリソースのCRDを確認します。制約テンプレートと制約でどの値を定義する必要があるのかを理解するためです。
今回は、GCSのAccesscontrolについて制約を記載していきたいと思います。

###書き方の流れ
1.任意のGCPリソースにおいて何を禁止したいかを決める
2.任意のGCPリソースのCRDを確認し、禁止したい項目を把握する
3.制約を記述する
4.制約テンプレートを記述する
*3と4はどちらが先でもよい

##StorageBucketAccessControlの例

###1.任意のGCPリソースにおいて何を禁止したいかを決める
本記事では、例として「Public bucketの作成を禁止」してみたいと思います。
具体的に言うと、AllUsersに対してRead権限が付与されるのを抑止する必要があります。
次に、StorageBucketAccessControlのCRDにおいて、AllUsersに対してRead権限を付与する記述箇所を確認します。

###2.任意のGCPリソースのCRDを確認し、禁止したい項目を把握する

以下が、StorageBucketAccessControlのCRDです。
https://github.com/GoogleCloudPlatform/k8s-config-connector/blob/master/crds/storage_v1beta1_storagebucketaccesscontrol.yaml

認証を設定する項目は「entity」になります。

```
entity:
  description: |-
    Immutable. The entity holding the permission, in one of the following forms:
      user-userId
      user-email
      group-groupId
      group-email
      domain-domain
      project-team-projectId
      allUsers
      allAuthenticatedUsers
    Examples:
      The user liz@example.com would be user-liz@example.com.
      The group example@googlegroups.com would be
      group-example@googlegroups.com.
      To refer to all members of the Google Apps for Business domain
      example.com, the entity would be domain-example.com.
  type: string
```

その他、Constraintに必要になる値を確認します。具体的には、CRDを特定するための値である、apiGroupsとkindです。

apiGroups

```
  group: storage.cnrm.cloud.google.com
```

kind

```
    kind: StorageBucketAccessControl
```

具体的な定義の例

```
apiVersion: storage.cnrm.cloud.google.com/v1beta1
kind: StorageBucketAccessControl
metadata:
  name: test-bucket-acl
spec:
  bucketRef:
    name: test-bucket
  entity: allUsers
  role: READER

```

###3.制約を記述する

Constraintを記述する対象を定義します。kindは、Constraint Temaplateで定義するCRDの名称になります。
spec.enforcementActionを指定します。
検証する対象としては、spec.match.kindsで制約をかけたいGCPリソースを指定します。
今回の場合は、以下になります。

spec.match.kinds.apiGroups: storage.cnrm.cloud.google.com
kinds: StorageBucketAccessControl

さらに、parametersとして制約をかけたい値を指定します。
spec.parameters.entityにallUsersを指定します。

```
kind: GCPStorageAclConstraintV1
metadata:
  name: disallow-public-bucket
spec:
  enforcementAction: deny
  match:
    kinds:
    - apiGroups:
      - storage.cnrm.cloud.google.com
      kinds:
      - StorageBucketAccessControl
    namespaces:
    - tutorial
  parameters:
    entity:
    - allUsers
```

###4.Constraint templatesを記述する

Constraint templatesに制約のロジックを記述しています。
決めないといけないこととしては、Constraint templatesのCRDのkind名です。
例えば、GCPStorageAclConstraintV1のような形で指定します。
こちらは、Kubernetesのお作法にならってCamel caseで記述します。

以下が、Constraint templateの抜粋になります。

```
apiVersion: templates.gatekeeper.sh/v1beta1
kind: ConstraintTemplate
metadata:
  name: gcpstorageaclconstraintv1
spec:
  crd:
    spec:
      names:
        kind: GCPStorageAclConstraintV1
  targets:
  - target: admission.k8s.gatekeeper.sh
    rego: |
      package gcpstorageaclconstraintv1
```

disallowPublicBucketやviolationが、ルールになります。
disallowPublicBucketでは、Constraintで指定した制約とCRDと作成されるGCPリソースの値を比較して条件を満たしているか確認しています。

violationは、上記の比較結果で制約に違反している場合、エラーメッセージを出力します。制約に違反している場合は、GCPのリソース自体作成されません。

```
  targets:
  - target: admission.k8s.gatekeeper.sh
    rego: |
      package gcpstorageaclconstraintv1

      disallowPublicBucket(reviewEntity) {
          entities := input.parameters.entity
          satisfied := [ good | entity = entities[_]
                                good = lower(entity) == lower(reviewEntity)]
          any(satisfied)
      }

      violation[{"msg": msg}] {
          bucketName := input.review.object.metadata.name
          entity := input.review.object.spec.entity
          disallowPublicBucket(entity)
          msg := sprintf("Violationed bucketname is <%v>, entity is <%v>", [bucketName, entity])
      }

```

disallowPublicBucketで、Constraintで指定したentityとCustom resourceで指定したentityを比較しています。
具体的には、Custom resourceで指定したentityが、配列としてentities[_]に代入されて、配列の数分allUsersと比較されます。
Custom resourceで指定したentityの値がallUsersの場合goodにtrueが代入され、すべての配列の値が検証され、その結果が配列として、satisfiedに代入されます。
any(array_or_set)は、Regoの組み込み関数で引数のarray_or_setに値が入っていればTrueを返します。
https://www.openpolicyagent.org/docs/v0.12.2/language-reference/#aggregates
そのため、any(satisfied)は、Custom resourceで指定したentityにallUsersが含まれている場合、　trueになります。


```
      disallowPublicBucket(reviewEntity) {
          entities := input.parameters.entity
          satisfied := [ good | entity = entities[_]
                                good = lower(entity) == lower(reviewEntity)]
          any(satisfied)
      }
```

violationでdisallowPublicBucketを呼んで、allUsersが指定されているかを確認しています。allUsersが含まれている場合は、式がTrueになるので、violationが発生します。

```
      violation[{"msg": msg}] {
          bucketName := input.review.object.metadata.name
          entity := input.review.object.spec.entity
          disallowPublicBucket(entity)
          msg := sprintf("Violationed bucketname is <%v>, entity is <%v>", [bucketName, entity])
      }
```


##各リソースごとの関係性

青色でマスクした値が、Constrantで指定した禁止したい値で、
赤色でマスクした値が、CRDで指定した検証したい値になります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/78184/1b5b9809-afba-c9ae-ca0b-aa0655087c76.png)


Constraint templateでは、Constaraintで指定した値を「input.parameters.entity」として使用可能です。「input.parameters.entity」は、spec.crd.spec.violation.openAPIV3Schema.proparies配下で宣言しています。

Constraint templateでは、CRDで指定した値を「input.review.object.spec.entity」として使用することがで関数の引数としてdisallowPublicBucketに渡しています。

#実機確認
ここまで内容を実際に試してみたいと思います。

##CRDを作成

```
$ cat bucket_acl.yaml
apiVersion: storage.cnrm.cloud.google.com/v1beta1
kind: StorageBucket
metadata:
  annotations:
    cnrm.cloud.google.com/project-id : kcc-opa
  name: config-connector-bucket
  namespace: tutorial
spec:
  lifecycleRule:
    - action:
        type: Delete
      condition:
        age: 7

---
apiVersion: storage.cnrm.cloud.google.com/v1beta1
kind: StorageBucketAccessControl
metadata:
  name: storage-acl
  namespace: tutorial
spec:
  bucketRef:
    name: config-connector-bucket
  entity: allUsers
  role: READER
```

```
$ k apply -f bucket_acl.yaml
storagebucket.storage.cnrm.cloud.google.com/config-connector-bucket created
storagebucketaccesscontrol.storage.cnrm.cloud.google.com/storage-acl created
```

```
$ gsutil ls
gs://config-connector-bucket/
```

```
$ gsutil acl get gs://config-connector-bucket                                          
[
  {
    "entity": "allUsers",
    "role": "READER"
  }
]
```

=>作成成功

一旦削除します。

```
$ k delete -f bucket_acl.yaml 
storagebucket.storage.cnrm.cloud.google.com "config-connector-bucket" deleted
storagebucketaccesscontrol.storage.cnrm.cloud.google.com "storage-acl" deleted
```

##Constraint template作成

```
$ cat acl-template.yaml
apiVersion: templates.gatekeeper.sh/v1beta1
kind: ConstraintTemplate
metadata:
  name: gcpstorageaclconstraintv1
spec:
  crd:
    spec:
      names:
        kind: GCPStorageAclConstraintV1
      validation:
        openAPIV3Schema:
          properties:
            entity:
              type: array
              items:
                type: string
  targets:
  - target: admission.k8s.gatekeeper.sh
    rego: |
      package gcpstorageaclconstraintv1

      disallowPublicBucket(reviewEntity) {
          entities := input.parameters.entity
          satisfied := [ good | entity = entities[_]
                                good = lower(entity) == lower(reviewEntity)]
          any(satisfied)
      }

      violation[{"msg": msg}] {
          bucketName := input.review.object.metadata.name
          entity := input.review.object.spec.entity
          disallowPublicBucket(entity)
          msg := sprintf("Violationed bucketname is <%v>, entity is <%v>", [bucketName, entity])
      }
```

```
$ k apply -f acl-template.yaml
constrainttemplate.templates.gatekeeper.sh/gcpstorageaclconstraintv1 created
```

```
$ k describe constrainttemplate.templates.gatekeeper.sh/gcpstorageaclconstraintv1 -n tutorial
Name:         gcpstorageaclconstraintv1
Namespace:
Labels:       <none>
Annotations:  <none>

略
Status:
  By Pod:
    Id:                   gatekeeper-audit-54b5f86d57-kfr42
    Observed Generation:  1
    Operations:
略
```
変数の指定に間違いがある場合、Status以下にエラーメッセージが出力されます。

##Constraint作成

```
$ cat acl-contraint.yaml
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: GCPStorageAclConstraintV1
metadata:
  name: disallow-public-bucket
spec:
  enforcementAction: deny
  match:
    kinds:
    - apiGroups:
      - storage.cnrm.cloud.google.com
      kinds:
      - StorageBucketAccessControl
    namespaces:
    - tutorial
  parameters:
    entity:
    - allUsers
```

```
$ k apply -f acl-contraint.yaml
gcpstorageaclconstraintv1.constraints.gatekeeper.sh/disallow-public-bucket created
```

##許可されない値でCRDを作成
先ほどと同じACLを作成してみます。

```
$ k apply -f bucket_acl.yaml
storagebucket.storage.cnrm.cloud.google.com/config-connector-bucket created
Error from server ([denied by disallow-public-bucket] Violationed bucketname is <storage-acl>, entity is <allUsers>): error when creating "bucket_acl.yaml": admission webhook "validation.gatekeeper.sh" denied the request: [denied by disallow-public-bucket] Violationed bucketname is <storage-acl>, entity is <allUsers>
```

allUsersが指定されているため、作成が拒否されます。

```
$ gsutil acl get gs://config-connector-bucket |grep allUsers
$
```

実際に作成もされていません。

#エラー時
k describe constrainttemplate.templates.gatekeeper.sh/gcpstorageaclconstraintv1で確認すると、記述ミスが有る箇所がStatus以下に表示されます。

```
Status:
  By Pod:
    Errors:
      Code:               ingest_error
      Message:            Could not ingest Rego: 1 error occurred: __modset_templates["admission.k8s.gatekeeper.sh"]["GCPStorageAclConstraintV1"]_idx_0:20: rego_type_error: undefined function allowedLocation
    Id:                   gatekeeper-audit-576f6d6f8d-wgmcp
    Observed Generation:  3
    Operations:
      audit
      status
```

#テストの書き方
opaコマンドを使ってテスト方法について別の記事で記載したいと思います。あとデバッグ方法も。
https://www.openpolicyagent.org/docs/latest/policy-testing/

#参考資料
チュートリアル
https://cloud.google.com/solutions/policy-compliant-resources

GCPリソースのCRD
https://github.com/GoogleCloudPlatform/k8s-config-connector/tree/master/crds

OPAのライブラリ
https://github.com/open-policy-agent/gatekeeper

Regoの説明
https://www.openpolicyagent.org/docs/latest/policy-language/