Amazon Web Services ブログ

SaaSテナント分離をAWS IAMとABACで実装する方法

この記事は、How to implement SaaS tenant isolation with ABAC and AWS IAMを訳したものです。

マルチテナントアプリケーションにおいては各テナントのリソースが他のテナントからアクセスできないように設計を行う必要があります。AWS Identity and Access Management (IAM) は多くの場合、この目的を達成するための重要な要素となりえます。一方で、IAMを用いることによる課題の一つとして、テナント分離を実現するのに必要な IAM ポリシーの数と複雑さが急速に拡大することにより分離モデルの規模と管理性に影響を与えることが挙げられます。IAM の 属性ベースのアクセスコントロール (ABAC) の仕組みはこの課題を解決するための方法を開発者に提供しています。

このブログ記事では、IAM の ABAC を用いてマルチテナント環境のテナント分離を実装する方法について解説します。

IAMによる分離方法の選択

IAM は他の AWS サービスと統合された形でのテナント分離とアクセス許可の絞り込みの実装を可能にします。IAMを利用することでシステムに強力な分離のための基盤を実現でき、開発者が意図せずテナント境界を侵害することに繋がるコードを実装してしまうリスクを軽減できます。 自社の分離モデルに適合するポリシーを IAM がサポートしている場合、IAM は 分離を実現するための AWS ネイティブな方法を提供します。

IAM には、テナントの分離やリソースへのアクセスを制限するためのいくつかの方法があります。アプリケーションに適した方法を選択するためには複数の要因を考慮する必要があります。テナント数とロール定義の数は考慮すべき重要な要素です。ほとんどのアプリケーションでは、ユーザーの役割ごとに複数のロール定義が必要です。ロール定義とは、ユーザまたはプログラムのコンポーネントがその職務を遂行するために必要な最小限の権限のセットを指します。例えば、ビジネスユーザーとデータアナリストは通常、使用するリソースへのアクセスを許可するための最低限必要な権限が異なります。

Software-as-a-service (SaaS) アプリケーションにおいては、役割の境界に加えて、テナントリソース間にも境界が存在します。結果として、全てのロール定義がそれぞれのテナントごとに存在することになります。非常にダイナミックな環境(例えばクロステナントアクセスによるコラボレーションシナリオなど)では、新しいロール定義をアドホックに追加することも考えられます。このようなケースでは、システムの進化に伴ってロール定義の数と複雑性が大幅に増加する可能性があります。

IAM には、テナントの分離方法が主に 3 つ存在します。次の章で ABAC に焦点を当てる前に、それらを簡単に振り返ってみましょう。

 

図1 : IAM によるテナント分離の方法

RBAC – それぞれのテナントがテナントリソースにアクセスするための専用の IAM ロールもしくは静的な IAM ロールを持つ方式です。 RBAC方式での IAM ロールの数はロール定義の数にテナント数を掛け算した数に等しくなります。RBAC方式 はテナント数が少なく、かつ比較的ポリシーが静的である場合に有効です。テナント数やアタッチされたポリシーの複雑さが増すにつれて、複数の IAM ロールを管理することが困難になる場合があります。

動的に生成された IAM ポリシー – この方式ではユーザーのアイデンティティに基づいて、テナントのIAM ポリシーを動的に生成します。ロール定義が変更されたり、頻繁に追加されるような非常にダイナミックな環境ではこの方式を選択します (テナントのコラボレーションシナリオなど)。組み込みの IAM サービスの機能ではなく、コードによって IAM ポリシーを生成・管理したいという場合もこの方式を選択できます。この方式の詳細については、ブログ記事「動的に生成された IAM ポリシーによる SaaS テナントの分離」を参照してください。

ABAC – ロール定義を頻繁に変更・追加する必要のあるようなユースケースでない限り、多くのSaaSアプリケーションではより簡単にIAM ポリシーの管理ができるこちらの方式が適しています。独自のメカニズムによってポリシーを管理・適用する必要のある動的生成されたIAMポリシー方式と異なり、 ABAC では ポリシーの管理適用を IAM に直接任せることができます。

