Amazon Web Services ブログ

動的に生成された IAM ポリシーで SaaS テナントを分離する

この記事は、AWS SaaS Factoryのシニアパートナーソリューションアーキテクトである Bill Tarrによって書かれました。

多くの Software-as-a-Service (SaaS) プロバイダーは、テナント分離戦略の根幹として AWS Identity and Access Management (IAM) を活用しています。

IAM により、組織は一連のポリシーとロールを定義することができ、リソースへのアクセス時にテナントの境界を越えないようにすることが可能です。

ここでの課題は、多くの組織がテナントごとに個別のポリシーを作成しなければならないことです。これにより、テナントポリシーの数が爆発的に増加し、その結果 IAM のアカウント制限に達してしまうことがあります。

さらに重要なのは、このポリシーの数が急増すると、管理と更新が手に負えなくなる可能性があることです。ポリシーの一部を変更し、その変更をシステム内のすべてのテナントに展開することを想像してみてください。すぐに環境の管理性と俊敏性が損なわれます。

AWS SaaS Factory によるこの記事では、動的なポリシー生成によって、よりスケーラブルで管理しやすいテナント分離エクスペリエンスがどのように生み出されるかを見ています。この投稿は、このエクスペリエンスの土台となる部分に焦点を当て、動的なポリシー生成を支えるのに必要なメカニズムを導入するための手法を説明します。

IAM による分離の基礎

本題に入る前に、Security Token Service (STS) を使用して IAM でテナント分離を実装する方法について、シンプルで静的なバージョンを見ていきましょう。

図 1 – テナント固有のロールとポリシーを持つマルチテナントのセキュリティモデル

受信するリクエストには認証ヘッダーが含まれ、そこには現在のテナントを識別するデータを内部に持つ JSON Web Token (JWT) (1) があります。これらのトークンは ID プロバイダーによって署名され、JWT が改竄不可能で、テナント ID が信頼できることを保証します。

IAM から (2) テナント固有のポリシーを取得し、ポリシーで制限された認証情報 (3) を返すように STS に依頼します。(4) Amazon DynamoDB にアクセスしようとすると、getItem への SDK 呼び出しを行うアクセス許可 (5) が IAM によってチェックされ、取得したテナント固有のポリシーに基づいてアクセスを許可または拒否します。

このモデルはシンプルで、データへのアクセスをテナントの特定の Amazon DynamoDB 内のデータに制限します。このポリシーは、DynamoDB データを使用する権限をテナントに与えるだけです。ただし、このモデルでは、以下のようなカスタムポリシーをシステム内の個々のテナントに対して作成する必要があります。

   { 
      "Effect": "Allow", 
      "Action": [ 
            "dynamodb:*" 
       ], 
       "Resource": [ 
            "arn:aws:dynamodb:us-west-2:123456789012:table/Employee" 
       ], 
       "Condition": { 
          "ForAllValues:StringEquals": { 
              "dynamodb: LeadingKeys": [ "Tenant1" ] 
          } 
      } 
   }

図 2 – テナントごとに必要なカスタム IAM ポリシー

このポリシーについては後ほど詳しく見ていきますが、ここではテナント ID がポリシーにハードコードされていることに注目してください。

この問題をより概念化するために、SaaS 環境では、システムを使用するテナントが数百、数千にもなり得ることを考慮する必要があります。このシナリオでは、各テナントにはほぼ同一の分離ポリシーのセットが必要です。新しいテナントがシステムに追加されるたびに、ポリシーの数がいかに急増するかがわかります。

IAM リソースのアカウント制限に当たらなくても、これらのテナント分離アーティファクトをすべて管理するのは難しいと感じるでしょう。

図 3 – テナントの追加に伴い、急速に倍増するIAM ロールの数

この方法に関するその他の問題は、スケーラビリティとセキュリティに影響を与えます。サービスの新機能をリリースしたら、既存の IAM リソースを変更し、オンボーディングのプロセスを更新する必要があります。これにより、サービスとセキュリティインフラストラクチャが密に結合し、デプロイプロセスが複雑になる場合があります。

これでは、チームが新機能の提供に集中することもできません。テナント分離やセキュリティ・ストーリーの維持やテストが困難になると、テナントデータを漏洩させるようなミスが発生する可能性も高くなります。

