Goのアプリケーションを使ってCloud Run間でセキュアに通信したい
はじめに
この記事では、Cloud Run上のサービス同士で認証付き通信を実現するために、
Goを用いてIDトークンを発行しリクエストを送信する方法にちょっとハマったので備忘録です
背景と目的
- APIのCloud Runは認証ありにして、悪意のあるユーザーからのリクエストを防ぎたい
- BatchやFrontのアプリケーションからはアクセスできるようにしたい
- GCPのドキュメントを見たが認証方法がいっぱいあって逆にどうすればいいのかパッとわからんので整理したい
システムの構成
システムの構成はざっくりこんな感じ。
認証付きリクエストの実装例
ドキュメントを漁ると似たような内容は出てくるが、サービス間でのやり取りはこれで良さそう。
公式ドキュメントより引用
import (
"context"
"fmt"
"io"
"net/http"
"google.golang.org/api/idtoken"
)
// makeIAPRequest は、IAP で保護されたアプリケーションにリクエストを送る例です。
func makeIAPRequest(w io.Writer, request *http.Request, audience string) error {
ctx := context.Background()
// 指定した audience(Cloud Run の URL や IAP のクライアントID)に対して ID トークンを自動取得
client, err := idtoken.NewClient(ctx, audience)
if err != nil {
return fmt.Errorf("idtoken.NewClient: %w", err)
}
response, err := client.Do(request)
if err != nil {
return fmt.Errorf("client.Do: %w", err)
}
defer response.Body.Close()
if _, err := io.Copy(w, response.Body); err != nil {
return fmt.Errorf("io.Copy: %w", err)
}
return nil
}
ふむふむ。
ライブラリもあるし思ったより簡単そうやな!
ってことですぐに実装してみたらエラーが出た。
could not find default credentials. See https://cloud.google.com/docs/authentication/external/set-up-adc for more information
credentials
が必要とのことなので、ログに出てたリンクに飛んでみる。
どうやらローカル環境での方法があるらしい。
検索の順序
ADC は次の場所で認証情報を検索します。
- GOOGLE_APPLICATION_CREDENTIALS 環境変数
- gcloud auth application-default login コマンドを使用して作成された認証情報ファイル
- 接続済みのサービス アカウント
3はローカルの話だからまずない。
1の環境変数の場合だとあまり推奨されてないjsonキーをローカルに置かないと行けないから選択肢としては基本なし。
なので2でやればいけそう!
以上、終了!
ライブラリ内の実装
めっちゃ簡単やないかと思ったが、ところがどっこい!
ローカルでこれを実行すると、以下のエラーが出た。
idtoken: unsupported credentials type
話が違うじゃないか。
ローカルではこれでええんちゃうんかいな!
とりあえずエラーを出してる部分のライブラリのコード読んでみた。
func tokenSourceFromBytes(ctx context.Context, data []byte, audience string, ds *internal.DialSettings) (oauth2.TokenSource, error) {
allowedType, err := getAllowedType(data)
if err != nil {
return nil, err
}
switch allowedType {
case serviceAccount:
cfg, err := google.JWTConfigFromJSON(data, ds.GetScopes()...)
if err != nil {
return nil, err
}
customClaims := ds.CustomClaims
if customClaims == nil {
customClaims = make(map[string]interface{})
}
customClaims["target_audience"] = audience
cfg.PrivateClaims = customClaims
cfg.UseIDToken = true
ts := cfg.TokenSource(ctx)
tok, err := ts.Token()
if err != nil {
return nil, err
}
return oauth2.ReuseTokenSource(tok, ts), nil
case impersonatedServiceAccount, externalAccount:
type url struct {
ServiceAccountImpersonationURL string `json:"service_account_impersonation_url"`
}
var accountURL *url
if err := json.Unmarshal(data, &accountURL); err != nil {
return nil, err
}
account := filepath.Base(accountURL.ServiceAccountImpersonationURL)
account = strings.Split(account, ":")[0]
config := impersonate.IDTokenConfig{
Audience: audience,
TargetPrincipal: account,
IncludeEmail: true,
}
ts, err := impersonate.IDTokenSource(ctx, config, option.WithCredentialsJSON(data))
if err != nil {
return nil, err
}
return ts, nil
default:
return nil, fmt.Errorf("idtoken: unsupported credentials type")
}
}
どうやらサービスアカウントの認証を想定してるようで、ユーザー認証だとダメらしい。
さて、どうしましょ...
サービスアカウントの権限を借用
ドキュメントを漁ってると普通ここに書いてた。
まあたどり着きづらかった。(そう思ったのは自分だけなのか?)
以下のように、オプションで権限を借用するサービスアカウントを指定できるらしい。
gcloud auth application-default login --impersonate-service-account SERVICE_ACCT_EMAIL
よし今度こそ!
と思ったら次はIAM_PERMISSION_DENIED
で怒られた。
"error": {
"code": 403,
"message": "Permission 'iam.serviceAccounts.getAccessToken' denied on resource (or it may not exist).",
"status": "PERMISSION_DENIED",
・・・
}
借用しているサービスアカウントにroles/iam.serviceAccountTokenCreator
権限がついていてもダメで、借用する側に権限をつけないといけないらしい。
CLIでログインしているユーザーアカウントに権限をつけてもう一回実行してみたが何も変わらず...
動かんかったので一旦キャッシュをクリアしてみる。
gcloud auth application-default revoke
これでうまくいった!
まとめ
やっぱ公式ドキュメントって初見だとわかりづらいことありますよね。
Discussion