テナント分離のためのABAC

ABAC はパラメーター (属性) を使用してリソースへのテナントアクセスを制御することによって実現されます。テナントの分離に ABAC を使用することで、リソースへの一時的なアクセスが可能になります。リソースへのアクセスは呼び出し元のアイデンティティと属性に応じて制限が行われます。

ABAC モデルの大きな利点の 1 つは、単一のロールを利用して、任意の数のテナントにスケールできる点です。これは、IAM ポリシーでタグ (テナント ID など) を使用して、テナントデータにアクセスするためだけに作成された一時セッションを使用して実現します。セッションはリクエストを行ったエンティティ(テナントユーザーなど)の属性をカプセル化します。ポリシー評価時に、IAM はこれらのタグをセッション属性に置き換えます。

ABAC のもう一つの要素は、特別な命名規則またはリソースタグを使用して、テナントリソースに属性を割り当てることです。リソースへのアクセスは、セッション属性とリソース属性が一致した場合に許可されます(たとえば、TenantID: yellow 属性を持つセッションは、TenantID: yellow とタグ付けされたリソースにアクセスできます)。

IAM の ABAC の詳細については AWS の ABAC とはを参照してください。

一般的な SaaS アーキテクチャにおける ABAC

IAM で ABAC をテナントの分離に使用する方法を示すために、典型的なマイクロサービスベースのアプリケーションの例について説明します。具体的には、マルチテナントの e コマースアプリケーションで出荷追跡フローを実現する 2 つのマイクロサービスに焦点を当てます。

ここで、多くの役割で多くのユーザーを持つテナント Yellow を考えます。テナント Yellow はこのテナントに属する出荷データに対して排他的にアクセスできます。これを実現するために、呼び出しチェーン内のすべてのマイクロサービスは制限されたコンテキストで動作し、クロステナントアクセスを防止します。

図 2: SaaS アプリケーションでの出荷追跡フローのサンプル

一連の流れについて確認し、実装の詳細について検討していきましょう。

出荷追跡のリクエストは、認証済みの Yellow テナントのユーザーによって開始されます。簡略化のために認証のプロセスは検討の範囲外とさせていただきます。JSON ウェブトークン (JWT)で表されるユーザーアイデンティティには、カスタムクレームが含まれており、そのうちの 1 つは TenantID です。今回の例では、TenantID は yellow になります。

JWT はユーザーのブラウザから GET Shipment リクエストの HTTP ヘッダー経由で Shipment サービスに渡されます。 Shipment サービスはリクエストを認証し、出荷予定到着時間(ETA)を取得するために必要なパラメータを収集します。その後、 Shipment サービスは、収集したパラメータを用いて JWTを付与した上で Tracking サービスへの GetShippingETA リクエストを行います。

Tracking サービスはデータストア内の出荷追跡データを管理します。データストアには全てのテナントのデータが格納されていますが、それぞれの出庫レコードには 今回の例での yellow のような TenantID を識別するデータが付与されています。
この例では TrackingServiceRole と呼ばれる、 Tracking サービスにアタッチされた IAM ロールによって、マイクロサービスがアクセスできる AWS リソースとそれが実行できるアクションが決まります。

TrackingServiceRole 自体にはデータストア内の追跡データにアクセスする権限が含まれていないことに注意してください。追跡レコードにアクセスするために、 Tracking サービスは TrackingAccessRole と呼ばれる別のロールを一時的に引き受けます。このロールは、一時セッションを表す資格情報が期限切れになるまでの限られた期間のみ有効です。
これがどのように動作するかを理解するには、まず TrackingAccessRoleTrackingServiceRole の信頼関係について説明する必要があります。次の信頼ポリシーでは、TrackingServiceRole を信頼されたエンティティとして許可します。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::<account-id>:role/TrackingServiceRole"
      },
      "Action": "sts:AssumeRole"
    },
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::<account-id>:role/TrackingServiceRole"
      },
      "Action": "sts:TagSession",
      "Condition": {
        "StringLike": {
          "aws:RequestTag/TenantID": "*"
        }
      }
    }
  ]
}

