knqyf263's blog

自分のためのメモとして残しておくためのブログ。

Terraform, Dockerfile, KubernetesなどIaCの脆弱な設定をCI/CDで検知する

概要

自分の所属企業であるAqua SecurityがTFsecというOSSを買収しました。

blog.aquasec.com

TFsecはどういうツールかというとTerraformの静的解析スキャナーです。Terraformの設定ファイルを渡すことでセキュリティに関する設定ミスを主に検知してくれます。

github.com

そのアナウンスに伴い、TFsecは自分が開発している脆弱性スキャナーであるTrivyに統合されました。TrivyではTerraformに加えDockerfileやKubernetesなど、いわゆるInfrastructure as Code(IaC)の設定ミスを検知するマネージドポリシーも提供しています。他にもJSONYAMLなど一般的なファイルフォーマットに対応しているため自分でポリシーを書くことでそれらの検知にも使えます。CloudFormationやAnsibleのマネージドポリシーは近いうちに提供される予定ですが、マネージドポリシーが提供される前でも自分でカスタムポリシーを書けば対応可能です。まずはIaCメインですが、将来的にはWordpressやNginxなどミドルウェアの設定上の問題も検知できるようにしていく予定です。

細かい機能や使い方は以下にまとめました。

aquasecurity.github.io

ちなみに満を持してリリースしたv0.19.0はめちゃくちゃバグってて必ずpanicが起きる状態でした。このリリースに合わせて入念に準備してきたのに非常に恥ずかしいです。OSSのハードルを下げる活動に貢献できたということでポジティブに捉えていきます。v0.19.1以降を使ってください。

実はまだ完全には統合できておらず、今後徐々にTFsecの機能をTrivyに移していきます。特にTFsecで該当箇所の行を表示する機能は非常に便利ですが、まだ対応できていません。将来的にはTrivyで全てできるようにする予定ですが、TFsecもそのまま使い続けられます。

概要としては以上です。以下ではもう少し細かい話をしていきます。

TFsec

TFsecについて少し具体的に説明します。TFsecはTerraformのファイルを解析し、AWSのセキュリティグループが0.0.0.0/0になっているとかAWS Load BalancerがHTTPSではなくHTTPになっているとか、Azureのディスクが暗号化されていないとか、セキュリティに関する設定ミスを中心に脆弱になりうる設定を検知します。中にはセキュリティだけではなくベストプラクティスに沿っているかなどのチェックも一部含まれます。もちろん中には意図的にそういう設定をしている場合もあるので、そういう場合の除外設定なども可能です。

https://github.com/tfsec/tfsec/raw/master/screenshot.png

特徴としては以下です。

重要な点としては、単にHCLをパースするだけでなく関数や変数の評価にも対応している点です。この辺りのexampleを見ると分かるかと思いますが、きちんとvariableやtfvarsの値を評価した上で脆弱な設定を検知しています。

github.com

世の中には terraform plan の出力をJSONに変換して検知するツールもあるのですが、Terraform使いの皆さんなら terraform plan にいかに時間がかかるか知っているかと思います。TFsecでは単にTerraformで書かれた設定ファイルを渡すだけで高速に検知できるので手元でも気軽に実行できます。

また、 terraform plan する場合はAWSで言うとS3バケットへのアクセスが必要だったりもします。つまりCI上で検知するためにはある程度の権限を持ったAPIトークンが必要になるわけですが、Codecovの一件で分かったようにCI上に気軽に認証情報を設定すると3rd partyツールのインシデントによって漏洩するリスクがあります。そういったリスクを下げるためにもCIのワークフローはきちんと分けて、可能な限り認証情報を渡さないようにするのが好ましいです。そういう観点でもTFsecは安全に利用できます。

about.codecov.io

TrivyのIaC設定ミス検知機能

概要

TrivyというOSSを知らない方もいると思いますが、元々はコンテナイメージの脆弱性スキャナーです。主にCI上で実行されることを意図して開発しており、コンテナレジストリにpushする前に脆弱性を検知してビルドを失敗させることができます。

github.com

