サービス間の認証

gRPC サービスでサービス アカウントを使用してサービス間認証を実装できます。認証されたリクエストをサポートするように gRPC サービスで Extensible Service Proxy(ESP)を構成する方法や、gRPC クライアントからサービスを呼び出す方法など、サービス間認証を詳細な例を挙げて説明します。

Cloud Endpoints API に対して認証済みの呼び出しを行うには、呼び出し元のサービスでサービス アカウントを設定し、呼び出しに認証トークンを含めて送信する必要があります。呼び出し元では、Google ID トークンを使用するか、呼び出し元のサービス アカウントのみで署名されたカスタムの JSON ウェブトークン(JWT)を使用する必要があります。ESP は、JWT 内の iss クレームがサービス構成の issuer 設定と一致することを検証します。ESP は、サービス アカウントに付与されている Identity and Access Management の権限を確認しません。

この例では、最も簡単な形式のサービス間認証を設定して使用します。クライアントは、Google Cloud サービス アカウントを使用して、認証を行う JWT を生成します。他の認証方法のアプローチも似ていますが、有効な認証トークンを取得するためのクライアント側の処理は、使用する認証方法によって異なります。

始める前に

このガイドでは、チュートリアルで使用した Bookstore の例を使用します。

  1. gRPC のサンプルコードがホストされる git リポジトリのクローンを作成します。

    git clone https://github.com/GoogleCloudPlatform/python-docs-samples.git
    
  2. 作業ディレクトリに移動します。

    cd python-docs-samples/endpoints/bookstore-grpc/
    
  3. プロジェクトがまだ存在しない場合は、チュートリアルの手順を行ってプロジェクトを設定します。

この例では、Google Kubernetes Engine へのデプロイを使用しますが、認証の設定は Compute Engine の場合も同じです。

この例では、参照されている 2 つの Google Cloud Platform プロジェクトがあります。

  • サービス プロデューサー プロジェクト。Cloud Endpoints for gRPC サービスを所有するプロジェクト。
  • サービス ユーザー プロジェクト。gRPC クライアントを所有するプロジェクト。

ユーザー サービス アカウントとキーの作成

ユーザー プロジェクトのサービス アカウントとキーを作成するには:

  1. Google Cloud Console で [API とサービス] に移動します。

    API とサービス

    ユーザー プロジェクト内にいることを確認します。
  2. [認証情報] ページの [認証情報を作成] プルダウン リストで、[サービス アカウント キー] を選択します。
  3. [サービス アカウントキーの作成] ページを開きます。使用するサービス アカウントがすでに存在する場合は、そのアカウントを選択します。それ以外の場合は、[サービス アカウント] プルダウン リストで [新しいサービス アカウント] を選択し、アカウント名を入力します。

    対応するサービス アカウント ID が作成されます。以降のセクションで必要になるので、この ID を書き留めておきます。例:

    service-account-name@YOUR_PROJECT_ID.iam.gserviceaccount.com
    
  4. [役割] プルダウン リストをクリックし、次の役割を選択します。

    • サービス アカウント > サービス アカウント ユーザー
    • サービス アカウント > サービス アカウント トークン作成者
  5. キーのタイプとして [JSON] が選択されていることを確認します。

  6. [作成] をクリックします。サービス アカウントの JSON キーファイルが、ローカルマシンにダウンロードされます。このキーファイルはトークンを生成するために後で使用するため、安全な場所に保管されていることを確認し、その場所を記録してください。

サービス用の認証の構成

このセクションのすべてのステップで、プロデューサー プロジェクトを使用します。

gRPC API 構成での認証のセットアップ

ESP 用の認証は、gRPC API の YAML 構成ファイル内にある authentication セクションで構成します。サンプル サービス用の認証の構成は api_config_auth.yaml にあります。

authentication:
  providers:
  - id: google_service_account
    # Replace SERVICE-ACCOUNT-ID with your service account's email address.
    issuer: SERVICE-ACCOUNT-ID
    jwks_uri: https://www.googleapis.com/robot/v1/metadata/x509/SERVICE-ACCOUNT-ID
  rules:
  # This auth rule will apply to all methods.
  - selector: "*"
    requirements:
      - provider_id: google_service_account