このポリシーは TrackingAccessRole に関連付ける必要があります。これは、IAM コンソールの [ロールの詳細] ページの [信頼関係] タブ、または AWS CLI のupdate-assume-role-policy サブコマンドから行うことができます。

この関連付けにより、 TrackingServiceRole がアタッチされた Tracking サービスが TrackingAccessRole を引き受けることを許可します。このポリシーでは、TrackingServiceRole が一時セッションに TenantID セッションタグをアタッチすることも許可します。

セッションタグはセッションを要求する際に指定することができるプリンシパルタグです。これは、セッション中に実行される API 呼び出しのコンテキストに変数を挿入するための方法です。これにより、後続の API 呼び出しで評価された IAM ポリシーが、aws:PrincipalTag コンテキストキーを使用して TenantID を参照できるようになります。

それでは、 TrackingAccessPolicy について見ていきましょう。これは TrackingAccessRole にアタッチされたポリシーです。このポリシーでは aws:PrincipalTag/TenantID キーを用いることにより、特定のテナントへのアクセスを動的に絞り込みます。

この記事の後半で、3 つの異なるデータストレージサービスに対するデータアクセスポリシーの例を紹介します。

これで、 Tracking サービスが一時セッションを作成し、 TenantID をリクエストコンテキストに挿入する準備が整いました。下記の Python の関数は AWS SDK for Python (boto3) を使用してこれを実装しています。この関数は、TenantID (JWT から抽出) と TrackingAccessRole アマゾンリソースネーム(ARN) を引数として取り、アクセス権限が絞り込まれた Boto3 セッション オブジェクトを返します。

import boto3

def create_temp_tenant_session(access_role_arn, session_name, tenant_id, duration_sec):
    """
    Create a temporary session
    :param access_role_arn: The ARN of the role that the caller is assuming
    :param session_name: An identifier for the assumed session
    :param tenant_id: The tenant identifier the session is created for
    :param duration_sec: The duration, in seconds, of the temporary session
    :return: The session object that allows you to create service clients and resources
    """
    sts = boto3.client('sts')
    assume_role_response = sts.assume_role(
        RoleArn=access_role_arn,
        DurationSeconds=duration_sec,
        RoleSessionName=session_name,
        Tags=[
            {
                'Key': 'TenantID',
                'Value': tenant_id
            }
        ]
    )
    session = boto3.Session(aws_access_key_id=assume_role_response['Credentials']['AccessKeyId'],
                    aws_secret_access_key=assume_role_response['Credentials']['SecretAccessKey'],
                    aws_session_token=assume_role_response['Credentials']['SessionToken'])
    return session

以下の引数を指定することで、特定のテナントに必要な有効期間を指定して一時セッションを払い出すことができます。

access_role_arn – 雛形となるポリシーがアタッチされた引き受け用のロール。IAM ポリシーには、aws:PrincipalTag/TenantID タグキーが含まれている必要があります。

session_name – セッション名。ロールを一意に識別するためにのセッション名です。ロールセッション名は引き受けたプリンシパルのARNで使用され、AWS CloudTrailのログに含まれます。

tenant_id – どのテナント用のセッションを作成するかを指定するためのテナント識別子。IAM ポリシーのリソース名との互換性のために、推測不可能な英数字の小文字のテナント ID を生成することをお勧めします。

duration_sec – 一時セッションの有効期間。

注:ブログ記事「動的に生成された IAM ポリシーによる SaaS テナントの分離」で説明されているように、トークンの生成を別のモジュールに抽出することで、トークン管理の詳細をアプリケーションから抽象化することができます。記事では、一時セッショントークンを取得するための再利用可能なアプリケーションコードはトークンベンディングマシンと呼ばれています。

