🐧

tfprovidercheck - 危険な Terraform Provider の実行を防ぐ

2023/11/05に公開

危険な Terraform Provider の実行を防いで Security を担保するためのコマンドラインツールを作ったので紹介します。

https://github.com/suzuki-shunsuke/tfprovidercheck

Example
# Only google provider and azurerm provider are allowed
$ cat .tfprovidercheck.yaml
providers:
  - name: registry.terraform.io/hashicorp/google
    version: ">= 4.0.0"
  - name: registry.terraform.io/hashicorp/azurerm

# tfprovidercheck fails because aws provider is disallowed
$ terraform version -json | tfprovidercheck
FATA[0000] tfprovidercheck failed                        error="this Terraform Provider is disallowed" program=tfprovidercheck provider_name=registry.terraform.io/hashicorp/aws tfprovidercheck_version=0.1.0

今回の話は以前自分が書いた以下の記事とも関係しています。

https://zenn.dev/shunsuke_suzuki/articles/secure-github-actions-by-pull-request-target

悪意のある Terraform Provider によって terraform plan, apply といったコマンドを通じて任意のコマンドの実行されるのを防がなければなりません。

例えば Mercari では事前にインストールされた Terraform Provider しか実行できないようにすることでこれを実現しています。

https://engineering.mercari.com/en/blog/entry/20220121-securing-terraform-monorepo-ci/

tfprovidercheck はそれとは別のアプローチで、設定で明示的に許可されていない provider が使用されていないかをチェックします。
tfprovidercheck はサブコマンドを持たないとてもシンプルなコマンドラインツールです。

まずは tfprovidercheck の設定を用意します。
次の例では AWS provider と google provider を許可しています。

.tfprovidercheck.yaml
providers:
  - name: registry.terraform.io/hashicorp/aws
    version: ">= 3.0.0"
  - name: registry.terraform.io/hashicorp/google
    # version is optional

tfprovidercheck は使用されている Provider とその version のリストを terraform version -json コマンドで取得しますが、正確なリストを取得するために terraform version -json を実行する前に terraform init を実行します。
そして terraform version -json の出力を標準入力として tfprovidercheck に渡してあげます。

terraform init
terraform version -json | tfprovidercheck

許可されていない provider や、許可していない provider のバージョンが使用されていると tfprovidercheck は失敗します。

許可されていない provider の実行を防ぐため、 tfproviderchekc は terraform validate, plan, apply といったコマンドの前に実行します。

自分の理解が正しければ危険な Provider が使用されていても terraform init や terraform version は安全なはずですが、間違ってたら教えてください。

設定方法

tfprovidercheck はいくつか設定方法があります。優先順位の高い順に並べると以下のとおりです。

  1. コマンドラインオプション -config (-c) で設定ファイルのパスを指定
  2. 環境変数 TFPROVIDERCHECK_CONFIG_BODY で設定(この環境変数の値は設定ファイルの内容と同じ)
  3. 環境変数 TFPROVIDERCHECK_CONFIG で設定ファイルのパスを指定
  4. カレントディレクトリの .tfprovidercheck.yaml

設定では許可する provider のフルネームと version 制約のリストを記述します。

.tfprovidercheck.yaml
providers:
  - name: registry.terraform.io/hashicorp/aws
    version: ">= 3.0.0"
  - name: registry.terraform.io/hashicorp/google
    # version is optional

name は必須で、正規表現や glob はサポートしておらず、完全一致でなければなりません。
name がわからないという方は、 terraform version -json を実行すれば分かります。
version 制約は任意で、省略すると任意のバージョンを許可します。
version は Terraform でも使われている hashicorp/go-version の version constraints です。

pull_request_target で設定の改竄を防ぐ

これは以前自分が書いた記事とも関係していますが、 workflow や設定ファイルが改竄されて tfprovidercheck によるバリデーションを無効化されるのを防ぐ必要があります。
GitHub Actions では pull_request_target を使い、 tfprovidercheck の設定を TFPROVIDERCHECK_CONFIG_BODY を使って workflow file に直接書き込むことで改竄を防ぐことができます。

- run: terraform version -json | tfprovidercheck
  env:
    TFPROVIDERCHECK_CONFIG_BODY: |
      providers:
        - name: registry.terraform.io/hashicorp/aws
          version: ">= 3.0.0"

