Introduction

這篇筆記一開始會先從如何手動透過 SES 發信開始,接著會介紹如何設定 SPF 和 DKIM。 邏輯是著手設定 SPF/DKIM 之前先看看目前 E-Mail 的狀態。

在開始之前建議先了解一下 E-Mail 寄送過程 An E-Mail’s Journey

email-jourany

▲ 圖片來源 (大推這篇文章): [Cymetrics] 關於 email security 的大小事 — 原理篇

Identity References in a Message

▲ 圖片來源: [Cymetrics] 關於 email security 的大小事 — 原理篇, RFC

透過 CLI 請 AWS SES 發信 (Using the command line to send email using the Amazon SES SMTP interface)

首先我們先測試一下 client 是否能碰到 AWS SES。Testing your connection to the Amazon SES SMTP interface

1
openssl s_client -crlf -quiet -starttls smtp -connect email-smtp.us-west-2.amazonaws.com:587

其中 us-west-2 請自行替換成你的 AWS SES SMTP endpoint region。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
Connecting to 52.13.3.84
depth=2 C=US, O=Amazon, CN=Amazon Root CA 1
verify return:1
depth=1 C=US, O=Amazon, CN=Amazon RSA 2048 M01
verify return:1
depth=0 CN=email-smtp.us-west-2.amazonaws.com
verify return:1
250 Ok  <=== 這個代表連線成功
451 4.4.2 Timeout waiting for data from client.
read:errno=0

確認網路沒有障礙後,再來需要一個 SES SMTP 的帳號密碼才能使用 SES 發信,就跟需要 Google 帳號密碼登入 Gmail 一樣。 Obtaining Amazon SES SMTP credentialsSMTP password 與 IAM secret key 不同,同一組 credentials 不能跨 AWS Region。

雖然說 IAM secret key 不能直接拿來當 SES credentials,但 IAM secret key 卻是產生 SES credential 必要的參數。 Obtaining SES SMTP credentials by converting existing AWS credentials

send email via ses 所需的 IAM policy

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "ses:SendRawEmail",
            "Resource": "*"
        }
    ]
}
  1. 首先,產生出 SES SMTP credentials
1
2
3
# Executing Linux Commands Without Storing Them in History.
# Method 1: By Prefixing with Space
 ./smtp_credentials_generate.py <IAM secret key> <REGION>
  1. 發信 shell script,username 是 IAM access key,password 是剛剛透過 smtp_credentials_generate.py 產生的 SES SMTP password
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
./send-email-via-ses.sh

Enter SMTP username <accesskey>:
Enter SMTP password:
Sender email address [non-exist@xxzk.me]:
Receiver email address: a@b.com
Email subject [TEST Email, Send Via AWS SES]:
Message to send [This is a test email]:
AWS Region Code [us-west-2]:

Connecting to 54.185.234.152
depth=2 C=US, O=Amazon, CN=Amazon Root CA 1
verify return:1
depth=1 C=US, O=Amazon, CN=Amazon RSA 2048 M01
verify return:1
depth=0 CN=email-smtp.us-west-2.amazonaws.com
verify return:1
250 Ok
250-email-smtp.amazonaws.com
250-8BITMIME
250-STARTTLS
250-AUTH PLAIN LOGIN
250 Ok
334 xxzzzzxxxxx
334 xxxxxzzzzzz
235 Authentication successful. <=== 登入 SES 成功
250 Ok
250 Ok
354 End data with <CR><LF>.<CR><LF>
554 Message rejected: Email address is not verified. The following identities failed the check in region US-WEST-2: non-exsit@xxzk.me
451 4.4.2 Timeout waiting for data from client.
read:errno=0

可以看到 554 Message rejected: Email address is not verified. The following identities failed the check in region US-WEST-2: non-exist@xxzk.me 錯誤,因為 no-exist@xxzk.me 不在 SES Identities 當中。將 Sender email address 修正後即可排除。

SPF (Authenticating Email with SPF in Amazon SES)

SPF 驗證 sender E-Mail server 是否在 SPF record 的白名單內。

攻擊手法: 阿貓阿狗都能自架 E-Mail server 發信,只有 SPF 不夠安全。

Messages that you send through Amazon SES automatically use a subdomain of amazonses.com as the default MAIL FROM domain.

透過 AWS SES 發信時,預設的 MAIL FROM (SMTP layer) 是 amazonses.com,AWS 已經幫我們設定好 SPF record 了,所以不用擔心 SPF 的問題。FROM (message header field) 的 domain 可以 denied all。

1
2
3
4
dig TXT amazonses.com +noall +answer


amazonses.com.          29      IN      TXT     "v=spf1 ip4:199.255.192.0/22 ip4:199.127.232.0/22 ip4:54.240.0.0/18 ip4:69.169.224.0/20 ip4:23.249.208.0/20 ip4:23.251.224.0/19 ip4:76.223.176.0/20 ip4:54.240.64.0/19 ip4:54.240.96.0/19 ip4:76.223.128.0/19 ip4:216.221.160.0/19 ip4:206.55.144.0/20 -all"

However, if you don’t want to use the SES default MAIL FROM domain, and would rather use a subdomain of a domain that you own, this is referred to in SES as using a custom MAIL FROM domain.

當然,AWS SES 也支援使用自己的 domain 作為 MAIL FROMUsing a custom MAIL FROM domain

