🔑 はじめに:なぜいま「キーレス」なのか
本記事はシリーズ 「BtoB SaaS セキュリティ設計」の第 5 回(インフラ・キーレス認証編) です。第 1 回 総覧 の「インフラ・キーレス認証」層を、CI/CD パイプラインからの実装観点まで掘り下げます。
クラウド時代のインフラ・セキュリティの根は、 「静的な鍵をどれだけ無くせるか」 にあります。
長寿命のサービスアカウントキー(GCP の JSON)や AWS のアクセスキーは、CI/CD パイプライン・スクリプト連携で長らく使われてきました。しかし、この方式には常に以下の問題がつきまといます。
- 漏洩時の被害が大きい:無効化されるまで攻撃者が継続利用できる
- ローテーション運用の負荷:定期的な再発行・全環境への配布
- 偶発的な漏洩リスク:リポジトリ・CI ログ・チャットへの混入
これらを根本から解決するのが Workload Identity Federation(WIF) と OpenID Connect(OIDC) を組み合わせた キーレス認証 です。
本記事では、CI/CD パイプライン設計で繰り返し登場する以下のシナリオに焦点を絞り、コピペ可能な構成例とベストプラクティスを示します。
- GitLab → GCP : GitLab CI から GCP リソースを操作する
- GitLab → AWS : GitLab CI から AWS リソースを操作する
- GCP ↔ AWS : クラウド間で互いに認証する(マルチクラウド)
🧠 キーレス認証の共通モデル
クラウド各社の WIF / OIDC IdP は命名こそ違いますが、 構造は完全に共通 しています。「外部 IdP が発行した短命の OIDC トークンを、クラウド IAM が直接検証して短命の credential に交換する」というモデルです。
検証されるのは主に以下の 4 点です。
- 署名:IdP の JWKs(公開鍵)で JWT 署名を検証
- issuer:信頼している IdP から発行されたか
- audience(
aud):自分宛のトークンか - subject(
sub)等の条件:許可した発行元かどうか(プロジェクト・ブランチ・ロール等)
このうち 「4. sub 等の条件」を省くと、同じ IdP を使う任意のテナントから認証できてしまう のが最大の落とし穴です。詳細は後述します。
各クラウドでの命名対応
| クラウド | 機能名 | 短命 credential 払出し API |
|---|---|---|
| GCP | Workload Identity Federation | iam.googleapis.com/.../token |
| AWS | IAM OIDC Identity Provider | sts:AssumeRoleWithWebIdentity |
| Azure | Workload Identity Federation | oauth2/v2.0/token |
🟢 GitLab → GCP:WIF + OIDC で SA キーを撤廃する
最も需要の高い経路です。GitLab CI のジョブから GCP リソース(Cloud Run・GCS・BigQuery 等)を操作する際、サービスアカウント JSON キーを完全に廃止 できます。
構築手順
① GCP:Workload Identity Pool と OIDC Provider を作成
# Pool(環境ごとに分割するのが推奨。横展開時の事故を防ぐ)
gcloud iam workload-identity-pools create gitlab-prod-pool \
--location="global" \
--display-name="GitLab Production Pool"
# OIDC Provider(GitLab SaaS の場合)
gcloud iam workload-identity-pools providers create-oidc gitlab-saas \
--location="global" \
--workload-identity-pool="gitlab-prod-pool" \
--issuer-uri="https://gitlab.com" \
--attribute-mapping="google.subject=assertion.sub,attribute.project_id=assertion.project_id,attribute.namespace_id=assertion.namespace_id,attribute.ref=assertion.ref" \
--attribute-condition="assertion.namespace_id=='1234567' && assertion.ref=='refs/heads/main'"
要点:
attribute-conditionを必ず付ける。省くと「GitLab.com 上の任意プロジェクト」から認証可能になり、ほぼ無防備になりますnamespace_id(数値 ID)で「自社グループのプロジェクトのみ」を担保。namespace_pathの prefix マッチは類似名グループでの成りすまし可能性があるため避けます- 本番デプロイは
assertion.ref=='refs/heads/main'を AND で重ねるのが定番
② GCP:直接アクセス か サービスアカウント なりすましか を選ぶ
GCP には 2 つの権限付与モデルがあります。
| 方式 | 推奨度 | 仕組み |
|---|---|---|
直接アクセス (principalSet://) | ★ デフォルト推奨 | 連携 ID 自身に IAM ロールを直接付与(最短経路) |
| サービスアカウント なりすまし | ★ 必要時のみ | SA に IAM ロールを与え、連携 ID には roles/iam.workloadIdentityUser を付与して SA を借りる |
なりすましが必要となる代表例は、 連携プリンシパルを直接受け取らない一部の GCP サービス(Cloud Storage の signed URL 生成など)です。それ以外は直接アクセスを推奨します。経路が短いほど監査が容易で、攻撃面も小さくなります。
# 直接アクセス:GitLab プロジェクト 9876543 全体に Cloud Run の roles/run.developer を付与
gcloud projects add-iam-policy-binding MY_PROJECT \
--role="roles/run.developer" \
--member="principalSet://iam.googleapis.com/projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/gitlab-prod-pool/attribute.project_id/9876543"
③ GitLab CI:id_tokens で OIDC トークンを取得
deploy_gcp:
id_tokens:
GCP_ID_TOKEN:
# WIF Provider の正規 URI を audience に指定
aud: https://iam.googleapis.com/projects/${GCP_PROJECT_NUMBER}/locations/global/workloadIdentityPools/gitlab-prod-pool/providers/gitlab-saas
script:
- echo "$GCP_ID_TOKEN" > /tmp/id-token.jwt
- |
cat <<EOF > /tmp/cred.json
{
"type": "external_account",
"audience": "//iam.googleapis.com/projects/${GCP_PROJECT_NUMBER}/locations/global/workloadIdentityPools/gitlab-prod-pool/providers/gitlab-saas",
"subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
"token_url": "https://sts.googleapis.com/v1/token",
"credential_source": { "file": "/tmp/id-token.jwt" }
}
EOF
- export GOOGLE_APPLICATION_CREDENTIALS=/tmp/cred.json
- gcloud auth login --cred-file=/tmp/cred.json
- gcloud run deploy my-service --image gcr.io/$MY_PROJECT/app:$CI_COMMIT_SHA
これで JSON キーは一切リポジトリ・GitLab Variables に置かない 構成になります。
よくある失敗パターン
| 症状 | 原因 |
|---|---|
Invalid token audience | aud が WIF Provider の正規 URI と一致していない |
| 任意のプロジェクトから認証されてしまう | attribute-condition 未設定 |
Invalid issuer | issuer-uri の trailing slash の有無(GitLab.com は https://gitlab.com、末尾なしが正) |
Permission denied | 直接アクセスの member 文字列を principalSet:// ではなく principal:// と書いている |
🟠 GitLab → AWS:OIDC Provider + AssumeRoleWithWebIdentity
AWS には「WIF」という名称はありませんが、 「IAM OIDC Identity Provider」+「sts:AssumeRoleWithWebIdentity」 が同じ役割を果たします。
構築手順
① AWS IAM:OIDC Provider を作成
aws iam create-open-id-connect-provider \
--url https://gitlab.com \
--client-id-list "https://gitlab.com" \
--thumbprint-list <GitLab の TLS 証明書の Thumbprint>
Thumbprint は GitLab の TLS 証明書から SHA-1 を計算します(多くの IaC ツールは自動取得をサポート)。
② AWS IAM:信頼ポリシー付きロールを作成
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::123456789012:oidc-provider/gitlab.com"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"gitlab.com:aud": "https://gitlab.com",
"gitlab.com:sub": "project_path:my-org/my-app:ref_type:branch:ref:main"
}
}
}
]
}
要点:
sub条件は必ず付ける。省くと GitLab.com 全プロジェクトから AssumeRole 可能になりますsubのフォーマットはproject_path:GROUP/PROJECT:ref_type:TYPE:ref:NAME。ブランチ・タグ・GitLab 環境(environment)のいずれでも条件化できます- ワイルドカード(
StringLikeの*)で許容範囲を広げる場合も、 必ずproject_pathを組織内に閉じる こと
③ GitLab CI:AssumeRoleWithWebIdentity を呼ぶ
deploy_aws:
variables:
AWS_REGION: ap-northeast-1
ROLE_ARN: arn:aws:iam::123456789012:role/gitlab-deploy
id_tokens:
AWS_ID_TOKEN:
aud: https://gitlab.com
script:
- >
creds=$(aws sts assume-role-with-web-identity
--role-arn "${ROLE_ARN}"
--role-session-name "gitlab-${CI_PROJECT_ID}-${CI_PIPELINE_ID}"
--web-identity-token "${AWS_ID_TOKEN}"
--duration-seconds 3600
--query 'Credentials.[AccessKeyId,SecretAccessKey,SessionToken]'
--output text)
- export $(printf "AWS_ACCESS_KEY_ID=%s AWS_SECRET_ACCESS_KEY=%s AWS_SESSION_TOKEN=%s" $creds)
- aws sts get-caller-identity
- aws s3 sync ./build s3://my-bucket/$CI_COMMIT_SHA/
セッション名(role-session-name)に CI_PIPELINE_ID を含めると、AWS CloudTrail での追跡が容易になります。
🌐 GCP ↔ AWS:クロスクラウド連携
「GCP の Cloud Run から AWS S3 を読みたい」「AWS Lambda から GCP の BigQuery を叩きたい」というマルチクラウド要件もキーレスにできます。両方とも長寿命キーは不要です。
GCP → AWS(GCP の SA を AWS に信頼させる)
GCP のサービスアカウントは OIDC ID トークンを自前で発行できる(issuer = https://accounts.google.com)ため、AWS が GCP を OIDC IdP として信頼すれば成立します。
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": { "Federated": "arn:aws:iam::123456789012:oidc-provider/accounts.google.com" },
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"accounts.google.com:aud": "sts.amazonaws.com",
"accounts.google.com:sub": "1234567890..."
}
}
}]
}
sub には GCP サービスアカウントの unique ID(メールアドレスではなく数値の unique_id) を入れます。GCP 側では SA の generateIdToken API(または ADC)で audience=sts.amazonaws.com の ID トークンを取得し、assume-role-with-web-identity を呼べば短命の AWS credential が得られます。
AWS → GCP(AWS IAM Role を GCP に信頼させる)
逆方向は OIDC ではなく、GCP の AWS provider(専用フロー)を使います。
gcloud iam workload-identity-pools providers create-aws aws-prod \
--location="global" \
--workload-identity-pool="aws-pool" \
--account-id="123456789012" \
--attribute-mapping="google.subject=assertion.arn,attribute.aws_role=assertion.arn.extract('assumed-role/{role}/')" \
--attribute-condition="attribute.aws_role=='LambdaPipelineRole'"
AWS 側では Lambda の実行ロールから GCP 用の credential config を生成し、gcloud または GCP クライアントライブラリの ADC が自動的に AWS の GetCallerIdentity を署名して GCP STS に渡します。
🛡️ 共通のベストプラクティス
| 観点 | 推奨 | 理由 |
|---|---|---|
| sub の条件 | attribute-condition / trust policy condition で必ず絞る | 省くと同 IdP の他テナントから認証可能 |
| prefix マッチではなく ID 一致 | namespace_id のような数値 ID で厳密比較 | 類似名のグループ・リポジトリでの成りすましを防ぐ |
| Pool / Provider の粒度 | 環境(prod/staging)ごとに Pool を分割 | 横展開時の事故防止・最小権限の徹底 |
| audience | デフォルト値ではなく Provider 固有 URI を明示 | リプレイ・流用攻撃の難易度を上げる |
| ロールの権限 | デプロイ単位の最小ロール | 万一の侵入経路を狭くする |
| セッション名 | CI_PIPELINE_ID 等を含める | CloudTrail / Audit Log での追跡性 |
| 直接アクセス vs SA なりすまし | デフォルト直接、必要時のみなりすまし | 経路が短いほど安全・観測しやすい |
| トークン有効期限 | 1 時間以内(既定値)を維持 | 長くするほど漏洩時のリスクが累積 |
sub 条件を省略してはいけない理由
attribute-condition を true にしたり、AWS trust policy の Condition を空にすると、 同じ IdP(例: gitlab.com)を使う第三者プロジェクト から AssumeRole できてしまいます。これは過去に GitHub Actions の OIDC で実際にインシデント化したパターンで、 「IdP を信頼する ≠ そこを使う全プロジェクトを信頼する」 という原則を覚えておく必要があります。
📝 CTS-EC での採用状況
CTS-EC では現状、 GitLab CI → GCP の経路で WIF を採用しています。
- サービスアカウントキーをリポジトリ・GitLab Variables のいずれにも保存していない
namespace_id+assertion.ref=='refs/heads/main'で本番デプロイを限定- staging 環境は別 Pool に分離(横展開事故の防止)
- GitLab の
secrets:キーワードと組み合わせ、 GCP Secret Manager からの DB パスワード取得もキーレス化済み
今後の対策候補:
- AWS との連携追加(ハイブリッドクラウド要件発生時)
- Terraform 実行ロールの最小権限化(IaC モジュール単位での粒度調整)
- OIDC audience に Pipeline 固有値を含める運用(リプレイ耐性のさらなる強化)
本記事はそのインフラ・キーレス認証層の 実装ハンドブック として位置付けられます。
🎯 まとめ
| やめるべきもの | 置き換え先 |
|---|---|
| GCP Service Account JSON キー | GitLab id_tokens + GCP WIF |
| AWS アクセスキー(CI 用) | GitLab id_tokens + AWS OIDC Provider + AssumeRoleWithWebIdentity |
| GCP ↔ AWS の長寿命キー | 双方の OIDC IdP / GCP の AWS Provider |
「鍵を作らない」という選択肢は、 2026 年時点ではすでに揃っています 。CI/CD パイプラインを設計するときに デフォルトでキーレスを選び、condition で必ず sub を絞る ─── これが現時点のベストプラクティスです。
関連用語
- Workload Identity Federation — 本記事の中心概念
- Service Account Impersonation — 「直接アクセス vs SA なりすまし」の SA なりすまし側の正式名
- GitLab id_tokens — 本記事 YAML 例で使う OIDC トークン発行キーワード
- OpenID Connect — 認証プロトコルの基盤
- 多層防御 — WIF が担うインフラ層のキーレス化
関連記事
- GitLab → GCP 認証方式の 3 世代と CTS の選択 — 本記事の知識を踏まえた CTS 内部の技術選定(SA Impersonation vs Direct Resource Access)
- GCP → AWS キーレス認証の落とし穴 — クロスクラウド連携でハマる 7 つの罠(aud/azp/oaud / Trust Policy)
- Terraform IaC 実践ガイド:CI/CD パイプライン編 — 本記事の WIF を Terraform 自動化に組み込む実装ガイド
- Terraform IaC 実践ガイド:概要 — IaC 観点での WIF 利用
- BtoB SaaS セキュリティ設計総覧 — 全体の中での位置づけ
外部資料