🐥

AWSでメール誤送信を防ぐ 3 層+α ガード

に公開

うっかり本番ユーザーにテストメールを送る事故を防ぐための調査メモ。
結論だけ欲しいなら TL;DR へ。


TL;DR

レイヤー やること 使うサービス
静的ガード コードに残った 【TEST】 などを CI で grep してビルドを落とす GitHub Actions 等
リアルタイムガード SES → SNS → Lambda で件名・タグを検査。NGならthrowして送信を失敗させる SES / SNS / Lambda
サーキットブレーカ 1 分あたり送信数がしきい値を超えたらSES全体を自動停止 CloudWatch Alarm → SNS → Lambda (ses:PutAccountSendingAttributes)
+α 開発環境から本番SESキーを使えないようにする IAM / Secrets Manager / AWS SSO

コストは従量課金。月数円で済むはず


誤送信が起きる定番パターン

  1. テスト件名/本文を残したままデプロイ
  2. 開発PCやCIが本番のAWSキーを使う
  3. バグやループで短時間に大量メールを出す

3 層ガードの実装

1. 静的ガード

# .github/workflows/mail_guard.yml
- name: Grep TEST mails
  run: |
    grep -R --line-number "【TEST】" ./templates && exit 1 || echo "OK"

本番用ブランチだけに仕込もう


2. リアルタイムガード

  1. SES Configuration Set を作成し send イベントを SNS Topic へ。
  2. SNS → Lambda で件名や Environment=staging タグをチェック。
  3. 条件NGなら throw new Error()
export const handler = async (event) => {
  const msg  = JSON.parse(event.Records[0].Sns.Message);
  const subj = msg.mail.commonHeaders.subject ?? '(no subject)';

  if (/【TEST】/i.test(subj)) {
    await notifySlack('🚨 TEST メールをブロック', msg);
    throw new Error('block');
  }

  await notifySlack('📧 正常送信', msg);
};

3. サーキットブレーカ

CloudWatchで Send メトリクスを 1 分ごとに監視。50 通 / 分を超えたら送信停止。

import boto3
ses = boto3.client('sesv2')

def lambda_handler(event, context):
    ses.put_account_sending_attributes(SendingEnabled=False)

復旧は手動で SendingEnabled=True


+α: 本番キーを開発で使わせない

ステップ やること 効果
1 prod / dev で AWSアカウントまたはIAMロールを分割 キーを物理的に渡せない
2 IAM条件付きポリシーで Environment=prod 以外は ses:SendEmail を拒否 キーをコピーしてもAPIが403
3 Secrets Manager を環境別に管理し、GitHub環境保護ルールで dev ジョブに prod シークレットを注入禁止 CI 渡さない / 受け取らない

アプリ側でできること

  1. Mail ドライバをスタブ化 — 非本番ならMailhog/Mailtrap。
  2. テストドメインで即 returnif (to.endsWith('@test.example.com')) return;
  3. 件名プリフィックス自動付与 — 非本番なら ★STG★【Local】 を件名先頭に。
  4. テンプレート Lintdummy@example.comTODO: が残っていないか Pre‑commit でチェック。
  5. 送信リハーサル — 本番と同量をMailtrapに送り、OKならフラグ変更で本番送信。

Discussion