providers セクションでは、使用する認証プロバイダを指定します。この例では、Google サービス アカウントを認証プロバイダとして使用することを指定しています。rules セクションでは、サービスのすべてのメソッドへのアクセスにこのプロバイダのトークンが必要であることを指定しています。

クローンのリポジトリに含まれるこのファイルの独自のコピーで、次のようにします。

  • MY_PROJECT_ID をプロデューサー プロジェクトの ID に変更します。
  • authentication セクションの SERVICE-ACCOUNT-IDissuerjwks_uri の両方の値にあります)を、前のセクションで記録しておいたユーザー サービス アカウント ID に変更します。これにより、この特定のサービス アカウントからの有効なトークンを提供するユーザーにサービスへのアクセスを許可するよう、ESP に指示します。
  • 必要に応じて、jwt_locationsproviders 要素の下に追加します。この値を使用して、カスタム JWT の場所を定義できます。デフォルトの JWT の場所は、Authorization メタデータ(接頭辞「Bearer」)と X-Goog-Iap-Jwt-Assertion メタデータです。

次のステップのためにファイルを保存します。

構成とサービスのデプロイ

次の手順は、GKE での gRPC のスタートガイドのものと同じです。

  1. Endpoints にサービス構成をデプロイします。チュートリアルでこの処理を行った場合でも、これは別の構成であるため、再度行う必要があります。返されるサービス名を記録しておきます。

    gcloud endpoints services deploy api_descriptor.pb api_config_auth.yaml --project PRODUCER_PROJECT
    
  2. まだ行っていない場合は、コンテナ クラスタを作成し、そのクラスタに対して kubectl を認証します。

  3. サンプルの API と ESP をクラスタにデプロイします。プロデューサー プロジェクトとユーザー プロジェクトを個別に使用する場合は、最初に、gcloud コマンドライン ツール内で適切なプロジェクトを設定してあることを確認します。

    gcloud config set project PRODUCER_PROJECT
    

gRPC クライアントからの認証されたメソッドの呼び出し

最後に、クライアント側で、サービス アカウント キーを使用して JWT トークンを生成してから、そのトークンを使用して認証された Bookstore メソッドを呼び出すことができます。まず、トークンを生成し、サンプル クライアントを実行するために必要な Python 要件をインストールします。クローンのクライアントの python-docs-samples/endpoints/bookstore-grpc フォルダに移動し、次のコマンドを実行します。

virtualenv bookstore-env
source bookstore-env/bin/activate
pip install -r requirements.txt

JWT トークンの生成

この例の Bookstore によって使用されているサービス間認証では、呼び出し側のサービスはサービス アカウントによってのみ認証されます。そのため、リクエストで送信する適切なトークンの作成は簡単です。生成されたトークンをさらに Google が(Google ID トークンを使用して)認証しなければならないという、もっと厳格なサービス間認証を要求することもできます。

この例では、用意されている Python スクリプトで、ダミーのユーザー ID とメールアドレスを使用して、前にダウンロードした JSON キーファイルからトークンを生成できます。

#!/usr/bin/env python

# Copyright 2016 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Example of generating a JWT signed from a service account file."""

import argparse
import json
import time

import google.auth.crypt
import google.auth.jwt

"""Max lifetime of the token (one hour, in seconds)."""
MAX_TOKEN_LIFETIME_SECS = 3600

def generate_jwt(service_account_file, issuer, audiences):
    """Generates a signed JSON Web Token using a Google API Service Account."""
    with open(service_account_file) as fh:
        service_account_info = json.load(fh)

    signer = google.auth.crypt.RSASigner.from_string(
        service_account_info['private_key'],
        service_account_info['private_key_id'])

    now = int(time.time())

    payload = {
        'iat': now,
        'exp': now + MAX_TOKEN_LIFETIME_SECS,
        # aud must match 'audience' in the security configuration in your
        # swagger spec. It can be any string.
        'aud': audiences,
        # iss must match 'issuer' in the security configuration in your
        # swagger spec. It can be any string.
        'iss': issuer,
        # sub and email are mapped to the user id and email respectively.
        'sub': issuer,
        'email': 'user@example.com'
    }

    signed_jwt = google.auth.jwt.encode(signer, payload)
    return signed_jwt