そこから色々と進化し、現在ではコンテナイメージだけでなくファイルシステムやGitリポジトリのスキャンも可能です。VMのOSパッケージの脆弱性をスキャンすることもできますし、コンテナを利用していないプロジェクトでNode.jsの依存性の脆弱性を検知することもできます。他にもGoのバイナリスキャンだったり色々と対応していますので、興味が湧いた方は以下のドキュメントをご参照ください。このブログでもちょこちょこ解説しています。

aquasecurity.github.io

今回、新たに脆弱性だけでなく設定ミスも検知するようになりました。英語ではmisconfigurationsとかconfiguration issuesとか呼んでます。さすがに脆弱性と設定ミスではまるで異なる機能なのでTrivyに実装する必要はないだろうと思い別ツールの開発を強く主張したのですが、何やかんやあって議論に敗北しました(恒例)。ツールは小さく作りたいという自分の哲学には反しますが、より多くの人がTrivy使ってくれるなら良いかと自分を強引に納得させました。

ということで少し脱線しましたが、やるからには良いものをということで気合を入れて作ったので結構良い仕上がりになったと思います(v0.19.0は壊れてたけど)。

使い方

使い方はコンテナイメージの脆弱性スキャン同様、非常に簡単でディレクトリを指定するだけです。例えば以下のように複数のIaCファイルが含まれるディレクトリがあるとします。 deployment.yamlKubernetesmain.tf はTerraformの設定ファイルです。

$ ls iac/
Dockerfile  deployment.yaml  main.tf

スキャンするためには trivy conf に続けてディレクトリを指定するだけです。

$ trivy conf ./iac
2021-07-09T11:51:08.212+0300    INFO    Need to update the built-in policies
2021-07-09T11:51:08.212+0300    INFO    Downloading the built-in policies...
2021-07-09T11:51:09.527+0300    INFO    Detected config files: 3

Dockerfile (dockerfile)
=======================
Tests: 23 (SUCCESSES: 22, FAILURES: 1, EXCEPTIONS: 0)
Failures: 1 (HIGH: 1, CRITICAL: 0)

+---------------------------+------------+-----------------+----------+------------------------------------------+
|           TYPE            | MISCONF ID |     CHECK       | SEVERITY |                 MESSAGE                  |
+---------------------------+------------+-----------------+----------+------------------------------------------+
| Dockerfile Security Check |   DS002    |   root user     |   HIGH   | Last USER command in                     |
|                           |            |                 |          | Dockerfile should not be 'root'          |
|                           |            |                 |          | -->avd.aquasec.com/appshield/ds002       |
+---------------------------+------------+-----------------+----------+------------------------------------------+

deployment.yaml (kubernetes)
============================
Tests: 28 (SUCCESSES: 15, FAILURES: 13, EXCEPTIONS: 0)
Failures: 13 (HIGH: 1, CRITICAL: 0)

+---------------------------+------------+----------------------------+----------+------------------------------------------+
|           TYPE            | MISCONF ID |           CHECK            | SEVERITY |                 MESSAGE                  |
+---------------------------+------------+----------------------------+----------+------------------------------------------+
| Kubernetes Security Check |   KSV005   | SYS_ADMIN capability added |   HIGH   | Container 'hello-kubernetes' of          |
|                           |            |                            |          | Deployment 'hello-kubernetes'            |
|                           |            |                            |          | should not include 'SYS_ADMIN' in        |
|                           |            |                            |          | 'securityContext.capabilities.add'       |
|                           |            |                            |          | -->avd.aquasec.com/appshield/ksv005      |
+---------------------------+------------+----------------------------+----------+------------------------------------------+

main.tf (terraform)
===================
Tests: 23 (SUCCESSES: 14, FAILURES: 9, EXCEPTIONS: 0)
Failures: 9 (HIGH: 6, CRITICAL: 1)