関数から返されたセッションは、ストレージサービスなどの IAM で管理可能なオブジェクトをインスタンス化するために使用できます。セッションが返された後、この一時認証情報を使って実行される 全てのAPI 呼び出しにはリクエストコンテキストとして aws:PrincipalTag/TenantID にキーと値のペアが含まれるようになります。

Tracking サービスが追跡データにアクセスしようとすると、 IAM は複数のステップによる評価を経て、リクエストを許可するか拒否するかを決定します。この中には、プリンシパルのアイデンティティベースのポリシー(この例では TrackingAccessPolicy )の評価が含まれます。このタイミングで、aws:PrincipalTag/TenantID タグキーが実際の値に置き換えられ、ポリシー条件に一致することで、テナントデータへのアクセスが許可されます。

一般的な ABAC シナリオ

ここからは、さまざまなデータストレージサービスを使用する一般的なシナリオをいくつか見てみましょう。それぞれの例ごとに、どのようにサービスでのデータの分割を行い、テナントデータへのアクセスを許可するかを図示します。

これらの例は、ここまで解説してきたアーキテクチャに依存しており、一時セッションのコンテキストに TenantID パラメータが含まれていると前提を置きます。それぞれ異なるサービスで利用するための TrackingAccessPolicy のさまざまなパターンについて説明を行います。aws:PrincipalTag/TenantID の使用方法は、サービスごとの IAM 機能(タグのサポート、ポリシーの条件キーの機能やリソースの ARN をタグによって指定する機能など)によって異なります。以下の例は、さまざまなサービスでこれらのテクニックを適用する例を示しています。

DynamoDB におけるプールモデルストレージ分離

多くの SaaS アプリケーションは、すべてのテナントのデータが同じテーブルに格納されるプールモデルのデータパーティション分割を利用しています。各テナントに関連付けられている項目を識別するためには、テナントの ID が利用されます。このモデルの具体例を図 3 に示します。

図 3. DynamoDB インデックスによるパーティション分割

この例では、Amazon DynamoDBを使用しており、それぞれのテナントの ID をテーブルのパーティションキーに格納しています。これにより、 ABAC と IAM によるきめ細やかなアクセス制御 ( FGAC ) を利用してテーブル内の項目に対するテナント分離を実装することができます。

以下の TrackingAccessPolicy では dynamodb:LeadingKeys 条件キーを使って、パーティションキーがセッションタグで渡されたテナントのIDと一致する項目にのみアクセスを制限しています。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "dynamodb:GetItem",
        "dynamodb:BatchGetItem",
        "dynamodb:Query"
      ],
      "Resource": [
        "arn:aws:dynamodb:<region>:<account-id>:table/TrackingData"
      ],
      "Condition": {
        "ForAllValues:StringEquals": {
          "dynamodb:LeadingKeys": [
            "${aws:PrincipalTag/TenantID}"
          ]
        }
      }
    }
  ]
}

上記の例では、テナントリソースへのアクセス制御の方法を記述するためにポリシー内で dynamodb:LeadingKeys 条件キーを使用しています。このポリシーは特定のテナントに直接紐づいているわけではないということがわかります。その代わりに、ポリシーは aws:PrincipalTag タグを使用して、実行時に TenantID パラメータを解決します。

このアプローチは、新しい IAM のリソースを作成しなくても、新しいテナントを追加できることを意味します。これにより、管理のための負担が軽減される他、 IAM のクォータを超過する可能性を抑制することができます。

Amazon Elasticsearch Service におけるサイロモデルストレージ分離

Amazon Elasticsearch Service リソースのテナント分離を実装する方法を示す別の例を見てみましょう。図 4 は、システムの各テナントごとに個別の Elasticsearch インデックスを保持するサイロモデルのデータパーティション分割を示しています。

図 4: Elasticsearch テナント毎のインデックス戦略

