オープンポリシーエージェント(OPA)とは?

SHARE:

システム内で設定し、維持管理しなければならない、いわゆるポリシー(またはルール)がどれほど多いか、考えたことはありますか?例えば、アプリケーション、ネットワーク、コードリポジトリ、コードデプロイメント、CI/CDパイプラインなど、システム内の様々な場所でポリシーを設定しているかもしれません。

朗報です!これらのポリシーを管理するより良い方法があります。さっそくご紹介しましょう!

OPAをご紹介いたします。OPA(オーパと発音)はOpen Policy Agentの略称で、スタック全体にわたるポリシー適用を統一するオープンソースの汎用ポリシーエンジンです。例えば、管理者ユーザーと一般ユーザーで異なるアクセスレベルを設定するアプリケーションの場合、OPAを活用してユーザータイプごとに異なるアクセス権を付与することが可能です。GitHub Actionsのようなツールをご利用の場合、OPAを活用して、コードがイメージを取得するリポジトリが正しく定義され許可されているかどうかの確認なども行えます。

オープンポリシーエージェント(OPA)とは?

ここで学ぶ内容

  • オープンポリシーエージェントとは何か、そしてその仕組みについて

  • Regoでのポリシー記述方法

  • KubernetesでOPAを有効化する方法

関心事の分離または分離設計

separation of concerns(関心事の分離)、model-view-controller (モデル・ビュー・コントローラ(MVC))、あるいはmodel-view-presenter (モデル・ビュー・プレゼンタ(MVP))といった用語をご存知でしょうか?

これらの手法が導入される以前には、スパゲッティコードと呼ばれる状態が存在しておりました。プログラミングにおいて、これはスタイルシート(CSS)がアプリケーションロジック(PHP、Python、Perlなどで記述される場合あり)と混在していたり、データ操作(データベースへのレコード追加・削除・更新など)がアプリケーションロジックやビジネスロジック内で定義されていた状態を指します。要するに、全てが一箇所に混在していたのです。

このようなスパゲッティコードが存在すると、プレゼンテーション層、データ層、アプリケーション層のいずれも相互に密接に関連しているため、変更を加えることが困難になります。また、本番環境への変更のデプロイも非常に困難な作業となる可能性があります。

この問題を解決するのに役立つのが分離(デカップリング)です。関心事の分離、MVC、MVPはいずれも分離の例と言えます。OPAでは、ポリシーの意思決定において分離(関心事の分離)が行われます。つまり、ポリシーをアプリケーションロジックから切り離すのです。これにより、分散した複雑なシステム群におけるポリシー管理の難しい課題を軽減することが可能となります。

ポリシーとは?

OPAは、ポリシー決定を必要とするあらゆるサービスやアプリケーション向けの汎用ポリシーエンジンとして機能します。

では、ポリシーとは何でしょうか?冒頭で述べたように、システム内には「許可される行為と禁止される行為」が定義されているでしょう。例えば、アプリケーション、データベースロジック、プレゼンテーション層、ネットワーク(ファイアウォール)、ストレージへのアクセス権限など、様々な領域においてです。ポリシーとは、システムやアプリケーション、ネットワーク、インフラストラクチャを特定の方法で動作させる(他の方法では動作させない)ために作成・実装されるルールやガイドラインに他なりません。

例えば、アプリケーションに一般ユーザーと管理者の2種類のユーザーが存在するとします。管理者には管理画面へのアクセスを許可し、一般ユーザーには非管理画面のみへのアクセスを許可するポリシーを作成できます。また、特定のIPアドレスやヘッダー情報へのアクセス制限、あるいは特定のコンテナレジストリ(GCR.io、Quay.io、または自社内のコンテナレジストリなど)のみへのアクセス制限も可能です。

この時点で、ポリシーエンジンを別途用意せず、そのロジックをアプリケーション内に組み込めないかとお考えかもしれません。その答えは「はい、可能です」となります。ただし繰り返しになりますが、ポリシーを分離して関心を切り離すことで、あるチームがポリシーに、別のチームがアプリケーション構築に集中できるようにすることが目的です。

オープンポリシーエージェントはどのように機能しますか?

ポリシーについてご理解いただけたところで、OPAの仕組みについてご説明いたします。まずは簡単な例から始めましょう。

以下のイメージは、アプリケーションのフローを示しており、ポリシーとOPAがユーザーのリクエストをどのように処理するかが含まれています:

What Is an Open Policy Agent (OPA)?
図1:ポリシーワークフロー

上記のイメージにおけるOPAの動作は次のとおりです:

  • ユーザーがアプリケーションまたはサービスにリクエストを送信します。
  • リクエストには、「role」: 「admin」という値を持つポリシー入力(JSON)が含まれます。
  • OPAは入力を受け取り、処理します。
  • OPAで定義されたポリシーに基づき、出力として「admin_allowed」: trueが返されます。これは、このユーザーがすべての管理者ページへのアクセスを許可されていることを意味します。

定義されたポリシーに基づき、入力ドキュメントと正しいポリシー決定を表す出力ドキュメントとの間には対応関係があります。この対応関係はOPAポリシーによって定義されます。

オープンポリシーエージェントはどのように実装されるのでしょうか?

ポリシーとは何か、またリクエストを処理し結果を返す仕組みをご理解いただいたところで、次にポリシーの記述方法をご説明いたします。

図1の例を用いてみましょう。最も基本的な(そして最もシンプルな)ポリシーは以下のようになります:

admin_allowed := trueCode language: Perl (perl)

この変数代入により、入力内容に関わらずadmin_allowedは常にtrueとなります。その結果、ユーザーの役割に関係なく、すべてのユーザーが管理ページにアクセスできるようになります。これは無条件代入と呼ばれます。

しかし、これは望ましい状態ではありません。管理ページにはクレジットカード番号などの請求情報や、特定のユーザーのみがアクセスできるべき機密情報が含まれる可能性があるためです。私たちが求めるのは条件付き代入と呼ばれるものです。具体的には、管理ページへのアクセスを管理者ロールを持つユーザーに限定したいのです。具体的には以下のようになります:

admin_allow := true if {
  input.role == "admin"
}Code language: Perl (perl)

上記は条件付き変数割り当てであり、条件ブロックが成功した場合にのみ実行されます。ただし、ポリシー入力でadminロールが指定されていない場合、条件ブロックは失敗し、ルールは適用されません。ルールが適用されないため、出力にはadmin_allowフィールドが含まれません。したがって、設定されたポリシーに基づいてポリシーエンジンからの期待される出力を理解することは、アプリケーションまたはサービスの責任となります。

これがOPAポリシー作成の核心となります。

OPA言語

ポリシーについてご説明いたしましたので、次にOPA言語についてもう少し詳しく見ていきましょう。

前のセクションでは、条件ブロックが満たされた場合に変数への代入を行う条件付きルールについてご説明いたしました。では、OR条件についてはどうでしょうか。OR条件は、同じ変数名を持つ複数のルールがある場合にご利用いただけます。

admin_allow := true if {
  input.role == "admin"
}

admin_allow := true if {
  input.is_billing_enabled == "yes"
}Code language: Perl (perl)

例えば、入力ロールが adminであり、is_billing_enablednoであるとします。すると、出力は admin_allowtrueとなります。

次にAND条件について見ていきましょう。早速例を見てみましょう:

admin_allow := true if {
  input.role == "admin"
}

can_see_billing := true if {
  input.is_billing_enabled == "yes"
}Code language: Perl (perl)

ここでは、2つの条件があります。1つはadmin_allow、もう1つはcan_see_billingです。両方の割り当て変数が真の場合(input.roleadminかつinput.is_billing_enabledyesの場合に発生します)、この出力はtrueとなります。しかし、両方が偽の場合(例えば、input.roleregular であり、かつ input.is_billing_enabledno である場合)、出力は false となります。基本的に、両方の代入変数が真であること(この場合、両方の変数代入に対する条件も真であること)が真の出力を得るための条件であり、そうでない場合は偽となります。

OPAではルールチェーニングも可能です。ルールチェーニングとは何でしょうか?ご説明いたします。

admin_allow := true if {
  is_billing_enabled == true
}

is_billing_enabled := true if {
  input.cc_info_onfile == "yes"
}

is_billing_enabled := true if {
  input.cc_not_expired == "yes"
}Code language: Perl (perl)

この状況では、出力変数を用いて他の出力変数を生成することが可能です。当ケースでは、is_billing_enabled変数が中間変数として機能し、admin_allow変数を決定する役割を果たします。上記の例におけるis_billing_enabled変数は、補助ルールと呼ばれることがあります。

