PostfixでSMTPサーバー構築したい
こんにちは!AWS事業本部のおつまみです!
皆さんはPostfixでSMTPサーバー構築したいと思ったことはありますか? 私はあります。
今回案件でNLB + SMTP(Postfix) + SendGrid(SMTP認証)の実装しました。
実装中にハマったポイントもあるので、手順とともにご紹介したいと思います!
なお同じ構成でSMTP認証をGmailで実施した記事はこちらになります。(今回大変参考にさせていただきました。ありがとうございます!)
構成図
今回の構成図です。
今回主役となるMTA(Message Transfer Agent)にPostfixをインストールし、Multi-AZ構成にしてNLBにぶら下がるように構築します。
メール配信の流れは以下です。
- MUA(Message User Agent)
- NLB
- MTA(Message Transfer Agent)
- SendGrid
環境構築
事前準備
まずはメール送信元に使用するドメインおよびSendGridアカウントを準備します。
ドメインはお名前.comやRoute53などお好きな場所からドメインを購入してください。
SendGridはこちらのサイトから無料登録できます。(登録には2~3営業日時間がかかりました。)
SendGrid | クラウドメール配信サービス・メルマガ配信システム
アカウント取得後、SMTP認証に使用するAPIキーを取得します。公式サイトで手順を確認し、取得してください。
APIキーを管理する - ドキュメント | SendGrid
AWSのアクセスキー同様、APIキーは外部に公開されないよう厳重に管理しましょう!
CDK構築初期準備
今回はAWS CDKで環境構築していきます。
利用したCDK versionは2.117.0
です。
CDKを使用したことない方はこちらの公式ドキュメントに従って、環境を整備してから始めましょう。
AWS CDK の開始方法 - AWS Cloud Development Kit (AWS CDK) v2
作成するファイル
今回は1つのスタックファイルでまとめて作成しました。
ハイライトをかけた部分がハマったポイントです。
/lib/test-mail.ts
import * as cdk from "aws-cdk-lib";
import * as s3 from "aws-cdk-lib/aws-s3";
import * as ec2 from "aws-cdk-lib/aws-ec2";
import * as logs from "aws-cdk-lib/aws-logs";
import * as iam from "aws-cdk-lib/aws-iam";
import * as elbv2 from "aws-cdk-lib/aws-elasticloadbalancingv2";
import * as elbv2_tg from 'aws-cdk-lib/aws-elasticloadbalancingv2-targets'
import * as fs from "fs";
export class TestMailStack extends cdk.Stack {
constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// Create S3 Bucket for NLB access log
const nlbAccessLogBucket = new s3.Bucket(this, "NlbAccessLogBucket", {
encryption: s3.BucketEncryption.S3_MANAGED,
blockPublicAccess: new s3.BlockPublicAccess({
blockPublicAcls: true,
blockPublicPolicy: true,
ignorePublicAcls: true,
restrictPublicBuckets: true,
}),
});
console.log(nlbAccessLogBucket.bucketRegionalDomainName);
// Create CloudWatch Logs for VPC Flow Logs
const flowLogsLogGroup = new logs.LogGroup(this, "FlowLogsLogGroup", {
retention: logs.RetentionDays.ONE_WEEK,
});
// Create VPC Flow Logs IAM role
const flowLogsIamrole = new iam.Role(this, "FlowLogsIamrole", {
assumedBy: new iam.ServicePrincipal("vpc-flow-logs.amazonaws.com"),
});
// Create SSM IAM role
const ssmIamRole = new iam.Role(this, "SsmIamRole", {
assumedBy: new iam.ServicePrincipal("ec2.amazonaws.com"),
managedPolicies: [
iam.ManagedPolicy.fromAwsManagedPolicyName(
"AmazonSSMManagedInstanceCore"
),
],
});
// Create VPC Flow Logs IAM Policy
const flowLogsIamPolicy = new iam.Policy(this, "FlowLogsIamPolicy", {
statements: [
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: ["iam:PassRole"],
resources: [flowLogsIamrole.roleArn],
}),
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: [
"logs:CreateLogStream",
"logs:PutLogEvents",
"logs:DescribeLogStreams",
],
resources: [flowLogsLogGroup.logGroupArn],
}),
],
});
// Atach VPC Flow Logs IAM Policy
flowLogsIamrole.attachInlinePolicy(flowLogsIamPolicy);
// Create VPC
const vpc = new ec2.Vpc(this, "Vpc", {
ipAddresses: ec2.IpAddresses.cidr('172.29.0.0/22'),
enableDnsHostnames: true,
enableDnsSupport: true,
natGateways: 2,
maxAzs: 2,
subnetConfiguration: [
{ name: "Public", subnetType: ec2.SubnetType.PUBLIC, cidrMask: 24 },
{ name: "Private", subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS, cidrMask: 24 },
],
});
// Setting VPC Flow Logs
new ec2.CfnFlowLog(this, "FlowLogToLogs", {
resourceId: vpc.vpcId,
resourceType: "VPC",
trafficType: "ALL",
deliverLogsPermissionArn: flowLogsIamrole.roleArn,
logDestination: flowLogsLogGroup.logGroupArn,
logDestinationType: "cloud-watch-logs",
logFormat:
"${version} ${account-id} ${interface-id} ${srcaddr} ${dstaddr} ${srcport} ${dstport} ${protocol} ${packets} ${bytes} ${start} ${end} ${action} ${log-status} ${vpc-id} ${subnet-id} ${instance-id} ${tcp-flags} ${type} ${pkt-srcaddr} ${pkt-dstaddr} ${region} ${az-id} ${sublocation-type} ${sublocation-id} ${pkt-src-aws-service} ${pkt-dst-aws-service} ${flow-direction} ${traffic-path}",
maxAggregationInterval: 60,
});
// Security Group for Internal MUA
const internalMuaSg = new ec2.SecurityGroup(this, "InternalMuaSg", {
allowAllOutbound: true,
vpc: vpc,
});
// Security Group for NLB
const NLBSg = new ec2.SecurityGroup(this, "NLBSg", {
allowAllOutbound: true,
vpc: vpc,
});
NLBSg.addIngressRule(
internalMuaSg,
ec2.Port.tcp(25),
"Allow SMTP for InternalMuaSg"
);
// Security Group for Internal MTA
const internalMtaSg = new ec2.SecurityGroup(this, "InternalMtaSg", {
allowAllOutbound: true,
vpc: vpc,
});
internalMtaSg.addIngressRule(
NLBSg,
ec2.Port.tcp(25),
"Allow SMTP for NLBSg"
);
// Create NLB
const nlb = new elbv2.NetworkLoadBalancer(this, "Nlb", {
vpc: vpc,
vpcSubnets: vpc.selectSubnets({ subnetGroupName: "Private" }),
crossZoneEnabled: true,
internetFacing: false,
securityGroups: [NLBSg],
});
nlb.logAccessLogs(nlbAccessLogBucket);
// Create NLB Target group
const targetGroup = new elbv2.NetworkTargetGroup(this, "TargetGroup", {
vpc: vpc,
port: 25,
targetType: elbv2.TargetType.INSTANCE,
});
// Create NLB listener
const listener = nlb.addListener("Listener", {
port: 25,
defaultTargetGroups: [targetGroup],
});
// User data for Internal MTA
const userDataforInternalMTA = cdk.aws_ec2.UserData.forLinux();
const userDataDefaultScript = fs.readFileSync(
"./src/ec2/userDataSettingDefault.sh",
"utf8"
);
const userDataPostfixMTA = fs.readFileSync(
"./src/ec2/userDataSettingPostfixMTA.sh",
"utf8"
);
const userDataPostfixMUA = fs.readFileSync(
"./src/ec2/userDataSettingPostfixMUA.sh",
"utf8"
);
userDataforInternalMTA.addCommands(userDataDefaultScript);
userDataforInternalMTA.addCommands(userDataPostfixMTA);
// User data for Internal MUA
const userDataforInternalMUA = cdk.aws_ec2.UserData.forLinux();
userDataforInternalMUA.addCommands(userDataDefaultScript);
userDataforInternalMUA.addCommands(userDataPostfixMUA);
// Create EC2 instance
// Internal MTA
vpc
.selectSubnets({ subnetGroupName: "Private" })
.subnets.forEach((subnet, index) => {
const ec2Instance = new ec2.Instance(
this,
`InternalMtaEc2Instance${index}`,
{
machineImage: ec2.MachineImage.lookup({
name: "RHEL-9.2.0_HVM-20230905-x86_64-38-Hourly2-GP2",
owners: ["309956199498"],
}),
instanceType: new ec2.InstanceType("t3.micro"),
vpc: vpc,
keyName: this.node.tryGetContext("key-pair"),
role: ssmIamRole,
vpcSubnets: vpc.selectSubnets({
subnetGroupName: "Private",
availabilityZones: [vpc.availabilityZones[index]],
}),
securityGroup: internalMtaSg,
userData: userDataforInternalMTA,
}
);
targetGroup.addTarget(
new elbv2_tg.InstanceIdTarget(ec2Instance.instanceId, 25)
);
});
// MUA
new ec2.Instance(this, `MuaEc2Instance`, {
machineImage: ec2.MachineImage.lookup({
name: "RHEL-9.2.0_HVM-20230905-x86_64-38-Hourly2-GP2",
owners: ["309956199498"],
}),
instanceType: new ec2.InstanceType("t3.micro"),
vpc: vpc,
keyName: this.node.tryGetContext("key-pair"),
role: ssmIamRole,
vpcSubnets: vpc.selectSubnets({
subnetGroupName: "Private",
}),
securityGroup: internalMuaSg,
userData: userDataforInternalMUA,
});
}
}
ハマったポイント:NLB・MTAに設定するセキュリティグループ
2023/8のアップデートでNLBにセキュリティグループが設定できるようになりました。
これによりターゲットグループとなるMTAでは、NLBのセキュリティグループIDのみ指定すればOKになりました。
このセキュリティグループの設定ができておらず、なかなか通信できない状態になりました。。
UserData
UserDataについてもご紹介します。
今回OSはRHEL9を使用しているため、SSMAgentが事前にインストールされていません。
そのため、UseDataで事前に設定します。なお、AmazonLinux2,2023などを使用する場合はこちらは不要です。
userDataSettingDefault.sh
#!/bin/bash
# -x to display the command to be executed
set -xe
# Redirect /var/log/user-data.log and /dev/console
exec > >(tee /var/log/user-data.log | logger -t user-data -s 2>/dev/console) 2>&1
# Install Packages
token=$(curl -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600")
region_name=$(curl -H "X-aws-ec2-metadata-token: $token" -v http://169.254.169.254/latest/meta-data/placement/availability-zone | sed -e 's/.$//')
dnf install -y "https://s3.${region_name}.amazonaws.com/amazon-ssm-${region_name}/latest/linux_amd64/amazon-ssm-agent.rpm" \
unzip \
nvme-cli
# SSM Agent
systemctl enable amazon-ssm-agent
systemctl start amazon-ssm-agent
# dnf upgrade
dnf upgrade -y
# Install AWS CLI
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
unzip -q awscliv2.zip
sudo ./aws/install
rm -rf aws
rm -rf awscliv2.zip
MTAにインストールするPostfixの設定もUserDataを使って行います。
ハイライト部分であるyourSendGridApiKey
は事前準備で作成したAPIキーを指定してください。
userDataSettingPostfixMTA.sh
#!/bin/bash
# Check if postfix is installed.
sudo dnf install -y postfix
# Install the necessary packages.
sudo dnf install -y cyrus-sasl-plain jq
# Check the current configuration of postfix.
postconf -n
# Back up the postfix configuration file.
sudo cp -a /etc/postfix/main.cf /etc/postfix/main.cf.`date +"%Y%m%d"`
# Edit the postfix configuration file.
sudo postconf -e "myhostname = internal-mta.o2mami.site" \
"mydomain = o2mami.site" \
"myorigin = \$mydomain" \
"inet_interfaces = all" \
"inet_protocols = ipv4" \
"mydestination = \$myhostname, localhost.\$mydomain, localhost" \
"mynetworks = 172.29.0.0/22, 127.0.0.0/8" \
"home_mailbox = Maildir/" \
"masquerade_domains = \$mydomain" \
"smtpd_banner = \$myhostname ESMTP unknown" \
"relayhost = [smtp.sendgrid.net]:587 " \
"smtp_sasl_auth_enable = yes" \
"smtp_sasl_security_options = noanonymous" \
"smtp_sasl_tls_security_options = noanonymous" \
"smtp_sasl_password_maps = hash:/etc/postfix/sasl_passwd" \
"smtp_sasl_mechanism_filter = plain, login" \
"smtp_use_tls = yes" \
"smtp_tls_security_level = encrypt" \
"smtp_tls_loglevel = 1" \
"smtp_tls_note_starttls_offer = yes"
# Create the postfix sasl_passwd file
sudo echo "[smtp.sendgrid.net]:587 apikey:yourSendGridApiKey" > /etc/postfix/sasl_passwd
# Set file permissions
sudo chmod 600 /etc/postfix/sasl_passwd
# Reload Postfix configuration
sudo postmap /etc/postfix/sasl_passwd
# Check the differences of postfix configuration files before and after editing.
sudo diff -u /etc/postfix/main.cf.`date +"%Y%m%d"` /etc/postfix/main.cf
# Check the postfix configuration file for incorrect descriptions.
sudo postfix check
# Start postfix.
sudo systemctl start postfix
# Check the status of postfix.
sudo systemctl status postfix
# Enable postfix auto-start.
sudo systemctl enable postfix
# Check the postfix auto-start setting.
sudo systemctl is-enabled postfix
Postfixの各設定値については、先人のブログをご確認ください。
NLB配下のPostfixでGmailのSMTP認証をしてメールを送信してみた | DevelopersIO
なおMUAもPostfixを使用して、メール配信を行うのでUserDataを使い、事前にインストールしておきます。
userDataSettingPostfixMUA.sh
#!/bin/bash
# Check if postfix is installed.
sudo dnf install -y postfix
# Install the necessary packages.
sudo dnf install -y sendmail
# Back up the postfix configuration file.
sudo cp -a /etc/postfix/main.cf /etc/postfix/main.cf.`date +"%Y%m%d"`
AWS CDKでリソースを払い出す
cdk deploy
でリソースを払い出します。
リソース作成後、NLBのDNS名はMUAからメールを送信する際に設定するので控えておきます。
MUAの設定
postconf
コマンドでrelayhost
にNLBのDNS名を指定し、postfixを再起動します。
sh-5.1$ sudo postconf -e "relayhost = TestMa-NlbBC-ErWAk0cwJjcy-73f5dc6943ffaa76.elb.ap-northeast-1.amazonaws.com:25 "
sh-5.1$ sudo systemctl restart postfix
sh-5.1$ sudo systemctl status postfix
● postfix.service - Postfix Mail Transport Agent
Loaded: loaded (/usr/lib/systemd/system/postfix.service; disabled; preset: disabled)
Active: active (running) since Fri 2024-01-12 20:25:32 JST; 3min 34s ago
(以下略)
これで準備完了です。
メール送信確認
MUAのEC2インスタンスより、自分の社用メールアドレスにメール送信します。
メールログを確認するために、MTAのEC2インスタンス上でそれぞれで以下のコマンドを実行します。egrepでNLBからの疎通確認のログは除外しています。
$ sudo tail -f /var/log/maillog | egrep -v 'disconnect|connect'
次に、MUAからメールを送信します。
Userdateでインストールしたsendmailを使用しました。
sh-5.1$ echo -e 'Subject: TestTitle1\n\nHello!\nWorld' | sendmail -f test@o2mami.site <社用アドレス>
sh-5.1$ echo -e 'Subject: TestTitle2\n\nHello!\nWorld' | sendmail -f test@o2mami.site <社用アドレス>
sh-5.1$ echo -e 'Subject: TestTitle3\n\nHello!\nWorld' | sendmail -f test@o2mami.site <社用アドレス>
MTAでメールログを確認します。
InternalMtaEc2Instance0
sh-5.1$ sudo tail -f /var/log/maillog | egrep -v 'disconnect|connect'
Jan 12 11:35:04 ip-172-29-3-21 postfix/smtpd[59970]: 4B0FA111A2EF: client=ip-172-29-2-76.ap-northeast-1.compute.internal[172.29.2.76]
Jan 12 11:35:04 ip-172-29-3-21 postfix/cleanup[60006]: 4B0FA111A2EF: message-id=<202401121135.40CBZ4rL060360@ip-172-29-2-76.ap-northeast-1.compute.internal>
Jan 12 11:35:04 ip-172-29-3-21 postfix/qmgr[59969]: 4B0FA111A2EF: from=<test@o2mami.site>, size=1036, nrcpt=1 (queue active)
Jan 12 11:35:05 ip-172-29-3-21 postfix/smtp[60007]: 4B0FA111A2EF: to=<社用アドレス>, relay=smtp.sendgrid.net[52.220.95.193]:587, delay=1.3, delays=0.01/0.06/1.1/0.14, dsn=2.0.0, status=sent (250 Ok: queued as dDjkTtTBSC6CSgCnV7Kv0Q)
Jan 12 11:35:05 ip-172-29-3-21 postfix/qmgr[59969]: 4B0FA111A2EF: removed
Jan 12 11:35:16 ip-172-29-3-21 postfix/smtpd[59970]: 40452111A2EF: client=ip-172-29-2-76.ap-northeast-1.compute.internal[172.29.2.76]
Jan 12 11:35:16 ip-172-29-3-21 postfix/cleanup[60006]: 40452111A2EF: message-id=<202401121135.40CBZGXj060363@ip-172-29-2-76.ap-northeast-1.compute.internal>
Jan 12 11:35:16 ip-172-29-3-21 postfix/qmgr[59969]: 40452111A2EF: from=<test@o2mami.site>, size=1036, nrcpt=1 (queue active)
Jan 12 11:35:17 ip-172-29-3-21 postfix/smtp[60007]: 40452111A2EF: to=<社用アドレス>, relay=smtp.sendgrid.net[52.220.95.193]:587, delay=0.86, delays=0/0/0.71/0.14, dsn=2.0.0, status=sent (250 Ok: queued as BaHuakO1Qrm9JV1m1XDkQw)
Jan 12 11:35:17 ip-172-29-3-21 postfix/qmgr[59969]: 40452111A2EF: removed
InternalMtaEc2Instance1
sh-5.1$ sudo tail -f /var/log/maillog | egrep -v 'disconnect|connect'
Jan 12 11:34:44 ip-172-29-2-210 postfix/smtpd[59971]: C211A13A2: client=ip-172-29-2-76.ap-northeast-1.compute.internal[172.29.2.76]
Jan 12 11:34:44 ip-172-29-2-210 postfix/cleanup[59977]: C211A13A2: message-id=<202401121134.40CBYi7S060353@ip-172-29-2-76.ap-northeast-1.compute.internal>
Jan 12 11:34:44 ip-172-29-2-210 postfix/qmgr[59893]: C211A13A2: from=<test@o2mami.site>, size=1032, nrcpt=1 (queue active)
Jan 12 11:34:46 ip-172-29-2-210 postfix/smtp[59979]: C211A13A2: to=<社用アドレス>, relay=smtp.sendgrid.net[52.220.95.193]:587, delay=1.4, delays=0.02/0.08/1.1/0.14, dsn=2.0.0, status=sent (250Ok: queued as p1BpEJGlR72GALHOCSLX5w)
Jan 12 11:34:46 ip-172-29-2-210 postfix/qmgr[59893]: C211A13A2: removed
errorが出ておらず、dsn=2.0.0
となっていることから正しくメールの配送が出来ていそうです。
メールボックスを確認してみます。
以下の通り、3通メールを受信できています。
メールの送信元はMUAで設定したアドレスになっており、「SendGrid」経由であることが確認できました!
さいごに
今回はNLB配下のPostfixでSendGridのSMTP認証をしてメールを送信してみました。
PostfixやSendGridを使用するのは今回が初めてでしたが、設定自体は簡単に行えました!
興味がある方はぜひ自分でSMTPサーバを構築してみてください!
最後までお読みいただきありがとうございました!
どなたかのお役に立てれば幸いです。
以上、おつまみ(@AWS11077)でした!
参考
Postfixでメール送信 - ドキュメント | SendGrid APIキーを管理する - ドキュメント | SendGrid NLB配下のPostfixでGmailのSMTP認証をしてメールを送信してみた | DevelopersIO Amazon Linux 2上のPostfixで宛先ドメイン毎にリレー制御してみた | DevelopersIO