Skip to main content

Rego 102:クエリのAND/ORとカスタムメッセージの組み合わせ

feature cloud security

2023年11月9日

0 分で読めます

このブログシリーズでは、Open Policy Agent(OPA)エンジンの開発元が作ったポリシー言語、Regoをやさしく解説します。Rego初心者で、ポリシーをコードとして記述する方法を学びたい方は、ぜひご覧ください。

全3回のシリーズで、次の内容を取り上げます。

おさらいすると、Regoは、Open Policy Agent(OPA)フレームワークの開発元が作った宣言型クエリ言語です。Cloud Native Computing Foundation(CNCF)は2019年4月、OPAをインキュベーションレベルのホストプロジェクトとして受け入れ、2021年にインキュベーション段階を卒業しました。

Regoはポリシー・アズ・コードの記述に使われます。これにより、バージョン管理やモジュール設計といったプログラミングの手法を、クラウドやInfrastructure as Code(IaC)リソースの評価に適用できます。OPAは、Regoで記述されたポリシー・アズ・コードを評価するエンジンです。Snykでは、カスタムルールにRego言語を使用しています。

第1回のおさらい

このブログシリーズの第1回では、Regoのルールとは条件付きの割り当てであると説明しました。ルールは入力をクエリして条件に一致するものを探し、一致が見つかると変数に値が割り当てられます。

ルールは次のように読みます。

THIS VARIABLE    :=	HAS THIS VALUE {
    IF THESE CONDITIONS ARE MET
}

以下は、ネットワーク管理者のAliceだけがprodアカウントで仮想ネットワークを作成・削除できるという社内ポリシーを表した例です。

allow := true {
  input.user == "alice"
}

OPAはJSONまたはYAMLの入力ドキュメントをルールに照らして評価し、ポリシー判断を行います。次の入力ドキュメントは、現在ログインしているユーザーを表します。

{
  "user": "alice"
}

OPAで入力をこのルールに照らして評価すると、クエリ input.user == "alice"との一致が見つかります。そのため、ルールヘッドの変数allowには、ルールヘッドの値trueが割り当てられます。これを示す出力は次のとおりです。

{
  "allow": true
}

OPAは、現在ログイン中のユーザーAliceがprodアカウントで仮想ネットワークを作成・削除することを許可すると判断しました。この入力はルールに準拠しています。

ANDとOR

ここまでは、クエリが1つだけのルールを見てきました。ルールには複数のクエリを含めることもできます。その場合、変数に値を割り当てるには、複数の条件をすべて満たす必要があります。暗黙のAND、つまり「この条件ANDあの条件も満たす」ということです。

たとえば、以下のルールでは、変数allowにtrueを割り当てるために、input.user == "alice" AND input.environment == "prod"の両方がtrueでなければなりません。

allow := true {
  input.user == "alice"
  input.environment == "prod"
}

場合によっては、ORが適切なこともあります。同じヘッドを持つ複数のルールを使うと、ORを表せます。

allow = true {
  input.user == "alice"
}

allow = true {
  input.user == "bob"
}

このルールの組み合わせは、次のように読めます。

allowは、userが"alice" OR userが"bob"ならtrueです。

厳密には、どちらもヘッドが同じなので、このルール群は1つのルールを構成します。ルールを複数の段階に分けて定義するため、これをインクリメンタルルールと呼びます。

2つ目のヘッドをなくして、本文をまとめることもできます。上の例をより簡潔に書くと、次のようになります。

allow = true {
  input.user == "alice"
} {
  input.user == "bob"
}

なぜ単一化演算子 =を使い、代入演算子 :=を使わないのか、疑問に思うかもしれません。Regoでは変数が不変だからです。同じヘッドを持つルールは1つのインクリメンタルルールとして扱われますが、代入演算子を使おうとすると、実質的に同じ変数を複数回「代入」することになり、Regoでは許可されません。そのためルールヘッドでは単一化演算子を使います。これは同じ名前の複数のルールを単一化するためです。

ルールヘッドでどちらの演算子を使うべきか覚えるのが難しい場合は、デフォルト値と構文糖衣を使うと簡単です。

ルールヘッドのデフォルト値

ルールヘッドで変数に設定されるデフォルト値はtrueです。そのためRegoでは、ここで構文糖衣を使えます。ルールで変数にtrueを割り当てる場合、ルールヘッドの:= trueを省略できます。

つまり、このANDルールは……

allow := true {
  input.user == "alice"
  input.environment == "prod"
}

……次のANDルールと同じです。

allow {
  input.user == "alice"
  input.environment == "prod"
}

同様に、このORルールは……

allow = true {
  input.user == "alice"
} {
  input.user == "bob"
}

……次のORルールと同じです。

allow {
  input.user == "alice"
} {
  input.user == "bob"
}

