Forgejo ActionsのOIDCでAssumeRoleするならaudは3箇所で揃える

  • Forgejo
  • AWS
  • IAM
  • OIDC

この記事は、構成と文章の整理にAIを使っています。
あくまで読みやすくするための補助で、内容そのものをAIにすべて任せて書いているわけではありませんが、ところどころ相談しながら整えています。

Forgejo Actions から aws-actions/configure-aws-credentials で IAM ロールを引き受けるとき、トークンの aud は次の3箇所で同じ文字列にしておく必要がある。

  • ワークフローの audience
  • IAM ID プロバイダの「対象者(Audience)」
  • ロールの信頼ポリシーの <ホスト>/api/actions:aud 条件

どれか1つでもズレると AssumeRoleWithWebIdentity が失敗する。エラーメッセージは「トークンを検証できなかった」程度の汎用的なものしか出ないので、どこがズレているのか分かりにくい。

自分のところでは全部 sts.amazonaws.com で揃えることにした。

全体の流れ

sequenceDiagram
    participant W as Forgejo Actions<br/>(ワークフロー)
    participant F as Forgejo<br/>(OIDC発行元)
    participant S as AWS STS
    participant I as IAM

    W->>F: IDトークン要求 (audience=sts.amazonaws.com)
    F-->>W: JWT (aud / sub 入り)
    W->>S: AssumeRoleWithWebIdentity (JWT, RoleArn)
    S->>F: discovery / JWKS 取得
    S->>I: IDプロバイダの対象者に aud があるか
    S->>I: 信頼ポリシーの aud / sub 条件を満たすか
    S-->>W: 一時クレデンシャル

STS 側のチェックは2段になっている。IAM ID プロバイダに登録した対象者リストに aud が含まれているかと、ロールの信頼ポリシーの条件を満たしているか。前者で落ちると、信頼ポリシーをどれだけ正しく書いても AssumeRole できない。

ワークフロー側

- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ env.ROLE_TO_ASSUME }}
aws-region: us-east-1
audience: sts.amazonaws.com

aws-region は STS を呼ぶリージョンであり、後続ステップで使う AWS CLI の既定リージョンにもなる。このパイプラインは us-east-1 で動かしている。

ジョブ側には enable-openid-connect: true が必要。GitHub Actions の permissions: id-token: write に相当する Forgejo 独自の設定。

IAM ID プロバイダ側

  • プロバイダ URL: https://forgejo.example.com/api/actions
  • 対象者: sts.amazonaws.com

プロバイダ URL はホスト直下ではなく /api/actions まで含める。STS はここから /.well-known/openid-configuration と JWKS を取りに来るので、WAF などで地域制限をかけている場合はこの2パスを通しておく必要がある(以前これで詰まった)。

信頼ポリシー側

"Condition": {
"StringEquals": {
"forgejo.example.com/api/actions:aud": "sts.amazonaws.com"
},
"StringLike": {
"forgejo.example.com/api/actions:sub": "repo:example-org/example-repo:ref:refs/tags/*"
}
}

条件キーのプレフィックスは IAM ID プロバイダの URL から https:// を除いたもの。

aud と sub の役割

ここを混同しやすいので分けておく。

クレーム 意味 何を絞るか
aud トークンの宛先 「AWS STS 向けに発行されたトークンか」
sub 発行元の主体 「どのリポジトリの、どの ref からの実行か」

aud を sts.amazonaws.com に揃えるのは、あくまで「このトークンは STS 宛て」という一致確認でしかない。どのリポジトリからの AssumeRole を許すかは sub で絞る。上の例だと example-org/example-repo のタグ push からの実行だけを許可している。

aud だけ設定して sub 条件を書かないと、同じ Forgejo インスタンス上の別リポジトリからも AssumeRole できてしまうので、sub 条件は必ず入れる。

audience を書かなかった場合

ここは少しややこしい。

Forgejo 自体は、トークン要求に audience を付けなかった場合の既定の aud を https://<ホスト>/<リポジトリのオーナー> にする。オーナーが組織 example-org なら https://forgejo.example.com/example-org、ユーザー example-user 個人のリポジトリなら https://forgejo.example.com/example-user になる。

一方で configure-aws-credentials の README では、Forgejo 向けでも audience の既定は sts.amazonaws.com だと書かれている。つまり Action 経由なら省略しても sts.amazonaws.com で要求されるはずで、Forgejo の既定値が効くのは ACTIONS_ID_TOKEN_REQUEST_URL を curl で直接叩いたときなどになる。

以前、デバッグ用に curl で取ったトークンの aud がオーナー URL だったので、IAM 側の対象者と信頼ポリシーをオーナー URL に合わせたら、Action のほうは sts.amazonaws.com で要求していて弾かれ続けた、ということがあった。手動デバッグと Action で要求している audience が違っていたのが原因。

どちらの既定値に依存するかで挙動が変わるのは分かりにくいので、ワークフロー側に audience: sts.amazonaws.com を明示して、IAM 側もそれに揃えるのが一番迷わない。

タグでステージングと本番のロールを切り替える

このパイプラインはタグ push で動き、-rc. を含むタグならステージング、それ以外なら本番に向ける。

タグ ロール ソースバケット
-rc. を含む vars.STAGING_ROLE_TO_ASSUME vars.STAGING_PIPELINE_SOURCE_BUCKET_NAME
それ以外 vars.PRODUCTION_ROLE_TO_ASSUME vars.PRODUCTION_PIPELINE_SOURCE_BUCKET_NAME

ロール ARN やバケット名は秘匿情報ではないので Secrets ではなく Variables に置いている。

切り替えの判定は次のような流れ。

flowchart TD
    A[タグ push] --> B{タグ名に -rc. を含む?}
    B -- Yes --> C[STAGING_* を使用]
    B -- No --> D[PRODUCTION_* を使用]
    C --> E{値が空でないか}
    D --> E
    E -- 空 --> F[ジョブを失敗させる]
    E -- OK --> G[Configure AWS Credentials]

条件 && A || B 形式の式で切り替えると、ステージング側の変数が未設定(空文字)のときに || 側へ落ちて本番のロールが使われてしまう。なので、選んだ値が空でないかを AssumeRole の前のステップで確認して、空ならジョブを落とすようにしている。

信頼ポリシーもステージング用・本番用でロールを分けておけば、sub 条件でタグのパターンまで絞ることもできる。ただし StringLike のワイルドカードは * と ? だけなので、-rc. を含むかどうかくらいの粒度までにしておくのが現実的。