+------------------------------------------+------------+------------------------------------------+----------+--------------------------------------------------------+
|                   TYPE                   | MISCONF ID |                  CHECK                   | SEVERITY |                        MESSAGE                         |
+------------------------------------------+------------+------------------------------------------+----------+--------------------------------------------------------+
|   Terraform Security Check powered by    |   AWS003   | AWS Classic resource usage.              |   HIGH   | Resource                                               |
|                  tfsec                   |            |                                          |          | 'aws_db_security_group.my-group'                       |
|                                          |            |                                          |          | uses EC2 Classic. Use a VPC instead.                   |
|                                          |            |                                          |          | -->tfsec.dev/docs/aws/AWS003/                          |
+                                          +------------+------------------------------------------+----------+--------------------------------------------------------+
|                                          |   AWS004   | Use of plain HTTP.                       | CRITICAL | Resource                                               |
|                                          |            |                                          |          | 'aws_alb_listener.my-alb-listener'                     |
|                                          |            |                                          |          | uses plain HTTP instead of HTTPS.                      |
|                                          |            |                                          |          | -->tfsec.dev/docs/aws/AWS004/                          |
+------------------------------------------+------------+------------------------------------------+----------+--------------------------------------------------------+

そうすると上のようなスキャン結果が出力されます(一部見やすさのためにカットしてます)。マネージドポリシーは自動的にGitHub Packages Container registryからダウンロードされるのでユーザは意識する必要がありません。その後も24時間ごとに最新のポリシーがないかを勝手にチェックして必要なら更新してくれます。

Terraformは内部的にはTFsecでスキャンしてその結果を出力しています。マネージドポリシーはKubernetesとDockerfileに対応しており、Open Policy Agentの提供しているRegoで書かれています。それらはAppShieldと呼ばれるリポジトリで管理されています。

github.com

もしポリシーに対してフィードバックがある場合はAppShield上にIssueやPRを上げてください。個人的にはマネージドポリシーの完成度はまだまだだと思っているのですが、中央でポリシーを管理することで多くのユーザの知見が集約されて今後より良いものになっていくと期待しています。

正直自分でRego書くのかなり大変で学習コストも高いと感じているので、マネージドポリシーを提供することで多くの人がなるべくRegoを書かずに済む世界になったら良いなという思いがあります。もちろん慣れるとRegoは便利なので余力がある組織は積極的に学んでカスタムポリシーをガシガシ書いていくのもありだと思います。ですが、それでもやはり各組織が共通でチェックしたいことというのはあるはずなので、そういったポリシーをAppShieldで提供していきたいです。

ちなみにセキュリティにそこまで関係ないものもベストプラクティスに含まれている場合はなるべく検知しています。そういったものはSeverityがやや低くなっています。

JSONの出力だったりSeverityによるフィルタリングなど、脆弱性に対して可能な操作の多くは設定ミスに対しても可能です。CLIオプションの使い方はドキュメントに多く例を載せているので参照してください。

aquasecurity.github.io

また、 trivy fs コマンドを使うと脆弱性と設定ミスを同時に検知できます。例えば以下のようにDockerfileとPipfile.lock(PipenvというPythonのパッケージマネージャ用のファイル)があるとします。これはPythonで開発をしていてDockerコンテナを利用している場合はよくある構成だと思います。

$ ls myapp/
Dockerfile Pipfile.lock

こういった場合に --security-checks vuln,configと指定すると両方に対してスキャンが実行されます。

$ trivy fs --security-checks vuln,config --severity HIGH,CRITICAL myapp/
2021-07-09T12:03:27.564+0300    INFO    Detected OS: unknown
2021-07-09T12:03:27.564+0300    INFO    Number of language-specific files: 1
2021-07-09T12:03:27.564+0300    INFO    Detecting pipenv vulnerabilities...
2021-07-09T12:03:27.566+0300    INFO    Detected config files: 1

Pipfile.lock (pipenv)
=====================
Total: 1 (HIGH: 1, CRITICAL: 0)

+----------+------------------+----------+-------------------+---------------+---------------------------------------+
| LIBRARY  | VULNERABILITY ID | SEVERITY | INSTALLED VERSION | FIXED VERSION |                 TITLE                 |
+----------+------------------+----------+-------------------+---------------+---------------------------------------+
| httplib2 | CVE-2021-21240   | HIGH     | 0.12.1            | 0.19.0        | python-httplib2: Regular              |
|          |                  |          |                   |               | expression denial of                  |
|          |                  |          |                   |               | service via malicious header          |
|          |                  |          |                   |               | -->avd.aquasec.com/nvd/cve-2021-21240 |
+----------+------------------+----------+-------------------+---------------+---------------------------------------+