もう一点重要な点として、ルールの順序は問題になりません。例えば、最上位のルールでは、後続のルールによって定義されるis_billing_enabled変数を使用しています。この順序が逆になるシーケンスも、OPAでは問題となりません。OPAは、Kubernetesの出力やTerraformプランのような階層的なデータとも完全に連携します。これについては後ほど詳しく説明します。

多くのプログラミング言語と同様に、OPAもパッケージをサポートしております。パッケージは、ポリシールールをモジュールと呼ばれるファイルに整理する方法です。各モジュールについて、これらのルール群が属するパッケージパスを指定するために、パッケージ宣言を定義する必要があります。例を見てみましょう:

package policy.access

admin_allow := true if {
  input.role == "admin"
}

admin_allow := true if {
  input.is_billing_enabled == true
}

regular_allow := true if {
  input.role == "regular"
}Code language: Perl (perl)

上記の例では、policy.access パッケージパス内にモジュール宣言しました。別のパッケージでは、以下のようにdata.policy.accessという参照を使用できます:

package main
import data.policy.access

can_see_billing := true if {
  access.admin_allow == true
}

can_place_order := true if {
  access.regular_allow := true
}Code language: Perl (perl)

ご覧の通り、OPAでは.(ドット)演算子を用いてデータのクエリやアクセスを行います。

Rego

Regoは、OPAポリシーを表現するために用いられる高水準の宣言型言語です。その宣言的な性質ゆえ、Regoでポリシーを記述する際には少々注意が必要です。Regoを最初から最後まで解説することは本記事の範囲を超えますので、ページ末尾の参考文献セクションにRego理解のための完全ガイドへのリンクを追加いたしました。ここでは、すぐに本題に入らせていただきます。

前のセクションで取り上げた条件付き代入またはルール(AND)の例を用いて説明いたします:

admin_allow if {
  input.role == "admin"
  input.is_billing_enabled == true
}Code language: Perl (perl)

期待される出力は以下の通りです:

  • input.roleadminであり、かつinput.is_billing_enabledtrueである場合、Trueを返します。
  • {}または、
    • input.roleadminでない場合、またはinput.is_billing_enabledfalseである場合、もしくは
    • input.roleadminでない場合、またはinput.is_billing_enabledfalseである場合、Falseを返します。

コード、入力、出力はOPAプレイグラウンドこちらでご確認いただけます。

次に、条件付き割り当てまたはルール(OR)の例を見てみましょう:

admin_allow := true if {
  input.role == "admin"
}

admin_allow := true if {
  input.is_billing_enabled == true
}Code language: Perl (perl)

期待される出力は以下の通りです:

  • input.roleadminである場合、またはinput.is_billing_enabledtrueである場合に真(True)を返します。
  • input.roleadminでない場合、かつinput.is_billing_enabledfalseである場合に、空({})または偽(false)を返します。

コード、入力、出力はOPAプレイグラウンドこちらでご確認いただけます。

それでは、セットと貪欲探索についてご説明いたします。

allow[reason] {
  a := input.access[_]
  m := a.mode
  m == "special"
  input.type == "POST"
  reason := sprintf("mode '%s' exists and allowed to special access!", [m])
}Code language: Perl (perl)

まず、_の意味を理解する必要があります。上記の例では、演算子がinput.accessから.(ドット)演算子を用いてセットを作成しています。このセット内のキーを指定することで、値を取得することが可能です。

例えば、次のような入力があるとします:

{
  "type": "POST",
  "access": [
    {
      "mode": "regular",
      "type": "member"
    },
    {
      "mode": "special",
      "type": "acct-admin"
    }
  ]
}Code language: Perl (perl)

_演算子は、配列の全内容、すなわち開始角括弧と終了角括弧[ ]の間に存在するすべての要素を取得します。m := a.modeは、modeを取得するための変数代入です。a := input.access [_]から得られるmodeは2つ存在しますが、m == 「special」は、『mode』: 「special」が集合の最初または最後のシーケンスにあるかに関わらず、真を返します。

少し分かりにくいかもしれませんので、実際の例でご説明いたします。こちらのOPAプレイグラウンドのコメントを参照しながら進めてください。先述の通り、Regoの記述方法を解説した資料は多数存在します。OPAプレイグラウンドで実践的な経験を積むことも可能です。ぜひお試しください。

これまでのセクションで学んだ情報を踏まえ、組織内でこの知識をどのように活用できるかお考えかもしれません。例えば、クラウドインフラ(Kubernetesなど)でOPAをどのように活用できるでしょうか?コードリポジトリ(Bitbucket、GitLab、GitHubなど)との連携はどのように機能するでしょうか?TerraformのようなInfrastructure-as-Code(IaC)ソフトウェアとの併用は可能でしょうか?

