この記事は、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を用いて認証を行い、セッショントークンを取得できます。
#!/usr/bin/env bash set -eo pipefail usage() { cat <<EOF Usage: $0 [options] Options: --aws-role-arn <AWS_ROLE_ARN>: AWS Role ARN to assume --aws-region <AWS_REGION>: AWS Region --snowflake-account-identifier: Snowflake Account Identifier (<organization name>-<account name>) --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 # Required Tools 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 # AWS 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') # https://docs.aws.amazon.com/IAM/latest/UserGuide/create-signed-request.html 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) # yyyyMMdd 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}" ### For debug verification # curl -sL -XPOST \ # -H "accept: application/json" \ # -H "host: ${CANONICAL_REQUEST_HOST}" \ # -H "x-amz-date: $X_AMZ_DATE" \ # -H "authorization: $AUTHORIZATION_HEADER_VALUE" \ # -H "x-amz-security-token: $AWS_SESSION_TOKEN" \ # "${CREDENTIAL_VERIFICATION_URL}" # AWS Attestation for Snowflake # ref. https://github.com/snowflakedb/snowflake-connector-python/blob/3427a80f71d371f8d08e594840d1e6f7f5559075/src/snowflake/connector/wif_util.py#L174-L220 # ref. https://github.com/snowflakedb/snowflake-connector-python/blob/3427a80f71d371f8d08e594840d1e6f7f5559075/src/snowflake/connector/auth/workload_identity.py#L49-L110 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 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スクリプトの全体像
いきなり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
SigV4署名付き AWS STS GetCallerIdentity の組み立て
全体像の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 Attestation JSON の構築
実装で言うと以下の箇所です。ここまでの実装で、「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に対して投げられるリクエストの詳細が格納されます。
Google Cloud の場合は headers に配列が要求されます。 headers:[{"key":"xxxxx", "value":"xxxxx"}] と言った形式です。クラウドプロバイダーごとに attestation の形式が微妙に異なるのがよく分かります。
Snowflake /session/v1/login-request へ認証リクエストを投げる
ここまで準備したので、あとは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開発をサポートするためのデータ基盤開発、時系列データ処理、リアルタイムデータパイプラインの構築などに興味がある方は、ぜひ一緒にチャレンジしましょう!
open.talentio.com open.talentio.com open.talentio.com
- 今回の実装は、Google Cloud Workload Identity Federation の実装を参考にしました。↩