Dockerfile (dockerfile)
=======================
Tests: 23 (SUCCESSES: 22, FAILURES: 1, EXCEPTIONS: 0)
Failures: 1 (HIGH: 1, CRITICAL: 0)

+---------------------------+------------+-----------+----------+------------------------------------------+
|           TYPE            | MISCONF ID |   CHECK   | SEVERITY |                 MESSAGE                  |
+---------------------------+------------+----------------------+----------+------------------------------------------+
| Dockerfile Security Check |   DS002    | root user |   HIGH   | Last USER command in                     |
|                           |            |           |          | Dockerfile should not be 'root'          |
|                           |            |           |          | -->avd.aquasec.com/appshield/ds002       |
+---------------------------+------------+-----------+----------+------------------------------------------

確かにhttplib2というPythonライブラリの脆弱性とDockerfileに対する設定上の問題が検知されています。現在はデフォルトでは脆弱性のみの検知となっているため、両方検知したい場合は --security-checks オプションの指定が必須です。

簡単な説明は以上です。もし興味を持ってもらえたら、まずは使ってみてフィードバックを頂ければと思います。もし良いと思ってもらえたらGitHubのスターもお願いします。あと200増えると自分がこの2年以上ずっと目標にしていたことが達成されます。

カスタムポリシー

上では主にマネージドポリシーを使って検知する方法について説明しましたが、自分でポリシーを書きたい場合もあると思います。そういった場合はRegoを使ってカスタムポリシーを定義できます。カスタムポリシーではパース可能なファイルフォーマット(JSON, YAML, HCLなど)であれば標準で対応しているKubernetes, Dockerfile, Terraform以外にも対応可能です。詳細はドキュメントを参照してほしいのですが、ブログ内でも簡単に説明します。

aquasecurity.github.io

Regoでポリシーを書いて設定ファイルをテストすると言うとConftest が有名です。ただTrivyではConftestと違いカスタムポリシー内でメタデータセレクターが定義できます。

まずはカスタムポリシーの例を見ると早いと思うので載せておきます。

package user.kubernetes.ID001

__rego_metadata__ := {
    "id": "ID001",
    "title": "Deployment not allowed",
    "severity": "LOW",
    "type": "Custom Kubernetes Check",
    "description": "Deployments are not allowed because of some reasons.",
}

__rego_input__ := {
        "combine": false,
    "selector": [
        {"type": "kubernetes"},
    ],
}

deny[msg] {
    input.kind == "Deployment"
    msg = sprintf("Found deployment '%s' but deployments are not allowed", [input.metadata.name])
}

これはKubernetesの設定ファイルに対するポリシーでDeploymentを使おうとしたら弾くというものです。実際にはあり得ないようなポリシーですが例なのでポリシーそのもの自体はスルーしてください。

パッケージ名

パッケージ名は重要で、Trivyでは原則1パッケージ1ポリシーにする必要があります。理由は技術的な制約でメタデータを同じパッケージ内に複数定義するとエラーになるからなのですが、とりあえずパッケージ名はユニークにする必要があると思ってください。1パッケージ1ポリシーというのは deny が1つでなくてはならないという意味ではなく、 deny が複数あっても問題ありません。意味的に検知したいものが1つであるべき、つまり1つのTitleで表現できるものであれば同じパッケージ内に複数定義しても問題ありません。

上の例ではパッケージ名を user.kubernetes.ID001 にしています。パッケージ名に関する制約はユニークであること以外には特にないのですが、 $prefix.$config_type.$id などにすると分かりやすくかつユニークになるので良いと思います。全然違うルールでパッケージ名を定義しても特に影響は出ません。ただし、実行して欲しいパッケージのprefixはCLIオプションとして指定する必要があるためmainでもcustomでも何でも良いですが $prefix の部分は統一することをおすすめします。

メタデータ

