1 of 88

Policy as Code

入門

セキュリティ・キャンプ 2023�Webセキュリティクラス�2023.8.10 13:30〜17:30

2 of 88

本講義の狙い

今日持ち帰ってほしいこと

  • ポリシーのコード化とは何か? なぜやるのか? を理解する
  • 汎用ポリシー言語 "Rego" とポリシーエンジン "OPA" がどのように利用できるか理解する
  • Regoでポリシーをどのように記述できるか理解する

3 of 88

講師紹介

4 of 88

水谷正慶 (@m_mizutani)

  • 経歴
    • 2010年 博士(政策・メディア)
    • 2011年〜 日本IBM
      • 基礎研究所
      • Security Operation Center
    • 2017年〜 クックパッド株式会社
    • 2021年〜 Ubie株式会社
  • 専門
    • 攻撃検知・防御
    • セキュリティエンジニアリング

5 of 88

ポリシーのコード化

6 of 88

そもそもポリシーとはなにか?(広義)

Oxford Learner's Dictionary より�https://www.oxfordlearnersdictionaries.com/definition/english/policy?q=policy

政党や企業などが合意または選択した行動の計画

7 of 88

そもそもポリシーとはなにか?(狭義)

  • 法律・ガイドライン
  • 学則
  • 社内規定
  • チーム内のルール
  • プログラミングの命名規則

組織やグループによって合意・選択された

あるべき状態、禁止された状態、基準などの定義

8 of 88

ポリシーの例

"博士課程の修了要件は、大学院に5年(修士課程に2年以上在学し、当該課程を修了した者にあっては、当該課程における2年の在学期間を含む。)以上在学し、研究科博士課程所定の単位を修得し、かつ研究上必要な指導を受けた上、博士論文の審査及び最終試験に合格することとする。ただし、在学期間に関しては、優れた研究業績を挙げた者については、大学院に3年(修士課程に2年以上在学し、当該課程を修了した者にあっては、当該課程における2年の在学期間を含む。)以上在学すれば足りるものとする"

慶應義塾大学大学院学則 第109条 ② より引用

https://www.sfc.keio.ac.jp/html/images/leftPhoto/students_mag/courses/mag_guide/S9_mag2004.pdf

9 of 88

ポリシーの例

"博士課程の修了要件は、大学院に5年(修士課程に2年以上在学し、当該課程を修了した者にあっては、当該課程における2年の在学期間を含む。)以上在学し、研究科博士課程所定の単位を修得し、かつ研究上必要な指導を受けた上、博士論文の審査及び最終試験に合格することとする。ただし、在学期間に関しては、優れた研究業績を挙げた者については、大学院に3年(修士課程に2年以上在学し、当該課程を修了した者にあっては、当該課程における2年の在学期間を含む。)以上在学すれば足りるものとする"

慶應義塾大学大学院学則 第109条 ② より引用

https://www.sfc.keio.ac.jp/html/images/leftPhoto/students_mag/courses/mag_guide/S9_mag2004.pdf

「所定の単位の修得」は人間が確認する

どこからが「優れた」研究業績?

「必要な指導」とは?

10 of 88

従来のポリシーの特徴

  • 自然言語で記述されるため曖昧さがある
  • 暗黙の前提知識や文脈がある
  • ポリシーに適合しているか人間がチェックする
  • 他のポリシーとの競合は人間が注意する

11 of 88

ポリシーと監査

  • 監査とは?
    • 業務や成果物がポリシーに則っているかを証拠に基づいて確認すること
    • 組織や企業において様々な場面で必要とされる
  • 具体的な監査の例
    • 会計が適正に執行されているか(会計監査)
    • 法令や社内規定を遵守して業務が遂行されているか(業務監査)
    • 社内サービスなどが適切に運用されているか(システム監査)
    • 情報セキュリティ保護のための施策が適切に運用されているか(情報セキュリティ監査)

12 of 88

現状の一般的なシステム関連の監査の課題

  • 基本的に人間による手作業で実施される
    • ログの収集、サービスやリソース、端末の状態確認
  • 部分的に監査することが多く網羅性が低い
    • 確認のための作業コストが高いため、全体は見れない
  • 監査の頻度が少ない
    • 例:数ヶ月〜1年に1度程度
    • 問題の発生と発見に時間差が生まれやすい

システムの監査であればソフトウェアの力で解決したい

13 of 88

継続的監査 (Continuous Auditing)

  • ソフトウェアによる監査は繰り返し実施しやすい
    • 実施コストが著しく小さくなるので高頻度で実施可能
    • 実施時間も短縮されるため、気軽に実施できる
  • 対象に変更があったタイミングで監査可能
    • 問題が発生した際に即座に検出できる
    • 仕組みによっては検出ではなく中断・防止が可能になる

14 of 88

ソフトウェアを利用したポリシーの適用例

  • 自動的なチェック
    • ポリシーが遵守されている状態かシステムを定期的に確認する
    • 違反されていた場合には通知、修正などの措置をする
  • ポリシーの強制
    • システムによる認可制御や機能の有効化・無効化
    • ポリシーにあわせて利用できる機能を制限し、そもそも違反できない状態にする

