Security
Data protection, encryption, IAM least-privilege permissions, and compliance guidance.
Data at rest
All subscriber data (emails, names, custom attributes) is stored in clear text in DynamoDB. This is appropriate for most use cases because DynamoDB encrypts all data at rest by default using AWS-owned keys.
Encryption options
| Option | How | When to use |
|---|---|---|
| AWS-owned key (default) | Automatic. No configuration needed. | Most deployments. No compliance requirement for key control. |
| AWS-managed key (KMS) | Set encryption: dynamodb.TableEncryption.AWS_MANAGED in the storage construct. | When you need CloudTrail audit logs for key usage. |
| Customer-managed key (CMK) | Create a KMS key and pass it to the table. | When you need key rotation control, cross-account access policies, or per-tenant isolation. |
| Application-level encryption | Encrypt fields before writing using the AWS Database Encryption SDK. | PCI-DSS, HIPAA, or when clear text PII is not acceptable even at the storage layer. |
S3 templates
The template bucket uses SSE-S3 (AWS-managed server-side encryption) with all public access blocked. Templates contain HTML with Liquid placeholders — no subscriber PII.
Data in transit
All AWS SDK calls use HTTPS by default. No additional configuration needed.
IAM permissions
Every Lambda function follows least-privilege — each gets only the actions and resources it needs. No function has dynamodb:* or s3:*.
SendEmailFn
| Action | Resource |
|---|---|
dynamodb:GetItem, PutItem, UpdateItem, Query | Main table |
s3:GetObject | Template bucket |
ses:SendEmail | SES identity ARNs and configuration set |
states:StopExecution | All execution ARNs (required — execution ARNs contain random IDs) |
states:DescribeStateMachine, ListExecutions | Sequence state machine ARNs |
states:DescribeExecution, GetExecutionHistory | Execution ARNs under sequence state machines |
CheckConditionFn
| Action | Resource |
|---|---|
dynamodb:GetItem, Query | Main table (read-only) |
UnsubscribeFn
| Action | Resource |
|---|---|
dynamodb:GetItem, PutItem, UpdateItem | Main table |
ses:PutSuppressedDestination | * (SES API does not support resource-level permissions) |
states:StopExecution | All execution ARNs |
BounceHandlerFn
| Action | Resource |
|---|---|
dynamodb:GetItem, PutItem, UpdateItem, Query | Main table |
ses:PutSuppressedDestination | * (SES API does not support resource-level permissions) |
states:StopExecution | All execution ARNs |
EngagementHandlerFn
| Action | Resource |
|---|---|
dynamodb:PutItem | Events table only (write-only) |
Backup and recovery
| Feature | Status |
|---|---|
| Point-in-time recovery | Enabled on both tables |
| Removal policy | RETAIN — tables persist if the CloudFormation stack is deleted |
| Send log TTL | 90 days (automatic cleanup) |
| Engagement event TTL | 365 days (automatic cleanup) |
PITR backups inherit the table's encryption settings.
Network security
Lambda functions run in the default Lambda execution environment (no VPC). If your compliance requirements demand that DynamoDB traffic stays within the AWS network:
- Place Lambdas in a VPC
- Add a DynamoDB gateway VPC endpoint
- Add an S3 gateway endpoint
This is not required for most deployments — all SDK traffic already uses HTTPS.
GDPR and data deletion
Mailshot stores subscriber PII (email, first name, custom attributes) in DynamoDB. To support data subject access requests:
- Delete a subscriber — Use the
delete_subscriberMCP tool or call the DynamoDB delete operations directly. This removes the profile, all execution records, and send logs. - Engagement events — The Events table has a 365-day TTL. For immediate deletion, query by
PK = SUB#{email}and delete all matching items. - SES suppression list — Use the
remove_suppressionMCP tool to remove an email from the account-level suppression list.
What not to store
Subscriber attributes are flexible — any key-value pair can be stored as a top-level DynamoDB column. However, do not store:
- Payment data (card numbers, bank accounts) — use a PCI-compliant vault
- Passwords or secrets — these belong in Cognito, Auth0, or similar
- Health information — requires HIPAA-compliant storage with BAA
Subscriber attributes are meant for personalization and segmentation: platform, country, plan, signup date, etc.