まさに「いい感じ」ですね!

defaultキーワード

第1回で説明したように、ルールのクエリに一致する入力がない場合、ルールヘッドの変数にはヘッドの値が割り当てられません。

これを説明するために、ネットワーク管理者のAliceだけがprodアカウントで仮想ネットワークを作成・削除できるという例に戻りましょう。

allow := true {
  input.user == "alice"
}

ここでは、現在ログインしているユーザーがBobである入力ドキュメントを用意します。

{
  "user": "bob"
}

input.userは"alice"ではないため、入力に対してルールを評価しても、OPAは一致を見つけません。そのため、allowにはtrueが割り当てられず、評価結果は空集合になります。

{}

この場合、allowの値は未定義であると言います。OPAはルールを評価するために入力をクエリするとき、一致する値のみを返します。一致する値がなければ返すものがないため、空集合となります。

allowが明示的にtrueでない場合に、OPAからfalseを返すにはどうすればよいでしょうか。defaultキーワードを使って、デフォルト値を設定できます。つまり、ルールの評価結果が明示的にtrueでない場合、空の結果セットではなく、指定した値(ここではfalse)を返します。そのためには、同じくallowを使うルールを追加します。

default allow = false

これで、OPAがinput.user は"alice"ではないと判断した場合も、allowの評価結果は空集合になりません。代わりに、宣言したデフォルト値falseになります。

{
  "allow": false
}

defaultキーワードを指定するときは、デフォルト値を定義するルールと条件付き割り当てを定義するルールの両方で、代入演算子:=ではなく単一化演算子=を使います。繰り返しになりますが、変数は不変だからです。代入演算子を使うと、同じ変数を複数回「代入」することになり、Regoでは許可されません。代わりに単一化演算子を使います。

default allow = false

allow = true {
  input.user == "alice"
}

もちろん、先ほど説明した構文糖衣を使い、条件付き割り当てのルールで= trueを省略することもできます。これは問題なく使えますし、条件付き割り当てでどの演算子を使うか覚える必要がないため、より簡単かもしれません。

default allow = false

allow {
  input.user == "alice"
}

カスタムメッセージ

単純な合否、つまりtrueやfalse/未定義の結果ではなく、複数のメッセージを返したい場合があります。その場合、ルールヘッドdeny[msg]を使い、必要なメッセージを変数msgに割り当てます。以下のルールは、ユーザーがAliceでないかを確認し、そうであれば文字列「User is denied access」をmsgに割り当て、それをdenyのセットに追加します(セットについては第3回で詳しく説明します)。

deny[msg] {
  input.user != "alice"
  msg := "User is denied access"
}

これを試すために、入力ドキュメントに現在ログインしているユーザー名が含まれているとしましょう。

{
	"user": "bob"
}

OPAのRego Playground、またはopa eval -i input.json -d check_user.rego "data.rules.check_user" --format prettyコマンドを使って、上記の入力に対してルールを評価すると(手順はブログ記事第1回をご覧ください)、次のような結果になります。

{
  "deny": [
	"User is denied access"
  ]
}

ここでは何が起きているのでしょうか。実際にはセットルールを作成しています。これは、このシリーズの次回の記事で取り上げる概念です。今は、trueまたはfalse/未定義という単一の結果ではなく、deny変数に割り当てられたメッセージのセットを返していることを理解してください。Regoのセットは、整数{ 1, 2, 3 }、文字列{ "alice", "bob", "carlotta" }、あるいは他のセット{ { 1, 2}, {3, 4} }など、一意の要素からなる順序なしリストです。Regoでサポートされている任意の型からセットを作成でき、1つのセット内で型を混在させることもできます。

この例のルールでは、denyセットの各要素がメッセージを含む文字列です。この入力ドキュメントではセットの要素は1つだけですが、複数になることもあります。この記事の後半で例を紹介します。

メッセージに追加情報を含めるにはどうすればよいでしょうか。組み込み関数sprintfを使うと、denyの結果につながったinput.userフィールドの値を表示できます。

deny[msg] {
  input.user != "alice"
  msg := sprintf("User %v is denied access", [input.user])
}

sprintf関数は、文字列と値の配列という2つの引数を取ります。この例では、配列内の唯一の要素はinput.userで表される文字列です。最初の引数にプレースホルダーとして%vを指定すると、ルールの評価時に配列内の値がそこに入ります。

次の入力を使ってルールを評価すると……

{
	"user": "bob"
}

……次の結果が得られます。

{
  "deny": [
    "User bob is denied access"
  ]
}

notキーワード

式の前にnotキーワードを付けると、意味を反転させて否定できます。多くの場合、入力にプロパティが存在しないことを指定するクエリで使います。たとえば、次のクエリは……

input.tags.environment

