権限設計プラクティス

マイクロサービス間通信における権限管理:認証と認可の設計と実装プラクティス

Tags: マイクロサービス, 権限管理, サービス間通信, 認証, 認可, JWT, mTLS, セキュリティ

マイクロサービスアーキテクチャを採用する際、各サービスが独立してデプロイ・スケールされることで開発の俊敏性が向上します。しかし、一つのリクエスト処理が複数のサービス間の通信を伴う場合、サービス間のデータアクセス権限管理が複雑化し、セキュリティ上の重要な課題となります。

ユーザーが直接アクセスするエッジサービスだけでなく、バックエンドの各サービス間で行われるAPIコールやメッセージングにおいても、適切な認証と認可の仕組みを構築することが不可欠です。これにより、不正なアクセスや意図しないデータ漏洩を防ぎ、システム全体のセキュリティレベルを維持することができます。

本記事では、マイクロサービスアーキテクチャにおけるサービス間通信の認証と認可に焦点を当て、その設計上の考慮事項と実践的な実装方法について解説します。

なぜサービス間通信の権限管理が必要なのか

マイクロサービスにおいては、ユーザーからのリクエストを受け付けたサービス(呼び出し元サービス)が、処理のために他のサービス(呼び出し先サービス)を呼び出すことが一般的です。このとき、呼び出し先サービスは、誰(どのサービス、あるいはどのユーザーの代理として)が要求しているのかを識別し、その要求が許可されているか判断する必要があります。

単にネットワークレベルの制限(ファイアウォールなど)だけでは不十分です。悪意のある攻撃者や、設定ミスによる内部からの不正アクセスを防ぐためには、通信を行うサービス自体、あるいはサービスが代理しているユーザーの権限に基づいたアクセス制御が必要です。

これは「最小権限の原則」をサービス間通信にも適用することを意味します。各サービスは、自身の機能遂行に必要最低限の権限のみを持つべきです。

サービス間認証:誰が通信しているかを識別する

サービス間認証の目的は、通信の送信元が正規のサービスであることを検証することです。これにより、認証されていないサービスからのアクセスを拒否できます。

いくつかの一般的なサービス間認証方法があります。

1. 共有シークレット/APIキー

事前に共有された秘密鍵やAPIキーを使用して認証を行う方法です。呼び出し元サービスはリクエストにシークレットを含め、呼び出し先サービスは受信したシークレットが期待するものと一致するか確認します。

2. トークンベース認証 (JWTなど)

認証局(Auth Serverなど)が発行したトークンを通信に含める方法です。呼び出し元サービスはトークンを提示し、呼び出し先サービスはトークンを検証します。JWT(JSON Web Token)がよく利用されます。

サービス間通信においては、ユーザーの代理としてサービスが通信する場合と、サービス自身のアイデンティティで通信する場合があります。JWTを利用する場合、サービス自身のアイデンティティを示すためのトークンを発行・利用することも考えられます。

3. Mutual TLS (mTLS)

クライアント(呼び出し元サービス)とサーバー(呼び出し先サービス)が相互に証明書を提示し合い、相手を認証する方法です。通信経路の暗号化も同時に行われます。

マイクロサービス環境では、サービス自身のアイデンティティを用いた認証が基本となります。サービスメッシュを導入している環境であれば、mTLSが非常に有力な選択肢となります。サービスメッシュがない場合やシンプルな構成の場合は、トークンベース認証や、安全に管理された共有シークレットを検討します。

サービス間認可:何を許可するかを判断する

認証によって通信元サービスが特定できた後、次にそのサービスが要求した操作(データへのアクセス、機能の実行など)が許可されているかを判断するのが認可です。

サービス間認可の判断基準としては、以下のようなものが考えられます。

1. サービスIDに基づいた認可

呼び出し元サービスの特定のID(サービスアカウント名、mTLS証明書のDNなど)に対して、特定の操作(例: ProductサービスのGET /products/{id} APIへのアクセス)を許可する方法です。

これは最も基本的で管理しやすい認可レベルです。

2. サービスに紐づくロール/権限に基づいた認可

各サービスにあらかじめ定義されたロール(役割)を付与し、そのロールに紐づく権限に基づいて認可を判断する方法です。ロールベースアクセス制御(RBAC)の考え方をサービスに適用する形です。

サービスの種類や責任範囲に応じてロールを定義することで、権限管理を構造化できます。

3. ユーザーの権限に基づいた認可(ユーザー代理)

ユーザーからのリクエストを受けて、そのユーザーの代理として別のサービスを呼び出す場合、呼び出し先サービスでは元のユーザーの権限に基づいて認可を判断する必要があります。この場合、ユーザーの認証情報(例: JWTに含まれるユーザーIDやロール)をサービス間で安全に伝搬させる必要があります。

