【GithubActions】サービスアカウントキーなしでactionsからGCPリソースを扱う via Terraform

# 概要
これまで（数ヶ月前まで）はGCP外のサービスからGCPリソースを操作するためには、
操作を行うサービスアカウントを作成->サービスアカウントキーを発行し、このキーをサービス側になんらかの形式で渡すことが必要でした。
しかし現在では、Githubactionsがワークフローに[OIDC機構を導入した](https://github.blog/changelog/2021-10-27-github-actions-secure-cloud-deployments-with-openid-connect/)ため、[Workload Identity 連携](https://cloud.google.com/iam/docs/configuring-workload-identity-federation#github-actions)を用いることでサービスアカウントキーの発行なくクラウドリソースへのアクセスが可能となっています。本稿ではその導入についてを記載します。

※N番煎じですが、Poolの準備にterraformを使っているものはなかったので記載しました。
※本稿ではOIDCの仕組みについては記載していません。

# 効用
- サービスアカウントキーの管理コスト（定期ローテーション等）がなくなる
- 漏洩リスクの低下
    - 仮に漏洩したとしても、ワークフローを実行するたびに短い時間（N分~N時間の単位）有効なアクセスキーを発行することになるため、攻撃可能な時間が小さくなる
- WorkloadIdentityPoolの利用ログをみることが可能

# やっていく
すでにGithubActionsでGCPリソース操作をしている状態からの改修を想定した記載となっています。
これから導入する場合はdiffのマイナス部分を無視してください。

## before/afterのイメージ
おそらく[setup-gcloud](https://github.com/google-github-actions/setup-gcloud)を利用し、シークレットに格納しているサービスアカウントキーを渡すことで認証を行っていたかと思います。

```yaml
- name: Set up Cloud SDK
  uses: google-github-actions/setup-gcloud@v0
  with:
    service_account_key: ${{ secrets.GCP_SA_KEY }}
    export_default_credentials: true
```

これを[auth](https://github.com/google-github-actions/auth) actionに変更し、WorkloadIdentityPoolを通じてサービスアカウントへ接続することで、サービスアカウントキーを発行・管理することがなくなります。

```yaml
- name: Authenticate google cloud
  uses: google-github-actions/auth@v0
  with:
    workload_identity_provider: ${WORKLOAD_IDENTITYPOOL_ID}
    service_account: ${SA_NAME}
```

## WorkloadIdentityPoolの準備
gcloudコマンドでの設定方法は[こちら](https://github.com/google-github-actions/auth#setting-up-workload-identity-federation)に記載されていますので、ここではterraformで準備をします。
なおterraformに関しても[module](https://github.com/terraform-google-modules/terraform-google-github-actions-runners/tree/master/modules/gh-oidc)が存在しますのでこれを利用するのが良いでしょう。ここでは説明のためにmoduleなしで記述しています。

1. Workload Identity連携を使用するために必要なAPIを有効化
1. リソース操作を実施するサービスアカウントを作成
1. WorkloadIdentityPoolを作成
1. WorkloadIdentityPoolプロバイダを作成
    1. 属性マッピングについては[こちら](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect#understanding-the-oidc-token)も参照
    1. Option: ここでは特定オーナーのみ受け入れるよう属性条件を設定
1. WorkloadIdentityPoolに2のアカウントへのアクセス（権限の借用）を許可する設定
    1. Option: 特定リポジトリの場合にのみ許可するよう設定


```terraform:main.tf
# 1
resource "google_project_service" "this" {
  for_each = [
    "iam.googleapis.com",                  # Identity and Access Management (IAM) API
    "iamcredentials.googleapis.com",       # IAM Service Account Credentials API
    "cloudresourcemanager.googleapis.com", # Cloud Resource Manager API
    "sts.googleapis.com",                  # Security Token Service API
  ]

  project = data.google_project.this.project_id
  service = each.value
}

# 2
# サービスアカウントに付与する権限は任意に設定
resource "google_service_account" "github_actions" {
  project      = data.google_project.this.project_id
  account_id   = "github-actions"
  display_name = "github actions"
  description  = "link to Workload Identity Pool used by github actions"
}

# 3
resource "google_iam_workload_identity_pool" "github" {
  provider                  = google-beta
  project                   = data.google_project.this.project_id
  workload_identity_pool_id = "github"
  display_name              = "github"
  description               = "for github actions"
}

# 4
resource "google_iam_workload_identity_pool_provider" "github" {
  provider                           = google-beta
  project                            = data.google_project.this.project_id
  workload_identity_pool_id          = google_iam_workload_identity_pool.github.workload_identity_pool_id
  workload_identity_pool_provider_id = "github-provider"
  display_name                       = "github actions provider"
  description                        = "OIDC identity pool provider for execute github actions"
  # See. https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect#understanding-the-oidc-token
  attribute_mapping = {
    "google.subject"       = "assertion.sub"
    "attribute.repository" = "assertion.repository"
    "attribute.owner"      = "assertion.repository_owner"
    "attribute.refs"       = "assertion.ref"
  }
  # Option: 特定のリポジトリオーナーのみトークン交換を許可
  attribute_condition = "attribute.owner=={OWNER_NAME}"

  oidc {
    issuer_uri = "https://token.actions.githubusercontent.com"
  }
}

# 5
resource "google_service_account_iam_member" "github_actions" {
  service_account_id = google_service_account.github_actions.name
  role               = "roles/iam.workloadIdentityUser"
  member             = "principalSet://iam.googleapis.com/${google_iam_workload_identity_pool.github.name}/attribute.repository/${REPO_NAME}"
}

# あとで使うID
output "service_account_github_actions_email" {
  description = "Actionsで使用するサービスアカウント"
  value       = google_service_account.github_actions.email
}

output "google_iam_workload_identity_pool_provider_github_name" {
  description = "Workload Identity Pood Provider ID"
  value       = google_iam_workload_identity_pool_provider.github.name
}
```

ローカルからapplyしましょう。
PoolとProviderは、プロジェクト作成時に作っておくのがよいかもしれません。

## actionsの設定
setup-gcloudからauthを使うように変更します、actionsの内容によってはこれ以外の権限設定が必要となるケースもあります。
また、GITHUB_TOKENにid-tokenのwrite権限を追加します。（デフォルトだと発行できない）
詳しくは[こちら](https://docs.github.com/en/actions/security-guides/automatic-token-authentication#permissions-for-the-github_token)を参照ください。

```diff:ci.yaml
+ permissions:
+     id-token: write

----


+ - name: Authenticate google cloud
+   uses: google-github-actions/auth@v0
+   with:
+     workload_identity_provider: ${output.google_iam_workload_identity_pool_provider_github_name}
+     service_account: ${output.service_account_github_actions_email}


# ※gcloudコマンドを使う場合はwithを消すだけでよい
- - name: Set up Cloud SDK
-   uses: google-github-actions/setup-gcloud@v0
-   with:
-     service_account_key: ${{ secrets.GCP_SA_KEY }}
-     export_default_credentials: true

- name: Setup terraform
  uses: hashicorp/setup-terraform@v1.3.2
  with:
    terraform_version: v1.1.0

- name: Terraform init
  run: terraform init

...
```

これで設定完了です！

## キーの削除
シークレットに格納しているサービスアカウントキーと、GCPからCI用に発行したサービスアカウントキーを削除します。

# Tips
## プロバイダの属性条件に反すると
↓のように絶対に属性条件にマッチしないように設定してactionsを実行するとどうなるでしょうか。

```terraform:main.tf
resource "google_iam_workload_identity_pool_provider" "github" {
  # attribute_condition = "attribute.owner=={OWNER_NAME}"
  attribute_condition = "attribute.owner==\'whoisthat\'"
}
```

### 結果
`Authenticate google cloud`ステップは成功し、`terraform init`で実行に失敗しました。
エラーメッセージを見ても、属性条件に反しているため認証できなかったということがわかります。
自分はstate fileの管理をgcsにしていたためここで失敗したと思われ、`Terraform Cloud`など別箇所でstate管理している場合はおそらく`plan`/`apply`でエラーになると思われます。

```
"https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/github-actions@zzzzzz.iam.gserviceaccount.com:generateAccessToken": oauth2/google:
status code 400: {"error":"unauthorized_client","error_description":"The given credential is rejected by the attribute condition."}
```

## サービスアカウントへのアクセス条件を満たしていないと
こちらも↓のように絶対に条件をみたさないような設定をして実行してみます。

```terraform:main.tf
resource "google_service_account_iam_member" "github_actions" {
  service_account_id = google_service_account.github_actions.name
  role               = "roles/iam.workloadIdentityUser"
  # member             = "principalSet://iam.googleapis.com/${google_iam_workload_identity_pool.github.name}/attribute.repository/${REPO_NAME}"
  member             = "principalSet://iam.googleapis.com/${google_iam_workload_identity_pool.github.name}/attribute.repository/whatisthis"
}
```

### 結果
こちらも`Authenticate google cloud`ステップは成功し、後続の`terraform init`で実行に失敗しました。
エラーメッセージを見る限り権限不足と出ており、権限の借用ができていないようにみえます。

```
Error: Failed to get existing workspaces: querying Cloud Storage failed: Get "https://storage.googleapis.com/storage/v1/b/mintak-tfstate/o?alt=json&delimiter=%2F&pageToken=&prefix=terraform%2F&prettyPrint=false&projection=full&versions=false": oauth2/google: status code 403: {
   "error": {
     "code": 403,
     "message": "The caller does not have permission",
     "status": "PERMISSION_DENIED"
   }
 }
```

# reference
- [GCPの紹介記事](https://cloud.google.com/blog/products/identity-security/enabling-keyless-authentication-from-github-actions)
    - [↑の日本語版](https://cloud.google.com/blog/ja/products/identity-security/enabling-keyless-authentication-from-github-actions)
- [Workload Identity 連携](https://cloud.google.com/iam/docs/configuring-workload-identity-federation#github-actions)
- [authAction](https://github.com/google-github-actions/auth)
- [GITHUB_TOKENの権限](https://docs.github.com/en/actions/security-guides/automatic-token-authentication#permissions-for-the-github_token)
- [terraform module(oidc)](https://github.com/terraform-google-modules/terraform-google-github-actions-runners/tree/master/modules/gh-oidc)
- [OAuth 2.0 Token Exchange](https://datatracker.ietf.org/doc/html/rfc8693)