If you’re running a multi-account AWS setup, you might need one account to send emails through SES while the SMTP credentials live in another account’s Secrets Manager. This guide walks you through setting up cross-account Secrets Manager access for AWS SES — the resource policy, IAM permissions, and a working Python script.
How Cross-Account SES Email Works
Account A owns the SES domain and stores SMTP credentials in Secrets Manager. Account B runs the Lambda or script that sends the email. Instead of duplicating credentials, you grant Account B permission to read the secret from Account A.
This needs two things: a resource-based policy on the secret in Account A, and an IAM policy on Account B’s role that allows secretsmanager:GetSecretValue. Both sides have to agree — that’s how cross-account access works in AWS. For a deeper look at the general pattern, see How to Access AWS Secrets Manager from Another Account.
Prerequisites
- Two AWS accounts — Account A (SES + Secrets Manager) and Account B (runs the email code)
- A verified SES domain or email address in Account A
- SES SMTP credentials stored in Secrets Manager (Account A)
- AWS CLI configured — see How to Install AWS CLI v2 on Ubuntu 22.04 if needed
- A Lambda function or Python environment in Account B with
boto3
Step 1: Add a Resource Policy to the Secret in Account A
In Secrets Manager (Account A), open the secret and attach this resource-based policy:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowCrossAccountSecretAccess",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::123456789012:role/your-lambda-execution-role"
},
"Action": "secretsmanager:GetSecretValue",
"Resource": "*"
}
]
}
Here’s what each field does:
Principal— the IAM role ARN from Account B that’s allowed to access the secret. Replace123456789012with Account B’s account ID andyour-lambda-execution-rolewith the actual role name.Action— only allowsGetSecretValue, which is the minimum permission needed to read the secret.Resource— set to*here, but you can tighten it by using the full secret ARN instead.
If your accounts are in an AWS Organization, you can also add an aws:PrincipalOrgID condition to restrict access to your org:
"Condition": {
"StringEquals": {
"aws:PrincipalOrgID": "o-your-org-id"
}
}
Step 2: Grant IAM Permissions in Account B
The resource policy alone isn’t enough. Account B’s Lambda role also needs an IAM policy that allows reading the secret:
{
"Effect": "Allow",
"Action": "secretsmanager:GetSecretValue",
"Resource": "arn:aws:secretsmanager:us-east-1:111111111111:secret:your-ses-smtp-secret-*"
}
Action— same as Account A’s policy, onlyGetSecretValue.Resource— the full ARN of the secret in Account A. Replace111111111111with Account A’s account ID. The-*suffix is needed because Secrets Manager appends a random string to the ARN.
Both policies must be in place. The resource policy says “I trust this role” and the IAM policy says “this role is allowed to reach out.” If either is missing, you’ll get an AccessDeniedException.
Step 3: Python Script to Retrieve Credentials and Send Email
This script pulls SMTP credentials from Secrets Manager in Account A and sends an email through the SES SMTP endpoint:
import boto3
import json
import smtplib
from email.mime.text import MIMEText
from botocore.exceptions import ClientError
AWS_REGION = "us-east-1"
SECRET_ARN = "arn:aws:secretsmanager:us-east-1:111111111111:secret:your-ses-smtp-secret"
SENDER_EMAIL = "noreply@example.com"
RECIPIENT_EMAIL = "recipient@example.com"
def get_ses_credentials():
client = boto3.client("secretsmanager", region_name=AWS_REGION)
try:
response = client.get_secret_value(SecretId=SECRET_ARN)
secret = json.loads(response["SecretString"])
return {
"server": secret["SMTP_SERVER"],
"port": int(secret["SMTP_PORT"]),
"username": secret["SMTP_USERNAME"],
"password": secret["SMTP_PASSWORD"],
}
except ClientError as e:
print(f"Failed to retrieve secret: {e}")
raise
def send_email():
creds = get_ses_credentials()
msg = MIMEText("Hello from AWS SES via cross-account Secrets Manager!")
msg["Subject"] = "Test Email from Account B"
msg["From"] = SENDER_EMAIL
msg["To"] = RECIPIENT_EMAIL
with smtplib.SMTP(creds["server"], creds["port"]) as server:
server.starttls()
server.login(creds["username"], creds["password"])
server.send_message(msg)
print("Email sent successfully.")
send_email()
Here’s what the key parts do:
boto3.client("secretsmanager")— creates a Secrets Manager client that uses Account B’s IAM role to make API calls.get_secret_value(SecretId=SECRET_ARN)— retrieves the secret. Use the full ARN, not just the name — cross-account access requires the ARN.SENDER_EMAIL— must be a verified email or domain in SES (Account A). This is separate from the SMTP username.smtplib.SMTP(server, port)— connects to the SES SMTP endpoint. The server is typicallyemail-smtp.us-east-1.amazonaws.comand port587is for STARTTLS.server.starttls()— upgrades the connection to TLS before sending credentials.
SES SMTP Credentials vs IAM Credentials
SES SMTP credentials are not the same as IAM access keys. When you create SMTP credentials in the SES console, AWS generates a separate username and password for SMTP authentication. These go into Secrets Manager and are used with smtplib. Your IAM credentials are what boto3 uses to call the Secrets Manager API. So there are two layers here: IAM handles the secret retrieval, SMTP credentials handle the email sending.
Why Use This Pattern
- Single source of truth — credentials exist in one place. Rotate once, every account picks up the change.
- Least privilege — Account B never stores credentials locally, only reads them at runtime.
- Auditing — CloudTrail in Account A logs every secret access.
- Scales easily — same pattern whether you have 2 accounts or 20.
This is the same trust model used for other cross-account resources. If you’ve done cross-account access with AssumeRole or copied S3 objects across accounts, you’ll recognize the pattern.
Troubleshooting
AccessDeniedException when retrieving the secret
Check that both policies (resource policy in Account A and IAM policy in Account B) are in place. Also make sure you’re using the secret’s full ARN in SecretId, not just the name.
SMTP authentication failure
Confirm the SMTP credentials in Secrets Manager are correct and haven’t been rotated without updating the secret value. Also check that the sender email or domain is verified in SES. If SES is still in sandbox mode, you can only send to verified addresses.
Connection timeout on port 587
If your Lambda runs inside a VPC, it might not have internet access. You’ll need a NAT gateway or VPC endpoint for Secrets Manager, plus outbound access for SMTP. Lambda outside a VPC has internet access by default.
Conclusion
That covers sending email through AWS SES with SMTP credentials stored in another account’s Secrets Manager. The main thing to remember is that cross-account access requires policies on both sides — one on the secret and one on the calling role. Once that’s in place, the Python script handles the rest.
For more cross-account patterns, check out Setting Up Cross-Account S3 Upload with Lambda or Setting Up Cross-Account SNS to SQS Subscription in AWS.


