A Terraform module that creates a secure file upload pipeline on AWS with automatic malware scanning using Amazon GuardDuty Malware Protection for S3.
┌─────────────┐
│ S3 Direct │
│ Upload │
└──────┬──────┘
│
┌─────────────┐ │
│ SFTP Ingress │ │
│ (Transfer ├───────┤
│ Family) │ │
└─────────────┘ │
▼
┌─────────────────────┐
│ Ingress Bucket │ ── [Audit Trail]
│ (KMS encrypted) │
└──────────┬──────────┘
│
┌────────────┴────────────┐
│ │
▼ ▼
┌─────────────────────┐ ┌──────────────────┐
│ GuardDuty Malware │ │ VT Hash Lookup │
│ Protection Scan │ │ (EventBridge, │
│ (seconds-minutes) │ │ tags object) │
└──────────┬──────────┘ └──────────────────┘
│
▼
┌─────────────────────┐
│ EventBridge Rule │
│ (scan result) │
└──────────┬──────────┘
│
▼
┌─────────────────────┐
│ Lambda File Router │───────────┐
│ (reads VT tags) │ │
└──────┬──────────────┘ │
│ │
(NO_THREATS_FOUND) (THREATS_FOUND)
│ │
[VT malicious?] │
│ │ │
YES NO │
│ │ │
│ [PI enabled?] │
│ │ │ │
│ YES NO │
│ │ │ │
│ ┌─┴────────┐ │ │
│ │ Prompt │ │ ── [Audit Trail] │
│ │ Injection │ │ │
│ │ Scanner │ │ │
│ └──┬───┬───┘ │ │
│ │ │ │ │
│ Safe Risky │ │
│ │ │ │ │
│ │ └───────┤ │
│ │ │ │
│ ▼ │ │
│ ┌───────────────────┐ │
│ │ Egress Bucket │ │
│ │ (verified) │ │
│ └────────┬──────────┘ │
│ │ │ │
│ [Audit Trail] │ │
│ │ │ │
│ ┌────────┴───┴──────┐ │
│ │ Egress SNS (opt) │ │
│ │ SFTP Egress (opt) │ │
│ └──────────────────┘ │
│ │
└──────────┬────────────────────────┘
│
┌─────────────┴──────────────┐
│ │
▼ ▼
┌───────────────────┐ ┌──────────┐
│Quarantine Bucket │ │ SNS │
│(threats/VT/inject) │ │ Alert │
└───────────────────┘ └──────────┘
- Automatic malware scanning — GuardDuty Malware Protection scans every object uploaded to the ingress bucket
- VirusTotal hash lookup — optional SHA-256 hash check against the VirusTotal API, running in parallel with GuardDuty via EventBridge. Results are stored as S3 object tags and read by the file router when making routing decisions. Files with detections above a configurable threshold are quarantined.
- Prompt injection scanning — optional AI-powered scanning of uploaded documents for prompt injection attacks using an ONNX model, with configurable score threshold and support for PDF, DOCX, PPTX, and plain text formats
- Automated file routing — Lambda moves verified files to the egress bucket and infected files to quarantine
- SNS alerting — email notifications when malware is detected
- Egress notifications — optional SNS topic fires when clean files reach the egress bucket, enabling downstream automation
- File processing audit trail — optional DynamoDB table records every file at each pipeline stage (received, GuardDuty, VirusTotal, prompt injection, routed) with configurable TTL retention
- SFTP upload support — optional AWS Transfer Family server with per-user home directories
- SFTP egress — optional read-only SFTP endpoint for pulling verified files from the egress bucket
- KMS encryption — all buckets encrypted with a shared KMS key (BYO or auto-created)
- S3 Object Lock — optional tamper-proof retention on the quarantine bucket
- Least-privilege IAM — scoped IAM roles for GuardDuty, Lambda, Transfer Family, and SFTP users
- TLS enforcement — bucket policies deny non-HTTPS requests on all buckets
- KMS enforcement — bucket policies use
StringNotEqualswith aNullcondition guard to deny uploads that explicitly specify wrong encryption, while allowing AWS services (e.g. Transfer Family) that rely on bucket default SSE-KMS without sending encryption headers - Access logging — dedicated log bucket for S3 server access logs
- Lifecycle management — configurable expiration/transition rules per bucket
- Dead letter queue — SQS DLQ for Lambda invocation failures
- Lambda error alarm — CloudWatch alarm on Lambda invocation errors (separate from DLQ), firing to the SNS alert topic
- CloudWatch dashboard — optional pipeline dashboard with metric filters for file routing outcomes, VirusTotal scans, prompt injection, egress notifications, Lambda health, DLQ depth, and S3 bucket metrics
- Deletion protection —
prevent_destroylifecycle on the KMS key and quarantine bucket to guard against accidental data loss - Chat & incident integrations — optional integrations for Slack, Microsoft Teams, PagerDuty, VictorOps, Discord, and ServiceNow (see integrations.md)
| Requirement | Version |
|---|---|
| Terraform / OpenTofu | >= 1.9 |
| AWS Provider | >= 5.60, < 6.0 |
| Archive Provider | ~> 2.0 |
| AWS Account | GuardDuty Malware Protection for S3 must be available in your region |
module "secure_upload" {
source = "path/to/terraform-secure-upload"
name_prefix = "myapp"
# Creates buckets: myapp-<hash>-ingress, myapp-<hash>-egress, etc.
}This creates the S3 pipeline (ingress, egress, quarantine, logs), GuardDuty scanning, Lambda router, and SNS alerting. Bucket names automatically include an 8-character hash of your AWS account ID for global uniqueness. No SFTP resources are created by default — enable them with enable_sftp_ingress or enable_sftp_egress.
module "secure_upload" {
source = "path/to/terraform-secure-upload"
name_prefix = "myapp"
# KMS — bring your own key or let the module create one
create_kms_key = false
kms_key_arn = aws_kms_key.custom.arn
# S3 lifecycle
ingress_lifecycle_days = 1
egress_lifecycle_days = 90
quarantine_lifecycle_days = 365
enable_object_lock = true
# Lambda tuning
lambda_memory_size = 512
lambda_timeout = 120
lambda_reserved_concurrency = 20
# Notifications
sns_subscription_emails = ["[email protected]"]
# Logging
log_retention_days = 180
# SFTP
enable_sftp_ingress = true
create_sftp_ingress_server = true
sftp_ingress_endpoint_type = "VPC"
sftp_ingress_vpc_id = "vpc-0abc123"
sftp_ingress_subnet_ids = ["subnet-aaa", "subnet-bbb"]
sftp_ingress_allowed_cidrs = ["10.0.0.0/8"]
sftp_ingress_users = [
{
username = "partner-a"
ssh_public_key = file("keys/partner-a.pub")
home_directory_prefix = "/uploads/partner-a/"
},
{
username = "partner-b"
ssh_public_key = file("keys/partner-b.pub")
home_directory_prefix = "/uploads/partner-b/"
},
]
# SFTP Egress — read-only pull from egress bucket
enable_sftp_egress = true
sftp_egress_users = [
{
username = "receiver-a"
ssh_public_key = file("keys/receiver-a.pub")
home_directory_prefix = "/"
},
]
tags = {
Environment = "production"
Team = "security"
}
}| Name | Description | Type | Default | Required |
|---|---|---|---|---|
name_prefix |
Prefix applied to all resource names. Must be lowercase alphanumeric with hyphens. | string |
— | yes |
tags |
Tags applied to every resource created by this module. | map(string) |
{} |
no |
| Name | Description | Type | Default | Required |
|---|---|---|---|---|
create_kms_key |
Create a new KMS key. Set to false when providing your own via kms_key_arn. |
bool |
true |
no |
kms_key_deletion_window_days |
Days before a KMS key is permanently deleted after scheduling deletion (7–30). | number |
30 |
no |
kms_key_arn |
ARN of an existing KMS key. Required when create_kms_key is false. |
string |
null |
no |
| Name | Description | Type | Default | Required |
|---|---|---|---|---|
enable_sftp_ingress |
Enable the SFTP upload path via AWS Transfer Family. When false (default), no Transfer Family resources are created for ingress. |
bool |
false |
no |
create_sftp_ingress_server |
Create a new Transfer Family server for ingress. Only takes effect when enable_sftp_ingress is true. Set false to attach users to an existing server via sftp_ingress_server_id. |
bool |
true |
no |
sftp_ingress_server_id |
ID of an existing Transfer Family server. Only used when enable_sftp_ingress is true and create_sftp_ingress_server is false. |
string |
null |
no |
sftp_ingress_endpoint_type |
Transfer Family endpoint type — PUBLIC or VPC. Only used when both enable_sftp_ingress and create_sftp_ingress_server are true. |
string |
"PUBLIC" |
no |
sftp_ingress_vpc_id |
VPC ID for a VPC-type ingress endpoint. Required when sftp_ingress_endpoint_type is VPC. |
string |
null |
no |
sftp_ingress_subnet_ids |
Subnet IDs for a VPC-type ingress endpoint. Required when sftp_ingress_endpoint_type is VPC. |
list(string) |
[] |
no |
sftp_ingress_allowed_cidrs |
CIDR blocks allowed to access the ingress SFTP server security group. Required when sftp_ingress_endpoint_type is VPC. |
list(string) |
[] |
no |
sftp_ingress_users |
Ingress SFTP users. Each must have username, ssh_public_key, and home_directory_prefix (must start/end with /, e.g. /uploads/partner-a/). Bare / is not allowed. |
list(object) |
[] |
no |
| Name | Description | Type | Default | Required |
|---|---|---|---|---|
enable_sftp_egress |
Enable an egress SFTP endpoint for read-only access to the egress bucket. When false (default), no Transfer Family resources are created for egress. |
bool |
false |
no |
create_sftp_egress_server |
Create a new Transfer Family server for egress. Only takes effect when enable_sftp_egress is true. Set false to attach users to an existing server via sftp_egress_server_id. |
bool |
true |
no |
sftp_egress_server_id |
ID of an existing Transfer Family server for egress. Only used when enable_sftp_egress is true and create_sftp_egress_server is false. |
string |
null |
no |
sftp_egress_endpoint_type |
Transfer Family endpoint type for egress — PUBLIC or VPC. Only used when both enable_sftp_egress and create_sftp_egress_server are true. |
string |
"PUBLIC" |
no |
sftp_egress_vpc_id |
VPC ID for a VPC-type egress endpoint. Required when sftp_egress_endpoint_type is VPC. |
string |
null |
no |
sftp_egress_subnet_ids |
Subnet IDs for a VPC-type egress endpoint. Required when sftp_egress_endpoint_type is VPC. |
list(string) |
[] |
no |
sftp_egress_allowed_cidrs |
CIDR blocks allowed to access the egress SFTP server security group. Required when sftp_egress_endpoint_type is VPC. |
list(string) |
[] |
no |
sftp_egress_users |
Egress SFTP users (read-only). home_directory_prefix is required and must start/end with / (use / for full bucket access or a subdirectory to scope). |
list(object) |
[] |
no |
| Name | Description | Type | Default | Required |
|---|---|---|---|---|
ingress_lifecycle_days |
Days before objects in the ingress bucket expire. | number |
1 |
no |
egress_lifecycle_days |
Days before objects in the egress bucket transition to Infrequent Access. | number |
90 |
no |
quarantine_lifecycle_days |
Days before objects in the quarantine bucket expire. | number |
365 |
no |
| Name | Description | Type | Default | Required |
|---|---|---|---|---|
lambda_runtime |
Lambda runtime identifier for the file-router function. | string |
"python3.12" |
no |
lambda_memory_size |
Memory (MB) allocated to the file-router Lambda (128–10240). | number |
256 |
no |
lambda_timeout |
Timeout (seconds) for the file-router Lambda (1–900). | number |
60 |
no |
lambda_reserved_concurrency |
Reserved concurrent executions for the file-router Lambda. Set to -1 to use unreserved account concurrency. |
number |
10 |
no |
| Name | Description | Type | Default | Required |
|---|---|---|---|---|
enable_prompt_injection_scanning |
Enable prompt injection scanning of uploaded documents. When true, files that pass malware scanning are additionally scanned before reaching egress. | bool |
false |
no |
prompt_injection_threshold |
Score threshold (0–100) above which a file is quarantined for prompt injection. | number |
80 |
no |
prompt_injection_memory_size |
Memory (MB) for the scanner Lambda (512–10240). Higher memory allocates more CPU, speeding up ONNX model loading. 3008 MB recommended. | number |
3008 |
no |
prompt_injection_timeout |
Timeout (seconds) for the scanner Lambda (1–900). | number |
120 |
no |
prompt_injection_reserved_concurrency |
Reserved concurrent executions for the scanner Lambda. Set to -1 for unreserved. |
number |
5 |
no |
prompt_injection_image_uri |
URI of a pre-built container image for the scanner. When set, skips ECR repo creation and image build. | string |
null |
no |
| Name | Description | Type | Default | Required |
|---|---|---|---|---|
enable_virustotal_scanning |
Enable VirusTotal hash lookup scanning. Files that pass GuardDuty are checked against the VirusTotal API. | bool |
false |
no |
virustotal_api_key |
VirusTotal API key. Required when enable_virustotal_scanning is true. Stored as SSM SecureString. |
string |
null |
no |
virustotal_threshold |
Number of positive detections at or above which a file is quarantined. | number |
3 |
no |
| Name | Description | Type | Default | Required |
|---|---|---|---|---|
enable_egress_notifications |
Enable SNS notifications when clean files reach the egress bucket. | bool |
false |
no |
egress_notification_emails |
Email addresses subscribed to the egress notification SNS topic. | list(string) |
[] |
no |
| Name | Description | Type | Default | Required |
|---|---|---|---|---|
enable_audit_trail |
Enable a DynamoDB audit trail recording every file at each pipeline stage. | bool |
false |
no |
audit_trail_retention_days |
Days to retain audit trail records. 0 retains forever. |
number |
365 |
no |
| Name | Description | Type | Default | Required |
|---|---|---|---|---|
sns_subscription_emails |
Email addresses subscribed to the malware-alert SNS topic. Each entry is validated as a well-formed email address. | list(string) |
[] |
no |
| Name | Description | Type | Default | Required |
|---|---|---|---|---|
enable_object_lock |
Enable S3 Object Lock on the quarantine bucket for tamper-proof retention. | bool |
false |
no |
object_lock_retention_days |
Default retention period in days for Object Lock on the quarantine bucket. | number |
365 |
no |
object_lock_retention_mode |
Object Lock retention mode — GOVERNANCE or COMPLIANCE. |
string |
"GOVERNANCE" |
no |
| Name | Description | Type | Default | Required |
|---|---|---|---|---|
enable_cloudwatch_dashboard |
Create a CloudWatch dashboard with metric filters for pipeline observability. | bool |
false |
no |
| Name | Description | Type | Default | Required |
|---|---|---|---|---|
create_log_bucket |
Create a managed S3 access-log bucket. Set to false when shipping logs to an existing bucket via log_bucket_name. |
bool |
true |
no |
log_bucket_name |
Name of an existing S3 bucket for access-log shipping. Required when create_log_bucket is false. |
string |
null |
no |
log_retention_days |
CloudWatch Logs retention period in days. Must be a valid CloudWatch retention value. | number |
90 |
no |
s3_log_retention_days |
Days to retain S3 access logs in the log bucket. | number |
90 |
no |
| Name | Description |
|---|---|
ingress_bucket_id |
Name of the ingress (upload) S3 bucket. |
ingress_bucket_arn |
ARN of the ingress (upload) S3 bucket. |
egress_bucket_id |
Name of the egress (verified) S3 bucket. |
egress_bucket_arn |
ARN of the egress (verified) S3 bucket. |
quarantine_bucket_id |
Name of the quarantine (malware-detected) S3 bucket. |
quarantine_bucket_arn |
ARN of the quarantine (malware-detected) S3 bucket. |
log_bucket_id |
Name of the S3 access-log bucket. |
log_bucket_arn |
ARN of the S3 access-log bucket. |
kms_key_arn |
ARN of the KMS key used for encryption. |
sftp_ingress_server_id |
ID of the AWS Transfer Family SFTP server (null if SFTP disabled). |
sftp_ingress_server_endpoint |
Endpoint hostname of the SFTP server (null if SFTP disabled). |
sftp_ingress_user_arns |
Map of ingress SFTP username to Transfer user ARN. |
sftp_egress_server_id |
ID of the egress SFTP server (null if egress disabled). |
sftp_egress_server_endpoint |
Endpoint hostname of the egress SFTP server (null if egress disabled). |
sftp_egress_user_arns |
Map of egress SFTP username to Transfer user ARN. |
sns_topic_arn |
ARN of the SNS topic for malware alert notifications. |
guardduty_protection_plan_arn |
ARN of the GuardDuty Malware Protection plan. |
lambda_function_arn |
ARN of the file-router Lambda function. |
dlq_arn |
ARN of the file-router Lambda dead letter queue. |
eventbridge_rule_arn |
ARN of the EventBridge rule for GuardDuty scan results. |
cloudwatch_dashboard_arn |
ARN of the CloudWatch pipeline dashboard (null when disabled). |
egress_sns_topic_arn |
ARN of the egress notification SNS topic (null when disabled). |
audit_trail_table_arn |
ARN of the audit trail DynamoDB table (null when disabled). |
audit_trail_table_name |
Name of the audit trail DynamoDB table (null when disabled). |
virustotal_scanner_function_arn |
ARN of the VirusTotal scanner Lambda function (null when disabled). |
prompt_injection_scanner_function_arn |
ARN of the prompt injection scanner Lambda function (null when disabled). |
prompt_injection_scanner_ecr_repository_url |
URL of the ECR repository for the scanner image (null when disabled or BYO image). |
| Module | Description |
|---|---|
modules/s3-buckets |
Creates the ingress, egress, quarantine, and access-log S3 buckets with encryption, versioning, public access blocks, TLS-only policies, and lifecycle rules. |
modules/guardduty-protection |
Configures a GuardDuty Malware Protection plan on the ingress bucket with an IAM role for scanning. |
modules/file-router |
Deploys the Lambda function, EventBridge rule, SNS topic, SQS DLQ, and IAM role that route files based on scan results. |
modules/sftp |
Optionally provisions an AWS Transfer Family SFTP server (or attaches to an existing one), creates SFTP users with scoped IAM roles and SSH key authentication. |
-
Upload — Files are uploaded to the ingress bucket, either via direct S3 PutObject or through the optional SFTP endpoint (AWS Transfer Family).
-
Scan — GuardDuty Malware Protection for S3 automatically scans every new object in the ingress bucket. If VirusTotal scanning is enabled, an EventBridge rule also triggers the VT scanner Lambda in parallel, which checks the file's SHA-256 hash and writes results as S3 object tags.
-
Event — When GuardDuty completes, it emits a
GuardDuty Malware Protection Object Scan Resultevent to EventBridge. -
Route — An EventBridge rule invokes the file-router Lambda function with the scan result:
- NO_THREATS_FOUND — if VirusTotal scanning is enabled, the file router reads VT results from S3 object tags (written earlier by the parallel VT scan). If VT positives meet the threshold, the file is quarantined. Next, if prompt injection scanning is enabled, the file router invokes the scanner Lambda synchronously. If the score exceeds the threshold, the file is quarantined. Otherwise (or if scanning is disabled), the file is copied to the egress bucket and deleted from ingress.
- THREATS_FOUND — the file is copied to the quarantine bucket, deleted from ingress, and an SNS notification is published with threat details.
- Other results (e.g.,
UNSUPPORTED,ACCESS_DENIED) — the file is left in ingress for manual review.
-
Alert — If threats are detected, subscribed email addresses receive a JSON-formatted alert with the file key, threat names, and timestamp.
-
Egress notification (optional) — When a clean file reaches the egress bucket, an SNS notification is published with file details and scan results.
-
Audit trail (optional) — Every file is logged at each pipeline stage (received, GuardDuty, VirusTotal, prompt injection, routed) in a DynamoDB table with configurable TTL retention.
-
Egress (optional) — Verified files in the egress bucket can be pulled by downstream receivers via a read-only SFTP endpoint. Egress users have
GetObjectandListBucketpermissions only (noPutObject).
When using an externally managed KMS key (create_kms_key = false), your key policy must grant the following permissions to the AWS services used by this module:
[
{
"Sid": "AllowSecureUploadServices",
"Effect": "Allow",
"Principal": {
"Service": [
"guardduty.amazonaws.com",
"lambda.amazonaws.com",
"s3.amazonaws.com",
"transfer.amazonaws.com",
"sns.amazonaws.com"
]
},
"Action": [
"kms:Decrypt",
"kms:Encrypt",
"kms:GenerateDataKey",
"kms:GenerateDataKeyWithoutPlaintext",
"kms:ReEncryptFrom",
"kms:ReEncryptTo",
"kms:DescribeKey"
],
"Resource": "*",
"Condition": {
"StringEquals": {
"aws:SourceAccount": "<YOUR_ACCOUNT_ID>"
}
}
},
{
"Sid": "AllowCloudWatchLogs",
"Effect": "Allow",
"Principal": {
"Service": "logs.<YOUR_REGION>.amazonaws.com"
},
"Action": [
"kms:Decrypt",
"kms:Encrypt",
"kms:GenerateDataKey",
"kms:GenerateDataKeyWithoutPlaintext",
"kms:ReEncryptFrom",
"kms:ReEncryptTo",
"kms:DescribeKey"
],
"Resource": "*",
"Condition": {
"ArnLike": {
"kms:EncryptionContext:aws:logs:arn": "arn:aws:logs:<YOUR_REGION>:<YOUR_ACCOUNT_ID>:log-group:*"
}
}
}
]The module's IAM roles (Lambda, GuardDuty, Transfer Family users) also need kms:Decrypt and kms:GenerateDataKey grants on the key. These are handled automatically by the module's IAM policies — you only need to ensure the key policy allows the services listed above.
Prompt injection scanning: When enable_prompt_injection_scanning is true and a BYO image is not provided (prompt_injection_image_uri is null), the module creates an ECR repository encrypted with the same KMS key. ECR requires kms:CreateGrant on the key — the principal running Terraform must have this permission in the key policy. For module-managed keys this is already covered by the AllowAccountManagement statement. For external keys, add the following to your key policy:
{
"Sid": "AllowECRGrants",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::<YOUR_ACCOUNT_ID>:root"
},
"Action": [
"kms:CreateGrant",
"kms:DescribeKey"
],
"Resource": "*",
"Condition": {
"Bool": {
"kms:GrantIsForAWSResource": "true"
}
}
}This scoped condition ensures grants can only be created for AWS services (ECR), not arbitrary principals. Alternatively, if you provide a pre-built image via prompt_injection_image_uri, no ECR repository is created and this grant is not needed.
To ship S3 access logs to a centralized logging bucket (e.g., in a separate AWS account), set create_log_bucket = false and provide the bucket name:
module "secure_upload" {
source = "path/to/terraform-secure-upload"
name_prefix = "myapp"
create_log_bucket = false
log_bucket_name = "central-logging-bucket"
}The external bucket must have a policy that allows S3 log delivery from the source account. Example bucket policy for the centralized logging bucket:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowS3LogDelivery",
"Effect": "Allow",
"Principal": {
"Service": "logging.s3.amazonaws.com"
},
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::central-logging-bucket/*",
"Condition": {
"StringEquals": {
"aws:SourceAccount": "<SOURCE_ACCOUNT_ID>"
}
}
}
]
}When using an external log bucket, the module does not manage the bucket's lifecycle, encryption, or access controls — the caller is responsible for those.
The log_bucket_arn output will be null when using an external bucket (since the module does not own it).
Depending on your configuration, you may need to make changes outside this module before deployment:
| Scenario | External Action Required |
|---|---|
External KMS key (create_kms_key = false) |
Update the KMS key policy to grant service principals access. See External KMS Key above. |
| External KMS key + prompt injection scanning | Additionally grant kms:CreateGrant for ECR repository encryption. See the prompt injection note under External KMS Key. Not needed when providing a pre-built image via prompt_injection_image_uri. |
| Cross-account KMS key | Add cross-account grants for the module's IAM roles (output as lambda_role_arn, guardduty_role_arn, etc.). |
External log bucket (create_log_bucket = false) |
Configure the bucket policy, encryption, and lifecycle. See Cross-Account Log Shipping above. |
Existing SFTP server (create_sftp_ingress_server = false) |
Ensure the server uses SERVICE_MANAGED identity and the SFTP protocol. |
| SNS email alerts | Recipients must manually confirm their email subscription before alerts are delivered. |
| GuardDuty first use | The calling principal needs iam:CreateServiceLinkedRole permission for GuardDuty to create its service-linked role on first use. |
For a comprehensive security checklist for external resources, see SECURITY.md.
- GuardDuty region availability — GuardDuty Malware Protection for S3 is not available in all AWS regions. Check AWS regional availability.
- Scan latency — GuardDuty scans are asynchronous. Files remain in the ingress bucket until the scan completes (typically seconds to minutes).
- File size limits — GuardDuty Malware Protection supports objects up to 5 GB. Larger files are not scanned.
- Object Lock requires bucket recreation — Enabling
enable_object_lockafter the quarantine bucket already exists requires destroying and recreating the bucket (S3 limitation). - SNS email confirmation — Email subscriptions require manual confirmation by each recipient before alerts are delivered.
- SFTP users are service-managed — This module uses Transfer Family's service-managed identity provider. Custom/external identity providers are not supported.
- Single ingress bucket — All SFTP users share the same ingress bucket, isolated by home directory prefix.
v0.2.1 contains two breaking changes:
-
Bucket names now include a hashed account ID — Bucket names changed from
<prefix>-ingressto<prefix>-<hash>-ingress(and similarly for egress, quarantine, logs). This ensures global uniqueness. Existing deployments will see Terraform plan to destroy and recreate all four buckets. Back up your data before upgrading or useterraform state mvto migrate. -
S3 bucket policy condition changed —
DenyNonKMSEncryptionandDenyWrongKMSKeystatements now useStringNotEqualswith aNullcondition guard (see v0.3.0 notes). This correctly allows AWS services that rely on bucket default encryption while denying uploads that explicitly specify wrong encryption. This is a policy-only change and does not affect bucket resources.
See CONTRIBUTING.md for development setup, testing, and pull request guidelines.
See SECURITY.md for the vulnerability disclosure policy and an overview of the module's security design.
MIT — see LICENSE for details.