Open Policy Agent (OPA) のユースケース4選

アドベントカレンダー5日目は、Policy as Codeを実現する話題のOSS「Open Policy Agent」の使い道（ユースケース）をいくつか紹介します。

# Open Policy Agentとは

<img width="400" alt="opa-horizontal-color.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/534839/03e58878-5ae7-097b-c45c-3731d91cc831.png">

クラウドネイティブの台頭により、昨今のシステムの構成は大規模化＆複雑化しています。
そのようなシステムをセキュアな状態にするには、システムを構成するサービス1つ1つに対して適切な権限設定をしていく必要があります。

そんなときに便利なのが、Open Policy Agent（OPA：おーぱ）です。
Open Policy Agentは様々なサービスのポリシー設定を同じ書き方（Rego）で表現することができます。
また、システム全体のポリシー管理を、サービス自体のコードから切り離すことになります。
これにより、システム全体のポリシーの管理（例：どのロールのユーザにどのような権限をあたえるのか）が容易になります。

<img width="600" alt="banner-image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/534839/8d1e3909-d502-dbfa-7ef6-1b3a3e59d273.png">
[※ Open Policy Agent 公式サイトより引用](https://www.openpolicyagent.org/)

# ユースケース１（SSH、sudo）

ここからはユースケースを紹介していきます。
一つ目は、Linux環境にてSSHやsudoが行われた場合の制御です。

LinuxにはPAM (Pluggable Authentication Modules for Linux) と呼ばれる認証モジュールが組み込まれており、これを用いてユーザ認証が行われています。
そして、PAMとOpen Policy Agentを用いることでポリシー管理ができます。

今回は、サービスA開発者とサービスB開発者はそれぞれの開発しているサービスのみ、管理者は両方のサービスにSSHできるケースを考えます。(sudoも同じ要領で記述できます。)

![Linuxの場合.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/534839/2c7d0808-38c3-7dca-081c-b1c891e5a45d.png)

ユーザから仮想マシンにアクセスがあった際、各サービスはOpen Policy Agentにアクセスし、ポリシーとデータを確認します。
今回のポリシーは下記のソースのようになります。

```rego:sshd_authz.rego
package sshd.authz

import input.pull_responses
import input.sysinfo

import data.hosts

# デフォルトでは、すべてのユーザを拒否
default allow = false

# 管理者（admin）の場合にSSHを認可
allow {
    data.roles["admin"][_] == input.sysinfo.pam_username
}

# 該当サービスの開発者であった場合にSSHを認可
allow {
    hosts[pull_responses.files["/etc/host_identity.json"].host_id].contributors[_] == sysinfo.pam_username
}

# 認可されなかった場合に、エラーメッセージを出力
errors["Request denied by administrative policy"] {
    not allow
}
```

このとき、`data.roles["admin"][_]`でシステム管理者（admin）を判別できるよう、下記データを読み込みます。

```json:roles.json
{
    "admin": ["admin"]
}
```

同じように、`hosts[pull_responses.files["/etc/host_identity.json"].host_id].contributors[_]`でサービス開発者（serviceA-developer、serviceB-developer）を判別できるよう、下記データを読み込みます。

```json:hosts.json
{
  "serviceA": {
    "contributors": [
      "serviceA-developer"
    ]
  },
  "serviceB": {
    "contributors": [
      "serviceB-developer"
    ]
  }
}
```


※OPAの責任範囲は「認可」のみであり、「認証」は対象外であるためSSH時のauthorized_keysによる認証は、ほかのID管理システムに任せることになります。

# ユースケース２（Docker）

### 【その１： `seccomp=unconfined`オプションの無効化】

![Dockerの場合.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/534839/1119520f-e3f4-29c5-79f7-ef0f57e0b0bd.png)

Dockerでは、[seccomp（secure computing mode）](https://matsuand.github.io/docs.docker.jp.onthefly/engine/security/seccomp/)というLinuxカーネルの機能を用いることで、コンテナ内で利用可能なDockerコマンドを限定することができます。

しかし、この機能はユーザが`seccomp=unconfined`というオプションをつけることで、seccompプロファイルの内容を無視してDockerコマンドを使うことができてしまいます。
そのため、ユーザがInsecureなコンテナを取り扱わないように、Open Policy Agentを用いて、`seccomp=unconfined`オプションを無効にすることが推奨されます。

```rego:docker_authz1.rego
package docker.authz

default allow = false

# denyの条件に該当しなければ、認可を許可する
allow {
    not deny
}

# seccomp_unconfinedで定義されているルールに該当する場合はdeny
deny {
    seccomp_unconfined
}

# コンテナを実行する際に`seccomp=unconfined`オプションを用いていた場合は該当
seccomp_unconfined {
    input.Body.HostConfig.SecurityOpt[_] == "seccomp:unconfined"
}
```

seccompプロファイルは、[mobyのGithubリポジトリ上にあるdefault.json](https://github.com/moby/moby/blob/master/profiles/seccomp/default.json)を編集して作成します。
デフォルトでは、300以上あるシステムコールのうち、下記URLのものが制限されています。（Dockerコマンドで使えないようにされています。）
https://matsuand.github.io/docs.docker.jp.onthefly/engine/security/seccomp/#significant-syscalls-blocked-by-the-default-profile

### 【その２： 特定ユーザのみ認可】

ユースケース１（SSH、sudo）と同様に、特定ユーザのみがDockerコマンドを使えるように設定することもできます。また、readOnly（write権限無し）ユーザを定義することも可能です。

![Dockerの場合3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/534839/6224a26f-8ae0-d7e9-54dd-95fcd2b8ad01.png)

この例では、ユーザAはDockerコンテナに対するすべてのコマンド、ユーザBはreadOnlyなコマンドのみが認可されています。

```rego:docker_authz2.rego
package docker.authz

default allow = false

# readOnlyでない（read/write権限のある）ユーザがすべてのメソッドを実施することを認可
allow {
    user_id := input.Headers["Authz-User"]
    user := users[user_id]
    not user.readOnly
}

# readOnlyと定義されたユーザは、readOnlyなコマンドのみ認可
allow {
    user_id := input.Headers["Authz-User"]
    users[user_id].readOnly
    input.Method == "GET"
}

# readOnlyのユーザと、readOnlyでない（read/write権限のある）ユーザを定義
users = {
    "userA": {"readOnly": false},
    "userB": {"readOnly": true},
}
```

# ユースケース３（Kubernetes）

![kubernetes-admission-flow.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/534839/96794ead-41d3-51ea-697f-1b5630717970.png)
[※ Open Policy Agent 公式ドキュメントより引用](https://www.openpolicyagent.org/docs/latest/kubernetes-introduction/#how-does-it-work-with-plain-opa-and-kube-mgmt)

Open Policy AgentをKubernetesの[Admission Controllers](https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers/)として導入すると下記のようなことが可能になります。

* 特定のラベルがついていないリソースへのアクセスのみを認可
* 取得可能なコンテナイメージを、企業が所持しているイメージレジストリのみに限定
* すべてのPodに対してリソースのリクエストとリミットを特定できるように定義
* すでに存在するIngressと競合しないように新規Ingressの設定を限定

ここでは2つのみ、Regoの記述方法を記載します。

### 【その１：取得可能なコンテナイメージを、企業が所持しているイメージレジストリのみに限定】

```rego:kubernetes_admission1.rego
package kubernetes.admission

deny[msg] {
    input.request.kind.kind == "Pod"
    # すでに存在するコンテナのイメージをすべて洗い出す
    some i
    image := input.request.object.spec.containers[i].image
    # コンテナイメージの名前が、"hooli.com/"からはじまっていない場合に、
    #「信用していないレジストリから取得したイメージ」とメッセージを出力
    not startswith(image, "hooli.com/")
    msg := sprintf("image '%v' comes from untrusted registry", [image])
}
```

### 【その２：すでに存在するIngressと競合しないように新規Ingressの設定を限定】

```rego:kubernetes_admission2.rego
package kubernetes.admission

# Ingressの設定が、すでに存在するIngressと競合した場合に、認可を行わない
deny[msg] {
    input.request.kind.kind == "Ingress"
    # インプット対象のIngressをすべて洗い出す
    some i
    newhost := input.request.object.spec.rules[i].host
    # すでに存在するIngressをすべて洗い出す
    some namespace, name, j
    oldhost := ingresses[namespace][name].spec.rules[j].host
    # インプット対象のIngressのホストと、すでに存在するIngressのホストが一致するかどうかを確認
    newhost == oldhost
    msg := sprintf("ingress host conflicts with ingress %v/%v", [namespace, name])
}
```

Admission ControllersとしてOpen Policy Agentを利用すると、Kubernetes環境の自動修正も行うこともできます。
例えば、下記のような修正が可能です。

* 特定のアノテーションを、すべてのリソースにいれる
* サイドカーコンテナをPodsの中に入れる
* 企業が所持しているイメージレジストリを使うようにコンテナの設定を変える
* デプロイメントにNodeとPodのアフィニティ・セレクタを含める

また、KubernetesとOpen Policy Agentの統合に特化したOPA Gatekeeperというモジュールがあります。
OPA Gatekeeperを使うことで、よりOpen Policy AgentがKubernetes上で使いやすくなります。

# ユースケース４（Envoy、Istio）

サービスメッシュに対しても、Open Policy Agentは有効です。
今回は簡略化のため、サイドカーとしてEnvoyを利用しているケースについてのせます。

![Envoyの場合.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/534839/97d10121-d02f-e475-9904-a0980ff9ffb0.png)

このケースでは、開発者からのリクエストをEnvoyが受け取った後に、Open Policy Agentがポリシーを確認します。（①）
そして認可がおりた場合、Envoyはサービスにアクセスして情報を取得します。（②）
①②両方を行うことで、Envoyは開発者にレスポンスを返すことができます。

```rego:envoy_authz.rego
package envoy.authz

import input.attributes.request.http as http_request

default allow = false

# action_allowedで定義されているルールに該当する場合はallow
allow {
    action_allowed
}

# パスが/pattern1であり、メソッドがGETの場合は該当
action_allowed {
  http_request.method == "GET"
  glob.match("/pattern1", ["/"], http_request.path)
}

# パスが/pattern3であり、メソッドがPOSTの場合は該当
action_allowed {
  http_request.method == "POST"
  glob.match("/pattern3", ["/"], http_request.path)
}
```

/pattern1はallowされているため求めていたレスポンスが返ってきますが、
/pattern2はallowされていないため、Envoyが②の処理を実施せず、求めていたレスポンスは
返ってこない仕組みとなっています。

# さいごに

他にもKong(API Gateway)やTerraform、Kafkaといった様々なものと組み合わせて使うことができます。Open Policy Agentは使い道が多くてとても便利なOSSですので、ぜひ皆さんご活用ください！

# 参考サイト

- https://www.openpolicyagent.org/
- https://www.openpolicyagent.org/docs/latest/