動的なポリシー生成

SaaS 開発者が直面する、IAMを使用したテナント分離の管理に関するいくつかの課題を確認しました。動的なポリシー生成を通じて、これらの問題にどのように対処するかについて説明します。

Amazon DynamoDB リソースへのユーザーのアクセスを制限しようとする例をもう一度考えてみましょう。ただし、今回は IAM にポリシーを保存しません。代わりに、ポリシーをテンプレート化して、静的なテナント参照をテンプレート化されたプレースホルダに置き換えます。

次のテンプレート内の {{table}} と {{tenant}} のプレースホルダーを、実行時に適切な値で置き換えられます。テンプレート置き換えのプロセスを容易にするいくつかの方法を後で見ていきます。ここでは、プレースホルダーはテンプレート内で置換する文字列と考えておけば問題ありません。

  {
    "Effect": "Allow",
    "Action": [
         "dynamodb:*"
    ],
    "Resource": [
         "arn:aws:dynamodb:*:*:table/{{table}}"
    ],
    "Condition": {
        "ForAllValues:StringEquals": {
            "dynamodb: LeadingKeys": [ "{{tenant}}" ]
        }
    }
 }

図 4 – マルチテナントで使われる動的ポリシーのテンプレート

ポリシーが最終的な形になったので、さらに詳しく見てみましょう。まず、このテンプレートの Action では権限が広めにとられていることがわかります。これにより、この権限をさまざまなテナントごとのさまざまなセキュリティのユースケースに適用できる柔軟性が高まります。

この戦略では、リソースはテナント固有ではありませんが、一部の戦略ではリソースレベルでテナントの分離が強制されることに注意してください。条件演算子は、プライマリキーが特定のテナント ID 値と一致する項目のみ表示するようにテナントを制限します。

この戦略の詳細については、Amazon DynamoDB を使用したマルチテナントストレージをお読みください。

ロールの引き受け

ポリシーは分離を実行するのに中核となる要素ですが、これらのポリシーがどのように適用され、実行されるかについて考える必要があります。そこで登場するのが「ロールを引き受ける」という概念です。ロールがどのようなものかを想像する最も簡単な方法は、ポリシーの集合体としてですが、ロールの真の力は、ロールをどのように使用できるかにあります。

ロールはユーザーから独立しており、ユーザーは一時的にロールとその権限を引き受けることができます。では、動的に生成されたポリシーがロールとどのように連携するのでしょうか。 STS では、ロールと生成したポリシーを結びつけることができます。STS は、ロールと動的に生成されたポリシーの権限を結合するので、ユーザーはポリシーとロールの両方に存在する権限だけを取得することができます。

この例では、Amazon DynamoDB リソースへのアクセスを許可するインラインポリシーがロールに含まれている必要があります。次のロールのポリシーに注目してください。これにより、テナント固有の制限なしに DynamoDB リソースに誰でもアクセスできます。

 {
  "Version": "2012-10-17",
  "Statement": [
    {
      "Action": [
        "dynamodb:GetItem", "dynamodb:BatchGetItem", "dynamodb:Query", "dynamodb:DescribeTable"
      ],
      "Resource": "arn:aws:dynamodb:us-west-2:123456789012:table/Employee",
      "Effect": "Allow"
    }
  ]
 }

図 5 – テナント固有の制限がないインラインポリシー

次に、動的に生成された次のポリシーを見て、前のインラインポリシーと比較します。次に示すのは、動的に生成されたポリシーを渡して、STS を使用してロールを引き受けた後のポリシーです。

 {
  "Version": "2012-10-17",
  "Statement": [
    {
      "Action": [
        "dynamodb:GetItem", "dynamodb:BatchGetItem", "dynamodb:Query", "dynamodb:DescribeTable"
      ],
      "Resource": "arn:aws:dynamodb:us-west-2:123456789012:table/Employee",
      "Effect": "Allow",
      "Condition": {
        "ForAllValues:StringEquals": {
          "dynamodb: LeadingKeys": [ "tenant1" ]
        }
      }
    }
  ]
 }

図 6 – 条件演算子付きの動的に生成されたポリシー

