Cloud Intelligence™Cloud Intelligence™

Cloud Intelligence™

マイクロサービス間の認証はそんなに難しい?

By Joshua FoxJun 28, 202210 min read

このページはEnglishDeutschEspañolFrançaisItalianoPortuguêsでもご覧いただけます。

マイクロサービス間で認証を行う方法を、最もシンプルだがセキュリティと保守性に劣るアプローチから、推奨アーキテクチャまで段階的に解説します。 ![authentication-microservices](https://media.doit.com/imports/wordpress/2022/06/60199b169d8a-authentication-microservices.jpg) マイクロサービスアーキテクチャの本質は、サービス同士が互いを呼び出し合うことにあります。では、自分のサービスだけが他のサービスを呼び出せるようにするには、どうすれば安全性を確保できるでしょうか。認証はどのように行えばよいのでしょうか。 ブラウザアプリでのやり方はおなじみでしょう。ユーザーが認証情報を入力すると、ブラウザがそれをサービスに送信し、サービスは一定期間ユーザーを認証するセッショントークンを返します。しかしマイクロサービスでは、パスワードや多要素認証キーを入力する人間が介在しません。 本記事では、Cloud RunやGoogle Kubernetes Engine (GKE) などのGoogle Cloud Platformサービス上で動作するサーバーを中心に、その実現方法を解説します。クライアント側のサービスはGCP、オンプレミス、AWSのいずれにあっても構いません。 本記事を書こうと思ったきっかけは、API GatewayとCloud Endpointsが強力な認証機能を備えつつ急速に進化している一方で、互いに、また競合する他サービスと比べて制約もあるからです。マイクロサービス間で認証を行う方法は数多くあります。最もシンプルだがセキュリティと保守性に劣るものから始め、推奨アーキテクチャへと段階的に進めていきます。 話を分かりやすくするため、両側を自分で管理できるマイクロサービスを前提としますが、クライアント側が組織外にある場合でも同じ原則が適用されます。 ## **基本:ヘッダー、キー、プロキシ** もっとシンプルな方法も複雑な方法もありますが、サービス間認証は非常に慎重な設計が求められます。基本的な流れは次のとおりです。 - クライアントは秘密鍵を使ってトークンに署名する - これを渡す標準フォーマットは [JSON Web Token](https://jwt.io/) です - トークンは以下のようにHTTP Authorizationヘッダーに格納します ``` Authorization: Bearer ``` ここで はbase64エンコードされたトークンです。 - サーバーは外部サービスに問い合わせてそのトークンを検証します。あるいはリバースプロキシがリクエストを受け取り、実際のサーバーに渡す前に外部サービスへ問い合わせてトークンを検証することもできます。 - GCPでは、その検証サービスはプラットフォーム側が提供します。 本記事では、シンプルで安全性の低いものから、より完成度の高い解決策まで、複数のやり方を紹介していきます。 ## **シンプルすぎる:自前管理の「APIキー」** 選択肢の全体像を知らない方がよく採る基本的な方法は、ユーザーがユーザー名とパスワードでログインするのと似たやり方です。クライアントサービスに「APIキー」と呼ばれる秘密の文字列を保存し、これを認証情報としてサーバー側で検証します。 ``` APIKEY=Microservice1:78eb9a45897f ``` ## **制約** これは安全ではありません。 **キーの漏洩** キーは想像以上にさまざまな経路で漏洩します。 これを防ぐには、シークレットをGitなどのソースコード管理に保存してはいけません。誤って公開してしまうケースが非常に多いからです。代わりにGoogle Cloud Secret ManagerやHashicorp Vaultのようなシークレット管理サービスを使いましょう。とはいえ、同じ問題が残ります。クライアントサービスはシークレット管理サービスにアクセスするための認証情報を保存しなければならないからです。 **キー管理** キーを保存するためのサーバー側データベースと、受け取ったキーが正しいかを検証するレイヤーを開発する必要があります。このレイヤーからもキーを漏らしたくないので、クライアントはAPIキーそのものではなくハッシュを送信し、サーバーは保存しておいたキーのハッシュと照合する形になります。これらをすべて維持するのはコストがかかります。さらに、あらゆる穴をふさぐだけの専門知識と労力を割けないため、安全性も低くなりがちです。セキュリティの仕組みは、可能な限り専門家に任せるべきです。 **ローテーション** 漏洩は避けられないため、ベストプラクティスはキーを頻繁にローテーションすることです。新しいキーを発行し、一定期間後に古いキーを無効化します。これにはクライアント側で新しいキーを要求する仕組み(このリクエストを古いキーで認証する!)の自動化が必要です。サーバー側でも、オンデマンドで新しいキーを生成し、古いキーを指定の期日に無効化する仕組みが要ります。 こうした仕組みは、最初に思うよりも常に複雑です。たとえば、同時に有効なキーのバージョン数に上限を設けたくなるはずです。2つの有効バージョンはローテーションのために必要ですが、100バージョンも有効になっていたら、漏洩を待っているようなものですから。 ## **クラウドプロバイダーのサービスアカウントキー** ハッシュ化、検証、ローテーション、有効期限の仕組みを自前で作る必要があるでしょうか。一段優れた方法は、サービスアカウントを作成し、Google Cloud Identity and Access Management (IAM) からキーファイルをダウンロードすることです。[サービスアカウントページ](https://console.cloud.google.com/iam-admin/serviceaccounts)からキーを作成できます。 ![authentication-between-microservices](https://media.doit.com/imports/wordpress/2022/06/bfe927fd4375-authentication-between-microservices.jpg) ![authentication-and-authorization-in-microservices](https://media.doit.com/imports/wordpress/2022/06/b7d369dcc0df-authentication-and-authorization-in-microservices.jpg) JSONをダウンロードし、有効期限を必ず設定してください。[これはGoogle Cloudの新機能です](https://cloud.google.com/iam/docs/service-accounts#key-expiry)。JSONは次のような形になります。(ご安心を。テキストはしっかりマスクしましたし😁、すでにキーは無効化済みです!) ``` { "type": "service_account", "project_id": "myproject", "private_key_id": "ded9d97108b…..5cfd179e95e0e1", "private_key": " — — -BEGIN PRIVATE KEY — — -\nMIIEvKIBADABNBg….QDA6woGjE4Q — — -END PRIVATE KEY — — -\n", "client_email": "[email protected]", "client_id": "106482...4210366919", "auth_uri": "https://accounts.google.com/o/oauth2/auth", "token_uri": "https://oauth2.googleapis.com/token", "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/kubeflowpipeline%40joshua-playground.iam.gserviceaccount.com" } ``` ## **制約** 良さそうに見えますよね。ですが(お察しのとおり)、これも望むほど安全で便利というわけではありません。 自作のAPIキーと同じく、サービスアカウントのキーファイルも漏洩する可能性があるため、ローテーションが必要です。Googleはキーに有効期限を設定する機能や、新しいキーを取得するためのAPIを提供していますが、それらを実際に使うのはあなた自身です。 後ほど、サービスアカウントをクライアントアプリケーションに組み込み、キーファイル自体を持たずに済ませる方法を解説します。その前に、キーファイル経由であれ組み込み型であれ、サービスアカウントをどう活用するかを見ていきましょう。 ## **アプリケーションコードでの認証** 認証にあたって、クライアントサービスは自身のサービスアカウントを使います。 これは [OpenID Connect (OIDC)](https://cloud.google.com/endpoints/docs/openapi/service-account-authentication) で行い、署名付きJSON Web Token (JWT) を渡します。これらのトークンは [有効期間が短く(数週間ではなく数時間)](https://cloud.google.com/iam/docs/creating-short-lived-service-account-credentials)、漏洩時のリスクを最小限に抑えられます。 クライアント側とサーバー側のソフトウェアライブラリを使えば、自前のコードレベルでこれを実装できます。 - まず、クライアントは [ソフトウェアライブラリ](https://developers.google.com/identity/protocols/oauth2/service-account)とサービスアカウントキーを使い、アクセスリクエスト用のJWTトークンを生成・署名します。 - 次に、そのトークンを使ってGoogle認証サーバーに別のトークン(アクセストークン)を要求します。Google認証サーバーは、サービスアカウントが確かにそのアクセスリクエストJWTに署名したことを確認し、その事実を証明するアクセストークンを返します。 - クライアントはこのアクセストークンを使って、あなたのマイクロサービスを呼び出します。 - あなたのマイクロサービスはソフトウェアライブラリを使い、アクセストークンが確かにGoogleのサービスによって検証・署名されていることを確認します。 [このフローと似ています](https://developers.google.com/identity/protocols/oauth2#serviceaccount)が、呼び出し先がGoogle APIではなく、あなた自身のマイクロサービスである点が違います。 ![authorization-in-microservices](https://media.doit.com/imports/wordpress/2022/06/cb0961744a25-authorization-in-microservices.jpg) ## **制約** **サーバーとの密結合** この方法では、サーバー内部にコードを書き込む必要があります。同じ要件を持つマイクロサービスが複数ある場合、その認証レイヤーを複数のコードベースで保守し、それぞれのセキュリティを担保することになります。 後ほど、このコードをアプリケーションに組み込まずに済ませる方法を紹介します。その前に、キーファイルの利用そのものを完全にやめる方法を見ていきましょう。 ## **組み込み型サービスアカウント:GCP** クライアントサービスをGCPにデプロイするなら、サービスアカウントキーは使わないでください。代わりに、指定したサービスアカウントを組み込んだ状態でサービスを起動します。 たとえばGoogle Compute Engine (GCE) インスタンスでは、次のように指定します。 ``` gcloud compute instances create [INSTANCE_NAME] --service-account [SERVICE_ACCOUNT_EMAIL] ... ``` これでサービスアカウントを指定できます。Cloud Runや、クライアントマイクロサービスから他のマイクロサービスを呼び出し得るその他のGCPサービスでも同様です。 これでキーファイルの漏洩を心配する必要はなくなります。そもそもキーファイルが存在しないからです。代わりに [メタデータサーバー](https://cloud.google.com/compute/docs/instances/verifying-instance-identity)が、サービスアカウントの身元を保証する署名付きインスタンストークンを生成します。(しかも、メタデータサーバーへのリクエストは、VMが動作する物理インスタンスから外に出ることはありません。) ## **Kubernetes上で** Kubernetesには独自のサービスアカウントの仕組みがあり、Kubernetes固有の認証システムに参加します。これはGCP IAMレイヤーとは別物なので、クライアントサービスがGoogle Kubernetes Engine上にある場合は、[Workload Identity](https://cloud.google.com/kubernetes-engine/docs/how-to/workload-identity) を使ってGCP IAMサービスアカウントをKubernetesレイヤーに割り当てます。Workload IdentityはGKEからGCP APIへのすべての呼び出しを透過的にインターセプトしてプロキシし、アクセストークンを付加します。 クライアントがAWSのElastic Kubernetes Service上にある場合は、IAMロールを割り当ててGCPのフローに参加させることもできます。詳しくは次のセクションで説明します。 ## **組み込み型ロール:AWSとWorkload Identity Federation** クライアントサービスがAWS上にある場合、GCPサービスアカウントで起動することはできませんが、AWSにおける相当物であるロールで起動できます。Lambdaは [実行ロール](https://docs.aws.amazon.com/lambda/latest/dg/lambda-intro-execution-role.html)で、EC2インスタンスは [ロール(「インスタンスプロファイル」でラップしたもの)](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-ec2.html)で起動します。 GCPはそのロールを直接信頼することができないため、[Workload Identity Federation](https://cloud.google.com/iam/docs/workload-identity-federation) (WIF) を使ってAWSとGCPを橋渡しします( [関連記事](https://medium.com/google-cloud/keyless-api-authentication-launching-gcp-workloads-from-aws-b715b4e6c99a))。 フローは次のとおりです。 - まず、AWS上のクライアントサービスが自身のロールを使ってトークン(トークン1)に署名します。 - そのトークン1を使い、AWS IAMが署名した別のトークン(トークン2)を要求します。 - トークン2を使い、GCP WIFにアクセストークン(トークン3)への署名を依頼します。Google WIFはあらかじめ当該AWSロールを信頼するよう設定されており、AWSがそのリクエストはロール由来であると証明したので、WIFはアクセストークン(トークン3)に署名して返します。 - クライアントサービスは、GCPベースのクライアントサービスがアクセストークンを使うのと同じ要領でトークン3を使います。ここから先のフローは同じです。 ![microservices-authentication-and-authorization](https://media.doit.com/imports/wordpress/2022/06/2f5e004b40a6-microservices-authentication-and-authorization.jpg) 複雑に見えますが、漏洩しやすい秘密の文字列をインターネット越しに(この場合は別のクラウドへ)送らずに済むのが利点です。 ## **GoogleからAWS workloadへの認証** 本記事の主題はGoogle上で動作するサービスですが、ここで [gtoken](https://github.com/doitintl/gtoken) にも簡単に触れておきます。これはWorkload Identity Federationの逆を行うもので、呼び出しに一時的なAWS IDを付与することで、GKE workloadからAWS APIを呼び出すための認証を行います。 ## **認証プロキシ:API Gateway** とはいえ前述のとおり、署名がGoogleの認可済みプリンシパル由来であるかを最終的に検証するステップを、自前のアプリケーションコードに任せている点は依然として制約です。可能な限り、セキュリティの専門家が作った実証済みのプロダクトを使うほうが望ましいでしょう。 そこで、堅牢で構成可能、かつ自分で保守する必要のない[サービス間認証](https://cloud.google.com/api-gateway/docs/authenticate-service-account)レイヤーとして、[API Gateway](https://cloud.google.com/api-gateway) を検討してみてください。 これはプロキシとして動作します。パブリックアドレスを公開し、クライアントとCloud Run、Cloud Functions、App Engine上のサーバーレスサービスとの間に立ちます。トークンを受け取り、Googleのサービスを呼び出してリクエストを認証したうえで、サーバーレスバックエンドにリクエストを転送します。API Gatewayからバックエンドへのリンクを保護するため、Googleが管理する特殊なヘッダー(攻撃者には付加できないもの)を挿入する仕組みです。 ## **制約** ただしAPI Gatewayは、Googleマネージドのサーバーレスサービスが公開するインターフェースと密に統合されているため、GKEには対応していません。 ## **認証プロキシ:Cloud Endpoint** では、GKEで認証を行いつつ、認証レイヤーからバックエンドサービスへのリンクの保護も維持するにはどうすればよいでしょうか。そのためには、Google Cloud EndpointsのExtensible Service Proxyを使います。これはAPI Gatewayの基盤となり、それを拡張した、やや古めのサービスです。 ESP(現在はv2)は、パブリックアドレスを公開してリクエストを認証するコンテナです。GKEで使うには、[podとしてクラスタにデプロイ](https://cloud.google.com/endpoints/docs/openapi/get-started-kubernetes-engine-espv2)します。(ちなみに、ドキュメントには新しいVPCネイティブ/IPエイリアスクラスタのみ対応と書かれていますが、実際は古いルートベースのクラスタでも動作します。) ESPv2とクラスタ内のKubernetesサービスとの間のリンクも保護する必要があります。これはESP以外のパブリックアドレスを公開しないことでクラスタネットワーキング層で実現できますし、相互TLSや [Istio Security](https://istio.io/latest/docs/concepts/security/) といった、より高度な手段を使うこともできます。 セキュリティをさらに高めるなら、ESPv2をサイドカーとしてデプロイし、プロキシとアプリケーション(Kubernetes Deployment)が同じpod内の安全な「localhost」空間に共存するようにします。(これはESPv2の主要なデプロイモードではありませんが、Google Cloud公式GitHubアカウントで公開されている [こちらのYAMLレシピ](https://github.com/GoogleCloudPlatform/endpoints-samples/blob/master/gke/echo.yaml)でサポートされています。) ## **まとめ:マイクロサービスを守ろう!** 誰でも自分のAPIを呼び出せる状態にしてはいけません。かつてはネットワーク境界、クラウドではVPCで解決されてきた課題です。しかし現代のアーキテクチャは、クラウドアカウントをまたぐ統合、クラウドプロバイダー間の統合、非クラウドシステムとの統合をサポートしています。さらにVPC内であっても、特定のクライアントとサーバーのリンクを狙い撃ちにした追加のセキュリティ層が必要です。エンドポイント同士が互いを信頼し合う必要があるのです。 そのために乗り越えるべき課題は次のとおりです。 - 漏洩しかねない場所に機密ファイルを置かずに認証を行う。 - 認証は信頼できるサービスに委ね、アプリケーションレベルのコードと密結合させない。 本記事では、セキュリティと保守性を段階的に高めつつ、より多くのテクノロジーの知識も求められるいくつかの方法を紹介しました。これらを学ぶ価値は十分にあります。攻撃の被害に遭うことを思えば、はるかに安い投資です!