Automate Bedrock Zero Data Retention Across All Accounts in Your Organization
When enforcing zero data retention on Amazon Bedrock with an SCP, there's an important prerequisite: new accounts default to inherit (not none). The SCP prevents changing the mode, but it doesn't set the mode. You must explicitly call put-account-data-retention --mode none in each account before (or immediately after) attaching the SCP. Use this article to help you create a posture that works for your organization.
Automate Bedrock Zero Data Retention Across All Accounts in Your Organization
When enforcing zero data retention on Amazon Bedrock with an SCP, there's an important prerequisite: new accounts default to inherit (not none). The SCP prevents changing the mode, but it doesn't set the mode. You must explicitly call put-account-data-retention --mode none in each account before (or immediately after) attaching the SCP.
This article covers four approaches to automate that process at scale.
Choosing the right solution
Each option serves a different purpose. The table below helps you select the right approach based on your operational needs and provides estimated monthly costs for a small-to-medium enterprise with 50–100 AWS accounts.
Solution comparison
| Option | Purpose | Best for | Ongoing infrastructure? |
|---|---|---|---|
| Option 1: Shell script | One-time baseline setting | Initial rollout across existing accounts | No |
| Option 2: Control Tower lifecycle | Automatic new-account coverage | Organizations using Control Tower Account Factory | Yes |
| Option 3: AWS Config rule | Continuous compliance monitoring + auto-remediation | Regulated environments requiring audit trails | Yes |
| Option 4: CloudFormation StackSets | Baseline setting + new-account coverage (IaC) | Organizations using StackSets for account baselining | Yes |
| Complete solution | Set + Enforce + Monitor (all three layers) | Organizations requiring defense-in-depth | Yes |
Monthly cost estimates (50–100 accounts, us-east-1)
The following estimates assume 75 accounts as a baseline, evaluated in a single region. Costs are for the infrastructure directly supporting the solution only.
| Option | AWS services used | Monthly cost estimate | Notes |
|---|---|---|---|
| Option 1: Shell script | None (runs on your workstation) | $0.00 | One-time execution; no deployed infrastructure |
| Option 2: Control Tower lifecycle | Lambda, EventBridge | $0.00 | Lambda free tier covers usage (1 invocation per new account). EventBridge delivers AWS service events at no charge, even beyond free-tier it's $0.01 cost per month |
| Option 3: AWS Config rule | AWS Config, Lambda (×2) | $2.25 – $4.50/mo | See breakdown below |
| Option 4: CloudFormation StackSets | CloudFormation, Lambda (per account) | $0.00 | CloudFormation and StackSets have no additional charge. Lambda executes once per account at deploy time (free tier) |
| SCP (enforcement layer) | AWS Organizations | $0.00 | SCPs are included with AWS Organizations at no additional cost |
| Complete solution (Option 4 + SCP + Option 3) | Config, Lambda, CloudFormation, Organizations | $2.25 – $4.50/mo | Config rule is the only ongoing-cost component |
Option 3 cost breakdown (AWS Config rule)
The cost of the Config rule depends on how frequently you evaluate. Since the SCP prevents real-time drift, the Config rule serves as an audit/verification layer — not a primary control. A less-frequent schedule is sufficient.
| Evaluation frequency | Evaluations/month (75 accounts) | Config rule cost | Lambda cost | Total/month |
|---|---|---|---|---|
| Hourly | 54,000 | $54.00 | $0.00 (free tier) | $54.00 |
| Every 6 hours | 9,000 | $9.00 | $0.00 | $9.00 |
| Every 12 hours | 4,500 | $4.50 | $0.00 | $4.50 |
| Daily | 2,250 | $2.25 | $0.00 | $2.25 |
Config rule evaluations are priced at $0.001 per evaluation. Lambda invocations fall within the free tier (400K GB-seconds/month) given the low volume and short duration (~1s per invocation at 128MB).
Recommendation: A 12-hour or daily evaluation schedule provides adequate compliance coverage at minimal cost when combined with the SCP enforcement layer. The summary table uses a 12-hour schedule.
Cost optimization tip: Deploy the Config rule as an Organization Config Rule from the management account. A single rule covers all member accounts, and you pay only for evaluations — not per rule per account.
Cost summary
For a complete deployment (StackSets + SCP + Config with 12-hour evaluation):
| Component | Monthly cost |
|---|---|
| CloudFormation StackSets | $0.00 |
| Service Control Policy | $0.00 |
| Lambda functions (all) | $0.00 (free tier) |
| EventBridge (CT lifecycle) | $0.00 |
| AWS Config rule evaluations (12-hr, 75 accounts) | $4.50 |
| Total estimated monthly cost | $4.50 |
Note: These estimates are based on published AWS pricing as of July 2026 in the us-east-1 region. Actual costs may vary. Use the AWS Pricing Calculator for your specific configuration. Lambda free tier (1M requests + 400K GB-seconds/month) applies per AWS account and is shared across all Lambda functions in that account.
Prerequisites
- AWS CLI 2.35+ (for
aws bedrock put-account-data-retention) - A cross-account role in each member account (e.g.,
OrganizationAccountAccessRoleorAWSControlTowerExecution) - IAM permissions to call
bedrock:PutAccountDataRetentionin each target account
Option 1: Shell script with Organizations account enumeration
The simplest approach, enumerate all active accounts in your organization, assume a role in each, and set the retention mode. This is an easy way to at least start getting retention set within your AWS accounts across your organization:
Review the below provided python script and modify as needed to use inside your AWS Organization.
#!/bin/bash # set_bedrock_retention_org.sh # Sets Bedrock data retention to 'none' across all accounts in the organization. # # Usage: # aws sso login --profile <management-account-profile> # ./set_bedrock_retention_org.sh # # Prerequisites: # - AWS CLI 2.35+ # - jq installed # - Run from the management account (or a delegated admin) # - OrganizationAccountAccessRole must exist in each member account set -euo pipefail ROLE_NAME="${ROLE_NAME:-OrganizationAccountAccessRole}" REGION="${REGION:-us-east-1}" MODE="none" echo "=== Bedrock Data Retention Enforcement ===" echo "Setting mode: $MODE" echo "Role: $ROLE_NAME" echo "Region: $REGION" echo "" # Get all active accounts ACCOUNTS=$(aws organizations list-accounts \ --query 'Accounts[?Status==`ACTIVE`].Id' \ --output text) TOTAL=$(echo "$ACCOUNTS" | wc -w) COUNT=0 SUCCESS=0 FAILED=0 for ACCOUNT_ID in $ACCOUNTS; do COUNT=$((COUNT + 1)) # Validate account ID format (12-digit number) if ! [[ "$ACCOUNT_ID" =~ ^[0-9]{12}$ ]]; then echo "[$COUNT/$TOTAL] Invalid account ID format: $ACCOUNT_ID — skipping" FAILED=$((FAILED + 1)) continue fi echo "[$COUNT/$TOTAL] Account $ACCOUNT_ID..." # Assume role and execute in a subshell to avoid credential leakage # Credentials are scoped to the subshell and never exported to the parent RESULT=$( CREDS_JSON=$(aws sts assume-role \ --role-arn "arn:aws:iam::${ACCOUNT_ID}:role/${ROLE_NAME}" \ --role-session-name "SetBedrockRetention" \ --query 'Credentials' \ --output json 2>&1) || { echo "ASSUME_FAILED: $CREDS_JSON"; exit 1; } AWS_ACCESS_KEY_ID=$(echo "$CREDS_JSON" | jq -r '.AccessKeyId') AWS_SECRET_ACCESS_KEY=$(echo "$CREDS_JSON" | jq -r '.SecretAccessKey') AWS_SESSION_TOKEN=$(echo "$CREDS_JSON" | jq -r '.SessionToken') AWS_ACCESS_KEY_ID="$AWS_ACCESS_KEY_ID" \ AWS_SECRET_ACCESS_KEY="$AWS_SECRET_ACCESS_KEY" \ AWS_SESSION_TOKEN="$AWS_SESSION_TOKEN" \ aws bedrock put-account-data-retention \ --region "$REGION" \ --mode "$MODE" 2>&1 ) if [ $? -eq 0 ]; then echo " ✅ Set to '$MODE'" SUCCESS=$((SUCCESS + 1)) else if [[ "$RESULT" == *"ASSUME_FAILED"* ]]; then echo " ⚠️ Could not assume role — skipping" else echo " ❌ Failed: $RESULT" fi FAILED=$((FAILED + 1)) fi done echo "" echo "=== Summary ===" echo "Total accounts: $TOTAL" echo "Successful: $SUCCESS" echo "Failed/Skipped: $FAILED"
When to use: One-time enforcement across existing accounts. Run this before attaching the SCP.
Limitations: Doesn't handle new accounts created after the script runs. Combine with Option 2 for full coverage.
Option 2: Control Tower lifecycle event (automatic for new accounts)
If you use AWS Control Tower, you can trigger a Lambda function automatically when new accounts are created via Account Factory. This helps to set every new account to none before anyone in your organization starts to leverage Amazon Bedrock.
Architecture
Account Factory creates account
→ Control Tower emits CreateManagedAccount lifecycle event
→ EventBridge rule matches the event
→ Lambda assumes role in new account
→ Lambda calls put-account-data-retention(mode='none')
Deployment
- Create the Lambda execution role with the following trust policy (allow
lambda.amazonaws.com) and permissions:sts:AssumeRoleonarn:aws:iam::*:role/AWSControlTowerExecutionbedrock:PutAccountDataRetentionon*logs:CreateLogStream,logs:PutLogEventsscoped to the Lambda's log group
- Deploy the Lambda function with the code above. Set timeout to 60 seconds and configure environment variables
ROLE_NAMEandREGION - Create the EventBridge rule with the event pattern above in the management account (us-east-1)
- Add Lambda invoke permission allowing
events.amazonaws.comto invoke the function (with source ARN condition for the rule) - Add the Lambda as the rule's target using
aws events put-targets
Note: Deploy this in the management account in us-east-1. Control Tower lifecycle events are emitted to the management account's default event bus. Review the code an make adjustments as needed for your organization.
Lambda function
""" Lambda handler triggered by Control Tower CreateManagedAccount lifecycle event. Sets Bedrock data retention to 'none' in newly created accounts. Environment variables: ROLE_NAME: Role to assume in the new account (default: AWSControlTowerExecution) REGION: Region to set retention in (default: us-east-1) """ import os import json import re import boto3 import urllib.request import urllib.error from botocore.auth import SigV4Auth from botocore.awsrequest import AWSRequest from botocore.credentials import Credentials ROLE_NAME = os.environ.get('ROLE_NAME', 'AWSControlTowerExecution') REGION = os.environ.get('REGION', 'us-east-1') ACCOUNT_ID_PATTERN = re.compile(r'^\d{12}$') def set_data_retention(credentials, region, mode='none'): """Set Bedrock data retention via SigV4-signed HTTP request.""" url = f"https://bedrock.{region}.amazonaws.com/data-retention" data = json.dumps({"mode": mode}) headers = {"Content-Type": "application/json"} request = AWSRequest(method="PUT", url=url, data=data, headers=headers) SigV4Auth(credentials, "bedrock", region).add_auth(request) req = urllib.request.Request( url=url, data=data.encode("utf-8"), headers=dict(request.headers), method="PUT" ) response = urllib.request.urlopen(req) return json.loads(response.read().decode()) def handler(event, context): # Extract account details from lifecycle event (do not log full event) detail = event.get('detail', {}) service_detail = detail.get('serviceEventDetails', {}) account_status = service_detail.get('createManagedAccountStatus', {}) account_info = account_status.get('account', {}) account_id = account_info.get('accountId') account_name = account_info.get('accountName', 'unknown') state = account_status.get('state') print(f"Lifecycle event received: account={account_id}, state={state}") if not account_id: print("No account ID found in event — skipping") return # Validate account ID format to prevent injection if not ACCOUNT_ID_PATTERN.match(account_id): print(f"Invalid account ID format: {account_id} — skipping") return if state != 'SUCCEEDED': print(f"Account creation state is '{state}', not 'SUCCEEDED' — skipping") return print(f"New account created: {account_id} ({account_name})") # Assume role in the new account sts = boto3.client('sts') try: creds = sts.assume_role( RoleArn=f'arn:aws:iam::{account_id}:role/{ROLE_NAME}', RoleSessionName='SetBedrockDataRetention' )['Credentials'] except Exception as e: print(f"Failed to assume role in {account_id}: {type(e).__name__}") raise # Create frozen credentials for SigV4 signing frozen_creds = Credentials( access_key=creds['AccessKeyId'], secret_key=creds['SecretAccessKey'], token=creds['SessionToken'] ) # Set data retention to none try: result = set_data_retention(frozen_creds, REGION, 'none') print(f"Set data retention to 'none' in account {account_id}") except urllib.error.HTTPError as e: print(f"HTTP error setting retention in {account_id}: {e.code}") raise except Exception as e: print(f"Failed to set data retention in {account_id}: {type(e).__name__}") raise return { 'statusCode': 200, 'accountId': account_id, 'mode': 'none' }
EventBridge rule
{ "source": ["aws.controltower"], "detail-type": ["AWS Service Event via CloudTrail"], "detail": { "eventName": ["CreateManagedAccount"], "serviceEventDetails": { "createManagedAccountStatus": { "state": ["SUCCEEDED"] } } } }
When to use: Ongoing enforcement for new accounts in Control Tower environments. Pair with Option 1 to cover existing accounts.
Option 3: AWS Config custom rule with auto-remediation
This approach provides continuous monitoring and automatic correction. If the retention mode drifts from none (for example, if the SCP is temporarily removed and someone changes the setting), Config detects and remediates it. A method to continue to monitor and correct is generally key in an organization, especially if you have a large number of AWS accounts, and a large staff of engineers working inside of your AWS accounts that may be looking to test and adopt new models. This helps to provide visibility and action without requiring you to manually check and remediate.
Architecture
AWS Config periodically evaluates custom rule
→ Lambda checks get-account-data-retention
→ If mode != 'none', marks account NON_COMPLIANT
→ Auto-remediation triggers SSM Automation or Lambda
→ Remediation sets mode back to 'none'
Setup steps
- Create the Lambda execution role with the following permissions:
bedrock:GetAccountDataRetention— read control plane retentionbedrock-mantle:GetAccountDataRetention,bedrock-mantle:GetProject,bedrock-mantle:ListProjects,bedrock-mantle:ListTagsForResource— read mantle retention and projectsconfig:PutEvaluations— report compliance resultslogs:CreateLogStream,logs:PutLogEvents— scoped to the Lambda's log group
- Deploy the evaluation Lambda with the code above and configure it with 60-second timeout
- Create a custom Config rule pointing to the evaluation Lambda with
ScheduledNotificationmessage type - Deploy the remediation Lambda with permissions to call
bedrock:PutAccountDataRetentionandbedrock-mantle:PutAccountDataRetention - Configure auto-remediation on the Config rule with the remediation Lambda as the action. Set retry attempts (e.g., 5 retries within 300 seconds)
- Add Lambda invoke permission allowing
config.amazonaws.comto invoke the evaluation Lambda
Custom Config rule (evaluation Lambda)
""" AWS Config custom rule: Comprehensive Bedrock data retention compliance check. Evaluates ALL data retention settings across both endpoints: 1. Bedrock control plane (bedrock-runtime account-level) 2. Bedrock-mantle account-level 3. All bedrock-mantle projects (project-level overrides) Reports NON_COMPLIANT if any setting is not 'none' or 'inherit'. Uses SigV4-signed HTTP requests for compatibility with Lambda runtime. """ import json import boto3 import urllib.request import urllib.error from datetime import datetime, timezone from botocore.auth import SigV4Auth from botocore.awsrequest import AWSRequest def sigv4_request(credentials, service, region, method, url): """Make a SigV4-signed HTTP request and return parsed JSON.""" headers = {"Content-Type": "application/json"} request = AWSRequest(method=method, url=url, headers=headers) SigV4Auth(credentials, service, region).add_auth(request) req = urllib.request.Request(url=url, headers=dict(request.headers), method=method) try: response = urllib.request.urlopen(req) return response.status, json.loads(response.read().decode()) except urllib.error.HTTPError as e: error_body = e.read().decode() try: return e.code, json.loads(error_body) except json.JSONDecodeError: return e.code, {"error": error_body} def check_bedrock_control_plane(credentials, region): """Check account-level retention on the Bedrock control plane (bedrock-runtime).""" url = f"https://bedrock.{region}.amazonaws.com/data-retention" status, data = sigv4_request(credentials, "bedrock", region, "GET", url) if status == 200: return data.get('mode', 'unknown') return 'error' def check_mantle_account_level(credentials, region): """Check account-level retention on the bedrock-mantle endpoint.""" url = f"https://bedrock-mantle.{region}.api.aws/v1/data_retention" status, data = sigv4_request(credentials, "bedrock-mantle", region, "GET", url) if status == 200: return data.get('mode', 'unknown') return 'error' def check_mantle_projects(credentials, region): """Check all projects on the bedrock-mantle endpoint for retention settings.""" non_compliant_projects = [] url = f"https://bedrock-mantle.{region}.api.aws/v1/organization/projects" while url: status, data = sigv4_request(credentials, "bedrock-mantle", region, "GET", url) if status != 200: print(f"Error listing projects: HTTP {status}") return [{"id": "error", "mode": "error"}] for project in data.get('data', []): project_id = project.get('id', 'unknown') mode = project.get('data_retention', {}).get('mode', 'inherit') # 'inherit' and 'none' are compliant; anything else is not if mode not in ('none', 'inherit'): non_compliant_projects.append({ 'id': project_id, 'name': project.get('name', ''), 'mode': mode }) # Handle pagination if data.get('has_more', False): last_id = data.get('last_id', '') url = f"https://bedrock-mantle.{region}.api.aws/v1/organization/projects?after={last_id}" else: url = None return non_compliant_projects def handler(event, context): config_client = boto3.client('config') region = context.invoked_function_arn.split(':')[3] session = boto3.Session() credentials = session.get_credentials().get_frozen_credentials() findings = [] # Check 1: Bedrock control plane (account-level, applies to bedrock-runtime) bedrock_mode = check_bedrock_control_plane(credentials, region) print(f"Bedrock control plane mode: {bedrock_mode}") if bedrock_mode != 'none': findings.append(f"bedrock control plane: {bedrock_mode}") # Check 2: Bedrock-mantle account-level mantle_mode = check_mantle_account_level(credentials, region) print(f"Bedrock-mantle account mode: {mantle_mode}") if mantle_mode not in ('none', 'inherit'): findings.append(f"bedrock-mantle account: {mantle_mode}") # Check 3: All bedrock-mantle projects non_compliant_projects = check_mantle_projects(credentials, region) print(f"Non-compliant projects: {non_compliant_projects}") for proj in non_compliant_projects: findings.append(f"project {proj['id']} ({proj.get('name','')}): {proj['mode']}") # Determine compliance if not findings: compliance = 'COMPLIANT' annotation = 'All data retention settings are compliant (none/inherit)' else: compliance = 'NON_COMPLIANT' annotation = f"Non-compliant settings found: {'; '.join(findings)}" # Truncate annotation to 256 chars (Config limit) if len(annotation) > 256: annotation = annotation[:253] + '...' print(f"Account: {event['accountId']}, Compliance: {compliance}") print(f"Annotation: {annotation}") # Report evaluation config_client.put_evaluations( Evaluations=[{ 'ComplianceResourceType': 'AWS::::Account', 'ComplianceResourceId': event['accountId'], 'ComplianceType': compliance, 'Annotation': annotation, 'OrderingTimestamp': datetime.now(timezone.utc) }], ResultToken=event['resultToken'] ) return { 'compliance': compliance, 'bedrock_mode': bedrock_mode, 'mantle_mode': mantle_mode, 'non_compliant_projects': non_compliant_projects }
IAM permissions required: The Lambda execution role needs the following least-privilege permissions:
bedrock:GetAccountDataRetention— read account-level retention on the control planebedrock-mantle:GetAccountDataRetention— read account-level retention on the mantle endpointbedrock-mantle:GetProject— read individual project settingsbedrock-mantle:ListProjects— enumerate projectsbedrock-mantle:ListTagsForResource— required by the project listing APIconfig:PutEvaluations— report compliance results back to AWS Config
Remediation Lambda
""" Auto-remediation: Sets Bedrock data retention back to 'none' on both endpoints. Triggered by AWS Config auto-remediation when the custom rule evaluates NON_COMPLIANT. Resets both the Bedrock control plane and bedrock-mantle account-level settings. Uses SigV4-signed HTTP requests for compatibility with the Lambda runtime. """ import json import boto3 import urllib.request from botocore.auth import SigV4Auth from botocore.awsrequest import AWSRequest def set_retention(credentials, service, region, url, mode='none'): """Set data retention via SigV4-signed PUT request.""" data = json.dumps({"mode": mode}) headers = {"Content-Type": "application/json"} request = AWSRequest(method="PUT", url=url, data=data, headers=headers) SigV4Auth(credentials, service, region).add_auth(request) req = urllib.request.Request( url=url, data=data.encode("utf-8"), headers=dict(request.headers), method="PUT" ) response = urllib.request.urlopen(req) return json.loads(response.read().decode()) def handler(event, context): region = context.invoked_function_arn.split(':')[3] session = boto3.Session() credentials = session.get_credentials().get_frozen_credentials() results = {} # Reset Bedrock control plane (applies to bedrock-runtime traffic) url = f"https://bedrock.{region}.amazonaws.com/data-retention" results['bedrock'] = set_retention(credentials, "bedrock", region, url) print(f"Bedrock control plane: {results['bedrock']}") # Reset bedrock-mantle account-level url = f"https://bedrock-mantle.{region}.api.aws/v1/data_retention" results['mantle'] = set_retention(credentials, "bedrock-mantle", region, url) print(f"Bedrock-mantle account: {results['mantle']}") print(f"Remediation complete: both endpoints set to 'none'") return {'statusCode': 200, 'mode': 'none', 'results': results}
When to use: Environments that require continuous compliance monitoring and audit trails. Config provides a compliance history dashboard showing when drift occurred and when it was remediated.
Bonus: Deploy the Config rule across all accounts using a Config Organizational Rule or StackSets.
Option 4: CloudFormation StackSets with a Custom Resource
Deploy a StackSet across your organization that includes a CloudFormation Custom Resource backed by a Lambda function. This runs automatically in all existing accounts in the target OUs and in any new accounts added to those OUs.
CloudFormation template
AWSTemplateFormatVersion: '2010-09-09' Description: Sets Bedrock data retention to 'none' via Custom Resource Resources: SetRetentionLogGroup: Type: AWS::Logs::LogGroup Properties: LogGroupName: !Sub '/aws/lambda/${AWS::StackName}-SetRetention' RetentionInDays: 14 SetRetentionFunction: Type: AWS::Lambda::Function Properties: Runtime: python3.12 Handler: index.handler Timeout: 30 Role: !GetAtt LambdaRole.Arn LoggingConfig: LogGroup: !Ref SetRetentionLogGroup Code: ZipFile: | import boto3 import json import urllib.request import cfnresponse from botocore.auth import SigV4Auth from botocore.awsrequest import AWSRequest def handler(event, context): if event['RequestType'] == 'Delete': cfnresponse.send(event, context, cfnresponse.SUCCESS, {}) return try: session = boto3.Session() credentials = session.get_credentials().get_frozen_credentials() region = context.invoked_function_arn.split(':')[3] url = f"https://bedrock.{region}.amazonaws.com/data-retention" data = json.dumps({"mode": "none"}) headers = {"Content-Type": "application/json"} request = AWSRequest(method="PUT", url=url, data=data, headers=headers) SigV4Auth(credentials, "bedrock", region).add_auth(request) req = urllib.request.Request( url=url, data=data.encode("utf-8"), headers=dict(request.headers), method="PUT" ) response = urllib.request.urlopen(req) result = json.loads(response.read().decode()) print(f"Set data retention to none: {result}") cfnresponse.send(event, context, cfnresponse.SUCCESS, {'Mode': 'none'}) except Exception as e: print(f"Error: {type(e).__name__}") cfnresponse.send(event, context, cfnresponse.FAILED, {'Error': str(e)}) LambdaRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: lambda.amazonaws.com Action: sts:AssumeRole Policies: - PolicyName: BedrockDataRetention PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - bedrock:PutAccountDataRetention Resource: '*' - Effect: Allow Action: - logs:CreateLogStream - logs:PutLogEvents Resource: !GetAtt SetRetentionLogGroup.Arn SetRetention: Type: Custom::SetBedrockDataRetention Properties: ServiceToken: !GetAtt SetRetentionFunction.Arn Outputs: Status: Value: "Data retention set to none"
Deployment
Save the template above as bedrock-retention-stackset.yaml, then deploy:
aws cloudformation create-stack-set \ --stack-set-name BedrockZeroRetention \ --template-body file://bedrock-retention-stackset.yaml \ --permission-model SERVICE_MANAGED \ --auto-deployment Enabled=true,RetainStacksOnAccountRemoval=false \ --capabilities CAPABILITY_IAM aws cloudformation create-stack-instances \ --stack-set-name BedrockZeroRetention \ --deployment-targets OrganizationalUnitIds=ou-abs-123456 \ --regions us-east-1 \ --operation-preferences FailureToleranceCount=5,MaxConcurrentCount=5
Replace ou-ab-123456 with your target OU ID (use r-abs for the root to cover all accounts).
With auto-deployment enabled, any new account added to the target OU will automatically get the stack deployed.
When to use: Organizations already using StackSets for account baselining. Clean infrastructure-as-code approach.
Comparison
| Approach | Existing accounts | New accounts | Continuous monitoring | Complexity |
|---|---|---|---|---|
| Shell script (Option 1) | ✅ | ❌ | ❌ | Low |
| Control Tower lifecycle (Option 2) | ❌ | ✅ | ❌ | Medium |
| AWS Config + auto-remediation (Option 3) | ✅ | ✅ | ✅ | High |
| CloudFormation StackSets (Option 4) | ✅ | ✅ | ❌ | Medium |
Recommended combination:
- Run Option 1 once to cover existing accounts
- Deploy Option 2 or Option 4 for new accounts going forward
- Add Option 3 if you need continuous compliance monitoring and audit trails
The Complete Solution: Set, Enforce, and Monitor
For comprehensive data retention governance, you can combine multiple approaches into a layered strategy. Each layer addresses a different aspect of the problem:
- Setting the mode configures your accounts to the state you intend
- Enforcing the mode gives you an organizational-level control that the setting remains consistent
- Monitoring provides continuous visibility into your compliance posture and an audit trail
Here's how these layers complement each other:
Figure 1: The three-layer governance architecture — StackSets set the baseline, SCPs enforce it, and AWS Config monitors for compliance.
| Layer | Purpose | What it provides |
|---|---|---|
| Set (StackSets) | Establish and maintain the baseline | Every account configured to none, including new accounts automatically |
| Enforce (SCP) | Organizational control | Restricts member accounts from changing the mode via IAM deny |
| Monitor (Config) | Continuous visibility | Dashboard showing compliance state, audit history, and automatic correction if needed |
Layer 1: Set — Establish your baseline
You can use CloudFormation StackSets (Option 4) to configure none across all existing accounts and automatically in new accounts joining your target OUs. StackSets provide a declarative, infrastructure-as-code approach with auto-deployment for new accounts.
StackSets are a production ready choice because they handle new accounts automatically via auto-deployment, integrate with CloudFormation's drift detection, and provide a clear record of what was deployed. As your organization grows, you want something that scales with it. The shell script (Option 1) is a practical alternative for a quick initial run, but routinely manually running the script takes time.
Layer 2: Enforce — Maintain your policy
You can attach a Service Control Policy (SCP) at the root OU to restrict member accounts from changing the data retention mode to anything other than none. The SCP acts as an organizational-level policy ceiling that applies a deny statement to the targeted actions.
The SCP should cover both the Bedrock control plane and the bedrock-mantle endpoint:
{ "Version": "2012-10-17", "Statement": [ { "Sid": "RESTRICTBEDROCKDATARETENTION", "Effect": "Deny", "Action": [ "bedrock:PutAccountDataRetention", "bedrock-mantle:PutAccountDataRetention", "bedrock-mantle:CreateProject", "bedrock-mantle:UpdateProject" ], "Resource": "*", "Condition": { "StringNotEquals": { "bedrock:DataRetentionMode": "none" } } } ] }
This provides:
- Account-level protection on the Bedrock control plane (bedrock-runtime users)
- Account-level and project-level protection on the bedrock-mantle endpoint
- The only permitted operation is setting the mode to
none(which is what your StackSet does)
Note: The AWS Organizations management account is exempt from SCPs.
Layer 3: Monitor — Continuous compliance visibility
You can deploy AWS Config (Option 3) with auto-remediation to provide ongoing visibility:
- Continuous evaluation: Config periodically checks every account's retention mode
- Compliance dashboard: A centralized view showing the compliance state of every account over time
- Auto-remediation: If an account is found in a non-compliant state (for example, if a StackSet deployment is delayed for a new account), Config can automatically set the mode to
none - Audit trail: Provides evidence for compliance reviews showing that your controls are continuously validated
How the layers work together
Figure 2: What happens when a new account is created, the normal path and the resilience path when StackSet deployment is delayed.
In standard operation:
- A new account is created → StackSet auto-deploys and sets retention to
none - A user attempts to change the mode → SCP returns Access Denied
- Your environment maintains the intended state with no ongoing manual effort
For additional resilience:
- If the SCP is temporarily detached during organizational maintenance → A user changes the mode → Config detects the account as NON_COMPLIANT within minutes → Auto-remediation restores the mode to
none - If a StackSet deployment is delayed for a new account → The account remains at
inherit→ Config detects this as NON_COMPLIANT → Auto-remediation sets it tonone
Recommended deployment order
Deploy the layers in this sequence to minimize the window where accounts are unprotected:
Step 1: Deploy the SCP (enforce)
└── Attach to root OU
└── Restricts mode changes while you configure the rest
Step 2: Deploy StackSets (set)
└── Target: all OUs where you want zero retention
└── Sets 'none' in all existing accounts
└── Enable auto-deployment for future accounts
Step 3: Deploy Config rule + remediation (monitor)
└── Deploy as an Organization Config Rule (covers all accounts)
└── Enable auto-remediation
└── Verify your compliance dashboard shows all accounts as COMPLIANT
Step 4: Validate your deployment
└── Review Config dashboard — all accounts should show COMPLIANT
└── Verify: attempt to change mode in a member account (should receive Access Denied)
└── Confirm StackSet auto-deployment is enabled for target OUs
Why deploy the SCP first? Deploying the SCP before StackSets reduces the window where a mode change could occur between the StackSet setting it and the policy protecting it. Once the SCP is attached, the deny statement applies to subsequent API calls in member accounts.
Organizational structure
Here's how the complete solution looks when deployed:
Figure 3: AWS Organization structure with data retention governance deployed, zero-retention OUs with full enforcement vs. research OUs with monitoring only.
For organizations that need provider_data_share in some accounts
If you have workloads that require access to models like Claude Fable 5, you can structure your OUs to selectively apply the SCP. The organizational structure diagram above (Figure 3) shows this pattern, the Zero-Retention OU has full enforcement while the Research OU permits data sharing.
In this structure:
- The Zero-Retention OU has the full set+enforce+monitor stack
- The Research OU has the Config monitoring rule (for visibility) but not the SCP, allowing teams to enable data sharing for models that require it
- You maintain full audit trail and visibility into which accounts have data sharing enabled, regardless of OU
Related
- Enforce Zero Data Retention on Amazon Bedrock with Service Control Policies — the companion blog post explaining SCPs, Projects, and data retention modes
- Amazon Bedrock Data Retention Documentation
- Service Control Policies (SCPs)
- AWS Control Tower Lifecycle Events
- AWS Config Custom Rules
*The content provided in this article is for informational purposes only. You should independently evaluate the information in this article, including any sample code provided, and its applicability to your specific use case and environment.
- Language
- English
Relevant content
asked 25 days ago
- Accepted Answer
asked 3 months ago
AWS OFFICIALUpdated 2 years ago