__rego_metadata__ という名前で定義する必要があります。これは結果のテーブルやJSONメタデータとして出力されます。optionalなので定義しなくても動くのですが、全部N/AやUNKNOWNといった表示になってしまうため分かりやすさのためこちらも定義することをおすすめします。TitleやDescriptionがあるとひと目で何のポリシーかを理解しやすくなります。 deny で返されるメッセージでも理解はできるのですが、変数などが入ることが多いですし説明的になるので一覧の視認性という意味だとTitle等があるとやはり便利かなと思っています。

インプット

__rego_input__ という名前で定義する必要があります。今現在は2つのフィールドが定義可能で、 combineselector になります。こちらのフィールドもoptionalなので省略可能です。

Trivyでは原則ファイルごとに独立して処理するのですが、複数のファイルを相互参照したい場合もあります。例えばKubernetesのServiceがDeploymentのselectorと一致しているか、などです。そういった場合にcombine: trueにするとパースされたファイルが全て結合されて配列としてポリシーに渡されます。これはポリシーごとに制御可能なので、複数ファイルを同時参照したいポリシー以外ではcombine: falseにしておけば良いですし、省略すればデフォルトはfalseです。

もう一つはselectorですが、こちらはポリシーが入力として受け取りたいファイルの種類を定義できます。今回の例では type: kubernetes を指定しているので、同じディレクトリ内にDockerfileやTerraformのファイルがあってもこのポリシーにはinputとして渡されません。省略すると全部のファイルが渡されるだけなので、ポリシー側でフィルタする場合にはselectorは定義しなくても構いません。ただしDockerfileのsuccessに表示されたりしてノイズになりうるので指定しておくほうが無難ではあります。ちなみに type: kubernetes のようにオブジェクトになっているのは、そこに kind: Pod なども将来的に指定できるようにするためです。Trivy側で判別できない設定ファイルの場合は単純に jsonyaml として渡されるので、 type: json などのように指定します。現在は kubernetes, ansible, cloudformation のみが判別可能です。

使い方

以下のように --policy オプションでポリシーのディレクトリを指定します。同時に --namespaces で評価したいパッケージのprefixを指定します。デフォルトではマネージドポリシーのパッケージ( appshield.* ) しか評価されません。この例では user.* が評価されます。

$ trivy conf --policy /path/to/custom_policies --namespaces user /path/to/config_dir

Conftestとの違い

Trivyの設定ミス検知機能はConftestに大きな影響を受けています。Conftestはシンプルで軽量でとても良いツールだと思います。ですが構造化されたファイルに対するテストツールとして汎用的に作られているため、セキュリティ関連の設定ミスを主に検知したいと思うと少し物足りなくなりTrivyではConftestを使わずエンジンを自作しました。内部ではConftestのロジックを流用させてもらっている所も多いですしここで感謝を述べておきます。両者のツールの目指すところが異なるので使い分けが必要かと思います。

ですが何が違うのかわからない人もいるかも知れないので違いについていくつかピックアップして簡単に説明します。ドキュメントには表も載せています。一覧で確認する場合はそちらのほうが分かりやすいと思うのでご確認ください。

aquasecurity.github.io

マネージドポリシー

Conftestでは基本的に自分でポリシーを書く必要がありますが、TrivyではDockerfile, Kubernetes, Terraformのマネージドポリシーを提供しているため、ツールをインストールしてすぐ利用可能です。

メタデータ

TrivyではID, Title, Description, Severityなどのメタデータに対応しています。Conftestも deny, warn, violationが定義できSeverityに近いことはできますが、そういった情報でフィルタリングなどはできません。

ポリシー単位のcombine

Conftestでも --combine オプションが提供されているのですが、全体で有効化されてしまうためファイルの配列を受け取るように全てのポリシーを書き直す必要があります。Trivyではポリシー単位で定義可能なので一部のポリシーだけをcombine対応することができます。

Input Selector

上で述べたように __rego_input__ を定義することでKubernetesに関するものだけを入力として受け取る、といったことが可能です。

Goでのテスト

Goでカスタムポリシーをテストできるようにライブラリとして動作するようにしています。Regoのユニットテストでは入力をJSONなどで定義する必要がありますが、こちらはファイルのパースからやってくれるためテストデータも用意・管理しやすいです。

