Skip to main content
友田 陽大
DynamoDB
AWS
DynamoDB
セキュリティ
IAM
マルチテナント
Terraform
アーキテクチャ設計

DynamoDB Security Complete Guide (2026 Edition): IAM Least Privilege, Fine-Grained Access Control (LeadingKeys), Encryption at Rest/in Transit, VPC Endpoints

An explanation of DynamoDB security faithful to the AWS official spec. From row-level multi-tenant isolation with dynamodb:LeadingKeys, column restriction with dynamodb:Attributes, least-privilege IAM ARNs per table/index/stream, always-on encryption at rest and 3 kinds of KMS keys, mandatory TLS and aws:SecureTransport, to VPC gateway/interface endpoints — summarized from a production viewpoint with IAM-policy JSON and real Terraform code.

Published
Reading time
25 min read
Author
友田 陽大
Share

The first notion to discard in DynamoDB security is "write access control in the app's code."

if (item.tenantId !== currentUser.tenantId) throw Forbidden — this one line will surely be broken someday by a forgotten check, a condition mistake, or the addition of a new endpoint. Most of the accidents where another tenant's data becomes visible in a multi-tenant SaaS are caused by "assuming the DB is all-visible and relying on the app-layer check."

The correct design is the reverse. Enforce access control not with the app's if statements but with the "unbreakable layers" of IAM, encryption, and the network boundary. DynamoDB officially has the primitives to realize this. Among them, the dynamodb:LeadingKeys condition key guarantees, at the IAM level, that "that principal can only touch rows of its own partition key." No matter what the app writes, if IAM doesn't permit it, not one byte reaches another tenant's row.

This article systematizes only DynamoDB's security design, based on my experience designing and leading the reliability layer of a serverless (Lambda + DynamoDB) multi-tenant payment platform and maintaining 0 double charges in production. Data-modeling and idempotency design I leave to single-table design & production reliability patterns, and capacity/cost design to the capacity/cost/performance design guide; here I narrow to "who, to what, from where, and how encrypted, can touch it."

All specs, condition-key names, encryption methods, and endpoint types are checked against the AWS official documentation (as of June 2026).

Note: the code is a key excerpt to convey the design intent. Account IDs, table names, and Regions are abstracted / exemplified.


1. The Shared Responsibility Model and the Whole Picture of Defense in Depth

The official docs organize DynamoDB security with the 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.

Put plainly, "the security of the cloud itself (physical, hypervisor, the managed-service foundation) is AWS's responsibility, and what you configure and how on top of it (IN the cloud) is your responsibility." Because DynamoDB is a managed service, as the official docs state, it is protected by AWS's global network security procedures.

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

That's exactly why what you should design is the "IN the cloud" part. The official security chapter is roughly composed of the following pillars. This article rearranges them from an implementation viewpoint.

Defense layer (responsibility: you)What it protectsMain weapons
Data protection (at rest)Data on diskEncryption at rest (KMS, always on) → Ch. 4
Data protection (in transit)Data on the networkMandatory TLS, aws:SecureTransport → Ch. 5
Access management (API)Which actions to allowIAM least privilege, resource ARN → Ch. 2
Access management (data)Which rows / columns to allowFine-grained access control (FGAC) → Ch. 3
Infrastructure / networkFrom where it's reachableVPC endpoint, endpoint policy → Ch. 6
Audit / detectionWho did whatCloudTrail, least-privilege CI/CD → Ch. 7

The crux of defense in depth is that even if one layer is broken, the next layer stops it. Even with a bug in the IAM policy, the VPC endpoint policy stops it. Even with a vulnerability in the app, LeadingKeys doesn't let it cross the tenant boundary. The design is to stack them so each layer works independently.


2. IAM Least Privilege: Narrow Actions and Resource ARNs

Before fine-grained access control, solidify the foundational IAM policy's least privilege. If this is loose, the FGAC on top of it loses meaning too.

dynamodb:* Is Forbidden, Resource: "*" Too

The worst anti-pattern is this.

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

dynamodb:* includes DeleteTable, UpdateContinuousBackups (disabling PITR), RestoreTableFromBackup, and even ExportTableToPointInTime. An app's runtime role almost never needs table-deletion privilege. Per the principle of least privilege, narrow it to just "the actions actually called" and "the tables actually touched."

Resource-Level ARNs: Table, Index, and Stream Separately

