🤧

GCP Cloud Build から IAP 認証の App Engine にアクセスする

2021/04/09に公開

Google App Engine へのアプリケーションレベルのアクセス制御の方法として有力なのが IAP (Identity-Aware Proxy) です。このIAPを有効にしているApp Engineに対してアクセスするテストをCloud Build上で実行しようとしたところ大ハマリしたので、本記事ではその解決策を書き残します。

IAPを有効にする方法

これは各所にドキュメントや参考となるサイトがあります。Webのコンソールからぽちぽちすれば終わります。
https://cloud.google.com/iap/docs/app-engine-quickstart?hl=ja
https://medium.com/google-cloud-jp/gae-2nd-gen-でのサービス間認証-1ed1d8b1abce
https://qiita.com/mag-chang/items/82d0aefbd988e4d749f2

プログラムからIAPで保護されたApp Engineにアクセスする

こうしてIAPを有効にしたApp EngineのエンドポイントをWebブラウザで見に行ってみると、OAuth2の認可の画面が出てくるはずです。

ではプログラムからアクセスする際、この認証はどうしたらよいでしょうか。これには前掲のサイトや以下ドキュメントが参考になりまして、OIDCトークンをHTTPヘッダに付加します
https://cloud.google.com/iap/docs/authentication-howto#authenticating_from_a_service_account

7つの言語での例があり、ほぼ不自由しないでしょう。ここではC#を使うことにし、xUnit.netのテストコードとして記述します。

using Google.Apis.Auth.OAuth2;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks;
using XUnit;

public class IAPClientTest
{
    [Fact]
    public async Task InvokeRequestAsync()
    {
        // https://cloud.google.com/iap/docs/authentication-howto を参考に参照
        const string iapClientId = "123456789012-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.apps.googleusercontent.com";
	// GET / で何か返ってくる適当なApp Engine
	const string uri = "https://your-sample-dot-your-project-id.appspot.com/";
	
	var cts = new CancellationTokenSource();
        var oidcToken = await GetOidcTokenAsync(iapClientId, cts.Token).ConfigureAwait(false);
        var token = await oidcToken.GetAccessTokenAsync(cts.Token).ConfigureAwait(false);

        using var httpClient = new HttpClient();
        httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
        var response = await httpClient.GetAsync(uri, cts.Token).ConfigureAwait(false);
	response.EnsureSuccessStatusCode();
    }

    public async Task<OidcToken> GetOidcTokenAsync(string iapClientId, CancellationToken cancellationToken)
    {
        var credential = await GoogleCredential.GetApplicationDefaultAsync(cancellationToken).ConfigureAwait(false);
        return await credential.GetOidcTokenAsync(OidcTokenOptions.FromTargetAudience(iapClientId), cancellationToken).ConfigureAwait(false);
    }
}

Cloud Buildからアクセスする (プログラム)

上記テストをCloud Buildで実行します。
https://console.cloud.google.com/gcr/images/google-appengine/GLOBAL/aspnetcore@sha256:979df3b921bb7d71cf8e8925a8f22d5bb1dc0bc15f575e502f7d672abf2b9b89/details?tab=info

cloudbuild.yaml の一例です。

steps:
- name: 'gcr.io/google-appengine/aspnetcore:3.1'
  id: 'test'
  entrypoint: dotnet
  args: ['test', 'MyProject.Tests', '--configuration', 'Release', '--runtime', 'ubuntu.16.04-x64']

しかしこれは失敗します。GoogleCredential.GetApplicationDefaultAsync() によって得られるのはCloud Buildのサービスアカウント (xxxxx@cloudbuild.gserviceaccount.com) の権限です。なぜか不明ですが、Cloud BuildのサービスアカウントではどうしてもOIDCトークンが取得できないようです。オーナー権限を与えたりも試しましたが実らずでした。

GCPのサポートに問い合わせたところ、何か別のサービスアカウントを使ってあげるようにすると通るとのことでした。以下では、App Engineのデフォルトサービスアカウント (<your-project-id>@appspot.gserviceaccount.com) を使うこととし、そのサービスアカウントの認証用JSONを発行しておきます。Secret ManagerにそのJSONの中身をべた書きして、Cloud Buildへ送り込みました。

https://cloud.google.com/build/docs/securing-builds/use-secrets

steps:
- name: 'gcr.io/google-appengine/aspnetcore:3.1'
  id: 'test'
  entrypoint: dotnet
  args: ['test', 'MyProject.Tests', '--configuration', 'Release', '--runtime', 'ubuntu.16.04-x64']
  secretEnv: ['GCP_CREDENTIAL']
availableSecrets:
  secretManager:
  - versionName: projects/PROJECT_ID/secrets/YOUR_SECRET_NAME/versions/latest
    env: GCP_CREDENTIAL

C#コードでは環境変数 GCP_CREDENTIAL を参照してGoogleCredentialを得ます。

public class IAPClientTest
{
    [Fact]
    public async Task InvokeRequestAsync()
    {
        // 同じなので省略
    }
    
    private async Task<OidcToken> GetOidcTokenAsync(string iapClientId, CancellationToken cancellationToken = default)
    {
        var credential = await GetCredentialAsync(cancellationToken).ConfigureAwait(false);
        var oidcTokenOptions = OidcTokenOptions.FromTargetAudience(iapClientId);
        return await credential.GetOidcTokenAsync(oidcTokenOptions, cancellationToken).ConfigureAwait(false);
    }

    private Task<GoogleCredential> GetCredentialAsync(CancellationToken cancellationToken)
    {
        var credentialJson = Environment.GetEnvironmentVariable("GCP_CREDENTIAL");
        if (!string.IsNullOrEmpty(credentialJson))
        {
            return Task.FromResult(GoogleCredential.FromJson(credentialJson));
        }
        return GoogleCredential.GetApplicationDefaultAsync(cancellationToken);
    }
}

だいぶゴリ押し感がありますが、これで何とか通りました。

Cloud Buildからアクセスする (コマンドライン)

ここからはcurlで問い合わせる例です。GCPのサポートから教えて頂きました。cloudbuild.yaml に以下のように書くと試すことができます。

ここでも同様に、Cloud Buildのサービスアカウントではダメなので、何か別のサービスアカウントを使います。

steps:
- name: gcr.io/cloud-builders/gcloud
  entrypoint: "bash"
  args:
    - "-c"
    - | 
        curl -X GET -H "Authorization: Bearer \
	  $(gcloud auth print-identity-token \
	  --impersonate-service-account="your-project-id@appspot.gserviceaccount.com" \
	  --audiences="123456789012-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.apps.googleusercontent.com" \
	  --include-email \
	  --verbosity=error)" \
	  "https://your-project-dot-your-project-id.appspot.com/"

これを応用すると、先に説明したプログラムからアクセスするのは別の方法も考えられます。gcloud auth print-identity-token の出力を環境変数かテキストファイルにでも入れておき、テストプログラムはそれを参照するというのも一手だと思います。

Discussion