SMTP認証に対応していないクライアントからlocalhostのpostfixをリレーしてAmazon SESを利用する

2023.08.14

初めに

現行のSMTPサーバを廃止しAmazon SESへの切り替えをご相談いただくケースにおいて現行ではSMTP認証を利用しておらずIP制限のみで対応しているというケースは一定お伺いすることのあるケースである印象です。

しかしながらAmazon SESでのメール送信にIP制限を行うことは可能ですがSMTP認証およびTLS通信が必須となっているため、どうしてもアプリ(クライアント)側の改修対応が必要で移行を断念せざるを得ないということもあります。

今回はそれの解決手段の1つの選択肢としてアプリ側からの送信先として同一サーバ上(localhost)のpostifxを指定し、postfix側で認証情報を付与しリレーすることでAmazon SESを使うという方法を紹介します。

解決できる問題・解決できない問題

今回紹介する方法を使うことでアプリ側のコード変更なしにAmazon SES等のSMTP認証必須のサーバを利用することができるのでメールサーバの選択肢も大きく広がります。
また仲介するのはlocalhost上のpostfixなので認証なしのポート25の解放というリスクも減らすことができ、冗長性に関しても良くも悪くもpostfixとアプリは同一サーバのためマシン単位の障害であれば一蓮托生です。

例えば現状で独自のメールサーバ稼働している状態でその可用性やスケーリング、維持のためのサーバコストに対して課題があるのであればこの方法はそれを解決する術となるかもしれません。
アプリが対応できた順からlocalhost上でのリレーをやめ徐々にAmazon SESに直接繋ぎに行く方法を取っても良いでしょう。

一方で各サーバのpostfixの設定は一定量必要であり完全にMTAの管理からは逃れることはできません。
(localhost内のリレーかつ最終的にユーザに送信するポイントではないので多少設定量は減るかと思いますが)

むしろAPサーバ側のpostfixを利用しない管理になっていたところを手を入れるようになるため利用・管理状況になっては煩雑になる可能性もあるでしょう。

この方法を利用する前には自分の環境で最も問題となっていることは何か、利用することで解決できることがデメリットに以上のものか(もしくは今後のためになるのか)を立ち止まって考えていただければと思います。

https://docs.aws.amazon.com/ja_jp/ses/latest/dg/security-protocols.html
STARTTLS 接続の場合、Amazon SES は TLS 1.2、TLS 1.3、および SSLv2Hello をサポートします。
...
TLS ラッパー接続の場合、Amazon SES は TLS 1.2 をサポートします。

また、Amazon SESに対しての通信はTLSv1.2もしくはTLSv1.3での通信が必要となります。

アプリ側の問題ではなくpostfixやopensslといったミドルウェア自体が対応していない場合には適用できませんのでご注意ください。

実証環境

Amazon Linux 2上で試します。

$ cat /etc/os-release
NAME="Amazon Linux"
VERSION="2"
ID="amzn"
ID_LIKE="centos rhel fedora"
VERSION_ID="2"
PRETTY_NAME="Amazon Linux 2"
ANSI_COLOR="0;33"
CPE_NAME="cpe:2.3:o:amazon:amazon_linux:2"
HOME_URL="https://amazonlinux.com/"
$ postconf | grep mail_version
mail_version = 2.10.1
$ openssl version
OpenSSL 1.0.2k-fips  26 Jan 2017

postfixの設定はデフォルト(のはず)です。

設定

Amazon SESには特段特別な設定はしないのでここでは省略します。

趣旨自体は異なりますがマネジメントコンソール上でのSMTP認証情報の発行は先日以下の記事の一部として記載しておりますので、こちらもご参照ください。

postfixの設定

以下の設定を/etc/postfix/main.cfに追記します。

/etc/postfix/main.cf

