Policy as Code
入門
セキュリティ・キャンプ 2023�Webセキュリティクラス�2023.8.10 13:30〜17:30
本講義の狙い
今日持ち帰ってほしいこと
講師紹介
水谷正慶 (@m_mizutani)
ポリシーのコード化
そもそもポリシーとはなにか?(広義)
Oxford Learner's Dictionary より�https://www.oxfordlearnersdictionaries.com/definition/english/policy?q=policy
政党や企業などが合意または選択した行動の計画
そもそもポリシーとはなにか?(狭義)
組織やグループによって合意・選択された
あるべき状態、禁止された状態、基準などの定義
ポリシーの例
"博士課程の修了要件は、大学院に5年(修士課程に2年以上在学し、当該課程を修了した者にあっては、当該課程における2年の在学期間を含む。)以上在学し、研究科博士課程所定の単位を修得し、かつ研究上必要な指導を受けた上、博士論文の審査及び最終試験に合格することとする。ただし、在学期間に関しては、優れた研究業績を挙げた者については、大学院に3年(修士課程に2年以上在学し、当該課程を修了した者にあっては、当該課程における2年の在学期間を含む。)以上在学すれば足りるものとする"
慶應義塾大学大学院学則 第109条 ② より引用
https://www.sfc.keio.ac.jp/html/images/leftPhoto/students_mag/courses/mag_guide/S9_mag2004.pdf
ポリシーの例
"博士課程の修了要件は、大学院に5年(修士課程に2年以上在学し、当該課程を修了した者にあっては、当該課程における2年の在学期間を含む。)以上在学し、研究科博士課程所定の単位を修得し、かつ研究上必要な指導を受けた上、博士論文の審査及び最終試験に合格することとする。ただし、在学期間に関しては、優れた研究業績を挙げた者については、大学院に3年(修士課程に2年以上在学し、当該課程を修了した者にあっては、当該課程における2年の在学期間を含む。)以上在学すれば足りるものとする"
慶應義塾大学大学院学則 第109条 ② より引用
https://www.sfc.keio.ac.jp/html/images/leftPhoto/students_mag/courses/mag_guide/S9_mag2004.pdf
「所定の単位の修得」は人間が確認する
どこからが「優れた」研究業績?
「必要な指導」とは?
従来のポリシーの特徴
ポリシーと監査
現状の一般的なシステム関連の監査の課題
システムの監査であればソフトウェアの力で解決したい
継続的監査 (Continuous Auditing)
ソフトウェアを利用したポリシーの適用例
しかしこの仕組みだけでは現実的な運用はうまくいかない
ポリシー運用の死
ポリシーを策定する
ポリシー担当
ポリシーが適切に機能しない
例外
環境の変化
ポリシーに従わなくなる
現場
一部のポリシー違反の無視から、全体の無視へ
違反防止の措置を掻い潜ろうとするようになる
ポリシーを修正しない
ポリシー担当
修正が面倒くさい
ポリシーが形骸化する
修正が面倒くさい
なぜポリシーの修正は「面倒くさい」のか
これらはプロダクト開発における課題と類似している
Policy as Code
[1] https://docs.hashicorp.com/sentinel/concepts/policy-as-code
ポリシーをコードで表現する
例)ポリシー:Roleが "admin" のユーザのみDBへのアクセスが許可される
Policy as Codeによる恩恵 (1/2)
Policy as Codeによる恩恵 (2/2)
Policy as Codeの本質
FAQ
A:
Infrastructure as Code は実現したいことを記述する
Policy as Code は制約を記述する
実現したいことと制約の関係
メモリ設定
1GB
2GB
3GB
4GB
5GB
0GB
Infrastructure as Codeによる設定
Policy as Code による制限
この範囲ならOK
2GB使いたい
5GB使いたい
OK!
NG
NG
NG!
利用箇所も違う
Code (Infrastructure)
生成されたリソース(インスタンスなど)
リソースが発生させたイベント
Code (Policy)
生成・変更
イベント
監査
監査
監査
Policy as Codeを実現するための
OPA & Rego
汎用的なポリシー記述言語:Rego
Go言語の場合
Regoの場合
Regoの特徴
Rego文法の基礎
package db
is_allowed_db_access {
input.role == "admin"
}
ファイルの冒頭に必ずパッケージ名を宣言する。パッケージ名は名前空間を意味する。
通常は任意の文字列でOKだが、プロダクトによってはパッケージ名が指定されている
ルール名。任意の名前を指定できる
{ } がポリシーの本体である「ルール」
基本的に input 経由で構造データが入力される
この書き方の場合、評価式が成立したら is_allowed_db_access に true がセットされる
一つ以上の評価式が必要
OPA:Rego用のポリシー評価エンジン
OPAをコマンドラインで実行してみる
% echo '{"role":"admin"}' | opa eval -b . -I data�{
"result": [
{
"expressions": [
{
"value": {
"db": {
"is_allowed_db_access": true
}
},
"text": "data",
"location": {
"row": 1,
"col": 1
}
}
]
}
]
}
パッケージ名
ポリシー名
評価結果
つまりOPAとは何か?
入力
(構造データ)
ポリシー
(Rego)
出力
(構造データ)
具体的なOPAの利用方法
OPA/Regoの利用事例
参考:OPAをSDKとして利用したツール
AlertChain
https://github.com/m-mizutani/alertchain
セキュリティイベントに対する自動対応のオーケストレーション制御をRegoで記述してコントロールできるツール
ポリシー(ルール)の評価エンジンを自作
しなくて良いのでOPA/Regoの利用はとても便利
FAQ
A: YesでもありNoでもある
目的を特化した言語を使う意味 (1/2)
データの問合せや操作
ポリシーの評価
言語 | 扱う人 | 特性 |
SQL | データアナリスト ソフトウェアエンジニア | データの扱いに特化している |
Python | ソフトウェアエンジニア | だいたいなんでもできる |
言語 | 扱う人 | 特性 |
Rego | 監査担当者 ソフトウェアエンジニア | ポリシーの扱いに特化している |
Python | ソフトウェアエンジニア | だいたいなんでもできる |
例
目的を特化した言語を使う意味 (2/2)
Rego入門
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条件」となる
評価式
このブロックを「ルール」と呼ぶ
Regoにおけるルールの評価例 (1)
package main
allow {
input.user != "bad-man"
input.role == "admin"
input.employee
}
OK
{
"user": "mizutani",
"role": "developer",
"employee": true
}
NG
(評価されない)
→ undefined
入力データ
ポリシー
Regoにおけるルールの評価例 (2)
package main
allow {
input.user != "bad-man"
input.role == "admin"
input.employee
}
OK
{
"user": "mizutani",
"role": "admin",
"employee": true
}
OK
→ true
入力データ
ポリシー
OK
Regoに評価用の入力を渡す方法
主な方法が2つあります
演習準備
演習1
Regoで以下のようなポリシーを作成してみましょう
実行例
オプション解説
$ opa eval -i input/ex1/good.json -f pretty -b ./exercises/ex1/ data
CLI
{
"ex1": {
"allow": true
}
}
bad1.json、bad2.json もあるのでそちらでは結果が false になるかを確認してみよう
解答例
package ex1
allow {
input.method == "GET"
input.path == "/api/v1/health"
}
ポリシーの評価結果を出力する方法
package main
allow {
input.user == "mizutani"
}
allowed_db := "production" {
input.role == "developer"
}
allowed_db := db {
input.role == "staff"
db := "staging"
}
ルール名だけだとポリシーの内容が全て成立すれば true が、一つでも失敗すればundefinedになります
"ルール名 = 定数" とすることで、ルール成立時に任意の値を出力することもできます
ルール内で代入された変数を出力結果にいれることもできます
同じルール名を複数していするのもOK
(ただし一度の評価時に異なる値がセットされるのはNG)
イテレーション
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]
}
実質的にはこのように展開される
イテレーション
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.role と role.name が一致しない場合、その要素の検査は中断されて次の要素が検査される
さらに allowed_db の要素が一つずつ検査され、一致するものがあったらポリシー評価に成功する
イテレーション
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
}
展開すると下記のようなポリシーと等価になる
イテレーション
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 となる
FAQ
A: エラーになります
エラーになる例
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
}
}
]
}
ポリシー
評価結果
これもマッチする
これがマッチする
解決方法
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)
}
ポリシー
評価結果
マッチしたら評価完了
(評価されない)
(評価されない)
演習2
Regoで以下のようなポリシーを作成してみましょう
$ opa eval -i input/ex2/good1.json -f pretty -b ./exercises/ex2/ data
演習2:ヒント
回答例
package ex2
allowed_regions := ["Tokyo", "Osaka"]
can_attend {
input.age >= 18
input.region == allowed_regions[_]
}
Regoのテスト
テストの書き方
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が成立しなかったことをテストする
テストの実行
$ 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
テストの失敗
$ 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
演習2のテストを書いてみよう
$ opa test -v ./exercises/ex2
解答例
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}
}
演習3
リソースにアクセスする際に許可できるか判定しましょう
$ opa test -v ./exercises/ex3
演習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に相当
演習3: 入力データのスキーマ
{
"user": "alice",
"resource": "D813122C-D5CC-4B6C-862D-1A4B117D0908",
"action": "write"
}
data.users[_].id に相当するユーザID
data.resources[_].id に相当するリソースID
アクション。今回は read と write のいずれか
演習3:ヒント
sample_map := {
"x": ["a", "b"],
"y": ["c", "d", "e"],
}
sample_map[input.v1][_] == input.v2
解答例
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
}
1つのルールに複数の値をセットする
deny[msg] {
instance := input.instances[_]
instance.network != "private"
msg := sprintf("network must be private: %s", [instance.name])
}
deny[msg] とすることで「denyという集合にmsgという値を追加する」という意味になる
どのような違反があったかを示す情報をmsgに格納する
実行してみる
{
"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"
]
}
}
入力データ
実行結果
違反状態を記述する書き方
演習4
RegoでAWSのEC2インスタンスの違反を探すポリシーを作成してみましょう
$ opa eval -i input/ex4/data.json -f pretty -b ./exercises/ex4/ data
演習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句を使う
演習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
演習4のテストを書いてみよう
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])
}
演習5
SecurityGroupのデータから違反している設定を検出するポリシーを記述しましょう
$ opa eval -i input/ex5/data.json -f pretty -b ./exercises/ex5/ data
演習5ヒント
以下のような感じで出力がでてきたら成功
{
"ex4": {
"deny": [
"Security group sg-01fb323b858611676 allows TCP traffic on port 443",
"Security group sg-0e936075a2285b8c7 allows TCP traffic on port 443"
]
}
}
複数データセットを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
}
条件式を満たす要素のみが残る
残った要素によって計算される
入力データ
ポリシー
演習6
GitHub Actionsを使ったAWSの継続的監査を実装する
演習6のステップ
GitHub Actions の設定
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 が定義されていると非ゼロ終了して失敗とみなされる
シークレットの設定
参考:Rego は built-in function が豊富
参考:Go言語のソフトウェアに組み込み可能
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"])
まとめ
参考文献