aquasecurity.github.io

また、そもそも想定していた入力とフォーマットが異なる場合はRegoのユニットテストでは検知できないためGoのテストを組み合わせるとよいかと思います。YAMLなどはパースしてどういう構造化データになるか容易に想定できますが、Dockerfileなどは難しいかと思います。ドキュメントには入力の例なども載せています。

aquasecurity.github.io

ただしGoのテストだけで言うかとそうは思っておらず、Regoのユニットテストと組み合わせると効果的かと思います。ユニットテストとインテグレーションテストのような関係だと考えています。

対応フォーマット

Conftestは現時点で以下の14種類に対応しています。

  • CUE, Dockerfile, EDN, HCL, HCL2, HOCON, Ignore files, INI, JSON, Jsonnet, TOML, VCL, XML, and YAML

一方でTrivyは6種類しか対応していません。

  • Dockerfile, HCL, HCL2, JSON, TOML, and YAML

互換性

TrivyはConftestとの互換性を意識して作っています。そのため、Conftestで利用していたポリシーをそのまま渡しても動く可能性が高いです。ですが結果が見にくいので、なるべく上述したメタデータなどの定義をおすすめします。

違いまとめ

もしマネージドポリシーが不要で自分でポリシーを書きたい、かつメタデータなども不要でメッセージだけで十分という場合はConftestを利用する方が良いです。またはTrivyで対応していないファイルフォーマットに対するテストをしたい場合もConftestになります。Conftestは機能もそこまで多くないため扱いやすく軽量です。

一方ですぐ使い始められるスキャナーが欲しいという方はマネージドポリシーを提供しているTrivyを使う方が良いです。CI/CDで使うのに適した多くの機能がありますし脆弱性も同時に検知できるため、プロジェクト内の脆弱性と設定ミスをCI/CDで同時に検知したい場合もTrivyが適しているかと思います。

余談

マネージドポリシーを提供するAppShieldプロジェクトは別のチーム担当で自分は最初関わっていませんでした。ですがTrivy側の開発が終わってあとはリリースに向けてAppShieldの完成を待つだけ、という状態で進捗を確認したらほぼ0で愕然とする事件がありました。このままではリリースには間に合わないということで自分が首を突っ込んで強引にポリシーの整備をしました。そういった背景もあって自分はここ数週間休日返上で働いていたのですが、正直全部のポリシーを直すには到底時間が足りず他に足したいポリシーも間に合わずでした。現時点でもかなり良くはなったのですが、個人的にはまだまだ満足していないので暖かく見守りつつフィードバック、あわよくばコントリビュートしてもらえるととても嬉しいです。

また、AppShieldはTrivyのみではなくAqua Securityの他のOSSであるStarboardでも利用される予定ですし、他にも複数プロジェクトでOPA/Regoを利用しています。そういうわけで社内の統一Regoフォーマットを策定していたのですが、関係者が多すぎて意見がまとまらずリリース直前になってもまだ定まっていない状況でした。このままだとまずいなということでフォーマットの変更があった場合に問題なくマネージドポリシーの移行ができるようにマネージドポリシーはGitHub Package Container registry (GHCR) にOCI artifactとして公開しています。

github.com

利点としてはコンテナイメージ同様にタグが付けられ、aliasが使える点です。Trivy側としてはv1をずっとpullしておくだけで1.0.0→1.1.0→1.2.0と上がっていっても追従できますし、破壊的変更が入った場合でもv2をリリースすれば新しいバージョンのTrivyではv2をpullするようにするだけで済みます。古いクライアントはv1をpullし続けるので動作しなくなるようなこともありません。GHCRが6/21にGAしたのを見て急遽実装しました。GHCRには本当に感謝です。

github.blog

まとめ

Infrastructure as Codeの脆弱な設定をデプロイ前に検知したい場合には是非TFsecやTrivyを試してみてご意見頂けますと幸いです。

あとCI/CDと言ってますが主にCIです。デプロイ前に落としたい場合もあるかもしれないのでCDの可能性も0ではないと思って一応CI/CDと言っているのですが、一番の理由はCIとだけ言うと何か字面が寂しいからです。