動的に制限を行うポリシーを渡すことで、テナント ID でキーが設定されたデータへのアクセスを制限する条件演算子が追加しつつ、ロールのインラインポリシーによって Action リストの制限を厳しくしていることに注目してください。 コードの観点から見ると、STS でロールを引き受けるのは簡単です。図 7 に、新しいロールを引き受けるコードの抜粋を示します。

  AssumeRoleResponse response = sts.assumeRole (ar -> ar
       .webIdentityToken(openIdToken)
       .policy(scopedPolicy)
       .roleArn(role));
  Credentials tenantCredentials = response.credentials();
  DynamoDbClient dynamoDbClient = DynamoDbClient.builder()
       .credentialsProvider(tenantCredentials)
       .build();

図 7 – 新しいロールを引き受けるコードの抜粋

STS assumeRoleを呼び出すとき、動的に生成されたポリシーとロールを渡すことが必要です。STS へのこの呼び出しの結果は、図 6 に示したポリシー内のアクセス許可だけに限定された認証情報になります。

DynamoDBClient などのサービス SDK クライアントは、結果として得られた認証情報を引き受けます。これで、このクライアントから行われたすべての呼び出しに、限定された権限が適用されます。

動的に生成されるポリシーのメリットは明らかでしょう。単一のマイクロサービスセキュリティシナリオのためだけに、各テナントに対してロールとポリシーの両方を作成し、メンテナンスする必要のあるモデルから始めました。しかし、ポリシーの生成とロールの引き受けを管理するメカニズムについて説明したので、あとはただこれを導入すればいいのです。

トークン自動発行機の導入

トークン自動発行機を導入して、動的なポリシー生成の管理を簡素化する方法を見てみましょう。トークン自動発行機の主な役割は、トークンの管理と生成方法の詳細を隠蔽しながら、トークンを取得するための単一の方法を作成することです。これにより、マイクロサービス内のコードが簡素化され、テナントの分離が日々の開発の視野から外れます。

ただし、トークン自動発行機が、実行時にポリシーを動的に生成するために使用できる権限テンプレートのコレクションを利用することも同様に重要です。これにより、オンボーディングしたテナントごとに IAM で実際のポリシーを作成、維持、心配する必要がなくなります。

これがトークン自動発行機の概念モデルです。

図 8 – トークン自動発行機の概念的なモデル

図 8 の主なポイントは、アプリケーション開発者がポリシーやロールを直接操作しなくなったことです。アプリケーションコードはトークン自動発行機を呼び出し、必要なテナントセキュリティ条件がすでに組み込まれているトークンを受け取ります。

この図の手順を踏み、トークン自動発行機の仕組みを見てみましょう。

受信する HTTP ヘッダーには、ベアラートークンを含む認証ヘッダーが含まれています。この例では、テナント ID をもつ JSON Web Token (JWT) (1) です。JWT Manager (2) はトークンを検証し、クレームからテナントを抽出します。次に、テナント (3) と必要なその他の変数を権限テンプレート (4) に挿入します。ファイルからテンプレートを読み込んだ後、渡された変数でポリシーが置き換えられます。

その結果、完全な形のポリシーが動的に作成されます。この動的ポリシーは、最初の例にあった多くの静的テナント固有のポリシーの代わりになります。次に、このポリシーはロールを引き受ける (6) 際に使用され、以前必要としていたテナント固有のロールに代わるものとなります。最後に、トークン自動発行機は、新しく作成されたトークン、または認証情報を開発者に返します。

私たちの実装では AWS Lambda レイヤーを使用しています。このレイヤーには AWS Lambda コードとは区別され、個別にデプロイできるという利点があります。他の環境では、Java Archive (JAR) ファイルまたは個別にデプロイ可能なアーティファクトとしてデプロイできます。

理論的なモデルをある程度詳しく見てきました。続いてこの例を動かすコードに飛び込んでみましょう。トークン自動発行機から権限が制限された認証情報を取得するために必要な数行のコードは次のとおりです。

  TokenVendingMachine tokenVendorMachine = new TokenVendingMachine();
  AwsCredentialsProvider tenantCredentials = tokenVendingMachine
    .vendToken(jwtToken);

