Open Policy Agent (OPA) でKubernetesにポリシーを強制する

# 1. Open Policy Agent (以下OPA)とは

Open Policy Agent(以下OPA)というのは、**ポリシーエンジン**です。
ポリシーエンジンは、種々のリクエストに対して、ポリシーを守ることを強制します。

![opa2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/96277/b207ffc3-8058-73ae-e106-b266cf59346c.png)

OPAは、リクエストが届いたときに、**ポリシー（会社のルール）**と**データ(会社の組織構造など)**を読み込み、**リクエストの中身を精査**し、**OK/NGなどの返答**を返します。

効果としては、複雑なリクエストやルールを解析して自動で守らせることができます。
OPAを使いこなすことで、会社や部署などの組織の決めたルールからの逸脱を防ぐことができます。

強制の仕方は２つあります。

- OK/NG判定を下し、NG(不正)なリクエストを停止する (Validationという)
- 不足している情報を自動で作成し追加する (Mutationという[→詳細は次回](https://qiita.com/Hiroyuki_OSAKI/items/666c752eac5b2caf7426))

![opa1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/96277/1a31ec5a-5846-e62a-5134-07dd58dea4d9.png)

他にも以下のような特徴があります。

- Kubernetes以外にも、DockerやSSHの制御、Terraform, Envoyなどに適用できます。
- 2020/3現在v0.17.2が公開されています。
- REGO言語という、少しGo言語に似た、でも実行方法が異なる言語を使っています。

# 2. 今回の目標

![e.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/96277/9da7e606-c8cd-cca8-7f84-1ca13370f63e.jpeg)

以下を目標として、検証を進めます。

- Open Policy AgentをKubernetesに適用します。
- Open Policy Agent向けの最低限のポリシーを[チュートリアル](https://www.openpolicyagent.org/docs/latest/kubernetes-tutorial/)に準ずる形で作成します。
- ポリシーが機能しているか検証します。

# 3. OPAを試す

まずはOPAを実行する以下の環境を用意します。

- Kubernetes (kindで用意する方法は[こちら](https://qiita.com/Hiroyuki_OSAKI/items/2395e6bbb98856df12f3))
- kubectl, helm

Open Policy AgentをKubernetesに組み込むためのパッケージには、以下２つあります。


1. [kube-mgmt](https://github.com/open-policy-agent/kube-mgmt)
   - Open Policy Agentの判断をWebhookでKubernetesに伝える。
   - ポリシーはConfigMap形式でregoファイルを登録する必要がある。
   - Helmでインストール可能。
2. [Gatekeeper](https://github.com/open-policy-agent/gatekeeper) 
   - 比較的新しい
   - Webhookの仕掛けは上記と同じだが、ポリシーやそのテンプレートをすべて独自のCRDで管理できる。
   - 2020年2月現在、まだValidatingにしか対応しておらず、Mutatingは未対応。

今回はHelmで手軽にインストールできてMutatingに対応しているkube-mgmtを採用します。

使用するHelmパッケージは、以下のレポジトリに公開されています。

[helm/charts](https://github.com/helm/charts/tree/master/stable/opa)

### 3-1. OPAをKubernetesにインストールする

まず、OPAを起動させる`Namespace`を作ります。名前を`opa`とします。

```sh
kubectl create namespace opa
```

次に、OPA起動用のHelm設定ファイルを作ります。

```helm-values.yaml
# 今回はMutating型にする
admissionControllerKind: MutatingWebhookConfiguration
# opaには、既存のポリシーのファイルパスなどを書くが、今回は新しく作るのでnull
opa: null
# mgmtには、新しいポリシーをConfigMapから読み込むかどうかを設定する
mgmt:
  configmapPolicies:
    enabled: true
    namespaces: [opa, opa-example]
    requireLabel: true
  replicate:
    cluster:
    - v1/namespaces
    namespace:
    - extensions/v1beta1/ingresses
# rbacにはOPAが必要とするKubernetes上の権限を設定する
rbac:
  create: true
  rules:
    cluster:
    - apiGroups:
        - "*"
      resources:
        - configmaps
      verbs:
        - get
        - list
        - watch
        - patch
        - update
    - apiGroups:
      - "*"
      resources:
      - namespaces
      - ingresses
      verbs:
      - get
      - list
      - watch
sar:
  enabled: true
authz:
  enabled: false
```

この設定を用いて、OPAを起動します。

```sh
helm repo add open-policy-agent https://open-policy-agent.github.io/kube-mgmt/charts
helm repo up
helm install opa open-policy-agent/opa -f helm-values.yaml --namespace opa
```

### 3-2. ポリシーを作成する

KubernetesにOPAを適用する構成を図で示すとこうなります。

![opa3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/96277/31a6f7b7-3549-0dc4-4930-94032fe35b47.png)

今回は、Ingressの追加のリクエストをOPAでポリシーチェックすることとします。

この絵の通り、今回のKubernetesとOPAの構成では、

- リクエストはKubernetesに対する`Ingress`リソース追加のリクエストです。
- dat`の部分もKubernetes(etcd)自身が持つ構成情報です。
- policyには、デフォルトのポリシー`main.rego`と、`Ingress`に関するリクエストだけに適用するポリシー`ingress-whitelist.rego`を配置します。

今回配置するポリシーを見ていきます。

1つ目のポリシー`main.rego`は、**「基本的にリクエストを許可する」**よう指定します。

<details><summary>main.rego (クリックで開く) </summary><div>

```main.rego
package system
import data.kubernetes.admission
main = {
        "apiVersion": "admission.k8s.io/v1beta1",
        "kind": "AdmissionReview",
        "response": response,
    }
default response = {"allowed": true}
response = {
    "allowed": false,
    "status": {
        "reason": reason,
    },
} {
    reason = concat(", ", admission.deny)
    reason != ""
}
```
</div>
</details>

次に、2つ目のポリシー`ingress-whitelist.rego`には、**`Ingress`の内容が`Namespace`の`annotations`内で許されたホスト名だけを許可し、それ以外を禁止する**ポリシーを指定します。

<details><summary>ingress-whitelist.rego (クリックで開く) </summary><div>

```ingress-whitelist.rego
package kubernetes.admission
import data.kubernetes.namespaces

operations = {"CREATE", "UPDATE"}

deny[msg] {
  input.request.kind.kind == "Ingress"
  operations[input.request.operation]
  host := input.request.object.spec.rules[_].host
  not fqdn_matches_any(host, valid_ingress_hosts)
  msg := sprintf("invalid ingress host %q", [host])
}

valid_ingress_hosts = {host |
  whitelist := namespaces[input.request.namespace].metadata.annotations["ingress-whitelist"]
  hosts := split(whitelist, ",")
  host := hosts[_]
}

fqdn_matches_any(str, patterns) {
  fqdn_matches(str, patterns[_])
}

fqdn_matches(str, pattern) {
  pattern_parts := split(pattern, ".")
  pattern_parts[0] == "*"
  str_parts := split(str, ".")
  n_pattern_parts := count(pattern_parts)
  n_str_parts := count(str_parts)
  suffix := trim(pattern, "*.")
  endswith(str, suffix)
}

fqdn_matches(str, pattern) {
    not contains(pattern, "*")
    str == pattern
}
```

</div>
</details>

上記２つのポリシーファイルを以下のように`ConfigMap`としてKubernetesに追加します。
今回は単一の`ConfigMap`として追加しますが、複数の`ConfigMap`に分けても構いません。
ポイントは、**OPAに認識させるための特別なラベル `openpolicyagent.org/policy=rego` を追記する**ことです。

```sh
kubectl create  -n opa configmap policy --from-file ./main.rego --from-file ./ingress-whitelist.rego
kubectl label -n opa configmap policy openpolicyagent.org/policy=rego
```

`kubectl get configmap -o yaml`で確認します。

```yaml
  kind: ConfigMap
  metadata:
    labels:
      openpolicyagent.org/policy: rego
    annotations:
      openpolicyagent.org/policy-status: '{"status":"ok"}'
```
OPAに認識された場合には、専用のアノテーション`openpolicyagent.org/policy-status: '{"status":"ok"}'`がつきます。
これで、ポリシーを有効化できました。

### 3-3. ポリシーが機能しているかを検証

それでは、これらのポリシーが機能しているかを検証します。

検証用の`Namespace`を作ります。
この`Namespace`には、特殊なアノテーション`ingress-whitelist`がついており、許可すべきホスト名が`*.ok.com`もしくは`*.secondok.com`であることが記載されています。

```namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
  annotations:
    ingress-whitelist: "*.ok.com,*.secondok.com"
  name: opa-example
```

```sh
kubectl apply -f test/namespace.yaml 
namespace/opa-example created
```

正しくポリシーが機能しているかを確認するための`Ingress`を追加してみます。

<details><summary>`ingress-ok.yaml` (クリックで開く) </summary><div>

```ingress-ok.yaml
apiVersion: extensions/v1beta1                                                                          
kind: Ingress                                                                                           
metadata:                                                                                               
  name: ingress-ok            
  namespace: opa-example
spec:                                                                                                   
  rules:                                                                                                
  - host: test.ok.com
    http:                                                                                               
      paths:                                                                                            
      - backend:                                                                                        
          serviceName: nginx                                                                            
          servicePort: 80                                                                               
```

</div></details>

<details><summary>`ingress-ok2.yaml` (クリックで開く) </summary><div>

```ingress-ok2.yaml
apiVersion: extensions/v1beta1                                                                          
kind: Ingress                                                                                           
metadata:                                                                                               
  name: ingress-ok-2          
  namespace: opa-example
spec:                                                                                                   
  rules:                                                                                                
  - host: test2.secondok.com
    http:                                                                                               
      paths:                                                                                            
      - backend:                                                                                        
          serviceName: nginx                                                                            
          servicePort: 80                                                                               
```

</div></details>

<details><summary>`ingress-bad.yaml` (クリックで開く) </summary><div>

```ingress-bad.yaml
apiVersion: extensions/v1beta1                                                                          
kind: Ingress                                                                                           
metadata:                                                                                               
  name: ingress-ok
  namespace: opa-example                                                                                     
spec:                                                                                                   
  rules:                                                                                                
  - host: signin.ng.com                                                                           
    http:                                                                                               
      paths:                                                                                            
      - backend:                                                                                        
          serviceName: nginx                                                                            
          servicePort: 80                                                                               
```

</div></details>

それぞれ`host`の部分のホスト名が以下のように違うだけです。

```
  - host: test.ok.com
```
```
  - host: test2.secondok.com
```
```
  - host: test.ng.com
```

`Namespace`に、以下の`Ingress`を追加します。

```sh
$ kubectl create -f test/ingress-ok.yaml
ingress.extensions/ingress-ok created

$ kubectl create -f test/ingress-ok2.yaml
ingress.extensions/ingress-ok-2 created

$ kubectl create -f test/ingress-bad.yaml
Error from server (invalid ingress host "test.ng.com"): error when creating "test/ingress-bad.yaml": admission webhook "webhook.openpolicyagent.org" denied the request: invalid ingress host "test.ng.com"
```

`ingress-bad.yaml`のみ、ポリシーの禁止判定に抵触しているため、`Ingress`追加が失敗しました。
エラー文章には、`ingress-whitelist.repo`で生成した`msg`の文章が表示されていることが分かります。

このことから、ポリシーが有効に機能していることが確認できます。


# まとめ

以下を実施し、OPAの基本的な動作を確認しました。

- Kubernetes上でOPAを稼働
- 最低限のポリシーを`ConfigMap`で登録
- `Ingress`追加による検証で、ポリシーが有効であることを検証

Rego言語というすこしだけ特殊なプログラミング言語でポリシーを書くのが、大きな特徴であり、同時に参入障壁を少し高めているとも感じます。
Rego言語を手軽に検証できる環境として以下のPlaygroundがあります。

https://play.openpolicyagent.org/

このRego言語、Prologがベースとなっているらしいのですが、考え方を押さえればスッキリ理解できるようになる気がするので、これについては次回記載したいと思います。

OPAのメリットが何なのか、少しだけ考察します。

Kubernetesからすると、OPAはあくまでポリシーエンジンの一実装であり、OPAの他にもMutatingAdmissionWebhookを自分で実装することでも同様のことができます。
では、OPAの強みは何かというと、Kubernetes以外の、データベースやネットワークなどのさまざまな部分でシステムワイドに「ポリシー」を一括して管理しなければならない、そうしたときに透明性や管理のしやすさを提供すると感じます。

例えば、SQLデータベースがあったとして、「誰々はどのテーブルにアクセスしてはいけません」というポリシーを`GRANT`文で書いたりします。しかし「どの列のどの行は機密情報なので誰々はアクセスしてはいけません」、といったポリシーを設定しようとすると、RDBMSごとに書き方が異なったりします。これは非常に煩雑です。
こういう適用レイヤによる実装の差異をなくすという大きな野心をOPAには感じます。これが成功（流行）したらほんとうにすごいことですね。