追記: required_providers block や .terraform.lock.hcl との違い

SNS でのこの記事への反応で「required_providers block.terraform.lock.hcl との違いが良くわからない」というのが見られたので追記しておきます。

まず .terraform.lock.hcl は provider の allow list としては機能しません。
というのも .terraform.lock.hcl に含まれない provider が使われている状態で terraform init を実行すると自動で .terraform.lock.hcl に provider が追加され、 terraform init は成功するからです。
自分の理解では .terraform.lock.hcl にない provider が使われている場合にエラーにするといった機能はないはずです。

required_providers block に関しては確かに required_providers block で定義されていないサードパーティ provider を使おうとしてもそもそも Terraform がどこから provider を取得すればいいのか分からないので失敗するはずであり、 allow list として機能しているようにも思えます。
ただ、 危険な provider を使った resource block が記述できるということは当然それを許可する required_providers block も簡単に記述できるので、危険な provider の利用を防ぐことはできないと言って良いでしょう。
これは悪意があろうがなかろうが簡単にできるので、ほとんどセキュリティ的な対策にはなっていません。

それは tfprovidercheck でも同じことが言えるのではと思うかもしれませんが、明確な違いがあります。
pull_request_target で設定の改竄を防ぐ にも書いた通り、 tfprovidercheck の設定は改竄を防ぐ方法があります。
改竄を防ぐというところまでやらなくても、 tfprovidercheck の設定を一箇所で管理し、適切なコードコメントを書いて管理者を codeowner にするぐらいのことをやれば、悪意なしにうっかり問題のある provider を許可してしまう、管理者の知らないところで問題のある provider が使われているといったリスクは低いでしょう(尤も、セキュリティのことを考えれば改竄を防ぐところまでやるべきですが)。
Monorepo の場合、 working directory ごとに設定する required_providers block と異なり、 tfprovidercheck は設定を共通化し一箇所で管理できます。
working directory ごとにも設定できますが、 tfprovidercheck はセキュリティのガードレール的なものなので working directory ごとに設定するよりは一箇所で一元管理すべきものです。
GitHub の CODEOWNERS を設定し、 tfprovidercheck の設定変更には管理者の review を必須にすることができます。
また tfprovidercheck は用途が明確なので安易に 危険な provider が追加されにくいし、レビューでも見過ごされにくいという意味合いもあるでしょう。

required_provider block でも codeowner を設定して管理者のレビューを必須にできるだろうと思うかもしれませんが、 required_provider block は任意の *.tf ファイルで定義できるので、管理者を codeowner にしようと思ったら管理者を任意の *.tf の codeowner にせざるを得ず、 required_provider block に関してのみ codeowner にするということが出来ません。
命名規則で制限すればいいのではと思うかもしれませんが、悪意があれば命名規則を無視できるのでセキュリティ的には意味がありません。
仮に管理者を codeowner に出来たとしても Monorepo では全ての working directory の required_provider block のレビューを管理者がしないといけないみたいな話になり、 Monorepo の規模に比例して管理者の負担が増え、スケールしません。
tfprovidercheck の設定を一箇所で管理するのであれば Monorepo が大きくなっても負担は増えません。

tfprovidercheck の設定は Terraform のコードと別に管理できるので、 Terraform のコードの ownership は各 product team に持たせつつ、セキュリティに関する部分は SRE や Platform Engineer が ownership を持つということが自然に出来ます。

なお、上記の話は SRE や Platform Engineer といったロールの人が Terraform Monorepo の workflow を整備し、各プロダクトの Terraform のコードの管理を Product Team に委譲するようなモデルを前提にしている部分があり、なかなかピンとこない人もいるかもしれません。

追記が長くなりましたが、 required_providers block や .terraform.lock.hcl は危険な provider の実行を防ぐのには不十分だと思います。

さいごに

以上、危険な Terraform Provider の実行を防いで Security を担保するためのコマンドラインツールを紹介しました。

https://github.com/suzuki-shunsuke/tfprovidercheck

前からやりたいと思いつつずっと後回しになっていましたが、やれてよかったです。

「うちではこうやって防いでるよ」とか「こうすればいいんでは」とか「これだと防げてなくない?」とかあればコメントもらえると助かります。

Discussion