GitHubへのメンバー追加をConftestでバリデーションする

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
    • alicealce と書いても気づけない (レビュープロセスがあっても見逃す場合もある)
  • 想定していないユーザーの指定
    • 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の導入経緯や全体像についてはこちらの記事で紹介しています。