
この記事は、LayerX Tech Advent Calendar 2025 の 5日目の記事です。
tech.layerx.co.jp
こんにちは。バクラク事業部 BizOps部 データグループの@civitaspoです。
先日、SnowflakeでWorkload Identity Federation機能がリリースされました。Workload Identity Federation機能は、Amazon Web Services(以下、AWS)やGoogle Cloud、Microsoft Azureなどのクラウドプロバイダー上のワークロードが持つIdentityを使ってSnowflakeとOpenID Connect(以下、OIDC)を使った認証を行える機能です。
docs.snowflake.com
docs.snowflake.com
このWorkload Identity Federation機能は、上記に挙げたクラウドプロバイダーだけでなく、Snowflake が定義する形式の OIDC attestationを発行できるカスタム OIDC プロバイダーを利用することもできます。なお、Snowflake が認識するOIDC attestationの正式な仕様(JWT claim や署名方式など)は公開されていません。そのため、カスタム OIDC プロバイダーを使う場合は、実際に手を動かして Snowflake が accept するトークンを試行錯誤で探す必要があります。
現実的には、非常に難易度の高い話なのでSnowflakeが提供するSDKやCLIがサポートするカスタム OIDC プロバイダーのみが利用可能と考えるのが良いでしょう。2025/12/05 時点ではカスタム OIDC プロバイダーとしてGitHub Actionsのサポートは確認しています。
今回の記事では、このWorkload Identity Federation機能を低レイヤーから理解するため、AWSからSnowflakeへWorkload Identity Federationを使って認証し、セッショントークンを取得するところまでをBashで実装してみようと思います。
「最初に結論から」と言うには非常に暴力的ですが、Bashスクリプトを貼り付けます。以下のBashスクリプトを実行すると、AWS上の特定のロールへAssume Roleを行なったあと、Snowflake Workload Identity Federationを用いて認証を行い、セッショントークンを取得できます。
set -eo pipefail usage() { cat <Usage: $0 [options] Options: --aws-role-arn : AWS Role ARN to assume --aws-region: AWS Region --snowflake-account-identifier: Snowflake Account Identifier (- --snowflake-username: Snowflake Username -h, --help: Show this help message and exit EOF } while [[ $# -gt 0 ]]; do case $1 in --aws-role-arn) aws_role_arn="${2}" shift 2 ;; --aws-region) aws_region="${2}" shift 2 ;; --snowflake-account-identifier) snowflake_account_identifier="${2}" shift 2 ;; --snowflake-username) snowflake_username="${2}" shift 2 ;; -h|--help) usage exit 0 ;; -*|--*) echo "[ERROR] Unknown option: ${1}" usage exit 1 ;; *) echo "[ERROR] Unknown argument: ${1}" usage exit 1 ;; esac done for v in aws_role_arn aws_region snowflake_account_identifier snowflake_username; do if [[ -z "${!v}" ]]; then echo "[ERROR] '--${v//_/-}' option is not defined." >&2 exit 1 fi done if [[ ! "$aws_role_arn" =~ ^arn:aws:iam::[0-9]+:role/ ]]; then echo "[ERROR] Invalid aws_role_arn: $aws_role_arn" >&2 exit 1 fi if [[ ! "$aws_region" =~ ^[a-z]{2}-[a-z]+-[0-9]+$ ]]; then echo "[ERROR] Invalid aws_region: $aws_region" >&2 exit 1 fi for cmd in curl jq date aws openssl xxd; do if ! command -v $cmd &> /dev/null; then echo "$cmd command not found" exit 1 fi done readonly AWS_ROLE_ARN="$aws_role_arn" readonly AWS_REGION="$aws_region" readonly SESSION_NAME="snowflake-wif-access-$(date +%s)" readonly CREDENTIALS=$(aws sts assume-role --role-arn $AWS_ROLE_ARN --role-session-name $SESSION_NAME --region $AWS_REGION --output json) readonly AWS_ACCESS_KEY_ID=$(echo $CREDENTIALS | jq -r '.Credentials.AccessKeyId') readonly AWS_SECRET_ACCESS_KEY=$(echo $CREDENTIALS | jq -r '.Credentials.SecretAccessKey') readonly AWS_SESSION_TOKEN=$(echo $CREDENTIALS | jq -r '.Credentials.SessionToken') readonly X_AMZ_DATE=$(TZ=UTC date +"%Y%m%dT%H%M%SZ") readonly X_AMZ_DATE_SHORT=$(echo $X_AMZ_DATE | cut -c 1-8) readonly CANONICAL_REQUEST_HOST="sts.${AWS_REGION}.amazonaws.com" readonly CANONICAL_REQUEST_METHOD="POST" readonly CANONICAL_REQUEST_URI="/" readonly CANONICAL_REQUEST_QUERY="Action=GetCallerIdentity&Version=2011-06-15" readonly SNOWFLAKE_AUDIENCE_HEADER_KEY="x-snowflake-audience" readonly SNOWFLAKE_AUDIENCE_HEADER_VALUE="snowflakecomputing.com" readonly SIGNED_HEADERS="host;x-amz-date;x-amz-security-token;${SNOWFLAKE_AUDIENCE_HEADER_KEY}" readonly CANONICAL_REQUEST_HEADERS="\ host:${CANONICAL_REQUEST_HOST} x-amz-date:${X_AMZ_DATE} x-amz-security-token:${AWS_SESSION_TOKEN} x-snowflake-audience:${SNOWFLAKE_AUDIENCE_HEADER_VALUE} " readonly EMPTY_PAYLOAD_HASH=$(printf "" | openssl dgst -binary -sha256 | xxd -p -c 256) readonly CANONICAL_REQUEST="\ ${CANONICAL_REQUEST_METHOD} ${CANONICAL_REQUEST_URI} ${CANONICAL_REQUEST_QUERY} ${CANONICAL_REQUEST_HEADERS} ${SIGNED_HEADERS} ${EMPTY_PAYLOAD_HASH}" readonly CANONICAL_REQUEST_HASH=$(printf "$CANONICAL_REQUEST" | openssl dgst -binary -sha256 | xxd -p -c 256) readonly STRING_TO_SIGN="\ AWS4-HMAC-SHA256 ${X_AMZ_DATE} ${X_AMZ_DATE_SHORT}/${AWS_REGION}/sts/aws4_request ${CANONICAL_REQUEST_HASH}" readonly K_SECRET=$(printf "AWS4$AWS_SECRET_ACCESS_KEY" | xxd -p -c 256) readonly K_DATE=$(printf "$X_AMZ_DATE_SHORT" | openssl dgst -binary -sha256 -mac HMAC -macopt "hexkey:${K_SECRET}" 2>/dev/null | xxd -p -c 256) readonly K_REGION=$(printf "$AWS_REGION" | openssl dgst -binary -sha256 -mac HMAC -macopt "hexkey:${K_DATE}" 2>/dev/null | xxd -p -c 256) readonly K_SERVICE=$(printf "sts" | openssl dgst -binary -sha256 -mac HMAC -macopt "hexkey:${K_REGION}" 2>/dev/null | xxd -p -c 256) readonly K_SIGNING=$(printf "aws4_request" | openssl dgst -binary -sha256 -mac HMAC -macopt "hexkey:${K_SERVICE}" 2>/dev/null | xxd -p -c 256) readonly SIGNATURE=$(printf "$STRING_TO_SIGN" | openssl dgst -binary -sha256 -mac HMAC -macopt "hexkey:${K_SIGNING}" 2>/dev/null | xxd -p -c 256) readonly AUTHORIZATION_HEADER_VALUE="AWS4-HMAC-SHA256 Credential=${AWS_ACCESS_KEY_ID}/${X_AMZ_DATE_SHORT}/${AWS_REGION}/sts/aws4_request, SignedHeaders=${SIGNED_HEADERS}, Signature=${SIGNATURE}" readonly CREDENTIAL_VERIFICATION_URL="https://${CANONICAL_REQUEST_HOST}${CANONICAL_REQUEST_URI}?${CANONICAL_REQUEST_QUERY}" readonly AWS_ATTESTATION_JSON="$( jq -nrc \ --arg url "$CREDENTIAL_VERIFICATION_URL" \ --arg authorization_header_value "$AUTHORIZATION_HEADER_VALUE" \ --arg http_method "$CANONICAL_REQUEST_METHOD" \ --arg host "$CANONICAL_REQUEST_HOST" \ --arg x_amz_date "$X_AMZ_DATE" \ --arg x_snowflake_audience "$SNOWFLAKE_AUDIENCE_HEADER_VALUE" \ --arg x_amz_security_token "$AWS_SESSION_TOKEN" \ '{ url: $url, method: $http_method, headers: { "authorization": $authorization_header_value, "host": $host, "x-amz-date": $x_amz_date, "x-amz-security-token": $x_amz_security_token, "x-snowflake-audience": $x_snowflake_audience } }' )" readonly AWS_ATTESTATION_B64="$(printf "%s" "$AWS_ATTESTATION_JSON" | base64 | tr -d '\n')" readonly SNOWFLAKE_LOGIN_URL="https://${snowflake_account_identifier}.snowflakecomputing.com/session/v1/login-request" readonly SNOWFLAKE_LOGIN_REQUEST_BODY="$( jq -nrc \ --arg snowflake_account_identifier "${snowflake_account_identifier}" \ --arg snowflake_username "${snowflake_username}" \ --arg token "${AWS_ATTESTATION_B64}" \ '{ data: { ACCOUNT_NAME: $snowflake_account_identifier, LOGIN_NAME: $snowflake_username, AUTHENTICATOR: "WORKLOAD_IDENTITY", PROVIDER: "AWS", TOKEN: $token } }' )" readonly SNOWFLAKE_LOGIN_RESPONSE_JSON="$( curl -sS -X POST \ -H 'Content-Type: application/json' \ -H 'Accept: application/snowflake' \ -H 'User-Agent: BASH-WIF-CLIENT/0.0.1' \ -d "${SNOWFLAKE_LOGIN_REQUEST_BODY}" \ "$SNOWFLAKE_LOGIN_URL" )" readonly SNOWFLAKE_MASTER_TOKEN="$(echo "${SNOWFLAKE_LOGIN_RESPONSE_JSON}" | jq -r '.data.masterToken // empty')" readonly SNOWFLAKE_SESSION_TOKEN="$(echo "${SNOWFLAKE_LOGIN_RESPONSE_JSON}" | jq -r '.data.token // empty')" if [[ -z "${SNOWFLAKE_MASTER_TOKEN}" ]]; then echo "Failed to get a snowflake master token." >&2 exit 1 fi if [[ -z "${SNOWFLAKE_SESSION_TOKEN}" ]]; then echo "Failed to get a snowflake session token." >&2 exit 1 fi jq -ncr \ --arg session_token "$SNOWFLAKE_SESSION_TOKEN" \ --arg master_token "$SNOWFLAKE_MASTER_TOKEN" \ '{ session_token: $session_token, master_token: $master_token }')
詳しく説明していきます。
いきなりBashを貼りましたが、中身でやっていることをざっくり分解すると次の3ステップです。
- AWS STS に対して AssumeRole を実行
- AssumeRole で取得したTemporalなCredentialを使って、AWS STS の GetCallerIdentity の SigV4 署名付きリクエストを組み立て
- その署名付きリクエストを Snowflake が期待するattestation形式に変換し、
/session/v1/login-requestに投げる
Snowflake の Workload Identity Federation のドキュメントにも書かれているとおり、Workload Identity Federation(以下、WIF) の基本的な流れは
- As a workload administrator, configure your service to use a native identity provider so that the provider can issue an attestation of your workload’s identity. This attestation is often, but not always, a JSON Web Token (JWT).
- As a Snowflake administrator, create a Snowflake service user for your workload. You set the properties of this user to values found in the attestation sent by the provider. For example, a user property might specify the name of an IAM role or the issuer URL of the provider.
- As a workload developer, configure your workload to use a Snowflake driver. Drivers send the attestation to Snowflake for verification.
となっています。AWS の場合、その「attestation」の中身が SigV4 で署名された GetCallerIdentity リクエストとなっています。この流れは、AWS から Google Cloud に対する Workload Identity Federation の流れとほとんど変わりません。1
全体像の1と2は、ドキュメントに忠実に実装しただけです。
特筆して説明すべきなのは、この署名のタイミングで x-snowflake-audience ヘッダーをCanonical Request / Signed Headers 両方に乗せる必要がある点です。Snowflake の Python コネクタ実装を見ると、AWS WIF の attestation 生成において X-Snowflake-Audience ヘッダを付与し、それも SigV4 署名の対象に含めていることがわかります。
これを抜いてしまうと Snowflake 側から code=394703 message=The AWS STS request contained unacceptable headers. For instance, the “X-Amz-Date” headers value may be too old as a request is only valid for 15 minutes. というエラーが返ってきます。
実装で言うと以下の箇所です。ここまでの実装で、「AWS STS に投げられる、GetCallerIdentity のSigV4署名付きリクエスト」が作られているので、これをSnowflakeが期待するJSON形式に変換します。
readonly AWS_ATTESTATION_JSON="$( jq -nrc \ --arg url "$CREDENTIAL_VERIFICATION_URL" \ --arg authorization_header_value "$AUTHORIZATION_HEADER_VALUE" \ --arg http_method "$CANONICAL_REQUEST_METHOD" \ --arg host "$CANONICAL_REQUEST_HOST" \ --arg x_amz_date "$X_AMZ_DATE" \ --arg x_snowflake_audience "$SNOWFLAKE_AUDIENCE_HEADER_VALUE" \ --arg x_amz_security_token "$AWS_SESSION_TOKEN" \ '{ url: $url, method: $http_method, headers: { "authorization": $authorization_header_value, "host": $host, "x-amz-date": $x_amz_date, "x-amz-security-token": $x_amz_security_token, "x-snowflake-audience": $x_snowflake_audience } }' )" readonly AWS_ATTESTATION_B64="$(printf "%s" "$AWS_ATTESTATION_JSON" | base64 | tr -d '\n')"
snowflake-connector-pythonの実装における、 create_aws_attestation(…) メソッドの結果を作っています。 url, method, headers をフィールドに持つJSONで、AWS STSに対して投げられるリクエストの詳細が格納されます。
ここまで準備したので、あとはSnowflakeに認証リクエストを投げるのみです。 /session/v1/login-request へ先ほど構築した AWS Attestation JSON を認証に必要なパラメータとともに投げ込みます。
readonly SNOWFLAKE_LOGIN_URL="https://${snowflake_account_identifier}.snowflakecomputing.com/session/v1/login-request" readonly SNOWFLAKE_LOGIN_REQUEST_BODY="$( jq -nrc \ --arg snowflake_account_identifier "${snowflake_account_identifier}" \ --arg snowflake_username "${snowflake_username}" \ --arg token "${AWS_ATTESTATION_B64}" \ '{ data: { ACCOUNT_NAME: $snowflake_account_identifier, LOGIN_NAME: $snowflake_username, AUTHENTICATOR: "WORKLOAD_IDENTITY", PROVIDER: "AWS", TOKEN: $token } }' )" readonly SNOWFLAKE_LOGIN_RESPONSE_JSON="$( curl -sS -X POST \ -H 'Content-Type: application/json' \ -H 'Accept: application/snowflake' \ -H 'User-Agent: BASH-WIF-CLIENT/0.0.1' \ -d "${SNOWFLAKE_LOGIN_REQUEST_BODY}" \ "$SNOWFLAKE_LOGIN_URL" )"
snowflake-connector-python における実装は以下になります。
Snowflake からのレスポンスは、ざっくり次のような JSON です。
{ "data": { "masterToken": "XXXXXXXXXX", "token": "XXXXXXXXXX", "validityInSeconds": 3600, "displayUserName": "TEST_USER", "firstLogin": false, ... }, "success": true }
snowflake-connector-python の実装でも、この token と masterToken を認証後に格納しています。
本記事では、AWSからSnowflakeへWorkload Identity Federationを使って認証し、セッショントークンを取得するところまでをBashで実装してみることで、Workload Identity Federation機能における低レイヤーなアクセスを理解しました。SDKの中で行われているWorkload Identity Federationによる認証の実装を再実装してみることで、詳細な処理を追うことが出来ました。もしリクエスト時に認証エラーなどの問題が発生しても、原因切り分けもやりやすくなることでしょう。
LayerXでは、Snowflakeを活用したデータ基盤の構築と、その上でのAI/MLシステムの開発を進めています。Production-ReadyなAI開発をサポートするためのデータ基盤開発、時系列データ処理、リアルタイムデータパイプラインの構築などに興味がある方は、ぜひ一緒にチャレンジしましょう!