概要

Open Policy Agent (OPA) は、クラウドのセキュリティポスチャを決定するための多彩なリソース検査機能を備えたオープンソースのポリシー言語である Rego を提供しています。Datadog では、Rego を使用してカスタムルールを記述し、インフラストラクチャーのセキュリティを制御することができます。

テンプレートモジュール

ルールの定義は、モジュールの内部で定義された Rego ポリシーから始まります。CSM Misconfigurations では、以下のようなモジュールテンプレートを使用して、ルールの記述を簡素化しています。

package datadog

import data.datadog.output as dd_output

import future.keywords.contains
import future.keywords.if
import future.keywords.in

eval(resource_type) = "skip" if {
    # リソースをスキップする場合に true と評価されるロジック
} else = "pass" {
    # リソースが準拠している場合に true と評価されるロジック
} else = "fail" {
    # リソースが非準拠の場合に true と評価されるロジック
}

# この部分は、すべてのルールで変更されません
results contains result if {
    some resource in input.resources[input.main_resource_type]
    result := dd_output.format(resource, eval(resource))
}

このモジュールの各パーツをよく見て、その仕組みを理解してください。

インポートステートメント

最初の行は package datadog という宣言を含んでいます。パッケージは Rego のモジュールを一つのネームスペースにグループ化し、モジュールを安全にインポートすることを可能にします。現在のところ、ユーザーモジュールのインポートはカスタムルールの機能ではありません。すべてのポスチャ管理ルールは、datadog ネームスペースの下にまとめられています。結果を正しく返すために、ルールは package datadog ネームスペースの下にグループ化してください。

import future.keywords.contains
import future.keywords.if
import future.keywords.in

次の 3 つのステートメントは、OPA が提供するキーワード containsifin をインポートしています。これらのキーワードは、可読性を高めるために、より表現力豊かな構文でルールを定義することを可能にします。注: import future.keywords で全てのキーワードをインポートすることは推奨しません

import data.datadog.output as dd_output

次の行では、Datadog のヘルパーメソッドをインポートして、Datadog のポスチャ管理システムの仕様に合わせて結果をフォーマットしています。datadog.output は Rego モジュールで、最初の引数にリソース、2 番目の引数にリソースの検査結果を表す passfailskip という文字列を指定するフォーマットメソッドを持っています。

ルール

インポートステートメントの後に、テンプレートモジュールの最初のルールが来ます。

eval(resource) = "skip" if {
    resource.skip_me
} else = "pass" {
    resource.should_pass
} else = "fail" {
    true
}

ルールはリソースを評価し、リソースの状態に応じて結果を文字列として提供します。passfailskip の順番は、必要に応じて変更することができます。上記のルールでは、リソースに skip_meshould_pass が false または存在しない場合、 fail がデフォルトとして設定されています。また、pass をデフォルトにすることもできます。

eval(resource) = "skip" if {
    resource.skip_me
} else = "fail" {
    resource.should_fail
} else = "pass" {
    true
}

結果

テンプレートモジュールの最後のセクションは、結果のセットを構築します。

# この部分は、すべてのルールで変更されません
results contains result if {
    some resource in input.resources[input.main_resource_type]
    result := dd_output.format(resource, eval(resource))
}

このセクションでは、メインリソースタイプからすべてのリソースを通過させ、それらを評価します。これは、ポスチャ管理システムで処理される結果の配列を作成します。some キーワードはローカル変数 resource を宣言し、これはメインリソースの配列から取得されます。eval ルールは各リソースに対して実行され、passfail、または skip を返します。dd_output.format ルールは、リソースと評価結果をクラウドセキュリティで処理できるように正しくフォーマットします。

ポリシーのこのセクションは、変更する必要はありません。代わりに、ルールの複製時に Choose your main resource type ドロップダウンでメインリソースの種類を選択すると、ポリシーのこのセクションに挿入されます。また、some_resource_typegcp_iam_policy など、選択したメインリソースの種類に置き換えて、input.resources.some_resource_type を通じてリソースの配列にアクセスすることができます。

その他のルールの書き方

このテンプレートは、カスタムルールを書き始めるのに役立ちます。このテンプレートに従わなければならないわけではありません。既存のデフォルトルールを複製することもできますし、ゼロから独自のルールを作成することもできます。ただし、ポスチャ管理システムが結果を解釈するためには、Rego モジュールの中で results という名前で、次のようなフォーマットで記述する必要があります。

[
    {
        "result": "pass" OR "fail" OR "skip",
        "resource_id": "some_resource_id",
        "resource_type": "some_resource_type"
    }
]

より複雑なルール

上記のルール例では、リソースに含まれる should_pass のような基本的な真偽フラグを評価します。論理的な OR を表現するルールを考えてみましょう。例:

bad_port_range(resource) {
    resource.port >= 100
    resource.port <= 200
} else {
    resource.port >= 300
    resource.port <= 400
}

このルールは、port100200 の間、または 300400 の間である場合に true と評価されます。このために、eval ルールを以下のように定義します。

eval(resource) = "skip" if {
    not resource.port
} else = "fail" {
    bad_port_range(resource)
} else = "pass" {
    true
}

これは、リソースに port 属性がない場合はスキップし、2 つの “bad” 範囲のいずれかに当てはまる場合は失敗します。

ルールの中で、複数のリソースタイプを調べたい場合があります。これを行うには、Advanced Rule Options のドロップダウンで、いくつかの関連するリソースタイプを選択します。関連リソースの配列には、input.resources.related_resource_type を使ってアクセスできます (related_resource_type は、アクセスしたい関連リソースに置き換えてください)。

複数のリソースタイプに対してポリシーを記述する場合、各メインリソースに対して関連するリソースタイプのすべてのインスタンスをループするのは時間がかかる場合があります。次の例を見てみましょう。

eval(iam_service_account) = "fail" if {
    some key in input.resources.gcp_iam_service_account_key
    key.parent == iam_service_account.resource_name
    key.key_type == "USER_MANAGED"
} else = "pass" {
    true
}

# この部分は、すべてのルールで変更されません
results contains result if {
    some resource in input.resources[input.main_resource_type]
    result := dd_output.format(resource, eval(resource))
}

このルールは、gcp_iam_service_account_key のインスタンスが gcp_iam_service_account (メインリソースの種類として選択したリソース) にマッチし、ユーザーが管理しているものがあるかどうかを判断します。サービスアカウントがユーザー管理されているキーを持っている場合、fail という結果が出ます。eval ルールはすべてのサービスアカウントに対して実行され、すべてのサービスアカウントのキーをループしてアカウントにマッチするものを探します。その結果、計算量は O(MxN) となります。ここで、M はサービスアカウント数、N はサービスアカウントキーの数です。

時間計算量を大幅に改善するために、ユーザーが管理するキー親の集合集合内包で構築します。

user_managed_keys_parents := {key_parent |
    some key in input.resources.gcp_iam_service_account_key
    key.key_type == "USER_MANAGED"
    key_parent = key.parent
}

サービスアカウントーにユーザが管理するキーがあるかどうかを調べるには、O(1) 時間に集合をクエリしてください。

eval(iam_service_account) = "fail" if {
    user_managed_keys_parents[iam_service_account.resource_name]
} else = "pass" {
    true
}

新しい時間計算量は O(M+N) です。Rego は集合、オブジェクト、配列の内包を提供し、クエリを作成するための複合値の構築を支援します。

詳細はこちら

ルール、モジュール、パッケージ、内包の詳細や、カスタムルールの書き方については、 Rego ドキュメントを参照してください。

参考資料

PREVIEWING: may/unit-testing