The official least-privilege policy enumerates actions and fixes the resource to a specific table's ARN. DynamoDB's resource ARNs have the following forms (from the official examples).

ResourceARN format
The table bodyarn:aws:dynamodb:<region>:<account-id>:table/<table-name>
An index (GSI/LSI)arn:aws:dynamodb:<region>:<account-id>:table/<table-name>/index/<index-name>
A streamarn:aws:dynamodb:<region>:<account-id>:table/<table-name>/stream/<timestamp>

What matters is that an index is a different ARN from the table body. To allow Query against a GSI, you must explicitly state .../index/<name> (or .../index/*) in Resource. The official "allow the table and all indexes" least-privilege policy is this.

{
  "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/*"
      ]
    }
  ]
}

The design boundaries:

  • Give a runtime role (Lambda, etc.) only data-plane actions (GetItem/Query/PutItem…), narrowed to a specific table + the needed indexes.
  • Don't give Scan as a rule. As described later, Scan ignores LeadingKeys and returns all items, so it easily becomes a hole in tenant isolation.
  • Give the control plane (CreateTable/DeleteTable/UpdateTable) only to the deploy role that runs IaC (Terraform), and remove it from the app's runtime role.
  • dynamodb:DescribeStream / GetRecords / GetShardIterator are needed only by a Stream consumer. Make it a separate role narrowed to the stream ARN.

SRP (single responsibility) works for IAM roles too. Don't mix "app execution," "deploy," "Stream consumer," and "backup operations" into one do-everything role. The more you separate roles, the smaller the blast radius on a leak.


3. Fine-Grained Access Control (FGAC): Bind Rows and Columns with IAM

Here is the core of this article, and where a multi-tenant SaaS gets the most value. The official docs clearly state that you can control not only access to DynamoDB API actions but also access to individual data items (rows) and attributes (columns) with IAM's Condition element.

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

FGAC has 2 granularities.

  1. Item level (row) — restrict to just items with a specific key value. The typical case is a tenant/user boundary.
  2. Attribute level (column) — restrict the subset of visible/writable attributes. Hiding sensitive attributes (personal info, internal flags).

3.1 Row-Level Isolation: dynamodb:LeadingKeys (the protagonist of this article)

The official dynamodb:LeadingKeys condition key represents the table's first key attribute = the partition key.

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.

Bind the principal's identifier to this, and "you can only touch rows where the partition-key value matches your own ID" holds. The IAM substitution variables the official docs prepare are the following 3 (Web Identity Federation).

IdPSubstitution variable
Login with Amazon${www.amazon.com:user_id}
Facebook${graph.facebook.com:id}
Google${accounts.google.com:sub}

The minimal policy for row-level multi-tenant isolation is this. It presupposes a design that places the tenant (or user) ID in the partition key.

{
  "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}"]
        }
      }
    }
  ]
}

When this policy is effective, even if you specify another tenant's PK in Query, IAM rejects it. Even if the app forgot to write the tenantId check, even if you try to write to another tenant's PK with PutItem, it doesn't reach. This is the concrete form of "enforce with IAM, not the app's if statements." In my payment foundation too, I don't make the tenant boundary depend only on app validation; I double-bind it with such IAM condition keys.

When using IAM Identity Center / Cognito: the above substitution variables are for Login with Amazon/Facebook/Google. In a configuration using temporary credentials from a Cognito Identity Pool, design it to resolve the tenant ID from a session tag or an ID-token claim and correspond it to the PK. The detailed whole design of multi-tenant authorization (including a comparison with PostgreSQL RLS) is compiled in the multi-tenant SaaS data-isolation / authorization design guide.

3.2 The Fatal Pitfall: Scan Ignores LeadingKeys

The official docs explicitly warn.

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

That is, even if you meant to lock down hard with LeadingKeys, if you allow Scan, all tenants' data is returned. This is not a bug but the spec. So, as stated in Chapter 2, never give Scan to a multi-tenant runtime role. This is the first iron rule of FGAC.

And one more. LeadingKeys works only on the partition key. In a design where the tenant ID is not the PK but in the sort key or a general attribute, row-level isolation doesn't hold. If you use FGAC, you need to place the tenant ID in the PK at the key-design stage (this is why the key design of the single-table design guide becomes the premise).

3.3 Column-Level Restriction: dynamodb:Attributes + Select + ReturnValues

The dynamodb:Attributes condition key represents the list of top-level attributes the request accesses. With an allowlist scheme, it enforces "you can only read/write these attributes."

But here is a serious caution that leads to information leakage if missed. Per the official docs, the attribute condition is evaluated only on attributes specified in the request, not on attributes in the response.

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.

That is, GetItem without a ProjectionExpression returns even the sensitive attributes you meant to restrict with dynamodb:Attributes, all of them. To plug this, as the official docs show, enforce "specific-attribute specification (SPECIFIC_ATTRIBUTES)" with dynamodb:Select, and further, for write operations, restrict dynamodb:ReturnValues to prevent ALL_OLD/ALL_NEW from returning all attributes.

The official column-level-restriction policy (both read and write, allowing only UserId and TopScore):

{
  "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"]
        }
      }
    }
  ]
}

The official notes are important on all 3 points.

  • Bind Select to SPECIFIC_ATTRIBUTES with StringEqualsIfExists and the app must explicitly specify the attributes, and "give me all attributes" is rejected.
  • Binding ReturnValues to NONE/UPDATED_OLD/UPDATED_NEW is because UpdateItem does an implicit read. Allow ALL_OLD/ALL_NEW and out-of-restriction attributes are returned.
  • Don't allow PutItem/DeleteItem/BatchWriteItem. Because these replace the whole item, they can delete or overwrite attributes you shouldn't be able to access. If you want to enforce "update only specific attributes," allow only UpdateItem.

A mandatory item when using dynamodb:Attributes (official): when using dynamodb:Attributes, always include the primary-key attributes and index-key attributes of the table and all indexes (that appear in the policy) in the allowlist. Otherwise DynamoDB can't use the key attributes and the operation itself fails.

3.4 Binding Rows and Columns Simultaneously

LeadingKeys and Attributes can coexist. The official Example 6 binds to "only rows with a Facebook ID's PK" and "only attribute-A/attribute-B." It's a form that realizes multi-tenant + sensitive-column protection in one policy.

{
  "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"]
        }
      }
    }
  ]
}

Allow scheme over deny scheme: the official docs say "deny-based policies are hard to write correctly and can be invalidated by future API changes, so avoid them in DynamoDB." The principle is to enumerate only what's needed with an allow-list.


4. Encryption at Rest: Always On, Can't Be Disabled, How to Choose Among 3 KMS Keys

4.1 First, the Big Premise: Encryption Is "Always" On

The most important fact about DynamoDB's encryption at rest is this. All data is always encrypted and can't be disabled.

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

And the protection scope is wide; per the official docs, all data landing on persistent media — primary keys, local/global secondary indexes, streams, global tables, backups, and DAX clusters — is encrypted. That is, the question "whether to enable encryption" doesn't exist; the question is only "with which KMS key to encrypt."

4.2 Comparing the 3 KMS Keys

The official docs define 3 kinds of KMS keys you can choose at new-table creation (and switch anytime).

TypeOwnership / managementDefaultPricingKey-policy controlCross-accountKey rotation
AWS owned keyOwned by DynamoDB◎ defaultNo additional chargeNot possible (invisible)NoAWS-managed
AWS managed key (aws/dynamodb)In your account, managed by AWS KMSAWS KMS charges applyLimitedNoAWS-managed
Customer managed key (CMK)You create, own, and fully manageAWS KMS charges applyYou fully controlYesSelf-configurable

Quoting the official points:

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 Which to Choose (the Decision)

  • AWS owned key (default, free) — if you have only the requirement of encryption "itself" and need no key visibility, audit, or rotation control, this suffices. A reasonable starting point for many general workloads.
  • AWS managed key aws/dynamodb — a middle ground when you want the point that key usage is recorded and visible in CloudTrail, but don't need fine key-policy control.
  • Customer managed key (CMK) — if any of the following is needed, CMK is the only choice:
    • You want to control "who can decrypt with this key" with a key policy (apply least privilege at the encryption-key level)
    • You want to manage the key-rotation cycle yourself, or want the reins to disable the key and make data instantly inaccessible (crypto shredding)
    • A cross-account key-sharing configuration
    • Regulatory / compliance (track key usage in audit logs, place the key-management entity with the customer)

Face the trade-off too. CMK adds KMS charges (the key's monthly fee + API-request billing) and increases the KMS dependency. Mistakenly disable/delete the key and that table becomes unreadable (this is the spec, the flip side of crypto shredding). In my foundation handling sensitive data like payments, I chose CMK from audit and cross-account requirements, but uniform CMK on all tables can be excessive. Selecting by data sensitivity is also correct from a cost-efficiency viewpoint.

4.4 Terraform: A Table Encrypted with a CMK

In aws_dynamodb_table's server_side_encryption block, specify the CMK. Omit kms_key_arn and the AWS managed key (aws/dynamodb) is used; omit the block itself and the AWS owned key (default, free) is used.

# 監査・キーローテ・最小権限を鍵レベルで効かせるための 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. Encryption in Transit: Mandatory TLS and aws:SecureTransport

5.1 Communication to DynamoDB Is Protected by TLS

The official docs clearly state that network access is protected by 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.

Furthermore, the client must support cipher suites with forward secrecy (PFS) (ECDHE/DHE), and requests must be signed with Signature Version 4. Even when not using a VPC endpoint, per the official docs, "communication with DynamoDB is HTTPS by default."

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

As a caution, the later AWS PrivateLink (interface endpoint) doesn't support TLS 1.1 (an official constraint). In effect, that means TLS 1.2 or above is the premise.

5.2 Reject Plaintext: aws:SecureTransport

"TLS by default" doesn't mean plaintext HTTP is prohibited (in theory, you want to plug the room for a non-TLS connection mixing in even when signed). So, with AWS's common condition key aws:SecureTransport, explicitly reject non-TLS requests. Add a deny like the following to a resource-based policy (the table's resource policy) or an IAM policy.

{
  "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" }
      }
    }
  ]
}

This is an explicit deny of "if it's not TLS (SecureTransport == false), reject all actions for anyone." Because in IAM deny takes precedence over allow, you can enforce transit encryption as an organizational policy. I wrote in Chapter 3 "avoid deny," but that was about the FGAC allow list. A safe-side guardrail deny like aws:SecureTransport == false is the standard, and the two don't contradict.


6. The Network Boundary: Don't Route Over the Internet with a VPC Endpoint

Once you bind "who" with IAM, next bind "from where." Because DynamoDB has a public endpoint (dynamodb.<region>.amazonaws.com), if you do nothing, traffic goes out via the internet gateway. What confines it within the AWS network is a VPC endpoint.

6.1 DynamoDB's VPC Endpoints Come in 2 Kinds

Here accuracy is vital, so let me use the official definitions as-is. DynamoDB has a gateway endpoint and an interface endpoint (using AWS PrivateLink).

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.

Gateway endpointInterface endpoint (PrivateLink)
Connection methodA gateway specified in the route tableAn ENI inside the subnet (private IP)
The IP usedDynamoDB's public IP (but the path is within the AWS network)The VPC's private IP
TrafficStays within the AWS networkStays within the AWS network
Access from on-premNot possiblePossible (via Direct Connect / VPN)
Access from another RegionNot possiblePossible (via VPC peering / Transit Gateway)
PricingNot charged (free)Charged

Per the official comparison table, both keep traffic within the AWS network ("In both cases, your network traffic remains on the AWS network."). The difference is "from where it's reachable" and "pricing."

6.2 First, a Gateway Endpoint (Free) Is Basic

If you just access from an EC2/Lambda within the VPC, a gateway endpoint suffices, and it's free. The official explanation:

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.

That is, place a gateway endpoint and the internet gateway and NAT become unneeded, and DynamoDB-bound traffic doesn't go over the public internet. Because the endpoint name (dynamodb.<region>.amazonaws.com) doesn't change, no app modification is needed either.

Terraform (a gateway endpoint + route-table association + endpoint policy):

# ゲートウェイ型: ルートテーブルに紐付ける(=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 The Endpoint Policy: The Second Gate

The endpoint policy, per the official docs, specifies which principal, which actions, on which resources can be executed via that endpoint.

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.

Here is the complementary relationship with FGAC, the crux of defense in depth. Even if the IAM role's policy (the principal side) is loose, you can bind it on the endpoint policy (the path side) with "from this VPC you can only touch SaaSTable." The reverse too. The official example is this form of allowing only a specific table.

{
  "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/*"
      ]
    }
  ]
}

Beware the endpoint-policy hole: the default endpoint policy is Action: "*", Resource: "*", Principal: "*" (allow all). Just creating it doesn't narrow anything. Always override it with least privilege. Also, when using a gateway endpoint in a shared subnet, the official docs caution that "any AWS account that can access that subnet can use DynamoDB" (unless the subnet owner restricts it individually). In a shared VPC, narrowing with the endpoint policy is all the more important.

Only when you want private reach from on-premises or another Region do you use an interface endpoint. It allocates a private IP to an ENI, enabling access via Direct Connect/VPN/peering. But it's a billing target, and you need to set a dedicated endpoint-specific DNS name (e.g. vpce-1a2b3c4d-5e6f.dynamodb.us-east-1.vpce.amazonaws.com) on the client.

An important official pitfall: don't create a private hosted zone that overrides DynamoDB's standard DNS name. Because DynamoDB's DNS configuration can change, an override risks unintentionally falling back to going via the public IP. If you use an interface endpoint, specify the VPC endpoint URL directly on the client. A joint use of a free gateway endpoint for in-VPC apps and an interface endpoint for on-prem is also a configuration the official docs allow.

The criterion: if access is only from within the VPC, the free gateway endpoint suffices. Only when private connection from on-prem/cross-Region is needed, add the charged interface endpoint. "PrivateLink just in case" is YAGNI and a cost increase.


7. Audit, Attribute-Based, and Least-Privilege CI/CD

7.1 Leave "Who Did What" with CloudTrail

Access control is completed not only by "preventing" but by "recording." AWS CloudTrail records DynamoDB control-plane operations (CreateTable/DeleteTable/UpdateTable, etc.) and can also record data-plane operations as data events. Use a CMK and KMS decrypt calls are also left in CloudTrail, so you can trace down to "when, with which key, who decrypted." The audit trail of writes including TransactWriteItems is the lifeline of audit and incident investigation in a domain like payments.

7.2 Attribute-Based Access Control (ABAC)

The official security chapter also lists attribute-based access control (ABAC) as a pillar. Use tags (e.g. Environment=production, Team=payments) as conditions to assemble tag-driven permissions like "only principals with the same tag can operate that table." It's effective for keeping least privilege without exploding the number of policies in a multi-tenant/multi-service environment where the number of tables and roles grows.

7.3 CI/CD Roles with OIDC + Least Privilege

Finally, the permission of the pipeline, not a human. To run Terraform from GitHub Actions, don't issue a long-lived access key; obtain short-lived temporary credentials with OIDC federation. This realizes "don't put keys in the repository" and "even if leaked, short-lived." Give the deploy role the control plane (CreateTable/UpdateTable) and the app's runtime role only the data plane, separating the two. Not hardcoding secrets into logs or code is the big premise.

In my operation, I reflect infrastructure changes to production after passing CI (GitHub Actions, OIDC, type/test verification gates), and configure the runtime role to hold neither Scan nor table-deletion privilege. To achieve both speed (fast implementation with one person × generative AI) and safety (enforcement with IAM, encryption, and the network), this "separation of permissions" is the foundation.


8. Pitfall Checklist

A priority-ordered checklist that actually works in a design review.

#PitfallWhy dangerousCountermeasure
1Allowing ScanIgnores LeadingKeys and returns all tenants' worthRemove Scan from the runtime role
2Tenant ID is not the PKLeadingKeys doesn't work and row isolation doesn't holdPlace the tenant ID in the PK in the key design
3Omitting ProjectionExpressionAttributes restriction doesn't work and sensitive columns are returnedEnforce Select=SPECIFIC_ATTRIBUTES
4Allowing ReturnValues: ALL_OLD/ALL_NEWOut-of-restriction attributes are returned on writeRestrict to NONE/UPDATED_*
5The endpoint policy is default (allow all)The path-side gate is defenselessNarrow to a specific table ARN
6Uniform CMK on all tablesKMS fees / latency / operational load are excessiveSelect by sensitivity, general use the default key
7Not plugging plaintext HTTPTLS enforcement stops at "default"Deny aws:SecureTransport=false
8CI/CD with a long-lived access keyThe damage on a leak is prolongedOIDC + short-lived credentials
9A dynamodb:* runtime roleEven deletion / PITR-disabling is possibleMinimize actions + separate roles

FAQ

Q1. Can DynamoDB encryption at rest be disabled? No. As the official docs clearly state, all DynamoDB user data (including primary keys, indexes, streams, backups, and DAX) is always encrypted at rest. What you can choose is not "whether to encrypt" but "with which KMS key (AWS owned = default free / AWS managed aws/dynamodb / CMK)." The key type can be switched anytime.

Q2. Can I enforce row-level isolation in multi-tenant with IAM alone? You can. Bind a principal identifier like ${www.amazon.com:user_id} to the dynamodb:LeadingKeys condition key and IAM guarantees "you can only touch rows where the partition key matches your own ID." There are 2 premises: (1) the tenant/user ID is the partition key, (2) you don't allow Scan (because Scan ignores LeadingKeys and returns everything). For the whole authorization design, see the multi-tenant SaaS data-isolation / authorization design guide.

Q3. How do I hide attributes at the column level? Enumerate the allowed attributes with dynamodb:Attributes, and further bind dynamodb:Select to SPECIFIC_ATTRIBUTES and dynamodb:ReturnValues to NONE/UPDATED_OLD/UPDATED_NEW. With Attributes alone, a request that omits ProjectionExpression returns all attributes (the official caution), so you only prevent leakage once you include enforcing Select. Don't allow PutItem/DeleteItem, which replace the whole item.

Q4. Is a VPC endpoint needed? Gateway or interface? If you just access from within the VPC (EC2/Lambda), the free gateway endpoint lets you avoid going over the internet, and this is basic. Only when you want a private connection from on-prem or another Region do you add the charged interface endpoint (PrivateLink). The two can be used jointly, and in both the traffic stays within the AWS network.

Q5. Should I use a customer managed key (CMK)? It depends on the requirement. Want to control decryption with a key policy / manage the key-rotation cycle / make data instantly inaccessible by disabling the key / cross-account sharing / track key usage in audit — if these are needed, CMK. If not, the default AWS owned key (free) suffices. CMK increases KMS fees and KMS dependency, and beware that mistakenly disabling the key makes the table unreadable. Select by sensitivity and avoid uniform CMK on all tables.

Q6. How do I enforce encryption in transit? Communication to DynamoDB is protected by HTTPS (TLS 1.2/1.3) by default. To structurally prohibit plaintext, add a deny with aws:SecureTransport: "false" as a condition to your policy, rejecting all non-TLS requests. Note PrivateLink (interface endpoint) doesn't support TLS 1.1, and in effect TLS 1.2 or above is the premise.


In Closing: Place the Trust Boundary on the Server Side

DynamoDB security is born from making 5 layers each work independently.

  • IAM least privilege — discard dynamodb:* and narrow to actions and specific table/index ARNs. Don't give Scan.
  • Fine-grained access control — bind rows with LeadingKeys and columns with Attributes+Select+ReturnValues, at the IAM level. Place the tenant ID in the PK.
  • Encryption at rest — on the premise of always-on, choose CMK only for sensitive data that needs audit, rotation, or cross-account.
  • Encryption in transit — on the premise of TLS 1.2/1.3, reject plaintext with aws:SecureTransport.
  • The network boundary — exclude the internet with the free gateway endpoint, and make a second gate with the endpoint policy.

The common philosophy is one — "don't trust the client. Place the trust boundary not on the app's if statements but on the server-side enforcement of IAM, encryption, and the network." A boundary protected by reviews and runbooks can be broken; a boundary protected by IAM condition keys and endpoint policies can't.

I have implemented and operated a serverless payment foundation with 0 double charges in production, in a one person × generative AI (Claude Code) setup, passing design judgments through human verification gates. On multi-tenant security design centered on DynamoDB (row/column isolation by FGAC, CMK encryption, VPC endpoints, least-privilege CI/CD), I can accompany you from design review to implementation.

The design of correctness is compiled in single-table design & production reliability patterns, cost/performance design in the capacity/cost/performance design guide, and the whole picture of multi-tenant authorization in the multi-tenant SaaS data-isolation / authorization design guide. Making DynamoDB earn in production fast, cheap, and safe — I'll work out that design with you, matched to your requirements.

If you're troubled by DynamoDB / serverless security design, consult me via contact. I'll start by inventorying your current IAM policy and trust boundary together.

友田

友田 陽大

Developer of a METI Minister's Award–winning product. With TypeScript + Python + AWS, I deliver SaaS, industry DX, and production-grade generative AI (RAG) end to end — from requirements to infrastructure and operations — single-handedly.

Got a challenge?

From design to implementation and operations — solo × generative AI

Implementation like this article's, end to end from requirements to production. Start with a free 30-minute technical consult and tell me about your situation.

Available for both project-based (contract) and advisory engagements. Start with a free 30-minute consult.

Also worth reading