しかしこの仕組みだけでは現実的な運用はうまくいかない

15 of 88

ポリシー運用の死

ポリシーを策定する

ポリシー担当

ポリシーが適切に機能しない

例外

環境の変化

ポリシーに従わなくなる

現場

一部のポリシー違反の無視から、全体の無視へ

違反防止の措置を掻い潜ろうとするようになる

ポリシーを修正しない

ポリシー担当

修正が面倒くさい

ポリシーが形骸化する

修正が面倒くさい

16 of 88

なぜポリシーの修正は「面倒くさい」のか

  • 追加・更新したポリシーが意図した通りに機能するか確認が必要
  • 変更に伴い、既存ポリシーに影響はないか確認が必要
  • 既存ポリシーの文脈を理解した上での変更が必要
    • 過去の変更の経緯、ドメイン知識の欠如
    • 変更者ごとの記述のブレの読み解き
  • 変更後のポリシー展開作業が必要

これらはプロダクト開発における課題と類似している

17 of 88

Policy as Code

  • ポリシーをコードで表現することで、ソフトウェア開発におけるベストプラクティスを活用する取り組み
  • HashiCorp の Sentinel の紹介で使われた言葉[1]で、以降広がりつつある

[1] https://docs.hashicorp.com/sentinel/concepts/policy-as-code

18 of 88

ポリシーをコードで表現する

  • あるべき状態・禁止された状態をコードで表現する
  • 例えば、検査する対象を入力、判定結果を出力とする関数として表現できる

例)ポリシー:Roleが "admin" のユーザのみDBへのアクセスが許可される

19 of 88

Policy as Codeによる恩恵 (1/2)

  • 機械可読性
    • ソフトウェアによってポリシーを解釈できるようになることで、自動的にポリシーの検査ができる
    • ポリシー自体の書き方のチェック(lintなど)も可能に
  • 再現性
    • 同じ検査対象(入力)に対しては同じ結果(出力)になる
  • テスト可能性
    • 記述したポリシーが意図通りに動作するかをテストによって自動的に確認できる
    • ポリシーを追加・変更・削除した際にサイドエフェクトが起きていないか確認できる

20 of 88

Policy as Codeによる恩恵 (2/2)

  • デプロイの自動化
    • 「ポリシーを変更する」作業を自動化することで、変更に関する心理的負荷を軽減する
    • 継続的インテグレーションや継続的デプロイ(CI/CD)と同じツールやサービスを利用可能
  • バージョン管理やレビュー
    • テキストベースのコードで表現することでGitなどのバージョン管理ツールを利用でき、変更の履歴を追える
    • GitHubのPull Requestのような機能を使うことで、変更する内容の確認、コメント、議論をしたり、変更に関する承認の制御ができる

21 of 88

Policy as Codeの本質

  • ポリシーは「1度決めたら変更しないもの」ではない
    • 実際にポリシーを運用することで不具合が明らかになる
    • 外的要因の変化によってポリシーが効果的ではなくなる
    • 効果的ではないポリシーで運用すると弊害の方が大きくなる
  • 必要に応じて「自信を持って積極的にポリシーを変更できるようにする」ことが重要
    • ソフトウェア開発におけるDevOpsと同じ考え方
    • その時点で適切なポリシーを設定し、変化に強い組織になる
    • 1年前に書いたコードのこと、覚えていますか?

22 of 88

FAQ

  • Q: IaC (Infrastructure as Code) と何が違うんですか

A:

Infrastructure as Code は実現したいことを記述する

Policy as Code は制約を記述する

23 of 88

実現したいことと制約の関係

メモリ設定

1GB

2GB

3GB

4GB

5GB

0GB

Infrastructure as Codeによる設定

Policy as Code による制限

この範囲ならOK

2GB使いたい

5GB使いたい

OK!

NG

NG

NG!

24 of 88

利用箇所も違う

Code (Infrastructure)

生成されたリソース(インスタンスなど)

リソースが発生させたイベント

Code (Policy)

生成・変更

イベント

監査

監査

監査

25 of 88

Policy as Codeを実現するための

OPA & Rego

26 of 88

汎用的なポリシー記述言語:Rego

  • 多様な目的のポリシーを記述可能
  • Prolog・Datalogから着想を得て作られた言語
    • 可読性の向上
    • 構造データを取り扱える

Go言語の場合

Regoの場合

27 of 88

Regoの特徴

  • 宣言型言語である
    • 一般的なプログラミングに用いられる手続き型言語ではない
    • 繰り返しや変数の取り回しのパラダイムが手続き型と異なる
  • (原則として)外部入出力は使わない
    • 判定に必要な引数(入力)と、判定結果となる返り値(出力)のみ
    • コアとなる判定のロジックのみに集中して記述する
  • 入力と出力のスキーマは任意
    • 構造データを自由に利用できるため、柔軟性が高い

