メインコンテンツへスキップ
友田 陽大
DynamoDB
AWS
DynamoDB
セキュリティ
IAM
マルチテナント
Terraform
アーキテクチャ設計

DynamoDB セキュリティ完全ガイド(2026年版):IAM最小権限・きめ細かなアクセス制御(LeadingKeys)・保存時/転送時の暗号化・VPCエンドポイント

DynamoDBのセキュリティを、AWS公式仕様に忠実に解説。dynamodb:LeadingKeysによる行レベルのマルチテナント分離、dynamodb:Attributesの列制限、テーブル/インデックス/ストリーム別のIAM最小権限ARN、常時ONの保存時暗号化と3種のKMSキー、TLS必須とaws:SecureTransport、VPCゲートウェイ/インターフェイスエンドポイントまでを、IAMポリシーJSONとTerraform実コードで本番目線にまとめます。

公開日
読了時間
29分
著者
友田 陽大
シェア
目次

DynamoDBのセキュリティで最初に捨てるべき発想は、**「アクセス制御はアプリのコードで書く」**です。

if (item.tenantId !== currentUser.tenantId) throw Forbidden —— この一行は、書き忘れ・条件ミス・新しいエンドポイントの追加で、いつか必ず破れます。マルチテナントSaaSで他テナントのデータが見えてしまう事故の大半は、「DBは全部見える前提で、アプリ層のチェックに頼っていた」ことが原因です。

正しい設計はその逆です。アクセス制御を、アプリのif文ではなく、IAM・暗号化・ネットワーク境界という"破れない層"で強制する。 DynamoDBは、これを実現するためのプリミティブを公式に備えています。中でも dynamodb:LeadingKeys 条件キーは、「そのプリンシパルは、自分のパーティションキーの行しか触れない」をIAMレベルで保証します。アプリが何を書こうと、IAMが許さなければ他テナントの行には1バイトも到達できません。

本記事は、私がサーバーレス(Lambda + DynamoDB)のマルチテナント決済プラットフォームの信頼性レイヤーを設計・主導し、本番稼働中の二重課金0件を維持してきた経験をベースに、DynamoDBのセキュリティ設計だけを体系化したものです。データモデリングや冪等性の設計はシングルテーブル設計&本番信頼性パターンに、キャパシティ・コスト設計はキャパシティ・コスト・性能設計ガイドに譲り、ここでは「誰が・何に・どこから・どう暗号化されて触れるか」に絞ります。

すべての仕様・条件キー名・暗号化方式・エンドポイント種別は、**AWS公式ドキュメント(2026年6月時点)**に照合しています。

注: コードは設計意図を伝えるための要点抜粋です。アカウントID・テーブル名・リージョンは抽象化/例示しています。


1. 責任共有モデルと多層防御の全体像

公式は、DynamoDBのセキュリティを**責任共有モデル(shared responsibility model)**で整理しています。

Security of the cloud – AWS is responsible for protecting the infrastructure that runs AWS services in the AWS Cloud. Security in the cloud – Your responsibility is determined by the AWS service that you use.

平たく言えば、「クラウドそのもの(物理・ハイパーバイザ・マネージドサービスの基盤)のセキュリティはAWSの責任、その上で何をどう構成するか(IN the cloud)はあなたの責任」です。DynamoDBはマネージドサービスなので、公式が明記するとおりAWSのグローバルネットワークセキュリティ手順で保護されています。

As a managed service, Amazon DynamoDB is protected by the AWS global network security procedures.

だからこそ、あなたが設計すべきは「IN the cloud」の部分です。公式のセキュリティ章は、おおむね次の柱で構成されています。本記事はこれを実装目線で並べ替えたものです。

防御層(責任:あなた)何を守るか主な武器
データ保護(保存時)ディスク上のデータ保存時暗号化(KMS・常時ON)→ 4章
データ保護(転送時)ネットワーク上のデータTLS必須・aws:SecureTransport → 5章
アクセス管理(API)どのアクションを許すかIAM最小権限・リソースARN → 2章
アクセス管理(データ)どの行・どの列を許すかきめ細かなアクセス制御(FGAC)→ 3章
インフラ/ネットワークどこから到達できるかVPCエンドポイント・エンドポイントポリシー → 6章
監査・検知誰が何をしたかCloudTrail・最小権限のCI/CD → 7章

多層防御(defense in depth)の要点は、1つの層が破れても次の層で止まること。IAMポリシーにバグがあっても、VPCエンドポイントポリシーで止まる。アプリに脆弱性があっても、LeadingKeys でテナント境界を越えられない。各層が独立して効くように積むのが設計です。


2. IAM最小権限:アクションとリソースARNを絞る

きめ細かなアクセス制御の前に、土台となるIAMポリシーの最小権限を固めます。ここが緩いと、その上のFGACも意味を失います。