この呼び出しでは、HTTP リクエストの一部として入った JWT トークンを単にそのまま渡します。この呼び出しによって返される認証情報は、他のリソースへのアクセスに使用でき、テナントポリシーが適用されることが保証されます。たとえば、コードにおいて、この認証情報を使用して Amazon Simple Storage Service (Amazon S3) にアクセスできます。

 S3Client s3Client = S3Client.builder()
             .credentialsProvider(tenantCredentials)
             .build();

Amazon S3 クライアントをインスタンス化するときに、これらの認証情報がどのように適用されるかに注目してください。すべてではないにしてもほとんどのサービスクライアントは AwsCredentialProvider を必要とするため、これらのトークンを使用して、多数のサービスにまたがるクロステナントアクセスを制限できます。

権限テンプレート

トークン自動発行機の中核は、権限テンプレートファイルの集合です。動的に生成されたポリシーの例ではすでに 1 つのテンプレートを見ました。これらのテンプレートファイルをどのように管理し、コードから独立して発展させるかを見てみましょう。

分離ポリシーを記述するアプローチは、テンプレートとサービスごとに異なる場合があります。例として、フォルダレベルでアクセスを制限する、Amazon S3 ポリシーを作成する方法を見てみましょう。

図 9 のテンプレートでは、ListBuckets アクション(4)へのアクセスを、テナント識別子と一致するプレフィックスを持つテナントに許可しています。このため、テナントは他のテナントに属するフォルダ内のオブジェクトを操作することができません。

  {
   "Effect": "Allow", 
   "Action": [
     "s3:ListBucket" 
   ], 
   "Resource": [
     "arn:aws:s3:::{{bucket}}" 
   ], 
   "Condition": {
    "StringLike": {
      "s3:prefix": [ "{{tenant}}", "{{tenant}}/", "{{tenant}}/*" 
      ]
    }
  }
 },
 {
  "Effect": "Allow", 
  "Action": [
    "s3:GetObject", "s3:PutObject", "s3:DeleteObject" 
   ], 
   "Resource": [
        "arn:aws:s3:::{{bucket}}/{{tenant}}/*" 
   ]
 }

図 9 – フォルダレベルでアクセス制限をするテンプレート

これら 2 つの戦略はもちろん出発点に過ぎません。時間の経過とともにテナントのセキュリティ戦略をいくつでも追加できるからです。 権限テンプレートのライフサイクルも重要です。テンプレートはコードとは別に管理され、個別にデプロイおよびバージョン管理されます。たとえば、独自のバージョン管理を持つすべてのテンプレートを含むテンプレートリポジトリを持つことができます。または、設定サーバーまたはテンプレートサービスが、関心を分離するための選択肢となり得ます。 この例をモジュールとしてコーディングし、レポジトリに格納しました。この例は Java で書かれているので、Maven pom.xml ファイルを追加しました。テンプレートは JAR アーティファクトとしてデプロイできます。 テンプレートは単なる JSON ファイルなので、コードのデプロイプロセスとして、インフラストラクチャの一部と考えるのが良いかもしれません。Amazon Elastic File System (Amazon EFS) や Amazon S3 などのファイルシステムにデプロイすると、どちらもアーキテクチャ全体でアクセス可能であり、マイクロサービスアーキテクチャにうまく適合します。

テンプレートからポリシーを生成する

IAM の外側でポリシーを定義したので、権限テンプレートをステートメントに読み込み、実行時に動的に制限されたポリシーに追加するコードを導入できます。権限テンプレートを置き換えてポリシーを作成する方法を考えてみましょう。 ポリシーの作成を担う PolicyGenerator というシンプルな Java クラスを次に示します。

 String scopedPolicy = PolicyGenerator.generator()
                .s3FolderPerTenant(bucket)
                .dynamoLeadingKey(tableName)
                .tenant(tenantIdentifier)
                .generatePolicy();

目的は、開発者が適切に形成された有効なセキュリティ権限をできるだけ簡単に追加できるようにすることです。このプロセスでは、JWT から抽出されたテナント ID にアクセスする必要があります。

追加するそれぞれの権限メソッドは、その特定のテンプレートを置き換えるために必要な値をパラメーターとして受け取ります。この例のトークン自動発行機は、マイクロサービスが必要とする権限を追加し、環境変数から必要な値を特定するように設定されています。

図 10 に示すように、PolicyGenerator を分割して (アプリケーションコードのように) 1 つのレイヤーで権限をインスタンス化し、トークン自動発行機でテナントを追加することもできます。開発者がテナント ID を処理する必要はありません。