28 of 88

Rego文法の基礎

package db

is_allowed_db_access {

input.role == "admin"

}

ファイルの冒頭に必ずパッケージ名を宣言する。パッケージ名は名前空間を意味する。

通常は任意の文字列でOKだが、プロダクトによってはパッケージ名が指定されている

ルール名。任意の名前を指定できる

{ } がポリシーの本体である「ルール」

基本的に input 経由で構造データが入力される

この書き方の場合、評価式が成立したら is_allowed_db_access に true がセットされる

一つ以上の評価式が必要

29 of 88

OPA:Rego用のポリシー評価エンジン

  • OPA: Open Policy Agent
  • 検査対象のデータをRegoで評価し、結果を出力するツール
  • 使い方は様々
    • コマンドラインとして使う
    • HTTPサーバとして使う
    • Go言語のSDKとして使う

30 of 88

OPAをコマンドラインで実行してみる

  • ポリシーファイルを policy.rego という名前で保存しておく
  • OPAコマンドをインストールする
  • CLIで使ったオプション
    • eval:cliプロセス内で一度だけポリシーを評価する
    • -b:ポリシーファイルがあるディレクトリを指定
    • -I:inputを標準入力から取得する
    • data:クエリ

% echo '{"role":"admin"}' | opa eval -b . -I data�{

"result": [

{

"expressions": [

{

"value": {

"db": {

"is_allowed_db_access": true

}

},

"text": "data",

"location": {

"row": 1,

"col": 1

}

}

]

}

]

}

パッケージ名

ポリシー名

評価結果

31 of 88

つまりOPAとは何か?

  • 入力された構造データ(JSONなど)をRegoで評価し、結果の構造データを出力するだけのエンジン
  • 入出力に使われる構造データのスキーマは自由であり、多様なデータをあつかえる

入力

(構造データ)

ポリシー

(Rego)

出力

(構造データ)

32 of 88

具体的なOPAの利用方法

  • opaコマンドでoneshotのポリシー評価をする (evalサブコマンド)
    • CIなどに組み込んで利用できる
    • 基本的に結果はファイル出力とコマンドの非ゼロ終了程度しかできない
      • なんらかの通知やアクションをするためには別ツールなどと連携が必要
  • opaコマンドでHTTPサーバとして利用する (runサブコマンド)
    • HTTPリクエストを入力としてポリシー評価した結果をレスポンスで返す
    • プロダクト・サービスの実装と分離して利用できる
      • 逆に言うと単独で利用できるユースケースはない
  • 3rd partyプロダクト・サービスがライブラリとして組み込む
    • 各プロダクト・サービスのポリシー評価エンジンとして利用される
    • 実際はこれが最も多い利用方法と思われる

33 of 88

OPA/Regoの利用事例

  • ソフトウェアへの組み込み例
    • Gatekeeper: Kubernetesのポリシーコントローラ
    • conftest: Kubernetesの設定ファイルの検査
    • tfsec: terraformの設定を検査
    • trivy: 検知する脆弱性の無視ルールを設定
  • OPA/Regoを利用した検査
    • クラウドサービスのリソース状態を継続的監査(AWS, GCP, GitHubなど)
    • コードの静的解析+規約違反の検出
    • 認可の制御

34 of 88

参考:OPAをSDKとして利用したツール

ghnotify

https://github.com/m-mizutani/ghnotify

GitHub上で発生したイベントをRegoで記述したルールを元に通知をフィルタ&内容を修正&通知先を選択

AlertChain

https://github.com/m-mizutani/alertchain

セキュリティイベントに対する自動対応のオーケストレーション制御をRegoで記述してコントロールできるツール

goast

https://github.com/m-mizutani/goast

Go言語のAST(抽象構文木)に対してRegoでチェックし、チーム内のコーディング規約などにマッチしているかを検査

ポリシー(ルール)の評価エンジンを自作

しなくて良いのでOPA/Regoの利用はとても便利

35 of 88

FAQ

  • Q: わざわざRegoで書かなくても一般的な手続き型プログラミング言語でポリシーを書けばよくないですか?

A: YesでもありNoでもある

36 of 88

目的を特化した言語を使う意味 (1/2)

  • 目的に特化した言語の方が(比較的)習得しやすい
  • ポリシーに集中する役割の人にとって適切な言語になっているか

データの問合せや操作

ポリシーの評価

言語

扱う人

特性

SQL

データアナリスト

ソフトウェアエンジニア

データの扱いに特化している

Python

ソフトウェアエンジニア

だいたいなんでもできる

言語

扱う人

特性

Rego

監査担当者

ソフトウェアエンジニア

ポリシーの扱いに特化している

Python

ソフトウェアエンジニア

だいたいなんでもできる

37 of 88

目的を特化した言語を使う意味 (2/2)

  • 実装とポリシーの分離
    • ソフトウェアを動作させるロジックとポリシーとの境界が明確になる
    • ポリシーの変更によってソフトウェアに影響がないことを保証できる
    • 関心の分離

