1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Google Cloud の IAPで保護されたAPIをプログラムから呼び出す

1
Posted at

はじめに

以前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

  1. Difyのダッシュボードにログインする。
  2. 上部メニューの「ナレッジ (Knowledge)」から、対象のデータセットを開く。
  3. 左側のナビゲーションメニューにある「APIアクセス (API Access)」をクリック。
  4. 画面右上にある「API Keys」のセクションで「新しいキーを作成」をクリックし、生成されたキー(dataset-xxx...)をコピーする。

② DifyのデータセットID(DIFY_DATASET_ID

対象のデータセットを開いているときのブラウザのURLから取得できます。

  • URLの例
    https://your-domain/datasets/【ここがデータセットID】/documents

③ IAPのクライアントID(IAP_CLIENT_ID

プログラムが「どのIAPの扉を開けたいのか」を指定するための身分証番号(オーディエンスコード)です。

  1. Google Cloud Console にログインし、対象のIAPプロジェクトを開く。
  2. 左側のメニューから「APIとサービス」 > 「OAuth 同意画面」をクリック。
  3. 「クライアント」の一覧から、対象のOAuth 2.0 クライアントを見つける。
  4. 「クライアントID」の列のxxxx-xxxx.apps.googleusercontent.com という形式の文字列をコピーする。

指定するのは、APIのURLではなく、対象のIAP保護リソースに紐づく OAuth 2.0 クライアントID です。

1. IAP(Identity-Aware Proxy)認証とは?

IAPは、Google Cloud上でアプリケーションへのアクセスを制御するゼロトラストなプロキシサービスです。プログラムからIAPを通過するためには、以下の条件を満たす必要があります。

  1. 適切な権限: 呼び出し元に「IAP セキュアウェブアプリユーザー(roles/iap.httpsResourceAccessor)」の権限があること。
  2. 正しいIDトークン: OIDC(OpenID Connect)トークンを取得し、リクエストヘッダーに含めること。
  3. 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には認証情報が届かずエラーになっていたのです。
APIヘッダーの衝突.png

3. 解決策

上記の失敗を踏まえ、以下の3つのポイントを押さえることで無事にIAPを突破できました。

① サービスアカウントへの「なりすまし(Impersonation)」を利用する

google.auth.impersonated_credentials.IDTokenCredentials を使って、IAPアクセス権限を持つサービスアカウントのIDトークンを生成します。

② トークンの宛先(Audience)にはIAPのクライアントIDを指定する

URLではなく、IAPの設定画面から取得できる xxx.apps.googleusercontent.comtarget_audience に指定します。

③ ヘッダーを分離する(Proxy-Authorization の活用)

これが最も重要です。IAPを通過させるためのトークンは Proxy-Authorization に入れます。これにより、IAPは Proxy-Authorization を見て認証を通し、本来の Authorization ヘッダーはそのままDifyへとパススルーしてくれます。

ヘッダー分離.png

成功した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 と管理」から、実行元のサービスアカウントに対して以下の権限を追加します。

  1. 実行元のサービスアカウント(Cloud Run等に割り当てたサービスアカウント)の編集ボタンをクリック。
  2. 「別のロールを追加」 をクリックし、「サービスアカウント トークン作成者 (Service Account Token Creator)」を付与して保存。

この設定を追加したところ、コードはそのままでCloud Run Jobs上でIAPを突破し、パイプラインを全自動化することに成功しました!

まとめ

Cloud IAPで保護されたAPIをプログラムから呼び出すためのポイントです。

  1. Proxy-Authorization ヘッダーを使ってIAPを突破し、本来の Authorization ヘッダーをバックエンドに届ける。
  2. 対象リソースの IAPクライアントID (xxx.apps.googleusercontent.com) をAudience(宛先)に指定したIDトークンを生成する。
  3. impersonated_credentials.IDTokenCredentials を使い、IAPアクセス権を持つサービスアカウントになりすましてトークンを発行する。
  4. なりすましを実行する主体(ローカルのアカウント、またはCloud Run等の実行サービスアカウント)には、必ず 「サービスアカウント トークン作成者」 のIAMロールを付与しておく。

Google Cloudの強固なセキュリティの裏返しとも言えるややこしい仕様ですが、一度仕組み(誰が、誰になりすまして、どこ宛のトークンを作って、どのヘッダーに入れるのか)を理解できれば応用が効きます。同じエラーで詰まっている方の参考になれば幸いです。

参考

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?