はじめに
以前IAPを活用して、Google CloudでセキュアなDify環境を構築する記事を書きました。
このように社内ツールやセキュアなアプリケーションを構築する際、Google Cloudの IAP (Identity-Aware Proxy) を使ってアクセス制限をかけることがあります。ブラウザからのアクセスならGoogleアカウントでログインするだけですが、プログラム(Cloud Run FunctionsやCloud Run、ローカルスクリプトなど)からIAP配下のAPIを呼び出そうとすると、必要な権限やトークンの扱いが増え、実装の難易度が一気に上がります。
今回、社内ドキュメントをスクレイピングしてGoogle Cloud上のDifyへ自動同期するRAGパイプラインを構築した際、IAP配下のAPIをプログラムから呼ぶ部分で、いくつもの失敗にハマりました。
この記事では、それらの失敗例と、最終的にどうやって突破したのかをまとめます。
0. 事前準備(必要なキーとIDの取得)
この記事のコードを動かすためには、Dify側とGoogle Cloud側の両方で事前に取得しておくべき情報があります。以下の3つを手元に用意しておいてください。
本記事ではDifyのナレッジを更新することを想定しておりますが、適宜自身が利用しているサービスなどの情報に置き換えてご覧ください。
① DifyのAPIキー(DIFY_API_KEY)
- Difyのダッシュボードにログインする。
- 上部メニューの「ナレッジ (Knowledge)」から、対象のデータセットを開く。
- 左側のナビゲーションメニューにある「APIアクセス (API Access)」をクリック。
- 画面右上にある「API Keys」のセクションで「新しいキーを作成」をクリックし、生成されたキー(
dataset-xxx...)をコピーする。
② DifyのデータセットID(DIFY_DATASET_ID)
対象のデータセットを開いているときのブラウザのURLから取得できます。
- URLの例
https://your-domain/datasets/【ここがデータセットID】/documents
③ IAPのクライアントID(IAP_CLIENT_ID)
プログラムが「どのIAPの扉を開けたいのか」を指定するための身分証番号(オーディエンスコード)です。
- Google Cloud Console にログインし、対象のIAPプロジェクトを開く。
- 左側のメニューから「APIとサービス」 > 「OAuth 同意画面」をクリック。
- 「クライアント」の一覧から、対象のOAuth 2.0 クライアントを見つける。
- 「クライアントID」の列の
xxxx-xxxx.apps.googleusercontent.comという形式の文字列をコピーする。
指定するのは、APIのURLではなく、対象のIAP保護リソースに紐づく OAuth 2.0 クライアントID です。
1. IAP(Identity-Aware Proxy)認証とは?
IAPは、Google Cloud上でアプリケーションへのアクセスを制御するゼロトラストなプロキシサービスです。プログラムからIAPを通過するためには、以下の条件を満たす必要があります。
-
適切な権限: 呼び出し元に「IAP セキュアウェブアプリユーザー(
roles/iap.httpsResourceAccessor)」の権限があること。 - 正しいIDトークン: OIDC(OpenID Connect)トークンを取得し、リクエストヘッダーに含めること。
- Audience(宛先)の厳格な指定: トークンの宛先が、対象となるIAPリソースの「OAuth クライアントID」と完全に一致していること。
要件自体はシンプルですが、いざ実装しようとすると様々なエラーに直面します。
2. ハマったポイント(失敗したやり方)
失敗1:gcloudコマンドでのトークン取得エラー
手軽にテストしようと、以下のコマンドでトークンを取得しようとしました。
gcloud auth print-identity-token --audiences=xxxx.apps.googleusercontent.com
結果
ERROR: Invalid account type for --audiences. Requires valid service account.
ユーザーアカウント(個人のGoogleアカウント)では、直接Audienceを指定したトークンを発行できない仕様になっており上記のエラーで弾かれました。テストにはサービスアカウントが必要です。
失敗2:IAM Credentials API (sign_jwt) を使った手動署名
サービスアカウントキー(JSON)を使わずに認証したいと考え、自前でJWTを作成し、署名するコードを書きました。
結果
401 Unauthorized`
Invalid IAP credentials: Audience specified does not match requested endpoint
というエラーが発生しました。自前でペイロードを組むと、Audienceの設定やトークンの形式をIAPが求める厳密な仕様に合わせるのが非常に難しく、今回はGoogle Authライブラリの impersonated_credentials.IDTokenCredentials を使って発行する方法に切り替えました。
失敗3:ローカルでの権限不足エラー
ローカルからテストするにあたり、「IAPアクセス権を持つサービスアカウント」
のトークンを発行する必要があります。
しかし、サービスアカウントの秘密鍵ファイル(JSON)を直接使う方法は
セキュリティリスクが高いため、代わりに なりすまし(Impersonation)
という仕組みを使います。
なりすまし(Impersonation)とは?
自分のアカウント(またはCloud RunなどのGoogle Cloudサービスの実行サービスアカウント)の権限を使って、
別のサービスアカウントのIDトークンを「代理で発行」する仕組みです。
秘密鍵ファイルを持ち歩かずに済むため、よりセキュアな認証が実現できます。
これを実現するのが google.auth.impersonated_credentials ライブラリです。
この方法を試すため、id_token.fetch_id_token を実行しました。
id_token.fetch_id_token とは?
Google Authライブラリに用意されている実行環境の認証情報(ADC)を自動検知して、指定した宛先用のOIDCトークンを良しなに発行してくれる便利関数です。公式ドキュメントなどでも紹介されており、これを使えば1行で簡単にトークンが取れるだろうと考えて採用しました。
結果
Permission 'iam.serviceAccounts.getOpenIdToken' denied...
ローカルのGoogleアカウントから、対象のサービスアカウントに「なりすます」ための「サービスアカウント トークン作成者」の権限が付与されていなかったことが原因でした。
失敗4 【最大の罠】 APIヘッダーの衝突(Authorizationが消される)
ようやくIAPを通るトークンが生成できるようになった際、以下のコードでDifyのAPIを呼び出しました。
headers = {
'Authorization': f'Bearer {iap_token}', # IAP用
'X-App-Api-Key': DIFY_API_KEY # Dify用
}
結果
401 Unauthorized: Authorization header must be provided and start with 'Bearer'
IAPもDifyも、本来どちらも Authorization: Bearer <トークン> というヘッダーを読み取ろうとします。しかし、IAPは自分が使った Authorization ヘッダーを、バックエンド(Dify)に渡す前に削除してしまうという仕様がありました。その結果、Difyには認証情報が届かずエラーになっていたのです。

3. 解決策
上記の失敗を踏まえ、以下の3つのポイントを押さえることで無事にIAPを突破できました。
① サービスアカウントへの「なりすまし(Impersonation)」を利用する
google.auth.impersonated_credentials.IDTokenCredentials を使って、IAPアクセス権限を持つサービスアカウントのIDトークンを生成します。
② トークンの宛先(Audience)にはIAPのクライアントIDを指定する
URLではなく、IAPの設定画面から取得できる xxx.apps.googleusercontent.com を target_audience に指定します。
③ ヘッダーを分離する(Proxy-Authorization の活用)
これが最も重要です。IAPを通過させるためのトークンは Proxy-Authorization に入れます。これにより、IAPは Proxy-Authorization を見て認証を通し、本来の Authorization ヘッダーはそのままDifyへとパススルーしてくれます。
成功したPythonコード
import os
import requests
import google.auth
import google.auth.transport.requests
from google.auth import impersonated_credentials
DIFY_API_KEY = os.environ.get('DIFY_API_KEY')
DIFY_DATASET_ID = os.environ.get('DIFY_DATASET_ID')
IAP_CLIENT_ID = os.environ.get('IAP_CLIENT_ID')
# なりすます対象のサービスアカウント(IAPへのアクセス権を持つもの)
TARGET_SA = os.environ.get('TARGET_SA', 'dify@your-project.iam.gserviceaccount.com')
API_URL = f"https://your-domain/v1/datasets/{DIFY_DATASET_ID}/document/create-by-file"
def get_iap_token(client_id):
"""サービスアカウントになりすまして、IAP認証用のIDトークンを取得する"""
# 1. ローカル、または実行環境の権限(ADC)を取得
source_credentials, _ = google.auth.default()
# 2. サービスアカウントになりすますためのベースCredentialsを作成
base_impersonated_creds = impersonated_credentials.Credentials(
source_credentials=source_credentials,
target_principal=TARGET_SA,
target_scopes=["https://www.googleapis.com/auth/cloud-platform"]
)
# 3. IDトークン発行専用のクラスを使用して、AudienceをIAPクライアントIDに設定
iap_creds = impersonated_credentials.IDTokenCredentials(
base_impersonated_creds,
target_audience=client_id,
include_email=True
)
# 4. トークンをフェッチ
auth_req = google.auth.transport.requests.Request()
iap_creds.refresh(auth_req)
return iap_creds.token
def main():
try:
iap_token = get_iap_token(IAP_CLIENT_ID)
# 【重要】ヘッダーの組み立て
headers = {
# IAP認証用(バックエンドに届く前にIAPが消費する)
'Proxy-Authorization': f'Bearer {iap_token}',
# Dify API認証用(IAPをパススルーしてDifyに届く)
'Authorization': f'Bearer {DIFY_API_KEY}'
}
print("IAP経由でAPIにリクエストを送信中...")
# 実際には files=..., data=... などDify API仕様に応じたペイロードを渡してください
response = requests.post(API_URL, headers=headers) # POST等の必要なメソッドを指定
response.raise_for_status()
print("成功!レスポンス:", response.text)
except Exception as e:
print(f"エラー発生: {e}")
if __name__ == "__main__":
main()
4. 【おまけ】本番環境(Cloud Run Jobs)へデプロイする際の注意
ローカルで無事にIAPを突破できたコードを、いざ本番環境(Cloud Run Jobs)にデプロイして自動化しようとしたところ、最後の最後でまたしてもエラーに遭遇しました。
失敗5:本番環境でのみ発生するエラー
Error getting ID token: {'error': {'code': 403, 'message': "Permission 'iam.serviceAccounts.getOpenIdToken' denied on resource...
原因:サービスアカウント間の「なりすまし権限」の不足
セキュリティを高めるため、Cloud Runの実行権限とAPIアクセスの権限を分離して設計しました。その結果、サービスアカウント間のなりすまし設定が必要になります。
そのため、実行用サービスアカウントがIAP用サービスアカウントに「なりすまして」トークンを発行しようとした際、Google Cloud側から「実行用サービスアカウントには他のサービスアカウントのトークンを発行する権限がない」と弾かれてしまったのです。
解決策:実行元のサービスアカウントにロールを追加する
Google Cloudコンソールの「IAM と管理」から、実行元のサービスアカウントに対して以下の権限を追加します。
- 実行元のサービスアカウント(Cloud Run等に割り当てたサービスアカウント)の編集ボタンをクリック。
- 「別のロールを追加」 をクリックし、「サービスアカウント トークン作成者 (Service Account Token Creator)」を付与して保存。
この設定を追加したところ、コードはそのままでCloud Run Jobs上でIAPを突破し、パイプラインを全自動化することに成功しました!
まとめ
Cloud IAPで保護されたAPIをプログラムから呼び出すためのポイントです。
-
Proxy-Authorizationヘッダーを使ってIAPを突破し、本来のAuthorizationヘッダーをバックエンドに届ける。 - 対象リソースの IAPクライアントID (
xxx.apps.googleusercontent.com) をAudience(宛先)に指定したIDトークンを生成する。 -
impersonated_credentials.IDTokenCredentialsを使い、IAPアクセス権を持つサービスアカウントになりすましてトークンを発行する。 - なりすましを実行する主体(ローカルのアカウント、またはCloud Run等の実行サービスアカウント)には、必ず 「サービスアカウント トークン作成者」 のIAMロールを付与しておく。
Google Cloudの強固なセキュリティの裏返しとも言えるややこしい仕様ですが、一度仕組み(誰が、誰になりすまして、どこ宛のトークンを作って、どのヘッダーに入れるのか)を理解できれば応用が効きます。同じエラーで詰まっている方の参考になれば幸いです。
参考