relayhost = [email-smtp.ap-northeast-1.amazonaws.com]:587
smtp_sasl_password_maps = hash:/etc/postfix/transport_auth
smtp_sasl_auth_enable = yes
smtp_sasl_security_options = noanonymous
smtp_tls_security_level = verify
# 今回の趣旨を守るためpostfixに接続するクライアントからの接続が間違ってもTLS有効/認証有にならないように無効化
# デフォルト値でもこの挙動であったはずではあるが念のため
smtpd_tls_security_level = none
smtpd_sasl_auth_enable = no

/etc/postfix/transport_authには以下のような設定を記載します。
usernamepasswordにはAmazon SESで作成した認証情報のユーザ名とパスワードを入れます。

/etc/postfix/transport_auth

[email-smtp.ap-northeast-1.amazonaws.com]:587 username:password

これをpostmapでDB化します。変更後はpostfixに読み込ませるためにreloadが必要です。

$ postmap /etc/postfix/transport_auth
$ service postfix reload

認証系は確か何かSASL周りのパッケージが必要だった記憶がありますがAmazon Linux 2の場合はデフォルトで(?)入っていました。

$ yum list cyrus-sasl*
Loaded plugins: extras_suggestions, langpacks, priorities, update-motd
223 packages excluded due to repository priority protections
Installed Packages
cyrus-sasl-lib.aarch64                                                                      2.1.26-24.amzn2                                                                   installed
cyrus-sasl-plain.aarch64                                                                    2.1.26-24.amzn2                                                                   installed

もし認証周りの挙動で怪しそうであればこの辺りを確認してみてください。

送信

telnetコマンドで直接SMTPで対話して送信します。

単発でなければmailxコマンド等メール送信用のコマンドを入れた方が楽です。

# example.comは実際には自分の保持するドメインを入れています。
$ telnet localhost 25
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
220 ip-xxx-xxx-xxx-xxx.ap-northeast-1.compute.internal ESMTP Postfix
HELO mail.example.com
250 ip-xxx-xxx-xxx-xxx.ap-northeast-1.compute.internal
MAIL FROM: postfix@example.com
250 2.1.0 Ok
RCPT TO: receive@example.com
250 2.1.5 Ok
DATA
354 End data with <CR><LF>.<CR><LF>
From: postfix@example.com
To: receive@example.com
Subject: from ses

Im from SES
.
250 2.0.0 Ok: queued as CF31DCBABEF
QUIT
221 2.0.0 Bye
Connection closed by foreign host.

送信した際のpostfixのログは以下のとおりです。

Aug 14 03:09:41 ip-xxx-xxx-xxx-xxx postfix/smtpd[19249]: connect from localhost[127.0.0.1]
Aug 14 03:09:55 ip-xxx-xxx-xxx-xxx postfix/smtpd[19249]: CF31DCBABEF: client=localhost[127.0.0.1]
Aug 14 03:10:02 ip-xxx-xxx-xxx-xxx postfix/cleanup[19230]: CF31DCBABEF: message-id=<20230814030955.CF31DCBABEF@ip-xxx-xxx-xxx-xxx.ap-northeast-1.compute.internal>
Aug 14 03:10:02 ip-xxx-xxx-xxx-xxx postfix/qmgr[3428]: CF31DCBABEF: from=<postfix@example.com>, size=445, nrcpt=1 (queue active)
Aug 14 03:10:03 ip-xxx-xxx-xxx-xxx postfix/smtp[19257]: CF31DCBABEF: to=<receive@example.com>, relay=email-smtp.ap-northeast-1.amazonaws.com[54.64.1.120]:587, delay=13, delays=13/0.02/0.14/0.2, dsn=2.0.0, status=sent (250 Ok 01060189f205a36a-b6171201-92c9-462b-b019-3403a5464943-000000)
Aug 14 03:10:03 ip-xxx-xxx-xxx-xxx postfix/qmgr[3428]: CF31DCBABEF: removed
Aug 14 03:10:05 ip-xxx-xxx-xxx-xxx postfix/smtpd[19249]: disconnect from localhost[127.0.0.1]

