Skip to main content

DynamoDB Tables Should Not Be Accessible from Other AWS Accounts

Overview

This check verifies that your DynamoDB tables do not have resource-based policies allowing access from external AWS accounts. DynamoDB tables should be restricted to principals within your own account unless there is a specific, documented business need for cross-account access.

Risk

When a DynamoDB table allows cross-account access, several security risks arise:

  • Data exposure: External accounts can read sensitive data stored in your table
  • Data tampering: Unauthorized writes or deletions from external principals
  • Availability impact: External requests can exhaust your table's capacity, causing throttling
  • Cost implications: You pay for all requests, including those from external accounts

Public principal access (allowing anyone) is especially dangerous and should be avoided entirely.

Remediation Steps

Prerequisites

You need permission to modify DynamoDB resource policies. Specifically, you need dynamodb:DeleteResourcePolicy or dynamodb:PutResourcePolicy permissions.

Check your permissions

To verify you have the necessary permissions, you can attempt to view the current policy:

aws dynamodb get-resource-policy \
--resource-arn arn:aws:dynamodb:us-east-1:<your-account-id>:table/<your-table-name> \
--region us-east-1

If you receive an access denied error, contact your AWS administrator to request the required permissions.

AWS Console Method

  1. Sign in to the AWS Management Console and open the DynamoDB service
  2. In the left navigation pane, click Tables
  3. Select the table that failed the Prowler check
  4. Click the Additional settings tab
  5. Scroll down to the Resource-based policy section
  6. Review the current policy to understand what cross-account access exists
  7. Click Delete policy to remove all cross-account access
  8. Click Delete to confirm

If you need to keep some access but restrict it to your account only, click Edit instead and modify the policy to use only your account ID in the Principal field.

AWS CLI (optional)

Option 1: Remove the resource policy entirely

This is the simplest approach if you don't need any resource-based policy:

aws dynamodb delete-resource-policy \
--resource-arn arn:aws:dynamodb:us-east-1:<your-account-id>:table/<your-table-name> \
--region us-east-1

Option 2: Replace with a same-account-only policy

If you need a resource policy but want to restrict it to your account:

aws dynamodb put-resource-policy \
--resource-arn arn:aws:dynamodb:us-east-1:<your-account-id>:table/<your-table-name> \
--policy '{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "RestrictToSameAccount",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::<your-account-id>:root"
},
"Action": [
"dynamodb:GetItem",
"dynamodb:PutItem",
"dynamodb:Query",
"dynamodb:Scan",
"dynamodb:UpdateItem",
"dynamodb:DeleteItem",
"dynamodb:BatchGetItem",
"dynamodb:BatchWriteItem"
],
"Resource": "arn:aws:dynamodb:us-east-1:<your-account-id>:table/<your-table-name>"
}
]
}' \
--region us-east-1

Replace <your-account-id> and <your-table-name> with your actual values.

Option 3: Restrict using AWS Organizations

If you must allow some cross-account access, restrict it to accounts within your organization:

aws dynamodb put-resource-policy \
--resource-arn arn:aws:dynamodb:us-east-1:<your-account-id>:table/<your-table-name> \
--policy '{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "RestrictToOrganization",
"Effect": "Allow",
"Principal": "*",
"Action": [
"dynamodb:GetItem",
"dynamodb:Query",
"dynamodb:Scan"
],
"Resource": "arn:aws:dynamodb:us-east-1:<your-account-id>:table/<your-table-name>",
"Condition": {
"StringEquals": {
"aws:PrincipalOrgID": "<your-org-id>"
}
}
}
]
}' \
--region us-east-1

Replace <your-org-id> with your AWS Organizations ID (e.g., o-xxxxxxxxxx).

CloudFormation (optional)

Use this template to create a DynamoDB table with a same-account-only resource policy:

AWSTemplateFormatVersion: '2010-09-09'
Description: DynamoDB table with same-account only resource policy

Parameters:
TableName:
Type: String
Description: Name of the DynamoDB table
Default: my-secure-table