38 of 88

Rego入門

39 of 88

Regoの基本的なポイント

package main

allowed_role := "admin"

allow {

input.user == "mizutani"

input.role == allowed_role

}

allow {

input.user == "alice"

input.role == allowed_role

}

変数の代入はできるが、再代入はできない

同じルール名(出力結果名)を複数持たせると、実質的に「OR条件」となる

ルール内の評価式は1つでも「偽」と評価されると、ルール全体が「偽」という扱いになり、実質的に「AND条件」となる

評価式

このブロックを「ルール」と呼ぶ

40 of 88

Regoにおけるルールの評価例 (1)

package main

allow {

input.user != "bad-man"

input.role == "admin"

input.employee

}

OK

{

"user": "mizutani",

"role": "developer",

"employee": true

}

NG

(評価されない)

→ undefined

入力データ

ポリシー

41 of 88

Regoにおけるルールの評価例 (2)

package main

allow {

input.user != "bad-man"

input.role == "admin"

input.employee

}

OK

{

"user": "mizutani",

"role": "admin",

"employee": true

}

OK

→ true

入力データ

ポリシー

OK

42 of 88

Regoに評価用の入力を渡す方法

主な方法が2つあります

  • input を使う
    • opaコマンド実行時に標準入力などで読み込ませ可能
    • 都度入力されるようなデータ(例えば評価されるリクエストなど)はこちらを利用することが多い
  • data を使う
    • 設置したファイルなどから複数のデータを読み込ませ可能
    • opa をサーバとして起動した場合、同期的にデータ変更ができない
    • 静的なデータの利用に向いている

43 of 88

演習準備

  • 以下のGitリポジトリをclone or ダウンロードしてください
    • https://github.com/m-mizutani/seccamp-2023-b7
    • 例)git clone https://github.com/m-mizutani/seccamp-2023-b7.git
  • 作業はこのディレクトリ内で実施してください

44 of 88

演習1

Regoで以下のようなポリシーを作成してみましょう

  • ファイル名 exercises/ex1/main.rego
  • パッケージ名 ex1
  • ルール名 allow (評価成功時には true になる)
  • 評価成功のための条件(AND)
    • input.method"GET" である
    • input.path/api/v1/health である
  • CLI、もしくは playground で実行してみてください

45 of 88

実行例

オプション解説

  • eval:cliプロセス内で一度だけポリシーを評価する
  • -f pretty:表示形式の指定。pretty はクエリを評価した結果だけを表示する
  • -b ./exercises/ex1:ポリシーファイルがあるディレクトリを指定
  • -I:inputを標準入力から取得する
  • data:クエリ

$ opa eval -i input/ex1/good.json -f pretty -b ./exercises/ex1/ data

CLI

{

"ex1": {

"allow": true

}

}

bad1.json、bad2.json もあるのでそちらでは結果が false になるかを確認してみよう

46 of 88

解答例

package ex1

allow {

input.method == "GET"

input.path == "/api/v1/health"

}

47 of 88

ポリシーの評価結果を出力する方法

package main

allow {

input.user == "mizutani"

}

allowed_db := "production" {

input.role == "developer"

}

allowed_db := db {

input.role == "staff"

db := "staging"

}

ルール名だけだとポリシーの内容が全て成立すれば true が、一つでも失敗すればundefinedになります

"ルール名 = 定数" とすることで、ルール成立時に任意の値を出力することもできます

ルール内で代入された変数を出力結果にいれることもできます

同じルール名を複数していするのもOK

(ただし一度の評価時に異なる値がセットされるのはNG)

48 of 88

イテレーション

package main

allowed_roles := [

"admin",

"developer",

]

allow {

input.role == allowed_roles[_]

}

リストやKeyValueマップの要素を一つずつ検査する手法

[_] という指定によって配列内の要素を展開して検証する

package main

allowed_roles := [

"admin",

"developer",

]

allow {

input.role == allowed_roles[0]

}

allow {

input.role == allowed_roles[1]

}

実質的にはこのように展開される

49 of 88

イテレーション

package main

roles := [

{"name": "admin", "allowed_db": ["staging", "production"]},

{"name": "staff", "allowed_db": ["staging"]},

]

allow {

role := roles[_]

role.name == input.role

role.allowed_db[_] == input.db

}

もう少し複雑な例

[_] という指定によって配列内の要素を一つずつ role に代入するような動作になる

input.rolerole.name が一致しない場合、その要素の検査は中断されて次の要素が検査される

さらに allowed_db の要素が一つずつ検査され、一致するものがあったらポリシー評価に成功する

50 of 88

イテレーション

allow {

role := {"name": "admin", "allowed_db": ["staging", "production"]}

role.name == input.role

role.allowed_db[0] == input.db

}

allow {

role := {"name": "admin", "allowed_db": ["staging", "production"]}

role.name == input.role

role.allowed_db[1] == input.db

}