dynamodb:* は禁止、Resource: "*" も禁止

最悪のアンチパターンは、これです。

{
  "Effect": "Allow",
  "Action": "dynamodb:*",
  "Resource": "*"
}

dynamodb:*DeleteTableUpdateContinuousBackups(PITR無効化)・RestoreTableFromBackupExportTableToPointInTime まで含みます。アプリのランタイムロールにテーブル削除権限が要ることはまずありません。least privilege(最小権限)の原則に従い、「実際に呼ぶアクション」と「実際に触るテーブル」だけに絞ります。

リソースレベルARN:テーブル・インデックス・ストリームを別々に

公式の最小権限ポリシーは、アクションを列挙し、リソースを特定テーブルのARNに固定します。DynamoDBのリソースARNは次の形です(公式の例より)。

リソースARN形式
テーブル本体arn:aws:dynamodb:<region>:<account-id>:table/<table-name>
インデックス(GSI/LSI)arn:aws:dynamodb:<region>:<account-id>:table/<table-name>/index/<index-name>
ストリームarn:aws:dynamodb:<region>:<account-id>:table/<table-name>/stream/<timestamp>

重要なのは、インデックスはテーブル本体とは別のARNだという点です。Query をGSIに対して許すには .../index/<name>(または .../index/*)を Resource に明示する必要があります。公式が示す「テーブルと全インデックスを許す」最小権限ポリシーはこうです。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AppRuntimeAccessToBooksTable",
      "Effect": "Allow",
      "Action": [
        "dynamodb:GetItem",
        "dynamodb:BatchGetItem",
        "dynamodb:Query",
        "dynamodb:PutItem",
        "dynamodb:UpdateItem",
        "dynamodb:DeleteItem",
        "dynamodb:BatchWriteItem",
        "dynamodb:ConditionCheckItem"
      ],
      "Resource": [
        "arn:aws:dynamodb:us-west-2:123456789012:table/Books",
        "arn:aws:dynamodb:us-west-2:123456789012:table/Books/index/*"
      ]
    }
  ]
}

設計上の線引き:

  • ランタイムロール(Lambda等)には、データプレーンのアクション(GetItem/Query/PutItem…)だけを、特定テーブル+必要なインデックスに絞って渡す。
  • Scan は原則与えない。後述のとおり ScanLeadingKeys を無視して全アイテムを返すため、テナント分離の穴になりやすい。
  • コントロールプレーンCreateTable/DeleteTable/UpdateTable)は、IaC(Terraform)を流すデプロイロールにだけ与え、アプリのランタイムロールからは外す。
  • dynamodb:DescribeStream / GetRecords / GetShardIterator が要るのはStreamコンシューマだけ。ストリームARNに絞って別ロールにする。

SRP(単一責任)はIAMロールにも効きます。「アプリ実行」「デプロイ」「Streamコンシューマ」「バックアップ運用」を1つの何でもロールに混ぜない。ロールを分けるほど、漏洩時の被害半径(blast radius)が小さくなります。


3. きめ細かなアクセス制御(FGAC):行と列をIAMで縛る

ここが本記事の核心、そしてマルチテナントSaaSが最も価値を得るところです。公式は、API アクションへのアクセスだけでなく、個々のデータアイテム(行)と属性(列)へのアクセスをIAMの Condition 要素で制御できると明記しています。

In addition to controlling access to DynamoDB API actions, you can also control access to individual data items and attributes.

FGACには2つの粒度があります。

  1. アイテムレベル(行) — 特定のキー値を持つアイテムだけに制限する。典型はテナント/ユーザー境界。
  2. 属性レベル(列) — 見える/書ける属性のサブセットを制限する。機微属性(個人情報・内部フラグ)の隠蔽。

3.1 行レベル分離:dynamodb:LeadingKeys(本記事の主役)

公式の dynamodb:LeadingKeys 条件キーは、テーブルの先頭キー属性=パーティションキーを表します。

dynamodb:LeadingKeys — Represents the first key attribute of a table—in other words, the partition key. (…)you must use the ForAllValues modifier when using LeadingKeys in a condition.

これにプリンシパルの識別子を束ねると、「パーティションキー値が自分のIDと一致する行しか触れない」が成立します。公式が用意しているIAM置換変数は次の3つ(Web Identity Federation)。

IdP置換変数
Login with Amazon${www.amazon.com:user_id}
Facebook${graph.facebook.com:id}
Google${accounts.google.com:sub}

マルチテナント行レベル分離の最小ポリシーはこれです。テナント(またはユーザー)IDをパーティションキーに置く設計が前提になります。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "TenantRowLevelIsolation",
      "Effect": "Allow",
      "Action": [
        "dynamodb:GetItem",
        "dynamodb:BatchGetItem",
        "dynamodb:Query",
        "dynamodb:PutItem",
        "dynamodb:UpdateItem",
        "dynamodb:DeleteItem",
        "dynamodb:BatchWriteItem"
      ],
      "Resource": "arn:aws:dynamodb:us-west-2:123456789012:table/SaaSTable",
      "Condition": {
        "ForAllValues:StringEquals": {
          "dynamodb:LeadingKeys": ["${www.amazon.com:user_id}"]
        }
      }
    }
  ]
}

このポリシーが効くと、Query で他テナントのPKを指定してもIAMが拒否します。アプリが tenantId のチェックを書き忘れていても、PutItem で別テナントのPKに書こうとしても、到達しません。これが「アプリのif文ではなくIAMで強制する」の具体形です。私の決済基盤でも、テナント境界はアプリのバリデーションだけに依存させず、こうしたIAM条件キーで二重に締めています。

IAM Identity Center / Cognito を使う場合: 上記の置換変数はLogin with Amazon/Facebook/Google向けです。Cognito Identity Poolの一時クレデンシャルを使う構成では、テナントIDをセッションタグやIDトークンのクレームから解決し、それをPKに対応させる設計にします。詳しいマルチテナント認可の全体設計(PostgreSQL RLSとの比較を含む)はマルチテナントSaaSのデータ分離・認可設計ガイドにまとめています。

3.2 致命的な落とし穴:ScanLeadingKeys を無視する

公式が明示的に警告しています。

The list of actions does not include permissions for Scan because Scan returns all items regardless of the leading keys.

つまり、LeadingKeys でガチガチに縛ったつもりでも、Scan を許可していれば全テナントのデータが返ります。これはバグではなく仕様です。だから2章で述べたとおり、マルチテナントのランタイムロールには Scan を絶対に与えない。これがFGACの第一の鉄則です。

そしてもう一つ。LeadingKeys はパーティションキーにしか効きません。 テナントIDがPKでなく、ソートキーや一般属性に入っている設計では、行レベル分離が成立しません。FGACを使うなら、キー設計の段階でテナントIDをPKに据える必要があります(シングルテーブル設計ガイドのキー設計が前提になる理由はここです)。

3.3 列レベル制限:dynamodb:AttributesSelectReturnValues

dynamodb:Attributes 条件キーは、リクエストがアクセスする最上位属性のリストを表します。許可リスト方式で「この属性しか読めない/書けない」を強制します。

ただしここに、見落とすと情報漏洩につながる重大な注意点があります。公式いわく、属性条件はリクエストで指定された属性に対してのみ評価され、レスポンスの属性には評価されません。

Attribute conditions are evaluated only on attributes specified in the request, not on attributes in the response. For read operations without a ProjectionExpression (GetItem, Query, Scan, etc.), all attributes will be returned regardless of attribute restrictions in your policy.

つまり、ProjectionExpression を付けずに GetItem すると、dynamodb:Attributes で制限したはずの機微属性も全部返ってきます。これを塞ぐには、公式が示すとおり**dynamodb:Select で「特定属性指定(SPECIFIC_ATTRIBUTES)」を強制し、さらに書き込み系ではdynamodb:ReturnValues を制限**して、ALL_OLD/ALL_NEW で全属性が返るのを防ぎます。

公式の列レベル制限ポリシー(読み書き両対応・UserIdTopScore だけ許可):

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "LimitAccessToSpecificAttributes",
      "Effect": "Allow",
      "Action": [
        "dynamodb:UpdateItem",
        "dynamodb:GetItem",
        "dynamodb:Query",
        "dynamodb:BatchGetItem",
        "dynamodb:Scan"
      ],
      "Resource": "arn:aws:dynamodb:us-west-2:123456789012:table/GameScores",
      "Condition": {
        "ForAllValues:StringEquals": {
          "dynamodb:Attributes": ["UserId", "TopScore"]
        },
        "StringEqualsIfExists": {
          "dynamodb:Select": "SPECIFIC_ATTRIBUTES",
          "dynamodb:ReturnValues": ["NONE", "UPDATED_OLD", "UPDATED_NEW"]
        }
      }
    }
  ]
}

公式の注記が3点とも重要です。

  • StringEqualsIfExistsSelectSPECIFIC_ATTRIBUTES に縛ると、アプリは必ず属性を明示しなければならず、「全属性をくれ」が拒否される。
  • ReturnValuesNONE/UPDATED_OLD/UPDATED_NEW に縛るのは、UpdateItem暗黙の読み取りを行うため。ALL_OLD/ALL_NEW を許すと制限外属性が返ってしまう。
  • PutItem/DeleteItem/BatchWriteItem は許可しない。これらはアイテム全体を置換するため、アクセスできないはずの属性を消したり上書きできてしまう。「特定属性だけ更新」を強制したいなら UpdateItem だけにする。

dynamodb:Attributes 使用時の必須事項(公式): dynamodb:Attributes を使うときは、テーブルと(ポリシーに載る)全インデックスの主キー属性・インデックスキー属性を、許可リストに必ず含めること。さもないとDynamoDBがキー属性を使えず、操作自体が失敗します。

3.4 行+列を同時に縛る

LeadingKeysAttributes は同居できます。公式の例6は、「Facebook IDのPKを持つ行だけ」かつ「attribute-A/attribute-B だけ」に縛ります。マルチテナント+機微列保護を一枚のポリシーで実現する形です。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "LimitAccessToCertainAttributesAndKeyValues",
      "Effect": "Allow",
      "Action": [
        "dynamodb:UpdateItem",
        "dynamodb:GetItem",
        "dynamodb:Query",
        "dynamodb:BatchGetItem"
      ],
      "Resource": [
        "arn:aws:dynamodb:us-west-2:123456789012:table/GameScores",
        "arn:aws:dynamodb:us-west-2:123456789012:table/GameScores/index/TopScoreDateTimeIndex"
      ],
      "Condition": {
        "ForAllValues:StringEquals": {
          "dynamodb:LeadingKeys": ["${graph.facebook.com:id}"],
          "dynamodb:Attributes": ["attribute-A", "attribute-B"]
        },
        "StringEqualsIfExists": {
          "dynamodb:Select": "SPECIFIC_ATTRIBUTES",
          "dynamodb:ReturnValues": ["NONE", "UPDATED_OLD", "UPDATED_NEW"]
        }
      }
    }
  ]
}

deny方式より allow方式: 公式は「deny ベースのポリシーは正しく書くのが難しく、将来のAPI変更で無効化されうるため、DynamoDBでは避けることを推奨」としています。許可リスト(allow-list)で必要なものだけを列挙するのが原則です。


4. 保存時暗号化:常時ON・無効化不可、KMSキー3種の選び方

4.1 まず大前提:暗号化は「常に」オン

DynamoDBの保存時暗号化について、最も重要な事実はこれです。全データが常に暗号化され、無効化できません。

All user data stored in Amazon DynamoDB is fully encrypted at rest.

しかも保護範囲は広く、公式によれば主キー・ローカル/グローバルセカンダリインデックス・ストリーム・グローバルテーブル・バックアップ・DAXクラスタまで、永続メディアに載るデータすべてが暗号化されます。つまり「暗号化を有効にするか」という問いは存在せず、問いは**「どのKMSキーで暗号化するか」**だけです。

4.2 KMSキー3種の比較

公式は、新規テーブル作成時に選べる(そしていつでも切り替えられる)3種類のKMSキーを定義しています。

種類所有・管理既定料金キーポリシー制御クロスアカウントキーローテ
AWS所有キー(AWS owned key)DynamoDBが所有◎ 既定追加料金なし不可(不可視)不可AWS任せ
AWSマネージドキー(AWS managed key, aws/dynamodbあなたのアカウントにあり、AWS KMSが管理AWS KMS料金が発生限定的不可AWS管理
カスタマー管理キー(CMK / customer managed key)あなたが作成・所有・完全管理AWS KMS料金が発生あなたが完全制御自分で設定可

公式の要点を引くと:

AWS owned key – Default encryption type. The key is owned by DynamoDB (no additional charge). Customer managed key – The key is stored in your account and is created, owned, and managed by you. You have full control over the KMS key (AWS KMS charges apply).

4.3 どれを選ぶか(意思決定)

  • AWS所有キー(既定・無料) — 暗号化"そのもの"の要件しかなく、キーの可視性・監査・ローテ制御が不要なら、これで十分。多くの一般ワークロードの妥当な出発点。
  • AWSマネージドキー aws/dynamodb — キーの使用がCloudTrailに記録され可視化される点が欲しいが、キーポリシーの細かい制御までは要らない中間解。
  • カスタマー管理キー(CMK) — 次のいずれかが要るなら CMK 一択:
    • キーポリシーで「誰がこのキーで復号できるか」まで制御したい(最小権限を暗号鍵レベルで効かせる)
    • キーのローテーション周期を自分で管理したい、または**キーを無効化してデータを即時アクセス不能化(暗号シュレッディング)**できる手綱が欲しい
    • クロスアカウントでキーを共有する構成
    • 規制・コンプライアンス(監査ログでキー使用を追える、鍵の管理主体を顧客に置く)

トレードオフも直視します。CMKはKMS料金(鍵の月額+APIリクエスト課金)が乗り、KMSへの依存が増えます。鍵を誤って無効化/削除すれば、そのテーブルは読めなくなります(これは仕様であり、暗号シュレッディングの裏返し)。決済のような機微データを扱う私の基盤では、監査要件とクロスアカウント要件からCMKを選んでいますが、全テーブル一律CMKは過剰になり得ます。データの機微度で選別するのがコスト効率の観点でも正解です。

4.4 Terraform:CMKで暗号化したテーブル

aws_dynamodb_tableserver_side_encryption ブロックで、CMKを指定します。kms_key_arn を省略すると AWSマネージドキー(aws/dynamodb)が、ブロック自体を省略すると AWS所有キー(既定・無料)が使われます。

# 監査・キーローテ・最小権限を鍵レベルで効かせるための CMK
resource "aws_kms_key" "ddb" {
  description             = "CMK for SaaSTable encryption at rest"
  enable_key_rotation     = true # 年次の自動ローテーション
  deletion_window_in_days = 30   # 誤削除に対する猶予
}

resource "aws_kms_alias" "ddb" {
  name          = "alias/saas-table"
  target_key_id = aws_kms_key.ddb.key_id
}

resource "aws_dynamodb_table" "saas" {
  name         = "SaaSTable"
  billing_mode = "PAY_PER_REQUEST"
  hash_key     = "PK" # ここに tenantId/userId を置くことが LeadingKeys の前提
  range_key    = "SK"

  attribute {
    name = "PK"
    type = "S"
  }
  attribute {
    name = "SK"
    type = "S"
  }

  # 保存時暗号化は常に有効。ブロックを「書かない」と AWS所有キー(無料)になる。
  # CMK を使うときだけ enabled=true + kms_key_arn を明示する。
  server_side_encryption {
    enabled     = true
    kms_key_arn = aws_kms_key.ddb.arn
  }

  point_in_time_recovery {
    enabled = true
  }

  deletion_protection_enabled = true # 本番テーブルの誤削除を止める
}

5. 転送時暗号化:TLS必須と aws:SecureTransport

5.1 DynamoDBへの通信はTLSで保護される

公式は、ネットワーク経由のアクセスがTLSで保護されることを明記しています。

You use AWS published API calls to access DynamoDB through the network. Clients can use TLS (Transport Layer Security) version 1.2 or 1.3.

さらに、クライアントは前方秘匿性(PFS)を持つ暗号スイート(ECDHE/DHE)をサポートし、リクエストはSignature Version 4 で署名される必要があります。VPCエンドポイント未使用時でも、公式いわく「DynamoDBとの通信は既定でHTTPS(SSL/TLS)」です。

By default, communications to and from DynamoDB use the HTTPS protocol, which protects network traffic by using SSL/TLS encryption.

注意点として、後述のAWS PrivateLink(インターフェイスエンドポイント)はTLS 1.1をサポートしません(公式の制約)。つまり実質、TLS 1.2以上が前提です。

5.2 平文を拒否する:aws:SecureTransport

「既定でTLS」は、平文HTTPを禁止しているわけではありません(理論上、署名付きでも非TLS接続が混ざる余地を塞ぎたい)。そこで、AWS共通の条件キー aws:SecureTransportTLSでないリクエストを明示的に拒否します。リソースベースポリシー(テーブルのリソースポリシー)やIAMポリシーに、次のような deny を足します。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DenyNonTLSRequests",
      "Effect": "Deny",
      "Principal": "*",
      "Action": "dynamodb:*",
      "Resource": [
        "arn:aws:dynamodb:us-west-2:123456789012:table/SaaSTable",
        "arn:aws:dynamodb:us-west-2:123456789012:table/SaaSTable/index/*"
      ],
      "Condition": {
        "Bool": { "aws:SecureTransport": "false" }
      }
    }
  ]
}

これは「TLSでない(SecureTransport == false)なら、誰であろうと全アクションを拒否」という明示的 deny です。IAMでは deny が allow に優先するため、転送暗号化を組織ポリシーとして強制できます。3章で「deny は避ける」と書きましたが、それはFGACの allow リストの話。aws:SecureTransport == false のような安全側に倒すガードレールの deny は定石で、両者は矛盾しません。


6. ネットワーク境界:VPCエンドポイントでインターネットを経由させない

IAMで「誰が」を縛ったら、次は**「どこから」を縛ります。DynamoDBはパブリックエンドポイント(dynamodb.<region>.amazonaws.com)を持つため、何もしなければトラフィックはインターネットゲートウェイ経由で出ていきます。これをAWSネットワーク内に閉じ込める**のがVPCエンドポイントです。

6.1 DynamoDBのVPCエンドポイントは2種類ある

ここは正確さが命なので、公式の定義をそのまま使います。DynamoDBにはゲートウェイエンドポイントと**インターフェイスエンドポイント(AWS PrivateLink)**の2種類があります。

You can use two types of Amazon VPC endpoints to access Amazon DynamoDB: gateway endpoints and interface endpoints (by using AWS PrivateLink). A gateway endpoint is a gateway that you specify in your route table to access DynamoDB from your Amazon VPC over the AWS network.

ゲートウェイエンドポイントインターフェイスエンドポイント(PrivateLink)
接続方法ルートテーブルに指定するゲートウェイサブネット内のENI(プライベートIP)
使うIPDynamoDBのパブリックIP(ただし経路はAWS網内)VPCのプライベートIP
トラフィックAWS網内に留まるAWS網内に留まる
オンプレからのアクセス不可可(Direct Connect / VPN経由)
別リージョンからのアクセス不可可(VPCピアリング / Transit Gateway)
料金課金されない(無料)課金される

公式の比較表どおり、両者ともトラフィックはAWSネットワーク内に留まります("In both cases, your network traffic remains on the AWS network.")。違いは「どこから到達できるか」と「料金」です。

6.2 まずはゲートウェイエンドポイント(無料)が基本

VPC内のEC2/Lambdaからアクセスするだけなら、ゲートウェイエンドポイントで十分で、しかも無料です。公式の説明:

A VPC endpoint for DynamoDB enables Amazon EC2 instances in your VPC to use their private IP addresses to access DynamoDB with no exposure to the public internet. Your EC2 instances do not require public IP addresses, and you don't need an internet gateway, a NAT device, or a virtual private gateway in your VPC. You use endpoint policies to control access to DynamoDB. Traffic between your VPC and the AWS service does not leave the Amazon network.

つまりゲートウェイエンドポイントを置けば、インターネットゲートウェイもNATも不要になり、DynamoDB宛のトラフィックはパブリックインターネットを通りません。エンドポイント名(dynamodb.<region>.amazonaws.com)は変わらないので、アプリの改修も不要です。

Terraform(ゲートウェイエンドポイント+ルートテーブル紐付け+エンドポイントポリシー):

# ゲートウェイ型: ルートテーブルに紐付ける(=route_table_ids)。無料。
resource "aws_vpc_endpoint" "dynamodb" {
  vpc_id            = aws_vpc.main.id
  service_name      = "com.amazonaws.us-east-1.dynamodb" # gateway は service だけで型が決まる
  vpc_endpoint_type = "Gateway"
  route_table_ids   = [aws_route_table.private.id] # この経路を持つサブネットだけが使える

  # エンドポイントポリシー: このエンドポイント経由で「何に」アクセスできるかを絞る。
  # IAM とは独立した第2の関門(多層防御)。ここでは SaaSTable への読み書きだけ許可。
  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Sid       = "AllowOnlySaaSTableViaEndpoint"
        Effect    = "Allow"
        Principal = "*"
        Action    = ["dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:Query", "dynamodb:UpdateItem"]
        Resource = [
          "arn:aws:dynamodb:us-east-1:123456789012:table/SaaSTable",
          "arn:aws:dynamodb:us-east-1:123456789012:table/SaaSTable/*"
        ]
      }
    ]
  })
}

6.3 エンドポイントポリシー:第2の関門

エンドポイントポリシーは、公式いわくそのエンドポイント経由で「どのプリンシパルが・どのアクションを・どのリソースに」実行できるかを指定します。

You can attach an endpoint policy to your Amazon VPC endpoint that controls access to DynamoDB. The policy specifies: the IAM principal that can perform actions, the actions that can be performed, the resources on which actions can be performed.

ここがFGACとの相補関係で、多層防御の肝です。IAMロールのポリシー(プリンシパル側)が緩くても、エンドポイントポリシー(経路側)で「このVPCからは SaaSTable 以外触れない」と縛れる。逆も然り。公式の例は、特定テーブルだけを許可するこの形です。

{
  "Version": "2012-10-17",
  "Id": "Policy-restrict-to-one-table",
  "Statement": [
    {
      "Sid": "Access-to-specific-table-only",
      "Principal": "*",
      "Effect": "Allow",
      "Action": ["dynamodb:GetItem", "dynamodb:PutItem"],
      "Resource": [
        "arn:aws:dynamodb:us-east-1:111122223333:table/SaaSTable",
        "arn:aws:dynamodb:us-east-1:111122223333:table/SaaSTable/*"
      ]
    }
  ]
}

エンドポイントポリシーの穴に注意: 既定のエンドポイントポリシーは Action: "*", Resource: "*", Principal: "*"(全許可)です。作っただけでは絞れていません。必ず最小権限に上書きしてください。また、ゲートウェイエンドポイントを共有サブネットで使う場合、公式は「そのサブネットにアクセスできる任意のAWSアカウントがDynamoDBを使える」(サブネット所有者が個別に制限しない限り)と注意しています。共有VPCではエンドポイントポリシーでの絞り込みが一層重要です。

6.4 インターフェイスエンドポイント(PrivateLink)が要るケース

オンプレミスや別リージョンからプライベートに到達したいときだけ、インターフェイスエンドポイントを使います。これはENIにプライベートIPを割り当て、Direct Connect/VPN/ピアリング経由のアクセスを可能にします。ただし課金対象で、専用のエンドポイント固有DNS名(例: vpce-1a2b3c4d-5e6f.dynamodb.us-east-1.vpce.amazonaws.com)をクライアントに設定する必要があります。

公式の重要な落とし穴: DynamoDBの標準DNS名を上書きするプライベートホストゾーンを作ってはいけません。DynamoDBのDNS構成は変わりうるため、上書きすると意図せずパブリックIP経由にフォールバックする恐れがあります。インターフェイスエンドポイントを使うなら、クライアントにVPCエンドポイントURLを直接指定します。VPC内アプリは無料のゲートウェイエンドポイント、オンプレはインターフェイスエンドポイント、という併用も公式が認める構成です。

判断基準: VPC内からのアクセスだけなら、無料のゲートウェイエンドポイントで十分。 オンプレ/クロスリージョンのプライベート接続が要るときだけ、課金されるインターフェイスエンドポイントを足す。「とりあえずPrivateLink」はYAGNIかつコスト増です。


7. 監査・属性ベース・最小権限のCI/CD

7.1 CloudTrailで「誰が何をしたか」を残す

アクセス制御は「防ぐ」だけでなく「記録する」ことで完成します。AWS CloudTrailはDynamoDBのコントロールプレーン操作(CreateTable/DeleteTable/UpdateTable 等)を記録し、データプレーン操作もデータイベントとして記録できます。CMKを使えば、KMSの復号呼び出しもCloudTrailに残るため、**「いつ・どの鍵で・誰が復号したか」**まで追えます。TransactWriteItems を含む書き込みの監査証跡は、決済のような領域では監査・インシデント調査の生命線です。

7.2 属性ベースアクセス制御(ABAC)

公式のセキュリティ章は属性ベースアクセス制御(ABAC)も柱に挙げています。タグ(例: Environment=production, Team=payments)を条件に使い、「同じタグを持つプリンシパルだけがそのテーブルを操作できる」といったタグ駆動の権限を組めます。テーブル数・ロール数が増えるマルチテナント/マルチサービス環境で、ポリシーの本数を爆発させずに最小権限を保つのに有効です。

7.3 CI/CDロールはOIDC+最小権限で

最後に、人間ではなくパイプラインの権限です。GitHub ActionsからTerraformを流すなら、長期のアクセスキーを発行せず、OIDCフェデレーションで短命の一時クレデンシャルを取得します。これにより「リポジトリに鍵を置かない」「漏洩しても短命」を実現します。デプロイロールにはコントロールプレーン(CreateTable/UpdateTableを、アプリのランタイムロールにはデータプレーンのみを渡し、両者を分離します。秘密情報をログやコードにハードコードしないのは大前提です。

私の運用では、CI(GitHub Actions、OIDC、型・テストの検証ゲート)を通してからインフラ変更を本番反映し、ランタイムロールには Scan もテーブル削除権限も持たせない構成にしています。速さ(一人 × 生成AIでの高速な実装)と安全(IAM・暗号化・ネットワークでの強制)を両立させるには、この「権限の分離」が土台になります。


8. 落とし穴チェックリスト

設計レビューで実際に効く、優先度順のチェックリストです。

#落とし穴なぜ危険対策
1Scan を許可しているLeadingKeys を無視し全テナント分が返るランタイムロールから Scan を外す
2テナントIDがPKでないLeadingKeys が効かず行分離が成立しないキー設計でテナントIDをPKに置く
3ProjectionExpression 省略Attributes 制限が効かず機微列が返るSelect=SPECIFIC_ATTRIBUTES を強制
4ReturnValues: ALL_OLD/ALL_NEW 許可書き込み時に制限外属性が返るNONE/UPDATED_* に制限
5エンドポイントポリシーが既定(全許可)経路側の関門が無防備特定テーブルARNに絞る
6全テーブル一律CMKKMS料金・レイテンシ・運用負荷が過剰機微度で選別、一般は既定キー
7平文HTTPを塞いでいないTLS強制が「既定」止まりaws:SecureTransport=false を deny
8CI/CDが長期アクセスキー漏洩時の被害が長期化OIDC+短命クレデンシャル
9dynamodb:* のランタイムロール削除・PITR無効化まで可能にアクション最小化+ロール分離

FAQ

Q1. DynamoDBの保存時暗号化は無効化できますか? できません。公式が明記するとおり、DynamoDBの全ユーザーデータ(主キー・インデックス・ストリーム・バックアップ・DAX含む)は常に保存時暗号化されます。選べるのは「暗号化するか」ではなく「どのKMSキー(AWS所有=既定無料/AWSマネージド aws/dynamodb/CMK)で暗号化するか」だけです。キー種別はいつでも切り替えられます。

Q2. マルチテナントで行レベル分離をIAMだけで強制できますか? できます。dynamodb:LeadingKeys 条件キーに ${www.amazon.com:user_id} 等のプリンシパル識別子を束ねると、「パーティションキーが自分のIDと一致する行しか触れない」をIAMが保証します。前提は2つ: (1) テナント/ユーザーIDがパーティションキーであること、(2) Scan許可しないこと(ScanLeadingKeys を無視して全件返すため)。全体の認可設計はマルチテナントSaaSのデータ分離・認可設計ガイドを参照してください。

Q3. 列レベルで属性を隠すにはどうしますか? dynamodb:Attributes で許可属性を列挙し、さらに dynamodb:SelectSPECIFIC_ATTRIBUTES に、dynamodb:ReturnValuesNONE/UPDATED_OLD/UPDATED_NEW に縛ります。Attributes だけだと、ProjectionExpression を省いたリクエストで全属性が返ってしまう(公式の注意点)ため、Select の強制まで含めて初めて漏洩を防げます。アイテム全体を置換する PutItem/DeleteItem は許可しないこと。

Q4. VPCエンドポイントは必要ですか? ゲートウェイとインターフェイスのどちら? VPC内(EC2/Lambda)からアクセスするだけなら、無料のゲートウェイエンドポイントでインターネットを経由させずに済み、これが基本です。オンプレや別リージョンからプライベート接続したいときだけ、課金される**インターフェイスエンドポイント(PrivateLink)**を足します。両者は併用可能で、いずれもトラフィックはAWS網内に留まります。

Q5. カスタマー管理キー(CMK)は使うべきですか? 要件次第です。キーポリシーで復号を制御したい/キーローテ周期を管理したい/キー無効化で即時アクセス不能化したい/クロスアカウント共有/監査でキー使用を追いたい——これらが必要ならCMK。不要なら**既定のAWS所有キー(無料)**で十分です。CMKはKMS料金とKMS依存が増え、鍵を誤って無効化するとテーブルが読めなくなる点に注意。機微度で選別し、全テーブル一律CMKは避けます。

Q6. 転送時の暗号化はどう強制しますか? DynamoDBへの通信は既定でHTTPS(TLS 1.2/1.3)で保護されます。平文を構造的に禁止するには、ポリシーに aws:SecureTransport: "false" を条件とする deny を追加し、TLSでないリクエストを全拒否します。なおPrivateLink(インターフェイスエンドポイント)はTLS 1.1を非サポートで、実質TLS 1.2以上が前提です。


おわりに:信頼境界をサーバー側に置く

DynamoDBのセキュリティは、5つの層をそれぞれ独立に効かせることで生まれます。

  • IAM最小権限dynamodb:* を捨て、アクションと特定テーブル/インデックスARNに絞る。Scan を与えない。
  • きめ細かなアクセス制御LeadingKeys で行を、AttributesSelectReturnValues で列を、IAMレベルで縛る。テナントIDはPKに置く。
  • 保存時暗号化 — 常時ONを前提に、監査・ローテ・クロスアカウントが要る機微データだけCMKを選ぶ。
  • 転送時暗号化 — TLS 1.2/1.3を前提に、aws:SecureTransport で平文を拒否する。
  • ネットワーク境界 — 無料のゲートウェイエンドポイントでインターネットを排除し、エンドポイントポリシーで第2の関門を作る。

共通する思想は一つ、**「クライアントを信じない。信頼境界をアプリのif文ではなく、IAM・暗号化・ネットワークというサーバー側の強制力に置く」**ことです。レビューや手順書で守る境界は破れますが、IAM条件キーとエンドポイントポリシーで守る境界は破れません。

私は、**一人 × 生成AI(Claude Code)**という体制で、設計判断を人間の検証ゲートに通しながら、本番二重課金0件のサーバーレス決済基盤を実装・運用してきました。DynamoDBを軸にしたマルチテナントのセキュリティ設計(FGACによる行/列分離・CMK暗号化・VPCエンドポイント・最小権限のCI/CD)について、設計レビューから実装まで伴走できます。

正しさの設計はシングルテーブル設計&本番信頼性パターンに、コスト・性能設計はキャパシティ・コスト・性能設計ガイドに、マルチテナント認可の全体像はマルチテナントSaaSのデータ分離・認可設計ガイドにまとめています。速く・安く・安全にDynamoDBを本番で稼がせる——その設計を、要件に合わせて一緒に詰めます。

DynamoDB/サーバーレスのセキュリティ設計でお困りの方は、お問い合わせからご相談ください。 まずは現状のIAMポリシーと信頼境界を棚卸しするところからご一緒します。

友田

友田 陽大

経済産業大臣賞 受賞プロダクト開発者。TypeScript + Python + AWS で、SaaS・業界DX・ 実用レベルの生成AI(RAG)を、要件定義からインフラ・運用まで一人で完遂します。

この記事で解説した技術の適用事例

環境分野のサーバーレス決済プラットフォーム(フルスタック開発・決済信頼性レイヤーを主導)

ケーススタディを見る