ユーザーの権限を適切に伝搬させ、バックエンドサービスでその権限に基づいた認可を行うことは、きめ細かいアクセス制御を実現する上で非常に重要です。

4. リソースに基づいた認可

アクセス対象となるリソース自体(例: 特定のユーザーのデータ、特定の商品の情報)に紐づく属性に基づいて認可を判断する方法です。属性ベースアクセス制御(ABAC)の考え方に近いです。

設計と実装のプラクティス

1. サービスアカウントと最小権限

各マイクロサービスには、自身のアイデンティティとなるサービスアカウントを付与し、そのサービスアカウントに対して必要最低限の権限のみを割り当てます。これにより、万が一特定のサービスが侵害されても、その影響範囲を限定できます。

2. 認証情報の安全な管理

共有シークレット、APIキー、証明書、トークン発行に必要なクレデンシャルなど、あらゆる認証情報は安全に管理する必要があります。

3. サービスメッシュの活用

IstioやLinkerdのようなサービスメッシュは、サービス間通信における認証(mTLS)と認可を一元的に管理するための強力なツールです。

サービスメッシュの導入は学習コストや運用負荷を伴いますが、マイクロサービス間のセキュリティと観測性を大幅に向上させることができます。

4. APIゲートウェイでの初期認証・認可

ユーザーからの最初のリクエストを受けるAPIゲートウェイで、ユーザー認証と初期の認可(例: このユーザーはこのサービスのAPIを呼び出す権限があるか)を行います。ここでユーザーのアイデンティティと権限を確立し、必要に応じてバックエンドサービスに伝搬させます。

APIゲートウェイでサービス間通信の認証も行うことも可能ですが、内部のサービス間通信全てをゲートウェイ経由にするのは現実的ではない場合が多いです。ゲートウェイは外部からのアクセスと、内部サービスの入り口としての役割に集中させ、内部サービス間の認証・認可は別の仕組みで行うのが一般的です。

5. 認可ロジックの配置

認可を判断するロジックをどこに配置するかは設計上の重要な選択です。

これらのアプローチは組み合わせて利用することも可能です。例えば、サービスメッシュでサービス間の基本的なアクセス制御を行い、呼び出し先サービス内部でより詳細なリソースベースの認可を行うなどです。

実装例(概念的なコードフロー)

ここでは、トークンベース認証とサービスID/ユーザー権限に基づいた認可を組み合わせた、呼び出し先サービスでの処理フローの概念をPythonの擬似コードで示します。

# 仮定:
# - リクエストヘッダーに "Authorization: Bearer <service_jwt>" の形式でサービス認証用のJWTが含まれる
# - 必要に応じて、"X-User-Info: <signed_user_claims>" の形式でユーザー情報が含まれる
# - サービスアカウントのロールに基づいて認可ポリシーが定義されている

def handle_request(request):
    # 1. サービス間認証 (呼び出し元サービスの検証)
    service_jwt = request.headers.get("Authorization")
    if not service_jwt or not service_jwt.startswith("Bearer "):
        return {"status": 401, "body": "Service authentication required"}

    service_token = service_jwt.split(" ")[1]
    try:
        # 認証局の公開鍵などでJWTの署名と有効性を検証
        service_claims = verify_jwt(service_token)
        calling_service_id = service_claims.get("sub") # 例: JWTのsubjectがサービスID
        service_roles = service_claims.get("roles", []) # 例: JWTに含まれるサービスロール
        if not calling_service_id:
             return {"status": 401, "body": "Invalid service token"}
    except InvalidTokenError:
        return {"status": 401, "body": "Invalid service token"}

    print(f"Authenticated service: {calling_service_id} with roles: {service_roles}")

    # 2. ユーザー認証情報(存在する場合)の検証と取得
    user_info_header = request.headers.get("X-User-Info")
    user_id = None
    user_roles = []
    if user_info_header:
        try:
            # 署名付きユーザー情報を検証し、ユーザーIDやロールを取得
            user_claims = verify_signed_user_info(user_info_header)
            user_id = user_claims.get("user_id")
            user_roles = user_claims.get("roles", [])
            print(f"Acting on behalf of user: {user_id} with roles: {user_roles}")
        except InvalidSignatureError:
            # 不正なユーザー情報ヘッダーはログに記録しつつ、処理を続行するかどうかはポリシーによる
            print("Warning: Invalid user info header signature")
            # return {"status": 403, "body": "Invalid user context"} # 厳格なポリシーの場合

    # 3. 認可判断
    requested_resource = request.path # 例: "/orders/123"
    requested_method = request.method # 例: "GET"

    # サービス自身の権限チェック
    if not is_service_authorized(calling_service_id, service_roles, requested_method, requested_resource):
        return {"status": 403, "body": f"Service {calling_service_id} is not authorized to access {requested_resource}"}

    # ユーザー代理の場合、ユーザーの権限もチェック
    if user_id:
         if not is_user_authorized(user_id, user_roles, requested_method, requested_resource):
             return {"status": 403, "body": f"User {user_id} is not authorized to access {requested_resource}"}
         # さらに、このサービスがこのユーザーの代理としてこの操作を行うことを許可されているかチェック(サービスアカウントに紐づく権限)
         if not is_service_permitted_to_act_on_behalf(calling_service_id, user_roles, requested_method, requested_resource):
              return {"status": 403, "body": f"Service {calling_service_id} cannot act on behalf of user {user_id} for this operation"}


    # 4. 認可された場合、処理を実行
    print("Authorization successful. Processing request...")
    # ... 実際のビジネスロジック ...
    return {"status": 200, "body": "Success"}

