
如果要把 GitHub Actions 接到 GCP, 我現在最不想再做的就是:

- one shared deploy service account
- one broad trust rule
- one workflow that can hit every environment

這樣一開始很省事, 但後面只要開始切 `dev / prod`, 風險和 debug 成本都會一起上升。

這篇整理我最後收斂出來的做法: 用 branch、workflow、service account 三層一起切最小權限。

## 想解的不是登入問題, 是信任邊界

很多人第一次做 OIDC / WIF, 會把問題理解成:

- how do I let GitHub Actions authenticate to GCP?

這只答到一半。

真正要答的是:

- 哪條 workflow 可以模擬哪個身分?
- from which branch?
- against which environment?
- 對應哪些權限?

如果這些沒寫清楚, 你只是把 long-lived credential 換成 short-lived credential, 不是把邊界收乾淨。

## 我最後的身分模型

我會把這三種角色分開看:

```text
ci identity
  -> no GCP auth needed

infra identity
  -> used by infrastructure workflow

service deploy identities
  -> deploy-dev-sa
  -> deploy-prod-sa
```

這樣的核心原則是:

- CI should not get cloud credentials if it does not need them
- infra 不該和應用部署共用同一個身分
- dev deploy 和 prod deploy 不該共用同一個身分

## 對應到 workflow 的切法

workflow 層我最後收斂成:

```text
CI
Deploy Dev
Deploy Prod
Infrastructure
```

再往下綁 branch boundary:

```text
dev branch   -> Deploy Dev
main branch  -> Deploy Prod
main branch  -> Infrastructure(shared, prod-platform)
dev branch   -> Infrastructure(dev-platform)
```

這樣 branch policy 和 environment policy 就能互相對齊。

## WIF provider 層應該管什麼

provider 層最適合管的是「哪些 workflow + branch 組合能進來」。

概念上像這樣:

```hcl
attribute_condition = join(" && ", [
  "assertion.repository_id == \"<REPO_ID>\"",
  "assertion.repository_owner_id == \"<OWNER_ID>\"",
  "(",
  "assertion.workflow_ref.startsWith(\"org/repo/.github/workflows/deploy-dev.yml@refs/heads/dev\") ||",
  "assertion.workflow_ref.startsWith(\"org/repo/.github/workflows/deploy-prod.yml@refs/heads/main\") ||",
  "assertion.workflow_ref.startsWith(\"org/repo/.github/workflows/infra.yml@refs/heads/dev\") ||",
  "assertion.workflow_ref.startsWith(\"org/repo/.github/workflows/infra.yml@refs/heads/main\")",
  ")"
])
```

這樣 token 連 exchange 都過不了, 會先被擋在第一層。

## 為什麼只靠 provider 還不夠

provider 知道的只有 token claim。它不知道 workflow 實際想做什麼。

例如 `infra.yml` 會接一個 `target` input:

- `shared`
- `dev-platform`
- `prod-platform`

provider 不會知道這個 input 是什麼, 所以它沒辦法單靠自己判斷:

- `shared` must be main only
- `dev-platform` must be dev only

這種事情還是要在 workflow 本身顯式驗證。

```yaml
- name: Validate branch
  run: |
    case "${TARGET}" in
      shared|prod-platform)
        test "${GITHUB_REF}" = "refs/heads/main"
        ;;
      dev-platform)
        test "${GITHUB_REF}" = "refs/heads/dev"
        ;;
    esac
```

這就是我最後採用的 defense-in-depth:

- workflow validation
- provider condition
- service account binding

## `workflow_ref` 和 `caller_workflow` 這個坑

這是整個 WIF 設計裡最容易踩的坑之一。

如果 workflow 裡又呼叫 reusable workflow, `job_workflow_ref` 會指向 reusable workflow 本身, 不是最外層入口 workflow。

這代表你如果用錯 claim 做 binding, 很可能會出現這種狀況:

- trust rule technically passes
- but the wrong workflow identity is bound

我最後比較穩的做法是:

```hcl
"attribute.caller_workflow" = "assertion.workflow_ref.split('@')[0]"
```

然後 service account binding 綁這個 caller workflow path。

這樣 reusable 那層和入口 workflow 那層才不會混在一起。

## 最後為什麼我又把 reusable workflows 拿掉

雖然這篇主題是 WIF, 但這兩件事其實有關。

reusable workflows 的問題不是不能用, 而是它會讓這些東西變得比較難看懂:

- who owns permissions
- where `id-token: write` is actually needed
- 哪條 workflow 才是真正的操作者入口
- what shows up in the GitHub Actions UI

所以我最後寧可接受一點重複, 也把 deploy workflows 扁平化回:

- `deploy-dev.yml`
- `deploy-prod.yml`

shared steps 再留在 composite actions。

這樣做完之後, 最小權限審查反而更直觀。

## `id-token: write` 不要開太大

另一個我會特別記住的點是: 不要在 workflow scope 就直接給 `id-token: write`, 除非真的整條 workflow 都需要。

比較乾淨的做法是:

- CI: no id-token
- validate job: no id-token
- only build/migrate/deploy jobs that need auth get id-token

```yaml
permissions:
  contents: read

jobs:
  build:
    permissions:
      contents: read
      id-token: write
```

這樣 branch validation job 就不會莫名其妙取得 OIDC 能力。

## GitHub environment-scoped vars 也屬於信任邊界

這一點很常被忽略。

如果某個 job 需要 environment-scoped variables, job 必須真的宣告對應 environment:

```yaml
environment: dev
```

這不只是取值問題, 也是一種 boundary。因為 GitHub environment 本身還可以掛 approval rule。

所以 environment-scoped variables、deploy service account、branch guard, 其實是同一條安全模型上的不同層。

## 我現在會怎麼檢查這套設計有沒有真的最小權限

我會問這幾題:

- Can PR CI run without any cloud credential?
- Can dev deploy impersonate only the dev deploy SA?
- Can prod deploy impersonate only the prod deploy SA?
- Can infra impersonate only the infra SA?
- Does the wrong branch fail before real work begins?
- Does the provider reject disallowed workflow+branch combinations?

如果其中有一題答不出來, 就還不夠乾淨。

## 結論

GitHub Actions 接 GCP 的難點從來不是把登入做通而已。

真正難的是把信任邊界切得夠清楚:

- correct workflow
- correct branch
- 正確的 service account
- 正確的 environment
- 正確的權限

這幾層一起成立, 才算真的做到最小權限。

## References

- [GitHub Actions documentation](https://docs.github.com/en/actions)
- [OpenID Connect in GitHub Actions](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect)
- [Workload Identity Federation](https://cloud.google.com/iam/docs/workload-identity-federation)
- [Workload Identity Federation with deployment pipelines](https://cloud.google.com/iam/docs/workload-identity-federation-with-deployment-pipelines)
