マイクロサービス間通信における権限管理:認証と認可の設計と実装プラクティス
マイクロサービスアーキテクチャを採用する際、各サービスが独立してデプロイ・スケールされることで開発の俊敏性が向上します。しかし、一つのリクエスト処理が複数のサービス間の通信を伴う場合、サービス間のデータアクセス権限管理が複雑化し、セキュリティ上の重要な課題となります。
ユーザーが直接アクセスするエッジサービスだけでなく、バックエンドの各サービス間で行われるAPIコールやメッセージングにおいても、適切な認証と認可の仕組みを構築することが不可欠です。これにより、不正なアクセスや意図しないデータ漏洩を防ぎ、システム全体のセキュリティレベルを維持することができます。
本記事では、マイクロサービスアーキテクチャにおけるサービス間通信の認証と認可に焦点を当て、その設計上の考慮事項と実践的な実装方法について解説します。
なぜサービス間通信の権限管理が必要なのか
マイクロサービスにおいては、ユーザーからのリクエストを受け付けたサービス(呼び出し元サービス)が、処理のために他のサービス(呼び出し先サービス)を呼び出すことが一般的です。このとき、呼び出し先サービスは、誰(どのサービス、あるいはどのユーザーの代理として)が要求しているのかを識別し、その要求が許可されているか判断する必要があります。
単にネットワークレベルの制限(ファイアウォールなど)だけでは不十分です。悪意のある攻撃者や、設定ミスによる内部からの不正アクセスを防ぐためには、通信を行うサービス自体、あるいはサービスが代理しているユーザーの権限に基づいたアクセス制御が必要です。
これは「最小権限の原則」をサービス間通信にも適用することを意味します。各サービスは、自身の機能遂行に必要最低限の権限のみを持つべきです。
サービス間認証:誰が通信しているかを識別する
サービス間認証の目的は、通信の送信元が正規のサービスであることを検証することです。これにより、認証されていないサービスからのアクセスを拒否できます。
いくつかの一般的なサービス間認証方法があります。
1. 共有シークレット/APIキー
事前に共有された秘密鍵やAPIキーを使用して認証を行う方法です。呼び出し元サービスはリクエストにシークレットを含め、呼び出し先サービスは受信したシークレットが期待するものと一致するか確認します。
- 利点: 実装が比較的容易です。
- 課題:
- シークレットの管理と配布が課題となります。特にサービスの数が増えると、管理対象のシークレットが爆発的に増加します。
- シークレットが漏洩した場合の影響が大きいです。
- シークレットを安全に保存し、コードや設定ファイルにハードコーディングしないよう徹底する必要があります。(後述のシークレット管理ツールが必須となります。)
2. トークンベース認証 (JWTなど)
認証局(Auth Serverなど)が発行したトークンを通信に含める方法です。呼び出し元サービスはトークンを提示し、呼び出し先サービスはトークンを検証します。JWT(JSON Web Token)がよく利用されます。
- 利点:
- ステートレスな検証が可能です(認証局に毎回問い合わせる必要がない)。
- トークン内にサービスIDや付加的な情報(スコープ、有効期限など)を含めることができます。
- ユーザーの認証情報(例: OAuth 2.0のAccess Token)をサービス間で伝搬させることも可能です。
- 課題:
- トークンの発行・検証基盤が必要です。
- トークンの漏洩リスク、特に有効期限の長いトークンには注意が必要です。
- 発行元の認証局が侵害されると、システム全体のセキュリティが脅かされる可能性があります。
サービス間通信においては、ユーザーの代理としてサービスが通信する場合と、サービス自身のアイデンティティで通信する場合があります。JWTを利用する場合、サービス自身のアイデンティティを示すためのトークンを発行・利用することも考えられます。
3. Mutual TLS (mTLS)
クライアント(呼び出し元サービス)とサーバー(呼び出し先サービス)が相互に証明書を提示し合い、相手を認証する方法です。通信経路の暗号化も同時に行われます。
- 利点:
- 強力な相互認証を提供します。
- 証明書はシークレットに比べて管理・失効の仕組みが整備されていることが多いです(PKI - 公開鍵基盤)。
- 通信が自動的に暗号化されます。
- 課題:
- 証明書の発行、配布、更新、失効といったPKIの運用が必要です。
- 各サービスに証明書を配置・設定する必要があり、初期導入や運用負荷が高い場合があります。
- サービスメッシュ(Istio, Linkerdなど)を活用することで、mTLSの実装と運用を簡素化できることが多いです。
マイクロサービス環境では、サービス自身のアイデンティティを用いた認証が基本となります。サービスメッシュを導入している環境であれば、mTLSが非常に有力な選択肢となります。サービスメッシュがない場合やシンプルな構成の場合は、トークンベース認証や、安全に管理された共有シークレットを検討します。
サービス間認可:何を許可するかを判断する
認証によって通信元サービスが特定できた後、次にそのサービスが要求した操作(データへのアクセス、機能の実行など)が許可されているかを判断するのが認可です。
サービス間認可の判断基準としては、以下のようなものが考えられます。
1. サービスIDに基づいた認可
呼び出し元サービスの特定のID(サービスアカウント名、mTLS証明書のDNなど)に対して、特定の操作(例: ProductサービスのGET /products/{id} APIへのアクセス)を許可する方法です。
- 例:
- UserサービスはOrderサービスの
/orders
エンドポイントにアクセス可能。 - InventoryサービスはOrderサービスの
/orders/{id}/items
エンドポイントにはアクセス可能だが、/orders/{id}/payment_info
にはアクセス不可。
- UserサービスはOrderサービスの
これは最も基本的で管理しやすい認可レベルです。
2. サービスに紐づくロール/権限に基づいた認可
各サービスにあらかじめ定義されたロール(役割)を付与し、そのロールに紐づく権限に基づいて認可を判断する方法です。ロールベースアクセス制御(RBAC)の考え方をサービスに適用する形です。
- 例:
order-processor
ロールを持つサービスは、Orderサービスの更新系APIにアクセス可能。InventoryサービスやPaymentサービスはこのロールを持つ。order-viewer
ロールを持つサービスは、Orderサービスの参照系APIにアクセス可能。UserサービスやNotificationサービスはこのロールを持つ。
サービスの種類や責任範囲に応じてロールを定義することで、権限管理を構造化できます。
3. ユーザーの権限に基づいた認可(ユーザー代理)
ユーザーからのリクエストを受けて、そのユーザーの代理として別のサービスを呼び出す場合、呼び出し先サービスでは元のユーザーの権限に基づいて認可を判断する必要があります。この場合、ユーザーの認証情報(例: JWTに含まれるユーザーIDやロール)をサービス間で安全に伝搬させる必要があります。
- フロー例:
- Gatewayサービスがユーザーからのリクエストを受信し、ユーザー認証・認可を行う。
- Gatewayサービスは、ユーザーのIDや権限情報を安全な方法(例: リクエストヘッダーに署名付きのユーザー情報を追加、またはJWTをそのまま転送)でバックエンドサービスに渡す。
- バックエンドサービスは、受け取ったユーザー情報と、サービス自身のアイデンティティの両方を考慮して認可を判断する。
- 例: このサービス(Inventoryサービス)は、この操作(在庫更新)をユーザーの代理として実行することを許可されているか? さらに、代理されているユーザー自身は、この特定のリソース(商品Xの在庫)に対する在庫更新を許可されているか?
ユーザーの権限を適切に伝搬させ、バックエンドサービスでその権限に基づいた認可を行うことは、きめ細かいアクセス制御を実現する上で非常に重要です。
4. リソースに基づいた認可
アクセス対象となるリソース自体(例: 特定のユーザーのデータ、特定の商品の情報)に紐づく属性に基づいて認可を判断する方法です。属性ベースアクセス制御(ABAC)の考え方に近いです。
- 例: Orderサービスは、呼び出し元サービスがCustomerサービスである場合にのみ、特定の顧客の全ての注文情報を返すことを許可する。他のサービスからのリクエストでは、集計情報のみを返す、といった制御。
設計と実装のプラクティス
1. サービスアカウントと最小権限
各マイクロサービスには、自身のアイデンティティとなるサービスアカウントを付与し、そのサービスアカウントに対して必要最低限の権限のみを割り当てます。これにより、万が一特定のサービスが侵害されても、その影響範囲を限定できます。
- 実装例の概念:
- クラウド環境(AWS, GCP, Azureなど)の場合、各サービスにクラウドプロバイダーが提供するサービスアカウント(IAMロール、GCP Service Account、Azure Managed Identityなど)を割り当てます。
- Kubernetes環境の場合、PodにServiceAccountを割り当て、Kubernetes RBACでそのServiceAccountにクラスタ内のリソースに対する権限(例: Secretへのアクセス)を付与したり、クラウドプロバイダーのIAMロールと紐づけたり(IRSA, Workload Identityなど)します。
- これらのサービスアカウントに対して、他のサービスへのAPI呼び出しや、データベース/メッセージキューへのアクセスに必要な権限ポリシーを設定します。
2. 認証情報の安全な管理
共有シークレット、APIキー、証明書、トークン発行に必要なクレデンシャルなど、あらゆる認証情報は安全に管理する必要があります。
- 実践方法:
- コードや設定ファイルに直接書き込まない。
- 環境変数として渡す場合も、実行環境へのアクセス制限を厳格に行う。
- 専用のシークレット管理ツール(HashiCorp Vault, AWS Secrets Manager, GCP Secret Manager, Azure Key Vault, Kubernetes Secretsなど)を利用し、サービスの起動時に安全に取得する仕組みを構築する。
3. サービスメッシュの活用
IstioやLinkerdのようなサービスメッシュは、サービス間通信における認証(mTLS)と認可を一元的に管理するための強力なツールです。
- サービスメッシュで実現できること:
- コードに手を加えることなく、サービス間のmTLS通信を自動的に有効化できます。
- サービスアイデンティティに基づいた認可ポリシーを集中管理できます。(例: Service AはService Bの/pathXにアクセス可能だが、/pathYには不可)
- ユーザーIDなどのカスタム情報をポリシー判断に利用できる場合もあります。
サービスメッシュの導入は学習コストや運用負荷を伴いますが、マイクロサービス間のセキュリティと観測性を大幅に向上させることができます。
4. APIゲートウェイでの初期認証・認可
ユーザーからの最初のリクエストを受けるAPIゲートウェイで、ユーザー認証と初期の認可(例: このユーザーはこのサービスのAPIを呼び出す権限があるか)を行います。ここでユーザーのアイデンティティと権限を確立し、必要に応じてバックエンドサービスに伝搬させます。
APIゲートウェイでサービス間通信の認証も行うことも可能ですが、内部のサービス間通信全てをゲートウェイ経由にするのは現実的ではない場合が多いです。ゲートウェイは外部からのアクセスと、内部サービスの入り口としての役割に集中させ、内部サービス間の認証・認可は別の仕組みで行うのが一般的です。
5. 認可ロジックの配置
認可を判断するロジックをどこに配置するかは設計上の重要な選択です。
- 呼び出し先サービス内部: 最もシンプルで、サービス固有のリソースに基づいたきめ細かい制御が可能です。しかし、認可ロジックが各サービスに分散するため、一貫性の維持や更新が課題となり得ます。
- 集中型認可サービス: 認可ポリシーを一元管理し、各サービスは認可要求をこのサービスに問い合わせます。ポリシーの一貫性を保ちやすく、変更管理が容易になります。ただし、認可サービスの可用性やパフォーマンスがボトルネックとなるリスクがあります。OPA (Open Policy Agent) のようなツールがこのアプローチをサポートします。
- サービスメッシュ: サービスメッシュの機能を利用して、ネットワークレベルまたはプロトコルレベルでの認可を定義します。サービスコードへの影響が最小限になります。
これらのアプローチは組み合わせて利用することも可能です。例えば、サービスメッシュでサービス間の基本的なアクセス制御を行い、呼び出し先サービス内部でより詳細なリソースベースの認可を行うなどです。
実装例(概念的なコードフロー)
ここでは、トークンベース認証とサービス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に署名をつけたり、暗号化したり、サービスメッシュの機能を利用するなどが考えられます。
- 複雑な認可ポリシー: ポリシーが複雑になりすぎると、管理やテストが困難になります。シンプルなサービスID/ロールベースの認可から始め、必要に応じてきめ細かい制御を追加していくのが現実的です。
- パフォーマンス: 各リクエストで認証・認可の検証を行うため、その処理がボトルネックにならないよう設計する必要があります。トークン検証結果のキャッシュなどが有効な場合があります。
- ログと監査: サービス間通信における認証・認可の試行と結果は、セキュリティ監査のために必ずログとして記録することが重要です。不正アクセスの検知や原因究明に役立ちます。
まとめ
マイクロサービスアーキテクチャにおけるサービス間通信の権限管理は、システム全体のセキュリティにおいて非常に重要な要素です。単にネットワークレベルの制限に頼るのではなく、サービス自身のアイデンティティに基づいた認証と、そのサービスまたは代理するユーザーの権限に基づいた認可を、通信の受け手側で適切に行う必要があります。
具体的には、サービスアカウント、トークンベース認証(JWT)、Mutual TLSなどの技術を活用し、サービスが必要最低限の権限のみを持つように設計します。また、認証情報の安全な管理、サービスメッシュの活用、そして認可ロジックの適切な配置も考慮すべき実践的なプラクティスです。
これらのプラクティスを適用することで、マイクロサービス間の安全なデータアクセスを実現し、より堅牢なシステムを構築できるでしょう。自身の開発するサービスが他のサービスとどのように連携し、どのようなデータにアクセスするのかを常に意識し、適切な権限設計と実装に取り組んでいただければ幸いです。