allow {

role := {"name": "staff", "allowed_db": ["staging"]},

role.name == input.role

role.allowed_db[0] == input.db

}

展開すると下記のようなポリシーと等価になる

51 of 88

イテレーション

allow {

role := {"name": "admin", "allowed_db": ["staging", "production"]}

role.name == input.role

role.allowed_db[0] == input.db

}

allow {

role := {"name": "admin", "allowed_db": ["staging", "production"]}

role.name == input.role

role.allowed_db[1] == input.db

}

allow {

role := {"name": "staff", "allowed_db": ["staging"]},

role.name == input.role

role.allowed_db[0] == input.db

}

下記のように評価され、唯一値がセットされた true が allow の値になる

{

"role": "staff",

"db": "staging"

}

入力データ

roleが一致しないのでNG

全て一致するので OK

roleが一致しないのでNG

→ undefined

→ undefined

→ true

全体の結果として� allow が true となる

52 of 88

FAQ

  • Q: 同じ名前のルールに異なる値が入るとどうなるんですか?

A: エラーになります

53 of 88

エラーになる例

package main

permission := "read_only" {

input.role == "staff"

}

permission := "write" {

input.db == "staging"

}

permission := "write" {

input.role == "admin"

}

下記のように評価され、唯一値がセットされた true が allow の値になる

{

"role": "staff",

"db": "staging"

}

入力データ

{

"errors": [

{

"message": "complete rules must not produce multiple outputs",

"code": "eval_conflict_error",

"location": {

"file": "main.rego",

"row": 3,

"col": 1

}

}

]

}

ポリシー

評価結果

これもマッチする

これがマッチする

54 of 88

解決方法

package main

permission := "read_only" {

input.role == "staff"

} else := "write" {

input.db == "staging"

} else := "write" {

input.role == "admin"

}

else句を使う

{

"role": "staff",

"db": "staging"

}

入力データ