Resources:
SecureDynamoDBTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: !Ref TableName
BillingMode: PAY_PER_REQUEST
AttributeDefinitions:
- AttributeName: pk
AttributeType: S
KeySchema:
- AttributeName: pk
KeyType: HASH
ResourcePolicy:
PolicyDocument:
Version: '2012-10-17'
Statement:
- Sid: RestrictToSameAccount
Effect: Allow
Principal:
AWS: !Sub 'arn:aws:iam::${AWS::AccountId}:root'
Action:
- dynamodb:GetItem
- dynamodb:PutItem
- dynamodb:Query
- dynamodb:Scan
- dynamodb:UpdateItem
- dynamodb:DeleteItem
- dynamodb:BatchGetItem
- dynamodb:BatchWriteItem
Resource: !Sub 'arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${TableName}'

Outputs:
TableArn:
Description: ARN of the DynamoDB table
Value: !GetAtt SecureDynamoDBTable.Arn

Deploy the stack:

aws cloudformation deploy \
--template-file template.yaml \
--stack-name secure-dynamodb-table \
--parameter-overrides TableName=my-secure-table \
--region us-east-1
Terraform (optional)

Use this configuration to manage a DynamoDB table with a same-account-only resource policy:

terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}

provider "aws" {
region = "us-east-1"
}

data "aws_caller_identity" "current" {}
data "aws_region" "current" {}

variable "table_name" {
description = "Name of the DynamoDB table"
type = string
default = "my-secure-table"
}

# DynamoDB table
resource "aws_dynamodb_table" "secure_table" {
name = var.table_name
billing_mode = "PAY_PER_REQUEST"
hash_key = "pk"

attribute {
name = "pk"
type = "S"
}
}

# Resource policy restricting access to same account only
resource "aws_dynamodb_resource_policy" "same_account_only" {
resource_arn = aws_dynamodb_table.secure_table.arn
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "RestrictToSameAccount"
Effect = "Allow"
Principal = {
AWS = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:root"
}
Action = [
"dynamodb:GetItem",
"dynamodb:PutItem",
"dynamodb:Query",
"dynamodb:Scan",
"dynamodb:UpdateItem",
"dynamodb:DeleteItem",
"dynamodb:BatchGetItem",
"dynamodb:BatchWriteItem"
]
Resource = aws_dynamodb_table.secure_table.arn
}
]
})
}

output "table_arn" {
description = "ARN of the DynamoDB table"
value = aws_dynamodb_table.secure_table.arn
}

Apply the configuration:

terraform init
terraform plan
terraform apply

To remove an existing cross-account policy from a table managed elsewhere, you can import and modify:

terraform import aws_dynamodb_resource_policy.same_account_only arn:aws:dynamodb:us-east-1:<account-id>:table/<table-name>

Verification

After making changes, verify the remediation was successful:

  1. In the AWS Console, navigate to DynamoDB > Tables > Your Table > Additional settings
  2. Check the Resource-based policy section - it should either show no policy or a policy with only your account ID
Verify using AWS CLI

Check if a policy exists:

aws dynamodb get-resource-policy \
--resource-arn arn:aws:dynamodb:us-east-1:<your-account-id>:table/<your-table-name> \
--region us-east-1

If you deleted the policy, you should receive a PolicyNotFoundException. If you replaced it, verify the Principal only contains your account ID.

Re-run the Prowler check to confirm:

prowler aws --checks dynamodb_table_cross_account_access

Additional Resources

Notes

  • Eventual consistency: Policy changes in DynamoDB are eventually consistent. After deleting or updating a policy, wait a few seconds before verifying, as the old policy may still be returned briefly.
  • Table indexes: Index permissions are defined in the table's policy. If you remove or modify the table policy, index permissions are affected as well.
  • Legitimate cross-account needs: If you have a valid business requirement for cross-account access, consider using conditions like aws:PrincipalOrgID (restrict to your organization) or aws:SourceVpc (restrict to specific VPCs) to limit exposure.
  • Block Public Access: Consider enabling DynamoDB Block Public Access at the account level to prevent accidentally creating publicly accessible tables.