Skip to content
Linuxbeast
  • Home
  • Today in Tech
  • Who is Hiring?
  • About Me
  • Work With Me
  • Tools
    • DevOps Onboarding
    • AWS VPC Subnet Planner
    • Pag-IBIG Housing Loan Calculator
  • Contact
How to Send Email Using AWS SES with Cross-Account Secrets Manager

How to Send Email Using AWS SES with Cross-Account Secrets Manager

March 9, 2026August 1, 2025 by Linuxbeast
5 min read

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. Replace 123456789012 with Account B’s account ID and your-lambda-execution-role with the actual role name.
  • Action — only allows GetSecretValue, 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, only GetSecretValue.
  • Resource — the full ARN of the secret in Account A. Replace 111111111111 with 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 typically email-smtp.us-east-1.amazonaws.com and port 587 is 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.

Categories AWS Tags AWS SES, Boto3, Cross-Account, Email Automation, IAM, Lambda, Python, Secrets Manager
Understanding Lambda’s Event and Context Parameters in Python
How to Parse Custom Logs in Datadog Using Grok Rules
← PreviousUnderstanding Lambda’s Event and Context Parameters in PythonNext →How to Parse Custom Logs in Datadog Using Grok Rules

Related Articles

Solve 'yum command not found' in AWS Lambda Python 3.12 base image
AWS

Solve ‘yum: command not found’ in AWS Lambda Python 3.12+ Base Image

AWS

How to Deploy EC2 Ubuntu 22.04 LTS Instance on AWS

How to Automate MySQL Database Backups on EC2 to Amazon S3
AWS

How to Automate MySQL Database Backups on EC2 to Amazon S3

© 2026 Linuxbeast • Built with GeneratePress
Manage Consent
To provide the best experiences, we use technologies like cookies to store and/or access device information. Consenting to these technologies will allow us to process data such as browsing behavior or unique IDs on this site. Not consenting or withdrawing consent, may adversely affect certain features and functions.
Functional Always active
The technical storage or access is strictly necessary for the legitimate purpose of enabling the use of a specific service explicitly requested by the subscriber or user, or for the sole purpose of carrying out the transmission of a communication over an electronic communications network.
Preferences
The technical storage or access is necessary for the legitimate purpose of storing preferences that are not requested by the subscriber or user.
Statistics
The technical storage or access that is used exclusively for statistical purposes. The technical storage or access that is used exclusively for anonymous statistical purposes. Without a subpoena, voluntary compliance on the part of your Internet Service Provider, or additional records from a third party, information stored or retrieved for this purpose alone cannot usually be used to identify you.
Marketing
The technical storage or access is required to create user profiles to send advertising, or to track the user on a website or across several websites for similar marketing purposes.
  • Manage options
  • Manage services
  • Manage {vendor_count} vendors
  • Read more about these purposes
View preferences
  • {title}
  • {title}
  • {title}