{

"result": [

(snip)

"main": {

"permission": "read_only"

}

(snip)

}

ポリシー

評価結果

マッチしたら評価完了

(評価されない)

(評価されない)

55 of 88

演習2

Regoで以下のようなポリシーを作成してみましょう

  • ファイル名 exercises/ex2/main.rego
  • パッケージ名 ex2
  • ルール名 can_attend (評価成功時には true になる)
  • 評価成功のための条件(AND)
    • input.age18 以上である
    • input.regionTokyo もしくは Osaka のいずれかである
  • 演習1と同じく input/ex2/ 以下にサンプルの入力データがあるので動作確認してみてください
    • good1.json, good2.json, bad1.json, bad2.json, bad3.json がそれぞれあります

$ opa eval -i input/ex2/good1.json -f pretty -b ./exercises/ex2/ data

56 of 88

演習2:ヒント

  • rego_unsafe_var_error: var x is unsafe というエラーがでる
    • だいたい未定義の x という変数を使おうとしている
  • printという組み込み関数で検査している途中経過のデータ内容を表示できます
    • 例:�mypolicy {� print(input)�}

57 of 88

回答例

package ex2

allowed_regions := ["Tokyo", "Osaka"]

can_attend {

input.age >= 18

input.region == allowed_regions[_]

}

58 of 88

Regoのテスト

  • 標準で opa test コマンドが用意されている
  • テストの書き方
    • ファイルは分割しても、ポリシーと同じファイル内でもOK
    • 基本的な記述方法はポリシーと同じで、評価に成功 → テストPASSとみなされる
    • ポリシー名に test_ prefixをつけることで、テストとみなされる
    • with / as 句を使うことで、任意のデータをテスト用に注入可能(詳細は後述)

59 of 88

テストの書き方

package example1

allow {

input.user == "mizutani"

}

対象のポリシー

package example1

test_allowed {

allow with input as {"user":"mizutani"}

}

test_not_allowed {

not allow with input as {"user":"bob"}

}

テスト

with input asすることでinputに任意の値をセットしてテストできる

test_ prefixをつける

not 句を使うことで、allowが成立しなかったことをテストする

60 of 88

テストの実行

$ opa test -v ./path/to/test

data.example1.test_allowed: PASS (1.402709ms)

data.example1.test_not_allowed: PASS (149.625µs)

--------------------------------------------------------

PASS: 2/2

61 of 88

テストの失敗

$ opa test -v ./path/to/test

FAILURES

--------------------------------------------------------------------------------

data.example.test_not_allowed_fail: FAIL (149.167µs)

query:1 Enter data.example.test_not_allowed_fail = _

src/basic/test/policy_test.rego:11 | Enter data.example.test_not_allowed_fail

src/basic/test/policy_test.rego:12 | | Fail not data.example.allow with input as {"user": "mizutani"}

query:1 | Fail data.example.test_not_allowed_fail = _

SUMMARY

--------------------------------------------------------------------------------

data.example.test_allowed: PASS (1.264083ms)

data.example.test_not_allowed: PASS (129.416µs)

data.example.test_not_allowed_fail: FAIL (149.167µs)

--------------------------------------------------------------------------------

PASS: 2/3

FAIL: 1/3

62 of 88

演習2のテストを書いてみよう

  • 成功ケース
    • can_attend が true になるテスト
  • 失敗ケース
    • age が条件にあわないテスト
    • region が条件にあわないテスト
    • age と region 両方が条件にあわないテスト

$ opa test -v ./exercises/ex2

63 of 88

解答例

package ex2

test_ok {

can_attend with input as {"region": "Tokyo", "age": 18}

}

test_bad_region {

not can_attend with input as {"region": "LA", "age": 18}

}

test_bad_age {

not can_attend with input as {"region": "Tokyo", "age": 16}

}

test_bad_all {

not can_attend with input as {"region": "LA", "age": 16}

}

64 of 88

演習3

リソースにアクセスする際に許可できるか判定しましょう

  • 前提
    • ユーザ一覧 exercises/ex3/users/data.json ( data.users でアクセス可能)
    • リソース一覧 exercises/ex3/resources/data.json ( data.resources でアクセス可能)
    • 許可の場合は allow ルールに true を格納する
  • ルール
    • owner自身がアクセスした場合はどのようなアクションも許可
    • そうでない場合はロールごとにアクションを許可
      • read: viewer もしくは editor 権限を持つロールは許可
      • write: editor 権限を持つロールは許可
  • テストが用意されているので、以下のコマンドで全てのテストをPASSしたらOK

$ opa test -v ./exercises/ex3

65 of 88

演習3: usersとresourcesのスキーマ

[

{

"id": "alice",

"role": "admin"

},

{

"id": "bob",

"role": "engineer"

},

{

"id": "charlie",

"role": "engineer"

},

{

"id": "david",

"role": "director"

},

{

"id": "eve",

"role": "designer"

}

]

[

{

"id": "D813122C-D5CC-4B6C-862D-1A4B117D0908",

"owner": "bob",

"permissions": {

"engineer": "editor"

},

"name": "bob's first project"

},

{

"id": "6F92A25A-2D05-4CA8-A335-0E8AB780169D",

"owner": "david",

"permissions": {

"engineer": "viewer",

"designer": "viewer"

},

"name": "product development plan"

},

(snip)

]

ユーザごとにロールが設定されている

リソースごとにロールの権限が設定されている

複数のロールに対して権限が設定されているものもある

data.users

data.resources

ownerはuserのidに相当

66 of 88

演習3: 入力データのスキーマ

{

"user": "alice",

"resource": "D813122C-D5CC-4B6C-862D-1A4B117D0908",

"action": "write"

}

data.users[_].id に相当するユーザID

data.resources[_].id に相当するリソースID

アクション。今回は readwrite のいずれか

67 of 88

演習3:ヒント

  • (繰り返し) printという組み込み関数で検査している途中経過のデータ内容を確認してみましょう
    • printを入れても何も表示されない場合は
  • key-value 型のmap形式を使うとシンプルにルールが書けるかも知れません…

sample_map := {

"x": ["a", "b"],

"y": ["c", "d", "e"],

}

sample_map[input.v1][_] == input.v2

68 of 88

解答例

package ex3

allow {

user := data.users[_]

user.id == input.user

resource := data.resources[_]

resource.id == input.resource

resource.owner == user.id

}

allowed_action := {

"viewer": ["read"],

"editor": ["read", "write"],

}

allow {

user := data.users[_]

user.id == input.user

resource := data.resources[_]

resource.id == input.resource

role := resource.permissions[user.role]

allowed_action[role][_] == input.action

}

69 of 88

1つのルールに複数の値をセットする

  • 1つの入力から複数の結果が検出される場合
    • 例) データに複数の検査対象が含まれている
    • 例) 1つの検査対象から複数の違反がでてくる
  • ルールの結果をSetとして、複数の結果を格納させる

deny[msg] {

instance := input.instances[_]

instance.network != "private"

msg := sprintf("network must be private: %s", [instance.name])

}

deny[msg] とすることで「denyという集合にmsgという値を追加する」という意味になる

どのような違反があったかを示す情報をmsgに格納する

70 of 88

実行してみる

{

"instances": [

{

"network": "private",

"name": "blue"

},

{

"network": "public",

"name": "orange"

},

{

"network": "public",

"name": "red"

}

]

}

$ cat input.json | opa eval -f pretty -b ./ -I data�{

"main": {

"deny": [

"network must be private: orange",

"network must be private: red"

]

}

}

入力データ

実行結果

71 of 88

違反状態を記述する書き方

  • 「ポリシーはあるべき状態を記述する」という発想と少し異なるので注意が必要
  • OPA/Regoと連携したプロダクトではこの記述方法を採用することが多い
    • 複数の違反を一度に検出できる
    • 違反に関する情報をメッセージできる

72 of 88

演習4

