Skip to content

Automate Bedrock Zero Data Retention Across All Accounts in Your Organization

22 minute read
Content level: Advanced
0

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

OptionPurposeBest forOngoing infrastructure?
Option 1: Shell scriptOne-time baseline settingInitial rollout across existing accountsNo
Option 2: Control Tower lifecycleAutomatic new-account coverageOrganizations using Control Tower Account FactoryYes
Option 3: AWS Config ruleContinuous compliance monitoring + auto-remediationRegulated environments requiring audit trailsYes
Option 4: CloudFormation StackSetsBaseline setting + new-account coverage (IaC)Organizations using StackSets for account baseliningYes
Complete solutionSet + Enforce + Monitor (all three layers)Organizations requiring defense-in-depthYes

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.

OptionAWS services usedMonthly cost estimateNotes
Option 1: Shell scriptNone (runs on your workstation)$0.00One-time execution; no deployed infrastructure
Option 2: Control Tower lifecycleLambda, EventBridge$0.00Lambda 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 ruleAWS Config, Lambda (×2)$2.25 – $4.50/moSee breakdown below
Option 4: CloudFormation StackSetsCloudFormation, Lambda (per account)$0.00CloudFormation and StackSets have no additional charge. Lambda executes once per account at deploy time (free tier)
SCP (enforcement layer)AWS Organizations$0.00SCPs are included with AWS Organizations at no additional cost
Complete solution (Option 4 + SCP + Option 3)Config, Lambda, CloudFormation, Organizations$2.25 – $4.50/moConfig 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 frequencyEvaluations/month (75 accounts)Config rule costLambda costTotal/month
Hourly54,000$54.00$0.00 (free tier)$54.00
Every 6 hours9,000$9.00$0.00$9.00
Every 12 hours4,500$4.50$0.00$4.50
Daily2,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):

ComponentMonthly 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., OrganizationAccountAccessRole or AWSControlTowerExecution)
  • IAM permissions to call bedrock:PutAccountDataRetention in 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

  1. Create the Lambda execution role with the following trust policy (allow lambda.amazonaws.com) and permissions:
    • sts:AssumeRole on arn:aws:iam::*:role/AWSControlTowerExecution
    • bedrock:PutAccountDataRetention on *
    • logs:CreateLogStream, logs:PutLogEvents scoped to the Lambda's log group
  2. Deploy the Lambda function with the code above. Set timeout to 60 seconds and configure environment variables ROLE_NAME and REGION
  3. Create the EventBridge rule with the event pattern above in the management account (us-east-1)
  4. Add Lambda invoke permission allowing events.amazonaws.com to invoke the function (with source ARN condition for the rule)
  5. 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

  1. Create the Lambda execution role with the following permissions:
    • bedrock:GetAccountDataRetention — read control plane retention
    • bedrock-mantle:GetAccountDataRetention, bedrock-mantle:GetProject, bedrock-mantle:ListProjects, bedrock-mantle:ListTagsForResource — read mantle retention and projects
    • config:PutEvaluations — report compliance results
    • logs:CreateLogStream, logs:PutLogEvents — scoped to the Lambda's log group
  2. Deploy the evaluation Lambda with the code above and configure it with 60-second timeout
  3. Create a custom Config rule pointing to the evaluation Lambda with ScheduledNotification message type
  4. Deploy the remediation Lambda with permissions to call bedrock:PutAccountDataRetention and bedrock-mantle:PutAccountDataRetention
  5. Configure auto-remediation on the Config rule with the remediation Lambda as the action. Set retry attempts (e.g., 5 retries within 300 seconds)
  6. Add Lambda invoke permission allowing config.amazonaws.com to 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 plane
  • bedrock-mantle:GetAccountDataRetention — read account-level retention on the mantle endpoint
  • bedrock-mantle:GetProject — read individual project settings
  • bedrock-mantle:ListProjects — enumerate projects
  • bedrock-mantle:ListTagsForResource — required by the project listing API
  • config: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

ApproachExisting accountsNew accountsContinuous monitoringComplexity
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:

Solution Architecture: Set, Enforce, and Monitor

Figure 1: The three-layer governance architecture — StackSets set the baseline, SCPs enforce it, and AWS Config monitors for compliance.

LayerPurposeWhat it provides
Set (StackSets)Establish and maintain the baselineEvery account configured to none, including new accounts automatically
Enforce (SCP)Organizational controlRestricts member accounts from changing the mode via IAM deny
Monitor (Config)Continuous visibilityDashboard 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

New Account Flow

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:

  1. A new account is created → StackSet auto-deploys and sets retention to none
  2. A user attempts to change the mode → SCP returns Access Denied
  3. Your environment maintains the intended state with no ongoing manual effort

For additional resilience:

  1. 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
  2. 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 to none

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:

Organizational Structure

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


*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.