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

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

- 公開日: 2026-06-25
- 著者: 友田 陽大
- タグ: AWS, DynamoDB, セキュリティ, IAM, マルチテナント, Terraform, アーキテクチャ設計
- URL: https://tomodahinata.com/blog/dynamodb-security-iam-fine-grained-access-control-encryption-vpc-endpoint-guide

## 要点

- セキュリティはアプリのif文ではなく、IAM・暗号化・ネットワーク境界で強制する。クライアントを信じない
- dynamodb:LeadingKeysに${www.amazon.com:user_id}等の変数を束ねると、「自分のパーティションキーの行しか触れない」をIAMが強制する＝マルチテナント行レベル分離。ただしテナントIDがPKであることが前提で、Scanには効かない
- dynamodb:Attributesとdynamodb:Select/ReturnValuesを併用して列レベルで制限する。ProjectionExpression省略時は全属性が返るため、Selectの強制まで含めて初めて漏洩を防げる
- 保存時暗号化は常時ON・無効化不可。AWS所有キー（既定・無料）/AWSマネージドキー aws/dynamodb/カスタマー管理キー(CMK)の3択。監査・キーローテ・クロスアカウント制御が要るならCMK
- 転送はTLS 1.2/1.3必須。aws:SecureTransportで平文を拒否し、VPCゲートウェイエンドポイント（無料）でインターネットを経由させない

---

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

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

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

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

すべての仕様・条件キー名・暗号化方式・エンドポイント種別は、**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: "*"` も禁止

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

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

`dynamodb:*` は `DeleteTable`・`UpdateContinuousBackups`（PITR無効化）・`RestoreTableFromBackup`・`ExportTableToPointInTime` まで含みます。アプリのランタイムロールにテーブル削除権限が要ることはまずありません。**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` に明示する必要があります。公式が示す「テーブルと全インデックスを許す」最小権限ポリシーはこうです。

```json
{
  "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` は原則与えない**。後述のとおり `Scan` は `LeadingKeys` を無視して全アイテムを返すため、テナント分離の穴になりやすい。
- **コントロールプレーン**（`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を**パーティションキー**に置く設計が前提になります。

```json
{
  "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のデータ分離・認可設計ガイド](/blog/multi-tenant-saas-data-isolation-authorization-design-guide)にまとめています。

### 3.2 致命的な落とし穴：`Scan` は `LeadingKeys` を無視する

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

> 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に据える**必要があります（[シングルテーブル設計ガイド](/blog/dynamodb-single-table-design-reliability-idempotency-patterns)のキー設計が前提になる理由はここです）。

### 3.3 列レベル制限：`dynamodb:Attributes` ＋ `Select` ＋ `ReturnValues`

`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` で全属性が返るのを防ぎます。

公式の列レベル制限ポリシー（読み書き両対応・`UserId` と `TopScore` だけ許可）:

```json
{
  "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点とも重要です。

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

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

### 3.4 行＋列を同時に縛る

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

```json
{
  "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_table` の `server_side_encryption` ブロックで、CMKを指定します。`kms_key_arn` を省略すると AWSマネージドキー（`aws/dynamodb`）が、ブロック自体を省略すると AWS所有キー（既定・無料）が使われます。

```hcl
# 監査・キーローテ・最小権限を鍵レベルで効かせるための 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:SecureTransport` で**TLSでないリクエストを明示的に拒否**します。リソースベースポリシー（テーブルのリソースポリシー）やIAMポリシーに、次のような deny を足します。

```json
{
  "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） |
| 使うIP | DynamoDBのパブリック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（ゲートウェイエンドポイント＋ルートテーブル紐付け＋エンドポイントポリシー）:

```hcl
# ゲートウェイ型: ルートテーブルに紐付ける（=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 以外触れない」と縛れる**。逆も然り。公式の例は、特定テーブルだけを許可するこの形です。

```json
{
  "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. 落とし穴チェックリスト

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

| # | 落とし穴 | なぜ危険 | 対策 |
| --- | --- | --- | --- |
| 1 | `Scan` を許可している | `LeadingKeys` を無視し全テナント分が返る | ランタイムロールから `Scan` を外す |
| 2 | テナントIDがPKでない | `LeadingKeys` が効かず行分離が成立しない | キー設計でテナントIDをPKに置く |
| 3 | `ProjectionExpression` 省略 | `Attributes` 制限が効かず機微列が返る | `Select=SPECIFIC_ATTRIBUTES` を強制 |
| 4 | `ReturnValues: ALL_OLD/ALL_NEW` 許可 | 書き込み時に制限外属性が返る | `NONE`/`UPDATED_*` に制限 |
| 5 | エンドポイントポリシーが既定（全許可） | 経路側の関門が無防備 | 特定テーブルARNに絞る |
| 6 | 全テーブル一律CMK | KMS料金・レイテンシ・運用負荷が過剰 | 機微度で選別、一般は既定キー |
| 7 | 平文HTTPを塞いでいない | TLS強制が「既定」止まり | `aws:SecureTransport=false` を deny |
| 8 | CI/CDが長期アクセスキー | 漏洩時の被害が長期化 | OIDC＋短命クレデンシャル |
| 9 | `dynamodb:*` のランタイムロール | 削除・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` を**許可しない**こと（`Scan` は `LeadingKeys` を無視して全件返すため）。全体の認可設計は[マルチテナントSaaSのデータ分離・認可設計ガイド](/blog/multi-tenant-saas-data-isolation-authorization-design-guide)を参照してください。

**Q3. 列レベルで属性を隠すにはどうしますか？**
`dynamodb:Attributes` で許可属性を列挙し、**さらに** `dynamodb:Select` を `SPECIFIC_ATTRIBUTES` に、`dynamodb:ReturnValues` を `NONE`/`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` で行を、`Attributes`＋`Select`＋`ReturnValues` で列を、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）について、設計レビューから実装まで伴走できます。

正しさの設計は[シングルテーブル設計＆本番信頼性パターン](/blog/dynamodb-single-table-design-reliability-idempotency-patterns)に、コスト・性能設計は[キャパシティ・コスト・性能設計ガイド](/blog/dynamodb-capacity-cost-performance-on-demand-vs-provisioned-guide)に、マルチテナント認可の全体像は[マルチテナントSaaSのデータ分離・認可設計ガイド](/blog/multi-tenant-saas-data-isolation-authorization-design-guide)にまとめています。**速く・安く・安全に**DynamoDBを本番で稼がせる——その設計を、要件に合わせて一緒に詰めます。

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