RegoでAWSのEC2インスタンスの違反を探すポリシーを作成してみましょう

  • ファイル名 exercises/ex4/main.rego パッケージ名 ex4
  • ポリシー名 deny (文字列をSetで格納する)
  • 以下の基準で違反を検出してください
    • env=production のタグがある場合 ( .Reservations[].Instances[].Tags 参照)
      • t2.nano、t2.micro は禁止 ( .Reservations[].Instances[].InstanceType 参照)
    • env=staging のタグがある場合 ( .Reservations[].Instances[].Tags 参照)
      • t2.nano、t2.micro 以外は禁止 ( .Reservations[].Instances[].InstanceType 参照)
  • 違反があった場合 "instance {instanceID} has bad instanceType" というメッセージを deny に格納する
  • (参考) スキーマ: describe-instances — AWS CLI 1.29.18 Command Reference

$ opa eval -i input/ex4/data.json -f pretty -b ./exercises/ex4/ data

73 of 88

演習4 ヒント1

「全てに一致しない」を表す書き方

in_some_list(a) {

list := ["x", "y"]

list[_] == a

}

allow {

not in_some_list(input.value)

}

package main

import future.keywords.every

allow {

list := ["a", "b", "c"]

every v in list {

input.value != v

}

}

1) 関数を使う

2) every句を使う

74 of 88

演習4 ヒント2

以下の出力がでてきたら成功

{

"ex4": {

"deny": [

"instance i-07836a5228963f219 has bad instanceType",

"instance i-0bff9bbecabdc5b8a has bad instanceType"

]

}

}

文字列のフォーマットに sprintf を使う(第2引数が配列な点に注意

color := "blue"

s := sprintf("color is %s", [color])

print(s)

color is blue

75 of 88

演習4のテストを書いてみよう

  • テストケース
    • envタグがproductionでインスタンスタイプがc2.large → 検出しない
    • envタグがproductionでインスタンスタイプがt2.nano → 検出する
    • envタグがstagingでインスタンスタイプがt2.nano → 検出しない
    • envタグがstagingでインスタンスタイプがc2.large → 検出する
  • ヒント
    • テストデータは必要なフィールドだけに絞ってもOK
    • 大きいJSONの場合、例えばポリシーディレクトリ内に test/case1/record.json のような配置で置くと、ポリシーから data.test.case1 としてアクセスできる

76 of 88

package ex4

is_burstable_type(t) {

list := ["t2.nano", "t2.micro"]

list[_] == t

}

deny[msg] {

instance := input.Reservations[_].Instances[_]

tag := instance.Tags[_]

tag.Key == "env"

tag.Value == "production"

is_burstable_type(instance.InstanceType)

msg := sprintf("instance %s has bad instanceType", [instance.InstanceId])

}

deny[msg] {

instance := input.Reservations[_].Instances[_]

tag := instance.Tags[_]

tag.Key == "env"

tag.Value == "staging"

not is_burstable_type(instance.InstanceType)

msg := sprintf("instance %s has bad instanceType", [instance.InstanceId])

}

77 of 88

演習5

SecurityGroupのデータから違反している設定を検出するポリシーを記述しましょう

  • ファイル名 exercises/ex5/main.rego パッケージ名 ex5
  • ポリシー名 deny (文字列をSetで格納する)
  • ポリシー
    • インターネット全体に公開して良いポートは443/tcpのみである。それ以外は違反
  • ヒント
    • .SecurityGroups[].IpPermissions[].IpProtocol にプロトコルが格納されている
    • .SecurityGroups[].IpPermissions[].FromPort に許可するポート範囲の開始番号が格納されている
    • .SecurityGroups[].IpPermissions[].ToPort に許可するポート範囲の終了番号が格納されている
    • .SecurityGroups[].IpPermissions[].IpRanges[].CidrIp に許可するネットワークの範囲の一覧が格納されている
      • 0.0.0.0/0 がインターネット全体への公開
  • 資料

$ opa eval -i input/ex5/data.json -f pretty -b ./exercises/ex5/ data

78 of 88

演習5ヒント

以下のような感じで出力がでてきたら成功

{

"ex4": {

"deny": [

"Security group sg-01fb323b858611676 allows TCP traffic on port 443",

"Security group sg-0e936075a2285b8c7 allows TCP traffic on port 443"

]

}

}

79 of 88

複数データセットをjoinする

{

"instances": [

{

"id": "i-aaaaa",

"cpu_id": "cpu-aaaaa",

"volume_id": "vol-aaaaa"

},

{

"id": "i-bbbbb",

"cpu_id": "cpu-bbbbb",

"volume_id": "vol-bbbbb"

},

{

"id": "i-ccccc",

"cpu_id": "cpu-ccccc",

"volume_id": "vol-ccccc"

}

],

"cpus": [

{

"id": "cpu-aaaaa",

"cost": 0.5

},

......

......

{

"id": "cpu-bbbbb",

"cost": 1.5

},

{

"id": "cpu-ccccc",

"cost": 2.5

}

],

"volumes": [

{

"id": "vol-aaaaa",

"cost": 3.8

},

{

"id": "vol-bbbbb",

"cost": 2.3

},

{

"id": "vol-ccccc",

"cost": 4.1

}

]

}

package main

expensive[msg] {

instance := input.instances[_]

cpu := input.cpus[_]

volume := input.volumes[_]

instance.cpu_id == cpu.id

instance.volume_id == volume.id

cpu.cost + volume.cost > 5

msg := instance.id

}

条件式を満たす要素のみが残る

残った要素によって計算される

入力データ

ポリシー

80 of 88

演習6

GitHub Actionsを使ったAWSの継続的監査を実装する

  • 記述するポリシー
    • インスタンスに env=production というタグがあったら
      • 443/tcp のみインターネット全体に公開を許可
      • 他のポートはインターネット全体に公開していなければOK
    • インスタンスに env=production というタグがなかったら
      • 全てのポートをインターネット全体に公開していなければOK
  • 結果
    • failed というルールに違反したインスタンスのID、およびSecurity GroupのIDを含むメッセージを格納する(形式はお任せ)
    • 注意:元の exercises/ex6/main.rego にある violated ルールはそのまま残してください

81 of 88

演習6のステップ

  • exercises/ex6/main.rego を記述する
    • instanceのデータは input.instances から読み込む
    • security groupのデータは input.security_groups から読み込む
    • opa eval -i input/ex6/data.json -f pretty -I -b ./exercises/ex6/ data と実行すると以下の組合せを検出すればOK
      • i-08c1f61337a6551b1 & sg-07a375977536b8dd9
      • i-0981f63cac5edede7 & sg-06481c1764f2ec53d
  • GitHub Actionsを設定する
    • public repositoryを作成する(名前は任意でOK)

82 of 88

GitHub Actions の設定

  • https://github.com/m-mizutani/seccamp-2023-b7-action/blob/main/.github/workflows/audit.yml をコピーして .github/workflows/audit.yml に追加
  • 自分で書いたポリシーファイルをリポジトリのルートに配置
  • シークレットの設定
    • Settings > Secrets and variables (左メニュー) > Actions > New repository secret
    • AWS_ACCESS_KEY_IDAWS_SECRET_ACCESS_KEY を追加
    • ↑は順次発行するので準備のできた人からdiscoardでメンションしてください
  • 実行してみて、手元で実行したのと同じ違反が検出できれば成功

83 of 88

Actionの説明

name: audit

on:

workflow_dispatch:

push:

# schedule:

# - cron: "30 17 1 * *"

# - cron: '30 5,17 * * *'

jobs:

check:

runs-on: ubuntu-latest

steps:

- name: Checkout repository

uses: actions/checkout@v2

- name: Set up AWS credentials

uses: aws-actions/configure-aws-credentials@v1

with:

aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}

aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

aws-region: ap-northeast-1

- name: Dump instances

run: aws ec2 describe-instances > instances.json

- name: Dump security groups

run: aws ec2 describe-security-groups > security_groups.json

- name: Merge json

run: |-

jq -n '{instances: input, security_groups: input}' instances.json security_groups.json > input.json

- uses: docker://openpolicyagent/opa:0.55.0

with:

args: |-

eval -f pretty -b ./ --fail-defined -i input.json data.ex6.violated

Schedule でイベントをトリガすれば継続的にチェックできる

今回はAPIキーを使うが、本来はOIDCを使うのが安全

./instances, ./security_groups 以下にそれぞれデータを格納してマージする

data.ex6.violated が定義されていると非ゼロ終了して失敗とみなされる

84 of 88

シークレットの設定

85 of 88

参考:Rego は built-in function が豊富

  • 文字列処理、配列・セット・オブジェクト操作、エンコード・デコード、JWTの署名や検証、IPアドレスの操作などなど
    • 困ったときは公式ドキュメントを参考 Policy Reference
    • ちなみにChatGPT 3.5, 4.0だといまいちなポリシーしか書けないのであまり信用しないほうが良い

86 of 88

参考:Go言語のソフトウェアに組み込み可能

  • Goのライブラリとして容易に利用できるので、自分で開発したツールのポリシー・ルールエンジンにOPAを活用できる

input := struct {

User string `json:"user"`

}{

User: "mizutani",

}

module := `package blue

allow {

input.user == "mizutani"

}

`

q := rego.New(

rego.Query(`x := data.blue.allow`),

rego.Module("module.rego", module),

rego.Input(input),

)

rs, err := q.Eval(context.Background())

if err != nil {

panic(err)

}

fmt.Println("allow =>", rs[0].Bindings["x"])

87 of 88

まとめ

  • ポリシーのコード化(Policy as Code)により、自動化やテストなど、近代的なソフトウェア開発のベストプラクティスを監査の世界に持ち込めるようになった
  • Policy as Codeの手段の一つとして注目されているのが OPA/Rego
  • まだ日本でも一部の先進的な組織しか取り組めておらず、今後の普及に期待

88 of 88

参考文献