You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

通过Nodemailer使用AWS SES SMTP时,程序化创建SMTP凭证

解决方案:避免Lambda硬编码SES SMTP凭证

一、通过CloudFormation自动化创建SMTP凭证并传递给Lambda

1. CloudFormation模板核心资源配置

创建IAM用户、生成Access Key、转换为SMTP凭证并存储到Secrets Manager:

AWSTemplateFormatVersion: '2010-09-09'
Resources:
  # 创建用于SES SMTP的IAM用户
  SESSMTPUser:
    Type: AWS::IAM::User
    Properties:
      Policies:
        - PolicyName: SESSendAccess
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - ses:SendEmail
                  - ses:SendRawEmail
                Resource: "*"

  # 为IAM用户生成长期Access Key
  SESSMTPAccessKey:
    Type: AWS::IAM::AccessKey
    Properties:
      UserName: !Ref SESSMTPUser

  # 自定义资源:将IAM Access Key转换为SES SMTP凭证
  SESSMTPSecret:
    Type: Custom::SESSMTPSecret
    Properties:
      ServiceToken: !GetAtt SESSMTPConverterFunction.Arn
      AccessKeyId: !Ref SESSMTPAccessKey
      SecretAccessKey: !GetAtt SESSMTPAccessKey.SecretAccessKey
      Region: !Ref AWS::Region

  # 存储SMTP凭证到Secrets Manager
  SESSMTPSecretStore:
    Type: AWS::SecretsManager::Secret
    Properties:
      SecretString: !Sub '{"smtp_user": "${SESSMTPSecret.SMTPUsername}", "smtp_pass": "${SESSMTPSecret.SMTPPassword}"}'

  # Lambda执行角色:允许读取Secrets Manager和SES访问
  LambdaExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole
      Policies:
        - PolicyName: SESAndSecretsAccess
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - secretsmanager:GetSecretValue
                Resource: !Ref SESSMTPSecretStore
              - Effect: Allow
                Action:
                  - ses:SendEmail
                  - ses:SendRawEmail
                Resource: "*"

  # 自定义资源用的Lambda:转换Access Key为SMTP凭证
  SESSMTPConverterFunction:
    Type: AWS::Lambda::Function
    Properties:
      Runtime: nodejs16.x
      Handler: index.handler
      Role: !GetAtt SESSMTPConverterRole.Arn
      Code:
        ZipFile: |
          const crypto = require('crypto');

          exports.handler = async (event) => {
            const { AccessKeyId, SecretAccessKey } = event.ResourceProperties;
            const smtpUsername = AccessKeyId;
            const message = `SendRawEmail`;
            const version = 0x02;
            const signature = crypto.createHmac('sha256', SecretAccessKey)
              .update(message)
              .digest();
            const smtpPassword = Buffer.concat([Buffer.from([version]), signature]).toString('base64');

            return {
              PhysicalResourceId: AccessKeyId,
              Data: {
                SMTPUsername: smtpUsername,
                SMTPPassword: smtpPassword
              }
            };
          };

  # 自定义资源Lambda的执行角色
  SESSMTPConverterRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole

2. Lambda代码修改:从Secrets Manager获取凭证

更新Node.js代码,通过AWS SDK读取Secrets Manager中的SMTP凭证,无需硬编码:

const nodemailer = require('nodemailer');
const { SecretsManagerClient, GetSecretValueCommand } = require('@aws-sdk/client-secrets-manager');

const secretsManager = new SecretsManagerClient({ region: process.env.AWS_REGION });
const SECRET_ARN = process.env.SES_SMTP_SECRET_ARN; // 通过CloudFormation传递到Lambda环境变量

async function getSmtpCredentials() {
  const command = new GetSecretValueCommand({ SecretId: SECRET_ARN });
  const response = await secretsManager.send(command);
  const secret = JSON.parse(response.SecretString);
  return { user: secret.smtp_user, pass: secret.smtp_pass };
}

exports.handler = async (event) => {
  const sourceEmailAddress = 'your-verified-email@example.com';
  const mailOptions = {
    from: sourceEmailAddress,
    to: 'myemail@something.com',
    text: 'This is some text',
    html: '<b>This is some HTML</b>',
    subject: 'Nodemailer SMTP transporter',
  };

  const region = process.env.AWS_REGION;
  const smtpCreds = await getSmtpCredentials();
  
  const smtpTransporter = nodemailer.createTransport({
    port: 587,
    host: `email-smtp.${region}.amazonaws.com`,
    auth: smtpCreds,
    secure: false,
    tls: { rejectUnauthorized: true }
  });

  await smtpTransporter.sendMail(mailOptions);
  return { statusCode: 200, body: 'Email sent successfully' };
};

3. 给Lambda配置环境变量

在CloudFormation的Lambda资源中添加环境变量,传递Secrets Manager的ARN:

YourLambdaFunction:
  Type: AWS::Lambda::Function
  Properties:
    # 其他配置(Runtime、Handler、Code、Role等)
    Environment:
      Variables:
        AWS_REGION: !Ref AWS::Region
        SES_SMTP_SECRET_ARN: !Ref SESSMTPSecretStore
    VpcConfig:
      # 你的VPC配置(子网、安全组等)

二、关键说明

  • SMTP凭证生成逻辑:AWS SES的SMTP密码是通过IAM用户的Secret Access Key对固定字符串SendRawEmail进行HMAC-SHA256加密,再加上版本号0x02后转base64得到的,自定义Lambda实现了这个转换。
  • 权限最小化:建议把IAM用户的SES权限限制到特定的验证邮箱或发送源,不要用*。
  • VPC终端节点:确保你的VPC终端节点涵盖Secrets Manager和SES的终端节点,Lambda才能在VPC内访问这些服务。

内容的提问来源于stack exchange,提问作者tteguayco

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.16 12:15:10