図 10 – ポリシージェネレーターによるテンプレートのポリシーへの置き換え

この PolicyGenerator オブジェクトの図は、実際にはテンプレートファイルを置き換えに必要な変数に結びつけているだけであることがわかります。この場合、Amazon S3 テンプレートにはアクセス対象を表す bucket 変数が必要で、Amazon DynamoDB テンプレートにはテーブル名が必要です。もちろん、どちらもテナント識別子が必要です。PolicyGenerator の最終結果は、動的に生成されるポリシーです。

テンプレート内の変数を置き換えることができる任意のテンプレートエンジンを使用して、ポリシー生成ソリューションを実装できます。選択肢の 1 つは、言語に依存しないシンプルなテンプレートエンジンである Mustache です。

この例では、PolicyGenerator によって生成されたテンプレートのリストは、次のように Mustache を使用してデータ (bucket、table、tenant) と結合され、ポリシーになります。

 String resolvedStatements = Mustache.compiler()
             .compile(statements)
             .execute(data);
 String scopedPolicy = "{ \"Version\": \"2012-10-17\",\n 
                    \"Statement\": [\n" + resolvedStatements + " ]\n}";

テンプレートエンジンは、権限テンプレートのファイルから読み込まれた文字列を受け取り、テナントを含む入力として提供したデータで置き換えます。ポリシーの外側の部分は、テンプレート自体にすることも、この例のように手動で追加することもできます。

ポリシーサイズ制限

ポリシーにはサイズ制限があることに留意することが重要です。IAM ロールを引き受ける際には、ポリシーはインラインポリシーとみなされ、記述できる文字数には 2,048 文字の制限があります。動的に生成されるポリシーの合計サイズがこの文字数制限を超えないようにする必要があります。

この制限に当たった場合は、一つのロールにあまりに多くの役割を持たせてしまっているかもしれません。マイクロサービスまたはアプリケーションに固有のロール、または類似した機能のグループによって共有されるロールについて考えてみましょう。ロールが具体的になるほど、ポリシーに必要なアクセス権限は少なくなります。ロールに割り当てる権限を制限すると、テナント分離モデルが改善されます。

AWS Lambda レイヤー

ソリューションのすべての変動部分を詳細に確認したので、実装の選択肢について説明しましょう。AWS Lambda の例では、Lambda レイヤーに対して単一のメソッド呼び出しを行い、トークンをリクエストしたことを覚えているでしょうか。Lambda レイヤーを実装してテナントのセキュリティをカプセル化し、トークン自動発行機のロジックを Lambda コードとは別に展開できるようにしました。

Lambda レイヤーを使用すると、Amazon S3 や Amazon DynamoDB の例のように、Lambda コードとは別にさまざまなセキュリティトークン呼び出しを実装できます。これは、Amazon Simple Queue Service (SQS)、AWS Secrets Manager、その他の AWS サービスでも実行できます。

最終的には、最小権限の原則に準拠できるアプリケーションコードになります。アプリケーションは、必要なサービスとテナントデータにしかアクセスできません。セキュリティとテナント分離コードの重複を最小限に抑えることで、開発者は機能の提供とスケーラビリティの向上に集中できるようになります。
次のサンプルコードは、Lambda レイヤー を実行する 1 つの方法を示しています。

 JwtClaimsManager jwtClaimsManager = JwtClaimsManager.builder()
      .headers(headers)
      .build();
 String tenant = jwtClaimsManager.getTenantId(“custom:tenant_id”);
 PolicyGenerator PolicyGenerator = PolicyGenerator.builder()
     .s3FolderPerTenant(bucket)
          .tenant(tenant);
 String scopedPolicy = policyGenerator.generatePolicy();
         
 AssumeRoleResponse assumeRoleResponse = sts
     .assumeRole(assumeRoleReq -> assumeRoleReq
                  .durationSeconds(durationSeconds)
                  .policy(scopedPolicy)
                  .roleArn(role)
                  .roleSessionName(tenant)
                );
 Credentials tenantCredentials = assumeRoleResponse.credentials();

図 11 – AWS Lambda レイヤーを実装する一つの方法