if __name__ == '__main__':
    parser = argparse.ArgumentParser(
        description=__doc__,
        formatter_class=argparse.RawDescriptionHelpFormatter)
    parser.add_argument('--file',
                        help='The path to your service account json file.')
    parser.add_argument('--issuer', default='', help='issuer')
    parser.add_argument('--audiences', default='', help='audiences')

    args = parser.parse_args()

    signed_jwt = generate_jwt(args.file, args.issuer, args.audiences)
    print(signed_jwt.decode('utf-8'))

スクリプトを使用してトークンを生成するには:

  • JWT トークンを生成し、それを変数 $JWT_TOKEN に割り当てます。

    JWT_TOKEN=$(python jwt_token_gen.py \
        --file=[SERVICE_ACCOUNT_FILE] \
        --audiences=[SERVICE_NAME] \
        --issuer=[SERVICE-ACCOUNT-ID])
    

    ここで

    • [SERVICE_ACCOUNT_FILE] は、ダウンロードした、ユーザー サービス アカウントの JSON キーファイルです。
    • [SERVICE_NAME] は、更新したサービス構成を Endpoints にデプロイしたときに返された Bookstore サービスの名前です。
    • [SERVICE-ACCOUNT-ID] は、サービス アカウントを生成したときに取得した完全なユーザー サービス アカウント ID です。

認証された gRPC 呼び出しの実行

この最後のステップでは bookstore_client.py を使用します。これは、チュートリアルで使用したのと同じクライアントです。認証された呼び出しを行う場合、クライアントはメソッドの呼び出しで JWT をメタデータとして渡します。

def run(host, port, api_key, auth_token, timeout, use_tls, servername_override, ca_path):
    """Makes a basic ListShelves call against a gRPC Bookstore server."""

    if use_tls:
        with open(ca_path, 'rb') as f:
            creds = grpc.ssl_channel_credentials(f.read())
        channel_opts = ()
        if servername_override:
            channel_opts += ((
                        'grpc.ssl_target_name_override', servername_override,),)
        channel = grpc.secure_channel(f'{host}:{port}', creds, channel_opts)
    else:
        channel = grpc.insecure_channel(f'{host}:{port}')

    stub = bookstore_pb2_grpc.BookstoreStub(channel)
    metadata = []
    if api_key:
        metadata.append(('x-api-key', api_key))
    if auth_token:
        metadata.append(('authorization', 'Bearer ' + auth_token))
    shelves = stub.ListShelves(empty_pb2.Empty(), timeout, metadata=metadata)
    print(f'ListShelves: {shelves}')

サンプルを実行するには:

  1. kubectl get services を使用して、デプロイされた Bookstore の外部 IP アドレスを取得します。

    #kubectl get services
    NAME                 CLUSTER-IP      EXTERNAL-IP      PORT(S)           AGE
    echo                 10.11.246.240   104.196.186.92   80/TCP            10d
    endpoints            10.11.243.168   104.196.210.50   80/TCP,8090/TCP   10d
    esp-grpc-bookstore   10.11.254.34    104.196.60.37    80/TCP            1d
    kubernetes           10.11.240.1     <none>           443/TCP           10d
    

    この場合は esp-grpc-bookstore サービスであり、その外部 IP は 104.196.60.37 です。

  2. IP アドレスを変数 EXTERNAL_IP に割り当てます。

    EXTERNAL_IP=104.196.60.37
    
  3. 次のコマンドを実行して、Bookstore サービスからすべての本棚のリストを取得します。

    python bookstore_client.py --port=80 --host=$EXTERNAL_IP --auth_token=$JWT_TOKEN
    

    このサービスは、現在の Bookstore に存在するすべての本棚を返します。トークンを提供しない、または JWT を生成するときに正しくないサービス アカウント ID を指定するとコマンドは失敗するので、これでダブルチェックできます。

次のステップ