Trivyのカスタムポリシーを動かすことに成功するまでの作業メモ
前回の記事では現在開発中のインフラプロジェクトに対してコンプライアンステストを実行する仕組みをConftestで実現した。
そして次のバージョンではセキュリティテストの実行担当としてTrivyを導入する予定だが、基本的にTrivyのインストールが完了したら、あとは以下のようなコマンドを実行するだけでセキュリティやバックアップ等に関する重大な問題が潜んでいないかをチェックしてくれる。
$ trivy fs --scanners vuln,secret,misconfig,license --exit-code=1 ./
これだけでセキュリティテストの導入は完了と言ってもいいかもしれないが、TrivyではRegoでカスタムポリシーを定義することでテストを拡張することが可能であるためConftestとの共存を模索してみることにしたのだが、実際に導入作業を行なってみると相当手こずったため、今回はカスタムポリシーのテストを実行出来るようにするまでに得られた知見やその実現方法について紹介する。
インストール
前回のTrivyの記事でも書いたが、後程説明するサンプルプロジェクトを実行するためにtrivyコマンドをインストールする必要があるので、その手順をあらためて書いておく。
Macのhomebrewを使用している場合、Trivyは以下のコマンドでインストールできる。
ちなみに自分が今回の動作検証で使用したバージョンは0.51.1となっている。
$ brew install trivy
$ trivy --version
2024/05/25 20:35:55 INFO Loaded file_path=trivy.yaml
Version: 0.51.1
...
その他のインストール方法はこちら。
事前準備
TrivyではMisconfiguration(設定ファイル)、Vulnerability(脆弱性)、Secret(セキュリティ情報)、License(ライセンス)といったスキャン機能があるが、カスタムポリシーはMisconfigurationのスキャンに分類される機能となる。
よってRegoでカスタムポリシーを実装する前に、Misconfigurationの詳細についてまず調査を行う必要がある。
Misconfigurationで実行されるテストはtrivy-checksリポジトリのコードあるいはドキュメントを確認することで実行されるテストを把握出来る。
自分が実装したポリシーが実は既にビルトインで提供されていたということがないようにするためにも、まずはこのリポジトリをしっかりチェックした方が良い。
ちなみにクラウドごとにテスト量が大きく異なっていて、自分が使用しているGCPのテストに比べてAWSのテストの方がサポートサービスがかなり充実している点については、やはりシェアの差を感じる。
リポジトリの調査が完了したら、あとはMisconfigurationのドキュメントを全部読むことでカスタムポリシーの実装準備は完了となる。
カスタムポリシーの挙動調査
当初は自分のポリシープロジェクトにTrivyのカスタムポリシー実行機能をいきなり導入しようとしたのだが、簡単なポリシーテストですらまったく動作しない上に原因も不明なままに時間が過ぎてしまったため、ひとまず原因調査を高速に回すために必要最低限の検証プロジェクトを作成することにした。
そこからひたすら試行錯誤し続けた結果、最終的には以下のようなプロジェクトでカスタムポリシーを動作させることが出来るようになった。
$ mkdir -p /tmp/sample-project/policy/
$ cd /tmp/sample-project
$ vi main.tf
# 設定内容は下記
$ vi policy/main.rego
# 設定内容は下記
$ vi trivy.yaml
# 設定内容は下記
$ tree -a
.
├── main.tf
├── policy
│ └── main.rego
└── trivy.yaml
2 directories, 3 files
main.tfはテスト対象となるHCLファイルだが、これはtrivy-checksのドキュメントにあったものをコピペして使用することにした。
# 以下のリソース定義ではビルトインポリシーのテストのエラーも発生するが、検証したいのはカスタムポリシーの挙動であるため、以下のビルトインポリシーのエラーは無視するよう設定
#trivy:ignore:AVD-GCP-0014 trivy:ignore:AVD-GCP-0015 trivy:ignore:AVD-GCP-0016 trivy:ignore:AVD-GCP-0017 trivy:ignore:AVD-GCP-0020 trivy:ignore:AVD-GCP-0022 trivy:ignore:AVD-GCP-0024 trivy:ignore:AVD-GCP-0025
resource "google_sql_database_instance" "postgres" {
name = "postgres-instance-a"
database_version = "POSTGRES_11"
settings {
tier = "db-f1-micro"
ip_configuration {
ipv4_enabled = false
authorized_networks {
value = "10.0.0.1/24"
name = "internal"
}
}
}
}
次にテストの実装だが、ここでは動作確認のためにポリシーテストでルールの適用が成功したら必ずエラーが出るようにしている。
# METADATA
# title: タイトル
# description: 説明
# custom:
# id: USR-GCP-0001
# severity: MEDIUM
# input:
# selector:
# - type: cloud
package trivy.user.cloud
import rego.v1
# カスタムポリシーのテスト起動が成功したら必ずエラーが発生するルール
deny contains res if {
res := result.new("error!", input)
}
最後に設定ファイルだが、以下の設定内容であれば設定ファイルを作成する必要性を感じないものの、このあとすぐにtrivy.yamlの設定で遭遇した問題について説明しているため、一応残している。
# ref. https://aquasecurity.github.io/trivy/v0.51/docs/references/configuration/config-file/
exit-code: 1
以上のファイル定義でカスタムポリシーのテストを実行すると、以下のような結果が出力されてポリシーテストの起動に成功したことが分かる。
$ trivy conf --policy ./policy --namespaces trivy .
2024/05/26 20:48:21 WARN '--policy' is deprecated. Use '--config-check' instead.
2024/05/26 20:48:21 INFO Loaded file_path=trivy.yaml
2024-05-26T20:48:21+09:00 INFO Misconfiguration scanning is enabled
2024-05-26T20:48:22+09:00 INFO Detected config files num=2
. (terraform)
Tests: 2 (SUCCESSES: 1, FAILURES: 1, EXCEPTIONS: 0)
Failures: 1 (UNKNOWN: 0, LOW: 0, MEDIUM: 1, HIGH: 0, CRITICAL: 0)
MEDIUM: error!
══════════════════════════════════════════════════════════════════════════════════════════════
説明
──────────────────────────────────────────────────────────────────────────────────────────────
(以降の説明の便宜上--policyオプションを使用しているが、現在はdeprecatedのオプションとなっており、代わりに--config-checkを指定する必要がある)
なお最初から解答を見せれば何故これだけの設定にハマったのか分からないかもしれないが、一から組み立てようとすると以下のような問題に遭遇する。
Rego Options
trivy.yamlの設定の中にRego Optionsという以下のような設定項目がある。
rego:
...
# Same as '--config-policy'
# Default is empty
policy:
- policy/repository
- policy/custom
- policy/some-policy.rego
...
# Same as '--policy-namespaces'
# Default is empty
namespaces:
- opa.examples
- users
カスタムポリシーのテストをtrivy confコマンドで実行する度に、--policy ./policy --namespaces trivy
オプションを指定するのも煩雑なので、はじめは上記設定を使用した上でtrivy conf ./
コマンドを実行していたが、明らかにポリシー違反のエラーが発生する状態であるにも関わらずエラーが出ないという状況に陥った。
原因は設定例のコメントをちゃんと見ればわかるのだが、policyは--policyオプションと同等ではなく--config-policy、namespacesも--namespacesと同等ではないために、これらの設定がtrivy confコマンドの実行時に読み込まれることはないという話だった。
とはいえカスタムポリシー以外にRegoの設定が必要になるケースがないような気がしていて、この設定が何に使われているのかは正直最後まで分からずじまいだった。
input.selector
Trivy独自の仕様でmetadataのcustomアノテーションに以下のようにinput.selectorを指定することで、テスト対象となるresourceを絞り込むことが出来る。
# METADATA
# title: "RDS Publicly Accessible"
# description: "Ensures RDS instances are not launched into the public cloud."
# custom:
# input:
# selector:
# - type: cloud
# subtypes:
# - provider: aws
# service: rds
なおinput.selector.typeにcloudを指定した場合、subtypesとしてproviderとserviceを指定する必要があるとドキュメントに書いているが、trivy-checksのコードのディレクトリ構造から推測したprovider:google
とservice:sql
を指定した場合にはポリシーテストの実行がうまく機能しなかった。
次にtrivy-checks内にあるRegoのコードを参考にしてみると、providerやserviceをcustom配下にも定義する必要がありそうだったので、これを試してみたのだがこれも失敗に終わった。
その後更なるコードの調査やtrivy confコマンドの--traceオプションなどを使用して、ようやくポリシーテストを実行する方法が分かったのだが、その方法では以下のようにsubtypesを設定しないものになった。
# METADATA
# title: "RDS Publicly Accessible"
# description: "Ensures RDS instances are not launched into the public cloud."
# custom:
# input:
# selector:
# - type: cloud
schemas
Regoの基本機能であるschemaだが、Trivyで実行するRegoのコードでもschema定義することができる。
# METADATA
# schemas:
# - input: schema["dockerfile"]
schemaの設定についてドキュメントを確認すると、Dockerfileの例はあるのに対してその他のビルトインのスキーマについては特に明記されていない。
ただtrivy-checks内にあるRegoのコードではschema["cloud"]
と定義していることから、これに倣って設定を記述してtrivy confコマンドを実行すると以下のようなstack overflowが発生した。
$ trivy conf --policy ./policy --namespaces trivy ./
2024/05/25 21:06:31 WARN '--policy' is deprecated. Use '--config-check' instead.
2024/05/25 21:06:31 INFO Loaded file_path=trivy.yaml
2024-05-25T21:06:31+09:00 INFO Misconfiguration scanning is enabled
runtime: goroutine stack exceeds 1000000000-byte limit
runtime: sp=0xc024b80388 stack=[0xc024b80000, 0xc044b80000]
fatal error: stack overflow
runtime stack:
runtime.throw({0x1100eb045?, 0x700007a26d01?})
runtime/panic.go:1023 +0x5c fp=0x700007a26d88 sp=0x700007a26d58 pc=0x108f9b11c
runtime.newstack()
runtime/stack.go:1103 +0x5bd fp=0x700007a26f38 sp=0x700007a26d88 pc=0x108fb787d
runtime.morestack()
runtime/asm_amd64.s:616 +0x7a fp=0x700007a26f40 sp=0x700007a26f38 pc=0x108fd1c1a
goroutine 1 gp=0xc0000061c0 m=5 mp=0xc0000ebb08 [running]:
github.com/open-policy-agent/opa/types.Nil({0x112ddfd00?, 0xc000407c60?})
github.com/open-policy-agent/opa@v0.64.1/types/types.go:1084 +0x287 fp=0xc024b80398 sp=0xc024b80390 pc=0x1096ef987
github.com/open-policy-agent/opa/types.Nil({0x112ddfd00?, 0xc00052b340?})
github.com/open-policy-agent/opa@v0.64.1/types/types.go:1106 +0x14d fp=0xc024b803e0 sp=0xc024b80398 pc=0x1096ef84d...
...
自分の古すぎるマシン環境が原因のような気もするが、一方でschema["kubernetes"]
のケースを試してみた場合には上記のようなエラーが発生することはなかった。
そもそもschemaを定義しない場合に何が起きるかというと、inputに渡される値の型チェックが行われないだけで、それくらいであればそこまで実害はないので自分はschemaを書かないという方法でいったんはやり過ごすことにした。
custom.id
Trivyで実行される各ポリシーにはAVD-GCP-0001のようなIDが付与されているが、自作のカスタムポリシーにも同様のフォーマットのIDを以下のようにmetadataのcustomアノテーションで定義することができる。
# METADATA
# custom:
# id: USR-GCP-0001
# avd_id: USR-GCP-0001
個人的な想定ではポリシー違反時にこのIDがテスト結果に出力されるのかと思ったが、上記trivy confコマンドの実行結果を見れば分かるように特に出力で使われている訳でもなさそうである。
(ちなみにこのIDの連番の管理は手動で行うものなのかも気になっている)
あとエラー出力に関連して、ドキュメントを確認する限りresult.new関数を使うことで出力結果のハイライトを行なってくれるはずなのだが、こちらについても特に行われないのは何かが起きている気がする。
data.lib.result
これに関しては古いドキュメント(v0.32)の内容を試した自分の問題ではあるが、カスタムポリシーがまったく動作しない上に原因が分からない時に、result.new関数を使用する場合に以下のimportを定義する方法があると知って試したのだが、結局は時間を無駄にしただけであった。
import data.lib.result
とはいえresult.new関数の挙動について検索を行うと、このバージョンの記事がヒットしてしまうこともまた事実であるため、Trivyのドキュメントを読む際には必ず自分が使用しているバージョンのドキュメントなのかを確認した方がいい。
.trivyignore.yaml
上記サンプルプロジェクトのmain.tfでは#trivy:ignore:
コメントでビルトインのエラーの検知を行わないようにしているが、これは.trivyignore.yamlの設定によって行うことも出来る。
方法としてはまずtrivy.yamlにignorefile: .trivyignore.yaml
という設定を追加して、さらに.trivyignore.yamlを以下のように定義するのだが、エラー出力された小文字のIDを追加してもうまくルール除外を行うことはできない。
misconfigurations:
- id: avd-gcp-0025
一方でAVD-GCP-0025
のように大文字表記に変えるとしっかり除外が行われるようになる。
なお#trivy:ignore:
コメントでは大文字小文字どちらでも除外可能である点は不思議に感じる。
その他
個人的に気になった点として、こちらのドキュメントにて
A single package must contain only one policy.
との一文が書かれていたため、自分は当初1つのpackageには1つのルール(deny)しか書けないものだと思っていたが、試しに複数のルールを書いてみるとどちらも実行することが出来た。
これに関してはone policy
の解釈が自分の場合one policy = one rule
でそれだと勘違いであるというだけかもしれないが、ただRego(OPA)界隈でa policy
とはコードベースで具体的にどの範囲を指しているのか真面目に知りたいと思った次第である。
あと最後に重箱の隅を突くような話だが、exceptionのドキュメントの中に__rego_metadata
とあるが、これだとうまく動作しなくて__rego_metadata__
に変更するとmetadataアノテーションを定義しなくてもdescriptionやseverityの値を設定することが可能だった。
(ただしtypeの値に関しては置き換え可能か未調査)
ポリシー実装
サンプルプロジェクトによってカスタムポリシーを実行可能なことは確認できたが、本題はあくまでセキュリティなどのポリシーテストを行えるようにすることである。
なおRegoのコードでルールを実装するにあたって、inputのデータ構造がどのようになっているかをまず最初に調べなければいけない。
TrivyがHCLファイルを読み込んだ際のinputの値がどのようになるのかについては特にドキュメントに記載されていなかったため、この点について調査にかなり時間がかかったが、最終的にはtrivy confコマンド実行時に--traceオプションを与えることでinputの値のJSONを閲覧できることに気づいた。
$ trivy conf --config-check ./policy --namespaces trivy ./ --trace
...
TRACE Enter data.trivy.user.cloud.deny = _
TRACE | Eval data.trivy.user.cloud.deny = _
TRACE | Unify data.trivy.user.cloud.deny = _
TRACE | Index data.trivy.user.cloud.deny (matched 1 rule)
TRACE | Enter data.trivy.user.cloud.deny
TRACE | | Eval __local2578__ = input
TRACE | | Unify __local2578__ = input
TRACE | | Unify {"aws": # このawsから始まるJSONをコピーしてjqコマンドなどで整形するとinputの構造が分かる
...
例として、main.tfのgoogle_sql_database_instanceリソースに定義されているデータベースバージョンを対象にしてルールを実装する場合、以下のような方法で参照を行う。
package trivy.user.cloud
import rego.v1
deny contains res if {
ins := input.google.sql.instances[_]
database_version := ins.databaseversion.value
res := result.new(sprintf("database_version: %v", [database_version]), database_version)
}
なおこの実装方法は公式ドキュメントの見解から逸脱したものであり、将来における動作の保証がない点を踏まえて利用の可否を検討する必要がある。
おわり
様々な困難はあったものの、これでTrivy経由でもポリシーテストを実行する環境を構築することが出来た。
ちなみに問題に気づいているならissueを起票すべきではと思うかもしれないが、英語S/Wな仕事をすると体感のエンジニア力が当社比で80%減くらいになる気がしていて、また時間もかかることからそこまでしてやりたいかというのが本音である。
何より今自分がやるべきことは自分のプロジェクトの開発なので、次回のinfra-testing-google-sampleへのTrivy導入に向かって引き続き作業を行っていく次第。
追記
Conftestの記事で書いたGCSのバケットにpublic_access_prevention = "enforced"
の設定がされているかをTrivyのポリシーテストで書いてみようとしたのだが、Trivyはpublic_access_preventionの値を認識できないようで、Conftestのようなフリースタイルなテストは諦めることにした。
Discussion