受信したメールは以下のとおりです。
自宅のメールサーバが諸事情で止まっているためAmazon SES(us-east-1) + Amazon S3で受信してます。

$ cat 8tpor8aqevgfuplvt7409odt534no0ecvqd0gdg1
Return-Path: <01060189f205a36a-b6171201-92c9-462b-b019-3403a5464943-000000@ap-northeast-1.amazonses.com>
Received: from e234-12.smtp-out.ap-northeast-1.amazonses.com (e234-12.smtp-out.ap-northeast-1.amazonses.com [23.251.234.12])
 by inbound-smtp.us-east-1.amazonaws.com with SMTP id 8tpor8aqevgfuplvt7409odt534no0ecvqd0gdg1
 for receive@example.com;
 Mon, 14 Aug 2023 03:10:35 +0000 (UTC)
X-SES-Spam-Verdict: PASS
X-SES-Virus-Verdict: PASS
Received-SPF: pass (spfCheck: domain of ap-northeast-1.amazonses.com designates 23.251.234.12 as permitted sender) client-ip=23.251.234.12; envelope-from=01060189f205a36a-b6171201-92c9-462b-b019-3403a5464943-000000@ap-northeast-1.amazonses.com; helo=e234-12.smtp-out.ap-northeast-1.amazonses.com;
Authentication-Results: amazonses.com;
 spf=pass (spfCheck: domain of ap-northeast-1.amazonses.com designates 23.251.234.12 as permitted sender) client-ip=23.251.234.12; envelope-from=01060189f205a36a-b6171201-92c9-462b-b019-3403a5464943-000000@ap-northeast-1.amazonses.com; helo=e234-12.smtp-out.ap-northeast-1.amazonses.com;
 dkim=pass header.i=@example.com;
 dkim=pass header.i=@amazonses.com;
 dmarc=pass header.from=example.com;
X-SES-RECEIPT: AEFBQUFBQUFBQUFIVXdPa2dtMFozUnZ3S2diOG9QWkw0U0hNVXIzUjNFMnJYU0I5OXg4bmNiOW0vZ1ZjbTFBRWNvcGR2bE1KWER4N3M3YjduVzgzMDBHWnJGMXFDWm9WWUVySnJkSTBRV2M2RUlaWnlia1hYOWllZENudGVjK25EQVMxTmpvdDlMaDVRQlZ3TnYyUVZCMWpVcEdHL1BaRVVXVnYycmIzbC9BdS9EWEFBTGpiMTlKZmpjcWpRTUxYMFAwSEEwQlN3d2huWWtCNmZvdEE0bS8xTTZ1dlFtMXgxQ21HemVqZE1GcDV0bEFHVkk3clRua3dJREcxTHhvaGdZeVJyWEhBZHNRT1JoamxFSG12MzdQZzY5YjQyMGJMeDhqbXBLSjRaRjhIV1Z0bXZtbk9BT09rM0Z6UXVsSE4rdU1NU1g3SEJPdkxJWDcyWFR6cmh6TVM1eHIyYzM1YkdOdlVDenRHaERHbjkyY0hoUGUzRW5Hall2OGQ3NHlPTVJWZ1VzN1QvSjd3PQ==
X-SES-DKIM-SIGNATURE: a=rsa-sha256; q=dns/txt; b=PVyIOOfAnvqLXKgX7bMu5TFXrxJc6tSpvT4qSkUBU951SUN/wIbRi8v1k7VuJmqbUppBNOKhWFaRu4xH7eoj7Iz8tc43swXr3aZUAPyc62Ep1tXzc1NSKWq6rcX9EJUg8YaD53I3Iwk5oxUCuqecMPDF8/WqqiVlA0jYDIifM04=; c=relaxed/simple; s=224i4yxa5dv7c2xz3womw6peuasteono; d=amazonses.com; t=1691982636; v=1; bh=o6solQa8Aa4YV3Xbb2eudWnbQ8TTQxiUB4zEXn3S7c0=; h=From:To:Cc:Bcc:Subject:Date:Message-ID:MIME-Version:Content-Type:X-SES-RECEIPT;
DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/simple;
	s=eogp72tvvmnugysm4vjttzghjnmzww36; d=example.com; t=1691982603;
	h=From:To:Subject:Message-Id:Date;
	bh=o6solQa8Aa4YV3Xbb2eudWnbQ8TTQxiUB4zEXn3S7c0=;
	b=cLPmiYmPBxbplo1vxSOJcscvxt/m3ktc0PdAhtVgoR7sWPaw31bDC0i8WeNXcmGL
	MEc/agcwNyNGete54ZJICny2hgcgbo3wjSvP/kL9ufkTWkXoGh0AgcNfig39KxZP1L1
	XgXEPKzPg8trXyBW4XzmrsnTlNXWf9dMNYSIcpyrSEN2NBQ8YbL6QSt37+7pRZUJG8Q
	AOGE4i6MMf0Dn+66ugHkawjSu8lwYKtsyPHfzUl4BjUsWITeFxw8AET+WfauPio3+qM
	pPxckll7vCUJvncY+h2I5m39JpXDJjlv47TdSuGbpXK5+QpICMdXo/v5eC1p+MCDSgm
	KiB/QLuXZA==
DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/simple;
	s=zh4gjftm6etwoq6afzugpky45synznly; d=amazonses.com; t=1691982603;
	h=From:To:Subject:Message-Id:Date:Feedback-ID;
	bh=o6solQa8Aa4YV3Xbb2eudWnbQ8TTQxiUB4zEXn3S7c0=;
	b=pPx24lewgxm01W6wjgzXnMHO+/wExtnBEkmZmtHzAARJ1jbPfgXmeKYCzG20XXDp
	GFcgWnAcy82ghaaO1+00N5H1IRu3oKYzphHWdsZDMtkaEhJLrCeeVjtnHzVoz7Ar+Yc
	pQA61BC1i9pT6ibgkPJZ8X48kp7hTR8301T/G3Ik=
From: postfix@example.com
To: receive@example.com
Subject: from ses
Message-ID: <01060189f205a36a-b6171201-92c9-462b-b019-3403a5464943-000000@ap-northeast-1.amazonses.com>
Date: Mon, 14 Aug 2023 03:10:03 +0000
Feedback-ID: 1.ap-northeast-1.l/gZ9AdA66AoXDKqqW7JL8Sqq70qA5ce8pt+MQx0Zi4=:AmazonSES
X-SES-Outgoing: 2023.08.14-23.251.234.12

Im from SES

Amazon SESの送信イベントは以下のようになります(同時追記)。
ログの設定外していたのを忘れたので送信環境は同じですが別のタイミングで送ったものになります。

Receivedヘッダよりlocalhostのpostfixを経由して、そこからAmazon SES向けに送信されていることがわかります。

