Google Cloud から AWS へ OIDC Federation で認証してアクセスする

Google Cloud Service Account に対して OIDC Federation を構成して AWS へのアクセスを行う #

前提として、AWS へアクセスするための認証方法は大きく 2 種類あります。

通常、セキュリティ的な観点から後者の方法が推薦されています。

Google Cloud 上で実行される処理から AWS リソースにアクセスを行う場合、そのコンテキストで利用可能なサービスアカウントを利用して AWS の一時的な認証情報を取得できるよう構成するのが推薦されます。具体的には、次のようなことを行います。

  1. (Google Cloud) サービスアカウントを作成します。
  2. (AWS) IAM ロールを作成し、その信頼ポリシーで 1. のサービスアカウントに対する OIDC Federation を構成します。また、必要に応じて適切な IAM ポリシーを割り当てます。
  3. (Google Cloud) サービスアカウントの ID トークンを用いて 2. で作成した IAM ロールに対して AssumeRoleWithWebIdentity を呼び出し、一時的な認証情報を取得します。この認証情報を利用して AWS リソースにアクセスします。

完全な例として Cloud Run Functions から S3 ファイルを取得する実装例を用意しました。以降ではこの例を踏まえた主要な部分を解説します。

1. Google Cloud サービスアカウントの作成 #

IAM ロールの設定に必要な値はサービスアカウント作成後にしか得られないため、まずサービスアカウントを作成しておきます。また、合わせて ID token audience として利用する値を準備しておきます。例としてランダム文字列を生成して利用するものとします。

resource "google_service_account" "this" {
  account_id = "oidc-federation-example"
}

resource "random_password" "id_token_audience" {
  keepers = { revision = 0 }
  length  = 32
  special = false
}

2. AWS IAM ロールの作成 #

ステップ 1 で準備したサービスアカウントに対応した信頼ポリシーを持つ IAM ロールを作成します。ID token の検証項目として次の条件を適切に設定する必要があります。

data "aws_iam_policy_document" "assume_role_policy" {
  statement {
    actions = ["sts:AssumeRoleWithWebIdentity"]
    effect  = "Allow"
    principals {
      type        = "Federated"
      identifiers = ["accounts.google.com"]
    }
    condition {
      test     = "StringEquals"
      variable = "accounts.google.com:oaud"
      values   = [random_password.id_token_audience.result]
    }
    condition {
      test     = "StringEquals"
      variable = "accounts.google.com:sub"
      values   = [google_service_account.this.unique_id]
    }
  }
}

resource "aws_iam_role" "this" {
  assume_role_policy = data.aws_iam_policy_document.assume_role_policy.json
  name               = "oidc-federation-example"
}

3. OIDC Federation による一時的な認証情報の取得 #

Google Cloud 上で実行される処理として、ここでは AWS SDK for JavaScript v3 を利用した例を示します。fromWebToken は AssumeRoleWithWebIdentity を利用するための認証情報プロバイダーです。この呼び出しに必要なサービスアカウントの ID token を取得するには google-auth-library を利用するのが簡単でしょう。

import { fromWebToken } from "@aws-sdk/credential-providers";
import { GoogleAuth } from "google-auth-library";

// Get AWS access credentials by using AssumeRoleWithWebIdentity.
const audience = process.env.AWS_OIDC_AUDIENCE; // random_password.id_token_audience.result
const roleArn = process.env.AWS_ROLE_ARN; // aws_iam_role.this.arn
const googleAuth = new GoogleAuth();
const googleClient = await googleAuth.getClient();
const webIdentityToken = await googleClient.fetchIdToken(audience);
const credentials = fromWebToken({ roleArn, webIdentityToken });

// Usage: For example, get s3 object.
import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3";

const s3 = new S3Client({ credentials });
const obj = await s3.send(new GetObjectCommand({ Bucket: "my-bucket", Key: "my-file.txt" }));

fromWebToken で得られるのは一時的な認証情報であり、有効期限が切れた場合には再取得が必要です。

次のような認証情報のリフレッシュを行う wrapper を実装しておくのがよいでしょう。
import { fromWebToken } from "@aws-sdk/credential-providers";
import { GoogleAuth } from "google-auth-library";

function fromGoogleToken(config) {
  const { audience, durationSeconds, refreshOffsetSeconds, roleArn, roleSessionName } = config;
  const googleAuth = new GoogleAuth();
  const state = { credentials: null };

  return async () => {
    // check expiration
    if (state.credentials?.expiration) {
      const tokenExpiration = state.credentials.expiration.getTime();
      let expirationThreshold = Date.now();
      if (typeof refreshOffsetSeconds === "number" && refreshOffsetSeconds > 0) {
        expirationThreshold += refreshOffsetSeconds * 1000;
      }
      if (tokenExpiration > expirationThreshold) {
        return state.credentials;
      }
    }

    // refresh access token
    const client = await googleAuth.getClient();
    const webIdentityToken = await client.fetchIdToken(audience);
    const provider = fromWebToken({ durationSeconds, roleArn, roleSessionName, webIdentityToken });
    const credentials = await provider();
    state.credentials = credentials;
    return credentials;
  };
}

// Intiialize provider.
const audience = process.env.AWS_OIDC_AUDIENCE; // random_password.id_token_audience.result
const roleArn = process.env.AWS_ROLE_ARN; // aws_iam_role.this.arn
const credentials = fromGoogleToken({ audience, roleArn });

// Usage: For example, get s3 object.
import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3";

const s3 = new S3Client({ credentials });
const obj = await s3.send(new GetObjectCommand({ Bucket: "my-bucket", Key: "my-file.txt" }));

まとめ #

Google Cloud サービスアカウントで OIDC Federation (AssumeRoleWithWebIdentity) を利用して AWS の一時的な認証情報の取得をする方法を説明しました。この方法では、アクセスキーのようなセキュリティリスクの高い認証情報を扱う必要が無く、より安全に AWS リソースへアクセスできます。

関連する話題