
GitHubのユーザーをTerraformで管理しているときの問題点
GitHubの組織メンバーやリポジトリのアクセス権限をTerraformで管理すると、誰がどのリポジトリにアクセスできるかがコードとして可視化され、変更にはPRとレビューが必要になります。手作業でポチポチ設定するよりも安全で、監査もしやすくなります。
10Xでもこの方針でGitHub管理用のTerraformモジュールを作り、GitHubリソースをコード管理しています。
module "repository" { source = "github-management-module" name = "repository-name" access = { users = [ { name = "alice", permission = "push" }, { name = "bob", permission = "push" }, ] } }
この access.users[].name に指定するGitHub IDは自由入力のため、いくつかの問題が起こり得ます。
- 退職者の残留
- 組織を離れたメンバーのIDが設定に残り続ける
- typo
aliceをalceと書いても気づけない (レビュープロセスがあっても見逃す場合もある)
- 想定していないユーザーの指定
- Outside Collaboratorとして意図しないユーザーにリポジトリのアクセス権限が付与される
- typoしたユーザーがGitHub上に存在していたときも同様
特に3つ目が危険です。10XではSAML SSOを設定しているため、Org招待が飛んでも 10x.co.jp ドメインでフィルタされ、組織メンバーとして参加される被害は発生しません。しかし、リポジトリのOutside Collaboratorへの追加はSAMLの認証フローを経由しないため、typoや誤記で意図しない外部ユーザーにリポジトリの読み取り・書き込み権限が付与される可能性があります。
リポジトリの数は数百とあり、レビューだけでこれらを防ぐのは限界があります。
Conftestと外部データの組み合わせ
この問題に対して、Conftest を使ったポリシーチェックを導入しました。
Conftestは通常、設定ファイルの内容をRegoで静的に検証するツールです。今回はGitHub APIから取得した組織メンバー一覧を外部データとしてConftestに渡すことで、「そのGitHub IDは本当に組織に存在するか」という動的なチェックを静的解析の枠組みで実現しています。
GitHub組織のメンバー一覧をSingle Source of Truth(SSoT)として、Terraformファイル内のGitHub IDをそれに対して検証します。
データの取得
CIで gh コマンドを使い、組織メンバー一覧をJSONに書き出します。
members=$(gh api --paginate "/orgs/10xinc/members" --jq '.[].login' | jq -R . | jq -s .)
jq -n --argjson members "$members" '{github_members: $members}' > /tmp/github-members.json
これを conftest test --data /tmp/github-members.json で渡すと、ポリシー内から data.github_members として参照できます。メンバーの増減はGitHub APIから毎回取得するため、メンバーリストファイルのメンテナンスは不要です。
ポリシー
ポリシーの核となるロジックはシンプルです。
package github.invalid_github_member
import rego.v1
valid_members := data.github_members
outside_collaborators := {
"outside-user-1",
"outside-user-2",
...
}
is_valid_member(github_id) if {
github_id in valid_members
}
is_outside_collaborator(github_id) if {
github_id in outside_collaborators
}
valid_members がGitHub APIから渡される動的なデータ、outside_collaborators が業務上アクセスが必要な外部コラボレーターの許可リストです。
チェック対象は4箇所あります。
| ルール | 対象 | レベル |
|---|---|---|
deny_invalid_repo_user |
リポジトリアクセス権限 (access.users[].name) |
deny |
deny_invalid_team_member |
チームメンバー (team_members[].github) |
deny |
warn_invalid_tfvars_user |
tfvarsのユーザー (users[].id) |
warn |
warn_invalid_tfvars_team_member |
tfvarsのチームメンバー (teams[].members[]) |
warn |
リポジトリアクセスとチームメンバーは deny(CIを失敗させる)、tfvarsは warn(警告のみ)としています。リポジトリへのアクセス付与は組織に所属しているメンバーに対して行う操作なので、組織にいないユーザーが書かれていたらブロックするのが妥当です。一方、tfvarsの users[].id は組織への招待(Org Invite)にも使われるため、まだ組織に参加していないユーザーのIDが含まれるケースがあります。これを deny にしてしまうと招待のPRが通らなくなるため、warn に留めています。
例として deny_invalid_repo_user の実装を示します。
deny_invalid_repo_user contains {"msg": reason} if {
some module_name, module_configs in input.module
module_config := module_configs[0]
contains(module_config.source, "tfmodule-gh-repo-kit")
module_config.access.users
user := module_config.access.users[_]
github_id := user.name
not is_valid_member(github_id)
not is_outside_collaborator(github_id)
reason := sprintf(
"`module.%v`: access.users[].name - @%v は 10X のメンバーではありません",
[module_name, github_id],
)
}
入力のHCLをたどって access.users[].name を取り出し、valid_members にも outside_collaborators にも含まれなければ違反とします。
テスト
ポリシーのテストは conftest verify で実行します。with キーワードでモック入力とモックデータを注入できるため、GitHub APIを叩かずにロジックを検証できます。
test_deny_invalid_repo_user if {
result := deny_invalid_repo_user with input as {"module": {"repository": [{
"source": "../../../../modules/tfmodule-gh-repo-kit",
"access": {
"users": [
{"name": "babarot", "permission": "push"},
{"name": "invalid_user", "permission": "push"},
],
},
}]}}
with data.github_members as ["babarot", "sota1235"]
count(result) == 1
some violation in result
contains(violation.msg, "@invalid_user")
}
CIへの組み込み
terraform plan ワークフローの中で、plan実行の前段にConftestを組み込んでいます。
- name: Fetch GitHub organization members run: | members=$(gh api --paginate "/orgs/10xinc/members" --jq '.[].login' | jq -R . | jq -s .) jq -n --argjson members "$members" '{github_members: $members}' > /tmp/github-members.json - name: Run conftest against HCL files uses: ./.github/actions/conftest with: policy_path: ./policy/tf check_type: hcl working_directory: ${{ matrix.dir }} data_file: /tmp/conftest-data.json inline_comment: "true"
チェック対象はPRで変更のあった .tf ファイルのみに絞っています。違反が検出されるとPRの該当行にインラインコメントが投稿されるため、レビュアーもすぐに気づけます。
まとめ
Conftestは設定ファイルの静的解析ツールですが、--data フラグで外部データを渡すことで、GitHub組織の状態をSSoTとした動的なチェックが可能になります。メンバー一覧を毎回APIから取得するため、ポリシーファイル自体のメンテナンスはほぼ不要です。
10Xでは今回紹介したGitHubメンバーの検証以外にも、GCPリソースの命名規則やラベルの必須チェックなど、30以上のポリシーをConftestで運用しています。Conftestの導入経緯や全体像についてはこちらの記事で紹介しています。