見てみましょう。

Kubernetes

KubernetesでOPAを有効化する方法は2通りございます:Admission Controller(具体的にはValidatingAdmissionWebhookアドミッションコントローラーを有効化する方法)と、OPA Gatekeeperを使用する方法です。後者の設定がより容易でございます。(Gatekeeperを使用したOPAの設定方法についてはこちらをご参照ください。)KubernetesでOPAを使用する際には、以下のような設定が必要となります:

  • 新規ネームスペースへの必須ラベルの追加。
  • セキュリティ上の理由から、Kubernetesリソースが使用できるリポジトリの定義。
  • コンテナ内のリソース制限と要求の定義。
  • その他の例はOPAのウェブサイトでご確認いただけます。

以下では、Gatekeeper を使用したネームスペースへの必須「owner」ラベルの例をご紹介します(Kubernetes での Gatekeeper の設定は既に完了しているものと仮定します)。

まず、ConstraintTemplate を定義する必要があります。この ConstraintTemplate 内で Rego および強制アクションを定義します。

apiVersion: templates.gatekeeper.sh/v1beta1
kind: ConstraintTemplate
metadata:
  name: k8srequiredlabels
spec:
  crd:
    spec:
      names:
        kind: K8sRequiredLabels
      validation:
        openAPIV3Schema:
          type: object
          properties:
            labels:
              type: array
              items:
                type: string
  targets:
target: admission.k8s.gatekeeper.sh
      rego: |
        package k8srequiredlabels

        violation[{"msg": msg, "details": {"missing_labels": missing}}]{
          provided := {label | input.review.object.metadata.labels[label]}
          missing := required - provided
          count(missing) > 0
          msg := sprintf("Label is required: %v", [missing])
        }Code language: Perl (perl)

ConstraintTemplateを作成した後、そのテンプレートに基づいて制約を作成する必要があります。

apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequiredLabels
metadata:
  name: ns-musth-have-gk
spec:
  enforcementAction: warn ### deny(default), dryrun, warn
  match:
    kinds:
apiGroups: [""]
        kinds: ["Namespace"]
  parameters:
    labels: ["gatekeeper"]Code language: Perl (perl)

ご覧の通り、enforcementActionには3つのタイプがございます:deny(デフォルト)、dryrun、およびwarnです。ご利用になるタイプは、組織のワークフローによって異なります。warnを選択した場合でも、新しいネームスペースの作成は可能です。ただし、そのネームスペース作成時にラベルを指定しない場合、警告が発生します。例えば:

$> kubectl create ns gatekeeper-test
Warning: [ns-must-have-gk] you must provide labels: {"gatekeeper"}
namespace/gatekeeper-test createdCode language: Perl (perl)

GitHub

GitHubは、もはや単なるコードリポジトリやコードのホスティングツールとしてのみ利用されるものではありません。コードリポジトリからCI/CDツールおよびアーティファクトリポジトリへと進化を遂げており(将来的にはさらに機能が追加される可能性があります)、

GitHubはCI/CDツールとして機能させるため、GitHub Actionsを導入しました。GitHub Actionsは、ビルド、テスト、デプロイのパイプラインを自動化できる継続的インテグレーションおよび継続的デリバリー(CI/CD)プラットフォームです。リポジトリへのプルリクエストごとにビルドとテストを実行するワークフローを作成したり、マージされたプルリクエストを本番環境にデプロイしたりすることが可能です。これにより、本番環境へのデプロイ前にリポジトリ内の全ポリシーに対してOPAを実行できるため、非常に有用です。

What Is an Open Policy Agent (OPA)?

上記のフローチャートでは、定義されたすべてのポリシーに対するOPAテストが失敗した場合、修正が完了するまでGitHubがコードのマージを防止すべきであることが確認できます。

フローチャートに示されているGitHub Actionsの例を以下に示します:

name: Run OPA Tests
on: [push]
jobs:
  Run-OPA-Tests:
    runs-on: ubuntu-latest
    steps:
    - name: Check out repository code
      uses: actions/checkout@v3

    - name: Setup OPA
      uses: open-policy-agent/setup-opa@v2
      with:
        version: latest

    - name: Run OPA Tests
      run: opa test tests/*.rego -vCode language: Perl (perl)

Terraform

Terraformは、インフラストラクチャを安全かつ予測可能に作成、変更、改善することを可能にするオープンソースのInfrastructure-as-Codeソフトウェアツールです。

以下は、すべてのGoogleプロジェクトに「ラベル」と「所有者」を必須とするポリシーの例です:

package terraform

import input.tfplan as tfplan
import input.tfrun as tfrun

identifiers {
  r := tfplan.resource_changes[_]

  "google_project" == r.type
  some i, j
  r.instances[i].attributes.labels
  r.instances[j].attributes.labels.owner
}

deny[reason] {
  not identifiers

  reason := "Type of 'google_project' has to have 'labels' and 'owner'."
}Code language: Perl (perl)

以下に、対応するTerraformプランのスニペットを記載いたします:

{
  "mock":
  {
    "project":
    {
      "tfplan":
      {
       "resource_changes": [
         {
           "mode": "data",
           "type": "vault_generic_secret",
           "name": "gcp-credential",
           "provider": "provider.vault",
           "instances": [
             {
               "schema_version": 0,
               "attributes": {
                 "data": {
                   "value": "BOGUS"
                 },
                 "data_json": "BOGUS",
                 "id": "bogus-id",
                 ...
               }
             }
           ]
         },
         {
           "mode": "managed",
           "type": "google_project",
           "name": "cluster-project",
           "provider": "provider.google",
           "instances": [
             {
               "schema_version": 1,
               "attributes": {
                 "auto_create_network": true,
                 "id": "gcp-opa-project",
                 "labels": {
                   "app": "opa-test",
                   "owner": "acme-inc"
                 },
                 "name": "gcp-opa-project",
                 "org_id": "bogus-org-id",
                 "project_id": "gcp-opa-project",
                 "skip_delete": true,
                 "timeouts": {
                   "create": null,
                   "delete": null,
                   "read": null,
                   "update": null
                 }
               },
               "private": "bogus-private"
             }
           ]
         },
         {
           "mode": "managed",
           "type": "google_project_iam_member",
           "name": "project-iam-member",
           "provider": "provider.google",
           "instances": [
             {
               "schema_version": 0,
               "attributes": {
                 "etag": "bogus-etag",
                 "id": "bogus-id",
                 "member": "bogus-serviceaccount-email",
                 "project": "gcp-opa-project",
                 "role": "roles/owner"
               },
               "depends_on": [
                 "google_project.gcp-opa-project",
                 "google_service_account.terraform-gcp-opa-project"
               ]
             }
           ]
         },
         {
           "mode": "managed",
           "type": "agoogle_storage_bucket_iam_binding",
           "name": "project-iam-member",
           "provider": "provider.google",
           "instances": [
             {
               "schema_version": 0,
               "attributes": {
                 "etag": "bogus-etag",
                 "id": "bogus-id",
                 "member": "bogus-member",
                 "project": "gcp-opa-project",
                 "role": "roles/owner"
               },
               "depends_on": [
                 "google_project.gcp-opa-project",
                 "google_service_account.terraform-gcp-opa-project"
               ]
             }
           ]
         }
       ]
      }
    }
  }
}Code language: Perl (perl)

Terraformのプラン出力において、以下の点が確認できます:

  • 2番目のインデックスresource_changes は、google_projectというタイプです。
  • ポリシーに基づき、google_projectタイプにはlabelsおよびlabels.ownerが必須となります。
  • 上記の結果が真の場合、Terraformプランは通過し、Googleプロジェクトコードを本番環境にデプロイできます。

最初にポリシーの例をご覧いただきましたので、引き続きTerraformのセキュリティベストプラクティスをご確認ください。

結論

多くの組織では、インフラストラクチャを定義・保護・管理するために複数のポリシー言語やモデルを使用しています。これを管理可能にするためには、それらを扱うための統一されたツールセットとフレームワークが必要です。Open Policy Agent(OPA)は、貴組織にとって適切なツールとなる可能性があります。CI/CDやオブジェクトストレージを含む多様な統合とユースケースを備え、複数のクラウドプロバイダーやプログラミング言語と連携します。さらに、CNCFによればOPAは現在「卒業プロジェクト」となっており、今後の機能強化や新機能追加の可能性が高いと言えます。またOPAは高水準宣言型言語であるRegoによって支えられています。Regoでの記述は少し難しそうに思えるかもしれませんが、その概念を理解すれば実に楽しいものとなるでしょう。.