レイヤーはポリシージェネレーターを作成し、制限付きポリシーを作成し、ロールとともに渡します。内部では、以前に確認したように、動的に生成されたポリシーで役割を引き受けるために STS を呼び出します。

レイヤーの内部を調べて、何が行われているのか見てみましょう。実装はコード内に隠されていますが、jwtClaimsManagerはトークンを処理し、テナント識別子を抽出します。これを PolicyGenerator に追加します。結果のポリシーは STS AssumeRoleに渡されます。最後に、STS からの応答から認証情報を取得します。

返される tenantCredentials は、明示的に有効にしたサービスのみにアクセスするために使用する、テナントに範囲が制限された権限です。この例では、Amazon DynamoDB テーブルと S3 フォルダのアクセス権限テンプレートがあるため、生成された認証情報は、このテナントに属する DynamoDB および S3 データの取得にのみ使用できます。

その他のアーキテクチャ

この例では AWS Lambda および Lambda レイヤーを使用していますが、いくつかの変更を加えることで、トークン自動発行機はさまざまなアーキテクチャに実装できます。JWT や AWS Lambda を使用しないアーキテクチャでこれがどのように見えるか見てみましょう。

SaaS プロバイダーの中には、リモート環境にエージェントを配置し、それをサーバに報告させるものがあります。これらのエージェントは、SSL x509 証明書のサブドメインを通じてテナントのアイデンティティを伝えることができます。Amazon Elastic Compute Cloud(Amazon EC2)インスタンス上に配置されたマイクロサービスが、これらのエージェントからデータを受け取る場合でも、トークン自動発行機を使用することができます。

ご覧のとおり、トークン自動発行機のコンセプトは変わりません。

図 12 – トークン自動発行機の代替アーキテクチャ

 

リバースプロキシ (NGINX や KONG など) は接続を終端し、テナント ID をカスタムヘッダーに追加します。トークン自動発行機は、その新しいヘッダーを取得し利用するように調整する必要があります。

もちろん、Amazon EC2 アプリケーションでも ID プロバイダと JWT も同じように簡単に使用できますが、このアーキテクチャを考慮すると便利です。証明書を使用してテナントのアイデンティティを判断するにもかかわらず、トークン自動発行機の概念はほぼ同じです。

まとめ

動的なポリシー生成は、SaaS プロバイダーがテナント分離を実装するのに役立ちます。トークン自動発行機の実装例は、動的ポリシー生成を実装するための動的で管理しやすいメカニズムを提供します。

独自のトークン自動発行機を実装する際には、いくつかの点に注意してください。

  • 権限テンプレートは、分離戦略に関する懸念事項を分離し、アプリケーションとは独立して展開できるようにする必要があります。
  • ポリシーは、最小権限の原則を有効にし、テナントのサービスやデータへのアクセスを可能な限り厳しく制限する必要があります。
  • テナント ID はソリューション内で一貫して解決され、検証可能である必要があります
  • ソリューションは、すべての SaaS サービスでカプセル化され、再利用可能で、AWS SDK の呼び出しに使用できるテナントに制限された認証情報を返す必要があります。

トークン自動発行機で考えられるアーキテクチャをいくつか見直しただけであることに留意してください。AWS Lambda と Amazon EC2 の例は、ほんの 2、3の変更だけで、全く異なるモデルのトークン自動発行機にアーキテクチャを適応させる方法を示しています。

この記事の例の詳細については、AWS SaaS Factory GitHub リポジトリを参照してください。リポジトリには、AWS アカウントに必要なインフラストラクチャを自動的にプロビジョニングするためのサンプルアプリケーションと AWS CloudFormation リソースが含まれています。

AWS SaaS Factory について

AWS SaaS Factory は、SaaS 導入のどの段階の企業でも支援します。新製品の構築、既存のアプリケーションの移行、AWS 上での SaaS ソリューションの最適化など、どのようなご要望にもお応えします。AWS SaaS Factory Insights Hub では、技術的、ビジネス的なコンテンツやベストプラクティスをご覧いただけます。
また、SaaS 開発者の方は、エンゲージメントモデルや AWS SaaS Factory チームとの連携について、アカウント担当者にお問い合わせください。
こちらにご登録いただくと、最新の SaaS on AWS ニュース、リソース、イベントの情報を入手できます。

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