AWS IAMのOIDC認可を理解する 〜 GitLab CI/CDからAWSのマネージメントコンソールにログイン案内を出してみる

今回はAWS IAMでサポートされている [OIDCプロバイダー機能](https://docs.aws.amazon.com/ja_jp/IAM/latest/UserGuide/id_roles_providers_create_oidc.html) について見ていきます。

## AWS IAM の OpenID Connect 連携機能とは、どのような機能か？

### 何ができる？

- 特定のIdPから発行された**IDトークンをAWSにわたすことで、AWS上のロールの引受(`sts:AssumeRole`)ができます**
  - 事前準備後にAWS STSのAPIにIDトークンを提示することによりAWSのロールに対応した一時的な `AccessKeyId` `SecretAccessKey` `SessionToken` を得ることができます
- ちゃんとオンラインで繋がる**発行主体(IdP)と、それが発行したIDトークンがあれば、AWS側はOIDC中の認証やコールバックなど一切関知しません**（本機能に関係ありません）

### いつ使う？

- たとえば、GitLabやGitHubにはCI/CDビルド中にIDトークンを発行する機能があります。 **CI/CDビルド中に一時的にAWSサービスを、アクセスキーの事前共有無しで利用したい場合に使えます**
  - IAMドキュメントに、**引受を許可できるIDトークンの内容について細かい条件指定をすることができます**
    - GitLab/GitHub発行のIDトークンであれば、IDトークン中にリポジトリ名・ブランチ名等があるので、それらを `sts:AssumeRole` 時の条件にすることができます。これにより、**特定リポジトリのリリースブランチのビルドのみAWS IAM許可といった動作が可能になります**
- 他にもIDトークンを発行する主体があればいいので、色々応用方法があるかもしれません
  - 別のIdPからのOIDCコールバックだけをもらってAWSにIDトークンを中継するサイトを作れば、IdPからAWSのマネージメントコンソールにSSOログインさせるということも可能です。マネージメントコンソールのURLの発行方法は本記事にて解説します

### 何ができない？

- **AWS上でOIDC認証によるSSOログインを直接提供する機能ではありません**
- **AWSマネージメントコンソールのIAMユーザのログインをOIDCで直接代用する機能でもありません**
- IDトークンの提示でIAMの認可(`sts:AssumeRole`)を受けられるだけです
- **ユーザーごとにSSOを行いたい場合は、IAM Identity CenterでSAML連携を設定したほうが良いです**

**IDトークン⇔IAMロール連携機能** という名前だったら分かりやすいのになあと感じています。（私だけ？）

## GitLab CI/CD でIDトークンを発行してAWSマネジメントコンソールのURLを表示してみる

利用例として、実際にGitLab CI/CD中で発行されたIDトークンを用いてAWS上の認可(`sts:AssumeRole`)を受けたいと思います。それだけだとつまらないので、認可情報を用いて、AWSマネージメントコンソールにログインできるURLを生成してみたいと思います。

要するにGitLab CI/CDが動いたら、AWSマネージメントコンソールへのURLをコンソール吐き出すCIを作ってみます。

### AWS上の設定

#### OIDCプロバイダの追加

`IAM` > `ID プロバイダ` から `プロバイダを追加` にて以下のプロバイダを追加します。

- プロバイダのタイプ: `OpenID Connect`
- プロバイダのURL: `https://your-gitlab.example.com`
  - 最後にスラッシュを追加しないように注意
- 対象者(audience): 任意。ここでは `your_audience_is_here` (あとで追加削除可能)

#### IAMロールの追加

続いてOIDCプロバイダに紐づくIAMロールを作成します。

- 名前: 任意
- 許可ポリシー: 任意
- 信頼関係: 以下のようなJSON

```json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Federated": "arn:aws:iam::<your_account_id>:oidc-provider/<your_provider_url>"
            },
            "Action": "sts:AssumeRoleWithWebIdentity",
            "Condition": {
                "StringEquals": {
                    "<your_provider_url>:aud": "your_audience_is_here"
                }
            }
        }
    ]
}
```

上記では条件として`aud`クレームにのみ条件がかかっていますが、GitLabの場合、IDトークンに載っている以下のようなクレームについて条件をつけてリポジトリやブランチを識別するとよいでしょう。

- `project_path`: プロジェクト名(パス付き)
- `ref`: ブランチ名など

https://docs.gitlab.com/ci/secrets/id_token_authentication/


### GitLab上の設定

GitLab CI/CD上でOIDCトークンを発行するには `id_tokens:` を設定すれば指定した環境変数にIDトークンが追加されるようになります。

```yaml:.gitlab-ci.yml
stages:
  - aws

issue-aws-console-url:
  stage: aws
  image: ghcr.io/astral-sh/uv:0.7.10-python3.13-bookworm
  id_tokens:
    MY_ID_TOKEN:
      aud: your_audience_is_here
  script:
    # uv コマンドで main.py スクリプトを実行
    - uv run main.py
```

上記のCI中で実行される `main.py` は以下です。CI中に示されているIDトークンを用いて、AWSから一時認証トークンを得て（それだけでもAWSの操作は可能です）、トークンから一時ログインURLを生成しています。

```python:main.py
# /// script
# requires-python = ">=3.13"
# dependencies = ["boto3>=1.38.28", "requests>=2.32.3"]
# ///

import boto3
import requests
import urllib
import datetime
import json
import os

# パラメータ
id_token = os.environ["MY_ID_TOKEN"] # .gitlab-ci.yml で指定した環境変数
role_arn = "arn:aws:iam::<your_aws_account>:role/<your_role_name>"
issuer = "https://your-gitlab.example.com"
region = "ap-northeast-1"

sts_client = boto3.client('sts', region_name=region)

# IDトークンからSTSのクレデンシャルを取得
assume_response = sts_client.assume_role_with_web_identity(
    RoleArn=role_arn,
    RoleSessionName='GitLabOIDCSession',
    WebIdentityToken=id_token
)
# （※ この時点でAWSの操作自体は可能）
assume_creds = assume_response['Credentials']
session_data = {
    "sessionId": assume_creds["AccessKeyId"],
    "sessionKey": assume_creds["SecretAccessKey"],
    "sessionToken": assume_creds["SessionToken"],
}

# STSのクレデンシャルからログインエンドポイント用の情報を取得
aws_federated_signin_endpoint = "https://signin.aws.amazon.com/federation"
fed_response = requests.get(
    aws_federated_signin_endpoint,
    params={
        "Action": "getSigninToken",
        "SessionDuration": str(datetime.timedelta(hours=12).seconds),
        "Session": json.dumps(session_data),
    },
)

# ログインエンドポイント用の情報からURLを作成＆出力
signin_token = json.loads(fed_response.text)
query_string = urllib.parse.urlencode(
    {
        "Action": "login",
        "Issuer": issuer,
        "Destination": "https://console.aws.amazon.com/",
        "SigninToken": signin_token["SigninToken"],
    }
)
federated_url = f"{aws_federated_signin_endpoint}?{query_string}"

print("AWS Management Console is here:")
print(federated_url)
```

:::note info
uvの実行については [Pythonの開発用適当ツールの作成・実行はuvを使うのがオススメ](https://qiita.com/ssc-ksaitou/items/9da75058489ebe8c2009) という記事を参考にしてください
:::

上記のGitLab CI/CDでの実行結果は以下のようになります。

```sh:実行結果
$ uv run main.py
Downloading botocore (13.0MiB)
 Downloading botocore
Installed 11 packages in 54ms

AWS Management Console is here:
https://signin.aws.amazon.com/federation?Action=login&Issuer=....すごく長いURL...
```

実際に発行されたURLをクリックするとAWSコンソールに移動できました！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1577592/f53ea9fb-3797-4559-af69-c3d2a1e587bd.png)

画面中ではロールに認可されている操作が可能です。

:::note warn
上記例は単純に技術検証としてログインURLをCIに出力しているのであって、実際に同様のことを行う場合は十分にセキュリティに配慮してください
:::

## まとめ

以下について、動作するところまでざっくり確認しました。

- AWS IAMにおけるIDトークン連携機能
- GitLab CI/CD上のIDトークン発行機能 (`id_tokens:`)
- それらを用いたAWSのサービスの認可の発行 (`sts:AssumeRole`)
- 認可情報を用いたAWSマネージメントコンソールへの一時ログインURLの発行

CI/CDからAmazon ECRにDockerイメージをプッシュしたりする場合など、アクセスキーの事前共有よりもこの手法のほうが安全と思われるので、積極的に使いたいところです。