……「入力ドキュメントにtags.environmentプロパティがあり、その値がfalseではない」という意味です。また、次のクエリは……

not input.tags.environment

……「入力ドキュメントにtags.environmentプロパティがない、またはtags.environmentがfalseに設定されている」という意味です。重なりや中間の状態はありません。式とその否定形は、互いに排他的です。

入力にdepartment タグがない場合、denyにtrueを割り当てるルールの例を見てみましょう。

deny {
  not input.tags.department
}

次の入力を使います。

{
  "tags": {
    "environment": "staging"
  }
}

この入力を上のルールに照らして評価すると、必要なdepartmentプロパティがないため、denyはtrueを返します。

{
  "deny": true
}

OPAでルールの例を評価する

この記事で説明した概念を試すために、ルールの例を評価してみましょう。第1回と同様、OPAを使う方法は次の2つです。

  • Rego Playgroundを使う

  • OPAのコマンドラインツールを使う

これらのインターフェースの使い方は、第1回をご覧ください。

今回は、Kubernetes Podを使った、より実践的な例を見てみましょう。入力として使うJSONマニフェストは次のとおりです。

{
  "apiVersion": "v1",
  "kind": "Pod",
  "metadata": {
    "name": "nginx-demo",
    "labels": {
      "release" : "stable"
    }
  },
  "spec": {
    "containers": [
      {
        "name": "nginx",
        "image": "nginx:1.14.2",
        "ports": [
          {
            "containerPort": 80
          }
        ]
      }
    ]
  }
}

そして、次のルールを評価します。これは「Kubernetes Podにはreleaseラベルとenvironmentラベルを付ける」という社内ポリシーを適用するために記述したものです。

deny[msg] {
  input.kind == "Pod"
  not input.metadata.labels.release
  msg := sprintf("Pod %v is missing release label", [input.metadata.name])
} {
  input.kind == "Pod"
  not input.metadata.labels.environment
  msg := sprintf("Pod %v is missing environment label", [input.metadata.name])
}

このルールには、この記事で取り上げた次の概念が使われています。

  • deny[msg]を使って、trueまたはfalse/undefinedではなく、カスタムメッセージのセットを返す

  • ANDとORの両方を使ったルール構造:

    • KubernetesオブジェクトがPodでAND releaseラベルがない場合、OR:

    • KubernetesオブジェクトがPodでAND environmentラベルがない場合に拒否する

  • プロパティが存在しないことを確認するnotキーワード

  • 準拠していないPodの名前を含むメッセージを返すsprintf関数

すぐに試せるよう、この内容を設定済みのPlaygroundを用意しました:https://play.openpolicyagent.org/p/KNVK9kEvIT

PlaygroundでEvaluateボタンを選択してルールを評価するか、OPAをローカルで実行している場合はopa eval -i input.json -d check_pod.rego "data.rules.check_pod" --format prettyのようなコマンドを実行すると、次の出力が表示されます。

{
  "deny": [
    "Pod nginx-demo is missing environment label"
  ]
}

確認した Kubernetes Pod は、入力に labels.environment プロパティが含まれていないため、ルールに準拠していません。

次に、labels.releaseプロパティを削除してみましょう。入力のlabelsセクションは次のようになります。

    "labels": {
    }

ここでルールを評価すると、denyセットに2つのメッセージが含まれていることがわかります。

{
  "deny": [
    "Pod nginx-demo is missing environment label",
    "Pod nginx-demo is missing release label"
  ]
}

最後に、入力にlabels.releaseとlabels.environmentの両方のプロパティを追加して、次のようにします。

    "labels": {
      "release" : "stable",
      "environment": "prod"
    }

もう一度ルールを評価すると、どうなるでしょうか?denyセットが空になっていることがわかります。

{
  "deny": []
}

これは、OPAがdenyセットにメッセージを追加しなかったため、Podが準拠していることを意味します。やりました!

次のステップ

ぜひブログに戻って「Rego for Beginners Part 3」をお読みください。次回は、セットルール、オブジェクトルール、関数、反復処理について解説します。

その間に、役立つリソースをいくつかご紹介します。

Regoを使ってSnyk IaCのカスタムルールを作成する方法に関心がある方は、こちらのドキュメントをご覧ください。Snykに組み込まれたセキュリティルールやコンプライアンス対応ルールセットに加え、IaC+のカスタムルールを使えば、SDLC全体にわたってセキュリティコントロールをカスタマイズできます。

IaC+では、課題を確認するUI、ルールセット、ポリシーエンジンを通じて、IDE、SCM、CLI、CI/CD、Terraform Cloudから、AWS、Azure、Google Cloudなどのデプロイ済みクラウド環境まで、コードからクラウドにわたる構成上の問題を一元的に確認し、管理できます。