tfpolicy を使った Terraform のセキュリティ対策
メディア統括本部 サービスリライアビリティグループ(SRG)の長谷川(@rarirureluis)です。
#SRG(Service Reliability Group)は、主に弊社メディアサービスのインフラ周りを横断的にサポートしており、既存サービスの改善や新規立ち上げ、OSS貢献などを行っているグループです。
tfpolicy でサプライチェーン攻撃を防ぐ.terraform.lock.hcl があるじゃない1. Terraform Policy は HCL で provider を止めるSentinel / OPA との違い2. ローカルで完結し、評価タイミングを分けられる3. tfpolicy のインストール3.1 前提3.2 バイナリの入手と配置3.3 インストール確認4. 使い方: ディレクトリを分けて validate / test する4.1 ディレクトリ構造4.2 validate — 構文検証4.3 test — ポリシーテスト4.4 日常コマンドの例4.5 terraform init -policies — provider ダウンロード前の評価何が起きるかコマンド例前提と制約(重要)tfpolicy test との役割分担4.6 テストの書き方(要点)5. 実際の出力(PASS / FAIL)5.1 PASS: validate5.2 PASS: 全テスト成功5.3 FAIL: ポリシー違反(expect_failure 無し)5.4 FAIL: expect_failure なのにポリシーを通過した5.5 FAIL: validate(構文エラー)6. ポリシー例(Cloudflare DNS を題材に)6.1 Provider バージョンと公式ソース6.2 運用規約(proxied / TTL / MX)6.3 DNS セキュリティCIDR 判定について6.4 Provider リリース成熟期間(サプライチェーン攻撃対策)仕組みテスト7. チーム全員で強制する7.1 原則: 防御を重ねる7.2 A+B: CI を required check にする(必須)7.3 C: pre-commit で validate / test7.4 D: terraform wrapper で init に policies を強制7.5 E: 各人の terraform init -policies(toolchain 前提)7.6 F: HCP Terraform policy set(GA 後の本命)7.7 導入チェックリスト終わりに参考
この記事は AI が出力していますが人間による添削、レビューを行っています。
tfpolicy でサプライチェーン攻撃を防ぐ
注意: Terraform Policy は執筆時点で Beta です。
悪意ある Terraform provider が Registry に載り、開発者の でマシンに落ちる——サプライチェーン攻撃の典型形です。.terraform.lock.hcl は「一度信頼した同じバージョン」の差し替えは止めますが、新しいバージョンを入れる瞬間と、初回から upstream が汚染されている場合は止められません。Terraform Policy(CLI 名: )は、provider のダウンロード直前に HCL で書いたポリシーを評価し、違反なら取得そのものを止められます。
本稿の結論を先に書きます。
- HCL のまま provider / resource に対するルールを書ける(Sentinel や Rego を新規学習しなくてよい)
- で mock 回帰テストを手元と CI に載せる
- (Terraform 1.16+ の experimental / GA 後)で、実 init の download 前に同じルールを効かせる
- 特に 「リリース後 72 時間未満の provider を拒否する」 といったポリシーが、サプライチェーン対策になる
.terraform.lock.hcl があるじゃない
.terraform.lock.hcl は「アップデート時に汚染された新バージョンの検知」はできません。
公式ドキュメントが明示しているモデルは Trust On First Use (TOFU) です。
- 初回インストール時
その時点のパッケージのチェックサムを lock に記録する(+ registry 署名付き zh: ハッシュも取り込む)
- 以降の同じバージョン
ダウンロードしたバイナリが lock のハッシュと一致しないとエラー
つまり防ぐのは次のような攻撃です。
| シナリオ | lock で防げる? |
|---|---|
| 一度信頼した 同一 version のバイナリが後から差し替えられる(registry 侵害・MITM・mirror 汚染) | 防げる |
| CI / 他メンバーが別バイナリを使う | 防げる |
| terraform init -upgrade で 新しい version を入れる | 防げない(新 version のハッシュに差し替わるだけ) |
| 初回/upgrade 時点で upstream が既に汚染 | 防げない |
1. Terraform Policy は HCL で provider を止める
Terraform Policy は HashiCorp が提供する policy-as-code です。ポリシーを HCL で書き、Terraform provider のリソース属性や meta 情報に条件をかけます。
ローカルでは CLI で validate / test を実行します。HCP Terraform(TFC)に接続すれば、run 中に policy set として評価できます。
Sentinel / OPA との違い
| 観点 | Terraform Policy | Sentinel | OPA |
|---|---|---|---|
| 言語 | HCL | Sentinel DSL | Rego |
| Terraform との統合 | provider / resource / module をネイティブ参照 | plan import 経由 | plan JSON をパース |
| 評価タイミング | init 前・plan 後・apply 後 | plan 後中心 | plan 後中心 |
| 関係リソース参照 | 手動走査 | 手動走査 | |
| Data source | で評価時に参照可 | 不可 | 不可 |
Terraform を日常的に書いているチームなら、新しい DSL を学ばずにガバナンスをコード化できるのが利点です。
2. ローカルで完結し、評価タイミングを分けられる
- HCL のまま書ける — Terraform と同じ構文・関数( 等)を使える
- Provider 統合 — の などを直接条件に書ける
- ローカルテスト — で pass/fail ケースを mock できる。CI に載せる前に手元で回せる
- 複数評価ステージ — provider は init 前、resource は plan / apply 後など、必要なデータが揃うタイミングで評価される
- enforcement level — / / をポリシー単位で選べる
3. tfpolicy のインストール
3.1 前提
| 項目 | 内容 |
|---|---|
| ランタイム | TFC 連携・ には Terraform v1.16 以降(ドキュメント記載)。 は experimental ビルド限定(§4.5) |
| ローカル test | バイナリ単体で validate / test 可能 |
| 配布形態 | Homebrew 公式 formula は執筆時点で無し。バイナリ zip を配置する |
| バージョン確認 | releases.hashicorp.com/tfpolicy |
3.2 バイナリの入手と配置
OS / CPU に合わせた zip をダウンロードし、 上に置きます。
| プラットフォーム | アーカイブ名の例 () |
|---|---|
| macOS Apple Silicon | |
| macOS Intel | |
| Linux amd64 | |
| Linux arm64 |
3.3 インストール確認
コマンドは 3 つだけです。
| コマンド | 役割 |
|---|---|
| の構文・構造チェック | |
| をポリシーに対して実行 | |
| バージョン表示 |
4. 使い方: ディレクトリを分けて validate / test する
4.1 ディレクトリ構造
ポリシーは Terraform 設定(例: )と分離し、ガバナンス層として独立させます。将来 TFC の policy set に VCS ルートとして繋ぐときもこの形が自然です。
- ポリシー: (1 ファイルが 1 評価単位)
- テスト: ( で対象ポリシーを指定)
4.2 validate — 構文検証
| フラグ | 説明 |
|---|---|
| ポリシーファイルまたはディレクトリ。省略時はカレントディレクトリ | |
| または (省略時は ) |
4.3 test — ポリシーテスト
| フラグ | 説明 |
|---|---|
| ポリシーのパス。省略時はカレントディレクトリ | |
| テストのパス。省略時は と同じディレクトリ | |
| または (省略時は ) |
入力変数を使うポリシーでは、環境変数 で値を渡せます。
4.4 日常コマンドの例
リポジトリルートから:
4.5 — provider ダウンロード前の評価
はポリシーの正しさを mock で検証するだけです。実際の init で provider を落とす直前に同じポリシーを評価するのが です。
何が起きるか
- を実行する
- Terraform が backend 初期化後に policy client を立てる
- provider / module の バージョンが確定した直後・バイナリ取得前 に / を評価する
- 違反なら download を中止する(メッセージ例: )
サプライチェーン向けポリシーはここに効きます。
| ポリシー例 | setup で見ること |
|---|---|
| かつ | |
| Registry の が 72h 以上前 |
コマンド例
設定ディレクトリとポリシーを分離している場合、chdir しつつ policies は相対パスで渡します。
複数パスを渡す場合は を繰り返します(CLI は string slice)。
前提と制約(重要)
| 項目 | 内容 |
|---|---|
| Terraform バージョン | Policy 連携は v1.16 以降(ドキュメント記載)。古い pin のままでは init 組み込み評価は使えない |
| 実験ビルド | は experimental build(alpha / 開発スナップショット)でのみ有効。安定版バイナリでは で拒否される |
| local backend | 動作する。HCP の entitlement チェックは backend が供給できない場合スキップされる |
| 既存キャッシュ | 既に に載っている provider は再 download しない。ポリシーを初めて効かせるときはクリーンな init、または意図的な を検討する |
| Actions との違い | Terraform Actions( / )は apply 時の day-2 操作。init 前フックではない。download 前ガードには使えない |
との役割分担
| 入力 | の mock | 実際の root module / lock / required_providers |
| タイミング | いつでも(CI・手元) | init 中・download 直前 |
| 目的 | ポリシーの回帰テスト | そのマシン上の実 init を止める |
| チーム強制 | CI で全員同じゲート | 各人の CLI に載せる必要あり(§7) |
両方必要です。test だけでは「誰かが 無しで init した瞬間」に抜けます。init だけでは「ポリシー自体が壊れていないか」を mock で固定できません。
4.6 テストの書き方(要点)
| 記法 | 意味 |
|---|---|
| リソース属性の mock | |
| provider / module の meta(source, version 等) | |
| ポリシー違反が起きることを期待する | |
| 依存 mock 用。このブロック自体はポリシー評価しない |
テスト結果の は「ポリシーが意図どおり動いた」ことを意味します。 のケースでは、リソースが拒否されて初めて になります。
5. 実際の出力(PASS / FAIL)
以下は 0.1.0 を Cloudflare DNS 向けポリシーで実行した実出力です(ANSI カラーは除去済み)。
5.1 PASS: validate
5.2 PASS: 全テスト成功
の違反ケースも含め、意図どおり判定できた状態です。ケース名の がテスト結果としては になる点に注意してください。
5.3 FAIL: ポリシー違反( 無し)
プライベート IP の A レコードを、違反を期待せずに置いた場合です。ポリシーの が診断にそのまま出ます。
テスト側:
出力:
5.4 FAIL: なのにポリシーを通過した
「違反するはず」と書いたのに通過した場合です。ポリシーが緩すぎる・テストデータが弱い、といった回帰に使えます。
テスト側:
出力:
5.5 FAIL: validate(構文エラー)
壊れた HCL を validate した例です。
出力:
6. ポリシー例(Cloudflare DNS を題材に)
対象例は Cloudflare DNS(provider v5, )です。既存の Terraform 設定には手を入れず、ポリシー側だけで規約をコード化します。
6.1 Provider バージョンと公式ソース
公式ソースかつ v5 以上を必須にします。fork や古い major の混入を init 前に止められます。
- / は provider の meta 属性
- は Terraform Policy 組み込み
6.2 運用規約(proxied / TTL / MX)
Cloudflare では proxied レコードの TTL は Auto()が定石です。逆に A/CNAME で Auto を使うなら proxied であるべき、という双方向の規約と、MX priority の正値チェックを入れます。
で type を絞るのがポイントです。 は A/CNAME 向け属性なので、MX には だけを当てます。MX に を参照すると で評価エラーになります。
6.3 DNS セキュリティ
パブリック DNS にプライベート IP やワイルドカードを載せない、という最低限のガードです。
CIDR 判定について
公式 examples でも CIDR 重なりは Go プラグイン()で実装しています。依存を増やさず、RFC1918 + loopback を prefix / regex で判定するのが手軽です。厳密な CIDR 演算が必要になったらプラグインに切り替えるのが筋です。
6.4 Provider リリース成熟期間(サプライチェーン攻撃対策)
Provider のリリース直後は、悪意あるコードが混入していてもコミュニティの検知が追いついていない可能性があります。リリース後一定期間(ここでは 72 時間)を経過したバージョンのみを許可することで、リスクを下げます。
Terraform Policy の で Registry API を叩き、 を取得して現在時刻と比較します。
仕組み
- で を呼び、(RFC3339)を取得
- で「72 時間前」の cutoff を算出
- RFC3339 はそのまま文字列比較できない(tfpolicy の は数値のみ)ため、 + で数値化して比較
- で 404 応答時に が無くても locals 評価が落ちないようにする
テスト
このポリシーは評価時に Registry へ HTTP で出ます。オフライン環境やレート制限のある CI では、stage を分けるか、ネットワーク依存ポリシーを分離して skip できるようにしておく必要があります(§7.3 も参照)。ト
7. チーム全員で強制する
一人の手元だけ を回しても意味がありません。サプライチェーン攻撃は チームの誰か一人の init が成功した時点 で成立します。防御は「善意の手順」ではなく、省略できないゲート で重ねます。
7.1 原則: 防御を重ねる
| レイヤ | 何を止めるか | 省略できるか |
|---|---|---|
| A. CI | 壊れたポリシー・テスト回帰の main 流入 | PR 必須 + required check なら不可 |
| B. branch protection | CI 赤のまま merge | admin bypass を閉じれば不可 |
| C. pre-commit / ローカル hook | commit 前の validate/test 忘れ | で回避可(弱い) |
| D. wrapper | 無しの init | や別パスで回避可(中) |
| E. (TF ≥ 1.16 exp/GA) | そのマシン上の provider download | wrapper 無しだと任意(手順依存) |
| F. HCP Terraform policy set(GA 後) | リモート run の setup/plan | workspace を TFC に寄せれば最強 |
local backend + 手元 apply の今は A+B を必須、C+D を推奨、E/F は toolchain と GA 待ち、が現実的です。
7.2 A+B: CI を required check にする(必須)
workflow があるだけでは「失敗しても merge できる」状態です。
GitHub の branch protection(または ruleset)で:
- 対象ブランチ: /
- Require status checks to pass に tfpolicy job を追加
- Require a pull request before merging(direct push 禁止)
- 可能なら Do not allow bypassing the above settings(admin も通さない)
これで「ポリシーやテストを壊す変更」は main に入りません。
入らないもの: ローカルで して汚染 provider を落とす行為。それは D/E の領域です。
7.3 C: pre-commit で validate / test
に local hook を足す例です。チームは を一度だけ。
| 注意 | 内容 |
|---|---|
| が PATH に必要 | 無いと hook が fail。onboarding に install を書く |
| で回避可能 | 強制力は CI より弱い。補助層 |
| network | は Registry に出る。オフライン開発では stage を分けるか skip 設計が要る |
7.4 D: wrapper で init に を強制
「手順を覚えさせる」は失敗します。 上で をラップし、 のときだけ を足します。
7.5 E: 各人の (toolchain 前提)
§4.5 のコマンドをチーム標準にする条件:
- Terraform を Policy 対応バージョン(ドキュメント上 v1.16+)かつ experimental が有効なビルド(alpha / 開発用。安定版は 拒否)に揃える
- onboarding に「init は必ず 」または wrapper 導入を書く
- CI でも同じ init を回す(experimental TF を PIN インストール)
実験ビルドを全員に配るコストが高い間は、A+B+C を厚くし、E は GA 後に本線化するのが無難です。
7.6 F: HCP Terraform policy set(GA 後の本命)
- VCS に を置く
- HCP で Terraform policy の policy set を作り VCS 接続
- 対象 workspace にアタッチ
- setup 段階の provider ポリシーが リモート run の init 相当で 評価され、違反時は download 前に停止
これが「誰かの手元手順」に依存しない唯一の形です。Beta のうちは接続検証まで、mandatory ブロックは GA 後。
7.7 導入チェックリスト
| # | 作業 |
|---|---|
| 1 | tfpolicy の CI を required status check にする |
| 2 | direct push を禁止し PR のみ |
| 3 | onboarding に install + 日常コマンドを書く |
| 4 | (任意)pre-commit に §7.3 を追加 |
| 5 | (任意)§7.4 wrapper を共有する |
| 6 | TF 1.16+ 実験ビルド or GA 後に §4.5 を標準 init にする |
| 7 | GA 後に HCP policy set |
終わりに
.terraform.lock.hcl だけでは止まらない「新しい provider を落とす瞬間」を、tfpolicy は HCL のまま download 前に止められます。手元では validate / test、チームでは CI の required check を必須にし、手元 init は wrapper か GA 後の HCP policy set で塞ぐ——それが本稿の実務ラインです。
Terraform(インフラ)はアプリより下のレイヤーで動くぶん、設定、レビュー漏れが影響が大きいです。
今後 Terraform を使って新しいものを導入する際に最初にポリシーを決めてから実装する、というフローをやってみようと思いました。
参考
SRGにご興味ありましたらぜひこちらからご連絡ください。