{
  "eventType": "Delivery",
  "mail": {
    "timestamp": "2023-08-14T08:46:58.328Z",
    "source": "postfix@example.com",
    "sourceArn": "arn:aws:ses:ap-northeast-1:xxxxxxxxxxxx:identity/example.com",
    "sendingAccountId": "xxxxxxxxxxxx",
    "messageId": "01060189f33a1918-152cfea8-fd15-49d8-a778-e6445c97986c-000000",
    "destination": [
      "receive@example.com"
    ],
    "headersTruncated": false,
    "headers": [
      {
        "name": "Received",
        "value": "from ip-xxx-xxx-xxx-xxx.ap-northeast-1.compute.internal (ec2-43-207-xxx-xxx.ap-northeast-1.compute.amazonaws.com [43.207.xxx.xxx]) by email-smtp.amazonaws.com with SMTP (SimpleEmailService-d-RJ9XEHDO0) id tSZrKRumQhOCiKN7EFdt for receive@example.com; Mon, 14 Aug 2023 08:46:58 +0000 (UTC)"
      },
      {
        "name": "Received",
        "value": "from mail.example.com (localhost [127.0.0.1]) by ip-xxx-xxx-xxx-xxx.ap-northeast-1.compute.internal (Postfix) with SMTP id 8C3C8CBABF3 for <receive@example.com>; Mon, 14 Aug 2023 08:46:44 +0000 (UTC)"
      },
      {
        "name": "From",
        "value": "postfix@example.com"
      },
      {
        "name": "To",
        "value": "receive@example.com"
      },
      {
        "name": "Subject",
        "value": "from ses"
      },
      {
        "name": "Message-Id",
        "value": "<20230814084650.8C3C8CBABF3@ip-xxx-xxx-xxx-xxx.ap-northeast-1.compute.internal>"
      },
      {
        "name": "Date",
        "value": "Mon, 14 Aug 2023 08:46:44 +0000 (UTC)"
      }
    ],
    "commonHeaders": {
      "from": [
        "postfix@example.com"
      ],
      "date": "Mon, 14 Aug 2023 08:46:44 +0000 (UTC)",
      "to": [
        "receive@example.com"
      ],
      "messageId": "01060189f33a1918-152cfea8-fd15-49d8-a778-e6445c97986c-000000",
      "subject": "from ses"
    },
    "tags": {
      "ses:operation": [
        "SendSmtpEmail"
      ],
      "ses:configuration-set": [
        "examplecom-configuration"
      ],
      "ses:source-ip": [
        "43.207.xxx.xxx"
      ],
      "ses:from-domain": [
        "example.com"
      ],
      "ses:caller-identity": [
        "ses-smtp-user.20230808-xxxxx"
      ],
      "ses:outgoing-ip": [
        "23.251.234.12"
      ]
    }
  },
  "delivery": {
    "timestamp": "2023-08-14T08:47:31.069Z",
    "processingTimeMillis": 32741,
    "recipients": [
      "receive@example.com"
    ],
    "smtpResponse": "250 OK cj0lrkkn6rgabtc1jrhn7e05idpcfeqaplkvnh81",
    "reportingMTA": "e234-12.smtp-out.ap-northeast-1.amazonses.com"
  }
}

なお送信時にpostfix側でno mechanism availableというエラーが出た場合はsmtp_sasl_security_optionsの設定の追記抜けがないかどうかを確認してください。

未設定(デフォルト)ではnoplaintextが含まれますがAmazon SESでは平文の認証のみが有効化されているため利用できる認証方式がなく認証に失敗します。

Aug 11 13:42:08 ip-xxx-xxx-xxx-xxx postfix/smtp[2966]: 8D94ECBABE8: SASL authentication failed; cannot authenticate to server email-smtp.ap-northeast-1.amazonaws.com[3.115.249.23]: no mechanism available

念の為誤解のないように書いておくとSMTPのプロトコルとしての話で通信経路自体はTLSで暗号化されています。

※ Amazon SESの送信エンドポイントに直接繋ぎEHLOを送った結果より(以前横道にそれて確認しておいて良かったです)

EHLO mail.example.com
250-email-smtp.amazonaws.com
250-8BITMIME
250-STARTTLS
250-AUTH PLAIN LOGIN
250 Ok

終わりに

アプリ事情でSMTP認証ができず選択肢が限定ケースにおいて同サーバ内のpostfixを利用してAmazon SESを利用してみました。

諸々事情で多少クライアントが乗っているサーバの設定を増やしても最終的に顧客にメールを配信しに行くメールサービスをマネージドなものにして管理を軽減したい場合は有効となるかもしれません。

今回はAmazon SESを例として試していますがAmazon SESだからこそできることではなく別のSMTPサービスでも利用可能ですので既に一部アプリが別のメールサービスを利用していてそこに統合したい...という場合に検討してみても良いでしょう(サービス仕様や制限による点はあるため注意)。