既然 MAIL FROM (SMTP layer) 不會是公司的 domain,那就全擋吧 ~

1
v=spf1 -all

mxtoolbox.com 線上服務可以幫你檢查 SPF record 是否正確。

SPF 茶包射手

InvalidChangeBatch 400: RRSet of type TXT with DNS name sub.r.com. is not permitted because a conflicting RRSet of type CNAME with the same DNS name already exists in zone r.com.

Error: InvalidChangeBatch 400: RRSet of type CNAME with DNS name test.example.com. is not permitted as it conflicts with other records with the same DNS name in zone | AWS re:Post

AWS Route53 不允許同一個 domain 同時有 CNAME 和 TXT/MX record (CloudFlare 就不會,沒研究什麼原因),原本 sub.r.com CNAME 到 CloudFront,現在必需改成 A record 搭配 Alias,方能上 SPF 的 TXT record。

DKIM

基本上 AWS SES DKIM 是一鍵開啟,完全不用擔心什麼,就連更改 key bit 1024 -> 2048 也是一鍵完成。

DKIM DNS records points to expired and 1024 bit keys when we selected 2048 bit key | AWS re:Post

DMARC

預設值即可,因為是使用託管 Mail server (AWS SES)。 (正常一點的 Mail server 看到 MailFrom (smtp layer) 是 amazonaws.com + SPF passed + DKIM passed,這 credit 應該夠高了吧?)

1
"v=DMARC1; p=none;"

smtp_credentials_generate.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
#!/usr/bin/env python3

import hmac
import hashlib
import base64
import argparse

SMTP_REGIONS = [
    "us-east-2",  # US East (Ohio)
    "us-east-1",  # US East (N. Virginia)
    "us-west-2",  # US West (Oregon)
    "ap-south-1",  # Asia Pacific (Mumbai)
    "ap-northeast-2",  # Asia Pacific (Seoul)
    "ap-southeast-1",  # Asia Pacific (Singapore)
    "ap-southeast-2",  # Asia Pacific (Sydney)
    "ap-northeast-1",  # Asia Pacific (Tokyo)
    "ca-central-1",  # Canada (Central)
    "eu-central-1",  # Europe (Frankfurt)
    "eu-west-1",  # Europe (Ireland)
    "eu-west-2",  # Europe (London)
    "eu-south-1",  # Europe (Milan)
    "eu-north-1",  # Europe (Stockholm)
    "sa-east-1",  # South America (Sao Paulo)
    "us-gov-west-1",  # AWS GovCloud (US)
    "us-gov-east-1",  # AWS GovCloud (US)
]

# These values are required to calculate the signature. Do not change them.
DATE = "11111111"
SERVICE = "ses"
MESSAGE = "SendRawEmail"
TERMINAL = "aws4_request"
VERSION = 0x04


def sign(key, msg):
    return hmac.new(key, msg.encode("utf-8"), hashlib.sha256).digest()


def calculate_key(secret_access_key, region):
    if region not in SMTP_REGIONS:
        raise ValueError(f"The {region} Region doesn't have an SMTP endpoint.")

    signature = sign(("AWS4" + secret_access_key).encode("utf-8"), DATE)
    signature = sign(signature, region)
    signature = sign(signature, SERVICE)
    signature = sign(signature, TERMINAL)
    signature = sign(signature, MESSAGE)
    signature_and_version = bytes([VERSION]) + signature
    smtp_password = base64.b64encode(signature_and_version)
    return smtp_password.decode("utf-8")


def main():
    parser = argparse.ArgumentParser(
        description="Convert a Secret Access Key to an SMTP password."
    )
    parser.add_argument("secret", help="The Secret Access Key to convert.")
    parser.add_argument(
        "region",
        help="The AWS Region where the SMTP password will be used.",
        choices=SMTP_REGIONS,
    )
    args = parser.parse_args()
    print(calculate_key(args.secret, args.region))


if __name__ == "__main__":
    main()

send-email-via-ses.sh

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#!/bin/bash

# Prompt user to provide following information
read -p "Enter SMTP username <accesskey>: " SMTPUsername
read -p "Enter SMTP password: " SMTPPassword
read -p "Sender email address: " MAILFROM
read -p "Receiver email address: " RCPT
read -p "Email subject [TEST Email, Send Via AWS SES]: " SUBJECT
SUBJECT=${SUBJECT:-TEST Email, Send Via AWS SES}
read -p "Message to send [This is a test email]: " DATA
DATA=${DATA:-This is a test email.}
read -p "AWS Region Code [us-west-2]: " REGION
REGION=${REGION:-us-west-2}

echo

# Encode SMTP username and password using base64
EncodedSMTPUsername=$(echo -n "$SMTPUsername" | openssl enc -base64)
EncodedSMTPPassword=$(echo -n "$SMTPPassword" | openssl enc -base64)

# Construct the email
Email="EHLO example.com
AUTH LOGIN
$EncodedSMTPUsername
$EncodedSMTPPassword
MAIL FROM: $MAILFROM
RCPT TO: $RCPT
DATA
From: $MAILFROM
To: $RCPT
Subject: $SUBJECT

$DATA
.
QUIT"

echo "$Email" | openssl s_client -crlf -quiet -starttls smtp -connect email-smtp.${REGION}.amazonaws.com:587