AWS SES Email Configuration Guide ================================= This guide explains how to configure Amazon Simple Email Service (SES) for sending emails in production. Overview -------- The application supports two email backends: - **AWS SES** (recommended for production) - Reliable, scalable email service - **SMTP** (fallback) - Traditional SMTP server configuration AWS SES can be used with: - **IAM Role** (recommended for EC2/ECS/Lambda) - No credentials needed ⭐ - **IAM User Credentials** (for non-AWS servers) - Access keys required Prerequisites ------------- 1. An AWS account 2. AWS CLI installed (optional, for testing) 3. Verified email addresses or domain in AWS SES Step 1: Set Up AWS SES ---------------------- 1.1 Create an AWS Account ~~~~~~~~~~~~~~~~~~~~~~~~~ If you don’t have one, sign up at https://aws.amazon.com 1.2 Verify Your Email Address or Domain ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ **Option A: Verify Individual Email Addresses** (Quick start) 1. Go to `AWS SES Console `__ 2. Navigate to **Verified identities** → **Create identity** 3. Select **Email address** 4. Enter your email address (e.g., ``aclark@aclark.net``) 5. Click **Create identity** 6. Check your email and click the verification link 7. Repeat for any email addresses you want to send FROM **Option B: Verify Your Domain** (Recommended for production) 1. Go to `AWS SES Console `__ 2. Navigate to **Verified identities** → **Create identity** 3. Select **Domain** 4. Enter your domain (e.g., ``aclark.net``) 5. Follow the instructions to add DNS records (DKIM, SPF, DMARC) 6. Wait for verification (can take up to 72 hours) 1.3 Request Production Access ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ By default, AWS SES starts in **sandbox mode**, which only allows sending to verified addresses. To send to any email address: 1. In the SES Console, go to **Account dashboard** 2. Click **Request production access** 3. Fill out the form explaining your use case 4. AWS typically approves within 24 hours 1.4 Choose Your AWS Region ~~~~~~~~~~~~~~~~~~~~~~~~~~ Select a region close to your server for better performance: - ``us-east-1`` (N. Virginia) - Default, most features - ``us-west-2`` (Oregon) - ``eu-west-1`` (Ireland) - See `AWS SES Regions `__ for full list Step 2: Set Up IAM Permissions ------------------------------ You have two options for authentication: Option A: IAM Role (Recommended for EC2/ECS/Lambda) ⭐ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ **Best for**: Applications running on AWS infrastructure **Benefits**: - ✅ No credentials in your code or .env file - ✅ Automatic credential rotation (every hour) - ✅ Most secure option - ✅ No manual key management - ✅ Can’t accidentally leak credentials 2A.1 Create IAM Role ^^^^^^^^^^^^^^^^^^^^ 1. Go to `IAM Console `__ 2. Navigate to **Roles** → **Create role** 3. Select **AWS service** → **EC2** (or ECS/Lambda depending on your setup) 4. Click **Next** 2A.2 Attach SES Permissions ^^^^^^^^^^^^^^^^^^^^^^^^^^^ 1. Search for and select **AmazonSESFullAccess** (or create a custom policy - see below) 2. Click **Next** 3. Enter role name (e.g., ``aclarknet-ses-role``) 4. Click **Create role** 2A.3 Attach Role to EC2 Instance ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 1. Go to `EC2 Console `__ 2. Select your instance 3. **Actions** → **Security** → **Modify IAM role** 4. Select the role you created (``aclarknet-ses-role``) 5. Click **Update IAM role** **That’s it!** No credentials needed in your ``.env`` file. boto3 will automatically use the instance role. Option B: IAM User Credentials (For Non-AWS Servers) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ **Best for**: Applications running outside AWS (on-premises, other cloud providers) 2B.1 Create IAM User ^^^^^^^^^^^^^^^^^^^^ 1. Go to `IAM Console `__ 2. Navigate to **Users** → **Create user** 3. Enter username (e.g., ``aclarknet-ses-user``) 4. Click **Next** 2B.2 Attach SES Permissions ^^^^^^^^^^^^^^^^^^^^^^^^^^^ 1. Select **Attach policies directly** 2. Search for and select **AmazonSESFullAccess** (or create a custom policy - see below) 3. Click **Next** → **Create user** 2B.3 Create Access Keys ^^^^^^^^^^^^^^^^^^^^^^^ 1. Click on the newly created user 2. Go to **Security credentials** tab 3. Click **Create access key** 4. Select **Application running outside AWS** 5. Click **Next** → **Create access key** 6. **IMPORTANT**: Save the Access Key ID and Secret Access Key securely 7. You won’t be able to see the secret key again! Minimal IAM Policy (Recommended for Both Options) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Instead of ``AmazonSESFullAccess``, use this minimal policy for better security: .. code:: json { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "ses:SendEmail", "ses:SendRawEmail" ], "Resource": "*" } ] } To create a custom policy: 1. In IAM Console, go to **Policies** → **Create policy** 2. Click **JSON** tab and paste the policy above 3. Click **Next** → Enter name (e.g., ``SESMinimalSendPolicy``) 4. Click **Create policy** 5. Attach this policy to your role or user instead of ``AmazonSESFullAccess`` Step 3: Configure Environment Variables --------------------------------------- If Using IAM Role (Option A) - Recommended ⭐ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Edit your production ``.env`` file (e.g., ``/srv/aclarknet/.env``): .. code:: bash # Enable AWS SES USE_SES=True # AWS SES Region (required) AWS_SES_REGION_NAME=us-east-1 # NO credentials needed! boto3 will use the EC2 instance role # Leave AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY commented out or empty # Optional: Configuration set for tracking (leave empty if not using) AWS_SES_CONFIGURATION_SET= # Use SES v2 API (recommended) USE_SES_V2=True **That’s it!** The application will automatically use the IAM role attached to your EC2 instance. If Using IAM User Credentials (Option B) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Edit your production ``.env`` file: .. code:: bash # Enable AWS SES USE_SES=True # AWS Credentials (from Step 2B.3) AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY # AWS SES Region (use the region where you verified your email/domain) AWS_SES_REGION_NAME=us-east-1 # Optional: Configuration set for tracking (leave empty if not using) AWS_SES_CONFIGURATION_SET= # Use SES v2 API (recommended) USE_SES_V2=True **Security Note**: Never commit these credentials to version control! Step 4: Test Your Configuration ------------------------------- Test from Django Shell ~~~~~~~~~~~~~~~~~~~~~~ .. code:: bash cd /srv/aclarknet sudo -u nginx /srv/aclarknet/.venv/bin/python manage.py shell --settings=aclarknet.settings.production .. code:: python from django.core.mail import send_mail send_mail( 'Test Email from AWS SES', 'This is a test email sent via AWS SES with IAM role authentication.', 'aclark@aclark.net', # Must be verified in SES ['recipient@example.com'], # Must be verified if in sandbox mode fail_silently=False, ) If successful, you should see no errors and receive the email. Test with AWS CLI (Optional) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code:: bash aws ses send-email \ --from aclark@aclark.net \ --destination ToAddresses=recipient@example.com \ --message Subject={Data="Test Email",Charset=utf8},Body={Text={Data="Test message",Charset=utf8}} \ --region us-east-1 Verify IAM Role is Working (EC2 Only) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ SSH into your EC2 instance and check: .. code:: bash # Check if instance has an IAM role attached curl http://169.254.169.254/latest/meta-data/iam/security-credentials/ # Should output the role name, e.g., "aclarknet-ses-role" # Get temporary credentials (these auto-rotate) curl http://169.254.169.254/latest/meta-data/iam/security-credentials/aclarknet-ses-role # Should output JSON with AccessKeyId, SecretAccessKey, Token, and Expiration Step 5: Monitoring and Troubleshooting -------------------------------------- Monitor Email Sending ~~~~~~~~~~~~~~~~~~~~~ 1. **SES Console Dashboard** - Go to `SES Console `__ - View sending statistics, bounces, and complaints 2. **CloudWatch Metrics** - Monitor delivery rates, bounces, complaints - Set up alarms for high bounce rates 3. **Django Logs** - Check ``/srv/aclarknet/logs/django.log`` for email errors Common Issues ~~~~~~~~~~~~~ Issue: “Unable to locate credentials” ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ **Solution**: - **If using IAM role**: Verify the role is attached to your EC2 instance ``bash curl http://169.254.169.254/latest/meta-data/iam/security-credentials/`` - **If using IAM user**: Check that ``AWS_ACCESS_KEY_ID`` and ``AWS_SECRET_ACCESS_KEY`` are set in ``.env`` Issue: “Email address is not verified” ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ **Solution**: Verify the sender email address in SES Console, or request production access to send to any address. Issue: “Access Denied” or “InvalidClientTokenId” ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ **Solution**: - **If using IAM role**: Verify the role has SES permissions (check IAM policy) - **If using IAM user**: Check that AWS credentials are correct and not expired Issue: Emails going to spam ^^^^^^^^^^^^^^^^^^^^^^^^^^^ **Solution**: - Verify your domain (not just email address) - Set up SPF, DKIM, and DMARC records - Use a verified domain for the FROM address - Avoid spam trigger words in subject/body Issue: “Daily sending quota exceeded” ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ **Solution**: - Check your sending limits in SES Console - Request a limit increase if needed - Implement rate limiting in your application Sending Limits ~~~~~~~~~~~~~~ **Sandbox Mode:** - 200 emails per 24 hours - 1 email per second - Can only send to verified addresses **Production Mode:** - Starts at 50,000 emails per 24 hours (can be increased) - 14 emails per second (can be increased) - Can send to any address Step 6: Advanced Configuration ------------------------------ Configuration Sets (Optional) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Configuration sets allow you to track email events (opens, clicks, bounces): 1. Create a configuration set in SES Console 2. Set up event destinations (SNS, CloudWatch, Kinesis) 3. Add to your ``.env``: .. code:: bash AWS_SES_CONFIGURATION_SET=my-config-set Custom Email Headers ~~~~~~~~~~~~~~~~~~~~ You can add custom headers in your Django email code: .. code:: python from django.core.mail import EmailMessage email = EmailMessage( 'Subject', 'Body', 'from@example.com', ['to@example.com'], headers={'X-SES-CONFIGURATION-SET': 'my-config-set'}, ) email.send() Bounce and Complaint Handling ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Set up SNS notifications for bounces and complaints: 1. Create SNS topics for bounces and complaints 2. Configure SES to publish to these topics 3. Subscribe your application endpoint to handle notifications 4. Automatically remove bounced/complained addresses from your mailing list Switching Between SES and SMTP ------------------------------ To switch back to SMTP (e.g., for testing): .. code:: bash # In your .env file USE_SES=False # Configure SMTP settings EMAIL_HOST=smtp.gmail.com EMAIL_PORT=587 EMAIL_USE_TLS=True EMAIL_HOST_USER=your-email@gmail.com EMAIL_HOST_PASSWORD=your-app-password Cost Estimation --------------- AWS SES Pricing (as of 2026): - First 62,000 emails per month: **FREE** (when sent from EC2) - After that: $0.10 per 1,000 emails - Attachments: $0.12 per GB Example: Sending 100,000 emails/month = ~$3.80/month See `AWS SES Pricing `__ for current rates. Security Best Practices ----------------------- For IAM Roles (Recommended) ~~~~~~~~~~~~~~~~~~~~~~~~~~~ 1. ✅ **Use IAM roles** when running on AWS infrastructure 2. ✅ **Apply least privilege** - use minimal IAM policy (see Step 2) 3. ✅ **No credentials to manage** - automatic rotation 4. ✅ **Enable MFA** on your AWS account 5. ✅ **Monitor CloudTrail** logs for suspicious activity 6. ✅ **Set up billing alerts** to detect unusual usage For IAM User Credentials ~~~~~~~~~~~~~~~~~~~~~~~~ 1. ⚠️ **Never commit credentials** to version control 2. ⚠️ **Rotate access keys** regularly (every 90 days) 3. ⚠️ **Use least privilege** IAM policies 4. ⚠️ **Store in secrets manager** (not .env file) for production 5. ⚠️ **Enable MFA** on your AWS account 6. ⚠️ **Monitor CloudTrail** logs for suspicious activity 7. ⚠️ **Set up billing alerts** to detect unusual usage Credential Rotation (IAM User Only) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ If using IAM user credentials, rotate them regularly: .. code:: bash # 1. Create new access key in IAM Console # 2. Update .env with new credentials # 3. Restart application sudo systemctl restart aclarknet.service # 4. Test email sending # 5. Delete old access key in IAM Console **Note**: With IAM roles, credentials auto-rotate every hour - no manual rotation needed! Comparison: IAM Role vs IAM User -------------------------------- ======================= ========================= ================== Feature IAM Role (EC2/ECS/Lambda) IAM User (Non-AWS) ======================= ========================= ================== **Credentials in .env** ❌ No ✅ Yes **Auto-rotation** ✅ Every hour ❌ Manual **Security** ⭐⭐⭐⭐⭐ ⭐⭐⭐ **Setup Complexity** Easy Easy **Credential Leaks** ✅ Impossible ⚠️ Possible **Works on AWS** ✅ Yes ✅ Yes **Works off AWS** ❌ No ✅ Yes **Recommended** ✅ Yes (on AWS) ✅ Yes (off AWS) ======================= ========================= ================== Additional Resources -------------------- - `AWS SES Documentation `__ - `django-ses Documentation `__ - `AWS SES Best Practices `__ - `Email Deliverability Guide `__ - `IAM Roles for EC2 `__ - `AWS Security Best Practices `__ Support ------- For issues specific to this application, check: - Application logs: ``/srv/aclarknet/logs/django.log`` - Django settings: ``aclarknet/settings/production.py`` - Environment variables: ``/srv/aclarknet/.env`` For AWS SES issues: - `AWS Support `__ - `AWS SES Forum `__ Quick Reference --------------- Minimal .env for IAM Role (Recommended on EC2) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code:: bash USE_SES=True AWS_SES_REGION_NAME=us-east-1 # No credentials needed! Minimal .env for IAM User (Non-AWS servers) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code:: bash USE_SES=True AWS_ACCESS_KEY_ID=your-key-id AWS_SECRET_ACCESS_KEY=your-secret-key AWS_SES_REGION_NAME=us-east-1 Test Email Sending ~~~~~~~~~~~~~~~~~~ .. code:: python from django.core.mail import send_mail send_mail('Test', 'Message', 'from@example.com', ['to@example.com']) Check IAM Role (EC2 only) ~~~~~~~~~~~~~~~~~~~~~~~~~ .. code:: bash curl http://169.254.169.254/latest/meta-data/iam/security-credentials/