An Automatic Kill Switch for Amazon SES
Sending stopped on its own, every site on a new key
Read case studyAmazon SES is cheap, fast, and easy to wire into any application, and that ease is also the risk. A leaked access key, a job stuck in a loop, or a bad mailing list can send thousands of emails before anyone notices. By the time someone does, the bounce and complaint rates have already hurt the account's reputation, and AWS may pause or review the account on its own terms.
SES Guard is a small stack I built to put a circuit breaker in front of that risk. When sending looks wrong, it stops SES in that region, keeps every event for investigation, and tells a person exactly who was sending. This note explains how it works and why I built it this way.
SES gives you sending quotas and reputation metrics, but no kill switch that fires on its own. The usual failure looks like one of these:
In every case the damage grows each minute nobody reacts. And when someone does react, the first question is always the same: which key, from which IP, with which sender address? Without logs, that is very hard to answer after the fact.
The whole thing is one CloudFormation template plus a few shell scripts. It is deployed once in every region where SES is used, since sending, metrics, and quotas are all regional in SES.
Logging
Every identity is attached to a configuration set so its events are captured.
ses-guard configuration set publishes email events to the default EventBridge bus.aws.ses events to a CloudWatch log group.Retain deletion policy, so the evidence survives even if the stack is deleted.Detection
Plain CloudWatch alarms on the metrics SES already publishes.
Response
All alarms publish to one SNS topic, which invokes a small Python Lambda.
PutAccountSendingAttributes with sending disabled. This is the actual stop.Watchdog
A guard that can fail silently is worse than no guard, because people believe they are protected.
The automation ends at the stop. Resuming is always a person’s decision, made after someone has read the logs.
Day-to-day work goes through one script, ses-guard.sh. Each command takes a region or all.
| Command | What it does |
|---|---|
status |
Shows whether auto stop is on, whether SES is sending, alarm states, and unconfirmed email subscriptions. |
top |
Lists the top senders by IAM user, IP, and from address for the last N minutes. |
logs |
Tails raw email events in a compact, readable format. |
stop |
Emergency stop. Asks you to type the region name to confirm. |
resume |
Turns sending back on and re-arms the alarms. |
deploy |
Deploys or updates the stack in every configured region, in live or test mode. |
setup |
Connects new SES identities to logging. |
Secrets such as the Telegram token and the PagerDuty key live in a local env file that git ignores, and they reach CloudFormation as NoEcho parameters. The per-region send limits live in the same file, so each region can have a threshold that matches its normal volume.
I don’t trust a safety system I have never seen fire, so the script has three test commands, each with a different level of impact.
Alert test
Switches auto stop off for a moment, forces the send spike alarm into ALARM, waits for the responder, and then switches auto stop back on. It confirms that Telegram, PagerDuty, and email all arrive without touching real sending. A trap restores auto stop even if the script is interrupted halfway.
Stop test
Fires the alarm for real and checks that sending stopped. It asks you to type the region name first, then offers to resume right away.
Watchdog test
Forces the Lambda error alarm into ALARM and back, to confirm the “responder failed” path reaches a person.
There is also a test mode for the whole stack. Deploying with AutoStop=false keeps every alert but never stops SES, which is useful for the first few days while tuning thresholds against real traffic.
Stop first, ask later
Pausing legitimate email for a few minutes is cheap. Letting a compromised key run for an hour is expensive. So the guard leans toward stopping.
Stop the region, not the key
Disabling sending for the whole region is blunt, but it works however the emails are being sent, whether through SMTP credentials, an API key, or a role. Narrowing it down is a person’s job, with the logs in hand.
Re-arm alarms on resume
CloudWatch alarm actions only run when the state changes. If an alarm stayed in ALARM after an incident, the next breach would trigger nothing, and SES would be unguarded without anyone knowing. So resume sets every guard alarm back to OK. The reset uses a marked reason, and the Lambda ignores those transitions so it doesn’t send a misleading “back to normal” message.
Keep the evidence
The log group is retained when the stack is deleted or replaced, because the logs are the only way to answer “who sent this” afterward.
Keep it small
One template, an inline Lambda, and plain shell scripts. There is no build step and nothing to install beyond the AWS CLI and jq. Something that runs rarely should be easy to read when it finally does.
Build the response before the incident
The most useful parts of the alert are the list of suspects and the exact commands to run next. Deciding those in advance is what makes the response fast. It is the same idea as rollback in A Practical Deployment Pipeline.
Guard the guard
Ask one question of every safety system: what happens if it fails? Without the watchdog, the answer here would be “nothing, silently.”
Make testing a command
When testing is one command with a safe default, it actually gets done. Running the alert test after each deploy checks every channel end to end, including email subscriptions nobody confirmed.
SES Guard is a few alarms, one function, and some scripts. Before it, a problem like this was discovered by angry recipients or an AWS review. Now it is stopped within minutes and explained in a single message.
Sending stopped on its own, every site on a new key
Read case studyNext step
Tell me what you have and what you need. I usually reply within one working day with how I'd approach it.