DynamoDB 用に作成したポリシーと同じように、プリンシパルの TenantID タグを変数として利用した IAM ポリシーを作成することでこれらのテナントのリソースを分離することができます。次の例では、ポリシー内の Resource 要素のインデックス名の一部としてプリンシパルタグを利用しています。アクセスのタイミングで、プリンシパルタグがリクエストコンテキストに含まれるテナントの ID に置き換えられ、結果として Elasticsearch インデックスの ARN が指定されます。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "es:ESHttpGet",
        "es:ESHttpPut"
      ],
      "Resource": [
        "arn:aws:es:<region>:<account-id>:domain/test/${aws:PrincipalTag/TenantID}*/*"
      ]
    }
  ]
}

同じテナントに複数のインデックスが属する場合は、ワイルドカードを使用してそれらのインデックスへのアクセスを許可できます。前述のポリシーはドキュメントがパターンに一致する名前のインデックスに属している場合、ドキュメントに対して es:ESHttpGet および es:ESHttpPut アクションを許可します。

重要:このポリシーを機能させるには、テナント ID がインデックスと同じ命名規則に従う必要があります。

このアプローチにより、テナントの分離戦略をスケールさせることができますが、Elasticsearchクラスターがサポートできるインデックス数の制約に注意する必要があります。

Amazon S3 におけるテナント毎のプレフィックス戦略

Amazon Simple Storage Service (Amazon S3) バケットは、一般的にテナント毎に専用プレフィックスを付与することで共有オブジェクトストアとして利用されます。セキュリティを強化するために、必要に応じて、テナントごとに専用のカスタマーマスターキー (CMK) を使用することができます。その場合は、対応する TenantID リソースタグを CMK にアタッチします。
ABAC と IAM を使用することで、各テナントが、対応するプレフィックスを持つ S3 バケット内のオブジェクトのみを取得して復号できるように制御できます。

 

テナント毎の S3 プレフィックス戦略

図 5: テナント毎の S3 プレフィックス戦略

下記ポリシーにおいて、最初のステートメントではResource 要素内で TenantID プリンシパルタグを使用しています。このポリシーでは s3:GetObject アクセス許可を付与していますが、要求されたオブジェクトキーがテナントのプレフィックスで始まる場合のみ許可されます。

2 番目のステートメントでは、要求されたオブジェクトを暗号化している KMS キーに対して kms:Decrypt 操作を許可します。KMS キーには TenantID リソースタグが付与されており、対応するテナント ID の値として設定されている必要があります。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::sample-bucket-12345678/${aws:PrincipalTag/TenantID}/*"
    },
    {
      "Effect": "Allow",
      "Action": "kms:Decrypt",
       "Resource": "arn:aws:kms:<region>:<account-id>:key/*",
       "Condition": {
           "StringEquals": {
           "aws:PrincipalTag/TenantID": "${aws:ResourceTag/TenantID}"
        }
      }
    }
  ]
}

重要:このポリシーを機能させるには、テナント ID が S3 オブジェクトキー名のガイドライン に従っている必要があります。

テナント毎のプレフィクスのアプローチでは、任意の数のテナントをサポートできます。ただし、テナントごとに専用のカスタマーマネージド KMS キーを使用する場合は、AWS リージョンごとの KMS キーの数によってテナント数が制限されます。

まとめ

IAM と ABAC を組み合わせることで、SaaS プラットフォームを構築するチームに、テナント分離を実装するための強制力のあるモデルが提供されます。この属性駆動型の動的なモデルを使用することで、 IAM による分離を実用的なテナント数までスケールさせることができます。さらに、このアプローチは IAM の機能を活用することで、テナントの ID 体系に統合した形で分離を管理、スケール、強制することが可能になります。 IAM ABAC の使用を開始するには、このブログ投稿の例を使用するか、IAM チュートリアル: タグに基づいて AWS リソースにアクセスするためのアクセス許可を定義するを参照してください。

翻訳はソリューションアーキテクト柴田 龍平が担当しました。原文はこちらです。