GCP Cloud Build から IAP 認証の App Engine にアクセスする
Google App Engine へのアプリケーションレベルのアクセス制御の方法として有力なのが IAP (Identity-Aware Proxy) です。このIAPを有効にしているApp Engineに対してアクセスするテストをCloud Build上で実行しようとしたところ大ハマリしたので、本記事ではその解決策を書き残します。
IAPを有効にする方法
これは各所にドキュメントや参考となるサイトがあります。Webのコンソールからぽちぽちすれば終わります。
プログラムからIAPで保護されたApp Engineにアクセスする
こうしてIAPを有効にしたApp EngineのエンドポイントをWebブラウザで見に行ってみると、OAuth2の認可の画面が出てくるはずです。
ではプログラムからアクセスする際、この認証はどうしたらよいでしょうか。これには前掲のサイトや以下ドキュメントが参考になりまして、OIDCトークンをHTTPヘッダに付加します。
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で実行します。
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へ送り込みました。
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