# --- 認可ロジックのプレースホルダー関数 ---
# これらはサービス固有のポリシーエンジンや集中型認可サービスへの問い合わせなどによって実装される
def is_service_authorized(service_id, service_roles, method, resource):
    # 例: Order ProcessorサービスはPUT /orders/{id} にアクセス可能
    if service_id == "order-processor" and method == "PUT" and resource.startswith("/orders/"):
        return True
    # 例: サービスロール "order-viewer" はGET /orders にアクセス可能
    if "order-viewer" in service_roles and method == "GET" and resource == "/orders":
        return True
    return False # デフォルトは拒否

def is_user_authorized(user_id, user_roles, method, resource):
     # 例: ユーザーロール "admin" は何でも可能
     if "admin" in user_roles:
         return True
     # 例: ユーザー自身のリソースへのアクセス許可 (例: /users/{user_id}/profile)
     if resource.startswith(f"/users/{user_id}/"):
         return True
     return False # デフォルトは拒否

def is_service_permitted_to_act_on_behalf(service_id, user_roles, method, resource):
    # 例: Inventoryサービスは、ユーザーが"seller"ロールを持っている場合に限り、PUT /products/{id}/inventory に代理アクセス可能
    if service_id == "inventory-service" and "seller" in user_roles and method == "PUT" and resource.startswith("/products/") and resource.endswith("/inventory"):
        return True
    # 例: Reportingサービスは、ユーザーのロールに関わらず、GET /reports に代理アクセス可能(集計データのため)
    if service_id == "reporting-service" and method == "GET" and resource == "/reports":
        return True
    return False # デフォルトは拒否

# --- JWT検証/署名検証関数(外部ライブラリや認証局連携で実装) ---
class InvalidTokenError(Exception): pass
class InvalidSignatureError(Exception): pass

def verify_jwt(token):
    # 実際のJWT検証ロジック (署名検証、有効期限チェックなど)
    # 例: jwt.decode(token, public_key, algorithms=["RS256"])
    print(f"Verifying JWT: {token}")
    # 仮の検証成功
    return {"sub": "some-service-id", "roles": ["some-service-role"]}

def verify_signed_user_info(signed_info):
     # 実際の署名検証ロジック
     print(f"Verifying signed user info: {signed_info}")
     # 仮の検証成功
     return {"user_id": "user-abc", "roles": ["viewer"]}

# --- リクエストの例 ---
# handle_request({"headers": {"Authorization": "Bearer service-jwt-token", "X-User-Info": "signed-user-info"}})
# handle_request({"headers": {"Authorization": "Bearer service-jwt-token"}})
# handle_request({"headers": {}})

上記のコードは概念的なものであり、実際の認証・認可ライブラリやフレームワークを活用して実装することになります。重要な点は、呼び出し元サービスの認証、ユーザー情報の検証(代理の場合)、そしてサービス自身の権限とユーザーの権限(代理の場合)に基づいた認可判断を、受信側サービスで行うというフローです。

注意点と落とし穴

まとめ

マイクロサービスアーキテクチャにおけるサービス間通信の権限管理は、システム全体のセキュリティにおいて非常に重要な要素です。単にネットワークレベルの制限に頼るのではなく、サービス自身のアイデンティティに基づいた認証と、そのサービスまたは代理するユーザーの権限に基づいた認可を、通信の受け手側で適切に行う必要があります。

具体的には、サービスアカウント、トークンベース認証(JWT)、Mutual TLSなどの技術を活用し、サービスが必要最低限の権限のみを持つように設計します。また、認証情報の安全な管理、サービスメッシュの活用、そして認可ロジックの適切な配置も考慮すべき実践的なプラクティスです。

これらのプラクティスを適用することで、マイクロサービス間の安全なデータアクセスを実現し、より堅牢なシステムを構築できるでしょう。自身の開発するサービスが他のサービスとどのように連携し、どのようなデータにアクセスするのかを常に意識し、適切な権限設計と実装に取り組んでいただければ幸いです。