時間制限付きカスタムリソースでSESのSMTP認証情報をAWS CDKで作る
はじめに
メール送信元のサーバなどがAPIを経由してSESのエンドポイントを使えなかったので、SMTPでメールを送る場面にぶつかりました。
今回は以下の2点が主旨です。
- SESを扱うためのSMTP認証情報を作成するカスタムリソースを作る。また、SESでメールを送れるようにする。
- カスタムリソースのLambdaが認証情報にアクセスできる権限を持つので、時間制限することで少しでも安全にする
SESのSMTP認証
AWS SESではSMTP認証でメールが送ることができます。
実状を考えれば当たり前な気もしますが、HTTPS以外のエンドポイントを持つAWSサービスと考えるとなかなか珍しいと思います。
ちなみにVPCエンドポイントは現時点だとAPIのVPCエンドポイントがないため、SMTPだけになるみたいです。
このSESでSMTPを使う認証情報の作り方は以下のAWSドキュメントで説明されています。
つまり、以下の二通りの方法があります。マネジメントコンソールも中で変換をやっていると思います。- SESのマネジメントコンソールから作る
- IAMユーザのアクセスキーを払い出し、シークレットキーをPythonで行っているような変換をかける
今回はこのPythonコードを使ったやり方をAWS CDKとカスタムリソースで実装しました。
AWS CDK
作ったものです。
以下のようなものを作成します。
- すでに存在するRoute 53のドメインに対してSESを使ったメール設定を行う
- 簡易DKIMおよびカスタムMAIL FROM ドメインの設定
- ConfigurationSetの作成とレピュテーションメトリクスの設定
- SMTP認証を行うためのユーザ・パスワードを作成してSecrets Managerに格納
SESの設定
mail.tsで行っています。
特筆すべきことはないのですが、SMTPを使わなくても最後以外は同じ構成で動くはずです。
このSmtpCredentialsGeneratorでSMTPの認証情報を作成し、IAMのアクセスキーを直接返すのと、シークレットキーをSecrets Managerの場所を返しています。
SmtpCredentialsGenerator
このコンストラクタでSMTPの認証情報を作成したり、権限を付与しています。
まずはIAMユーザとそのアクセスキーを作成しています。
SecretAccessKeyをSecretsManagerにuserSecretAccessKey
というキー名で格納しています。
const user = new User(this, 'SmtpUser');
emailIdentity.grantSendEmail(user);
this.smtpAccessKey = new AccessKey(this, 'SmtpAccessKey', { user });
this.smtpSecretAccessKey = new Secret(this, 'SmtpSecretAccessKey', {
secretObjectValue: {
userSecretAccessKey: this.smtpAccessKey.secretAccessKey,
},
});
AWSドキュメントに記載のあったPythonコードを少しだけ変更してLambdaのカスタムリソースとして使えるようにしています。
SecretAccessKeyやSecrets Mangerの保存時のキーに使うstringを環境変数で渡しています。このLambdaではSecretAccessKeyと同じ場所に変換したSMTPの認証情報を別のキーで格納しています。
時間制限
AWS CDKのカスタムリソースに限らず、例えばAWS RDSとSecrets Mangerのパスワードローテーション用のLambdaなどが、パスワードにアクセスできる権限を持っていることはAWSでは時折見かける構成だと思います。
開発者がLambdaのコードを修正したり、実行できること自体は当たり前にできる一方で、制限を怠っていると、Lambdaのコードを書き換えることで該当のSecrets Mangerに格納されているパスワードにアクセスしたり、書き換えたりできてしまいます。
上記の場合では、開発者の持つIAMを制限することをまず初めに考えたほうがいいですが、自衛ということでデプロイするLambdaを対象にアクセス制限をする方法を考えました。
今回はカスタムリソースがIAMのSecretAccessKeyやそれを変換したSMTP認証情報が格納されたSecrets Mangerにアクセス可能な状態です。
カスタムリソースのLambdaはCloudFormationのデプロイ時・変更時のみ動けばよいといところに注目し、それ以外のタイミングではDenyのポリシーでアクセスを拒否しました。
雑なイメージです。
実装としては簡単で、Lambdaに以下のような時間に基づくポリシーを追加するだけです。
cdk deploy
時にexpirationTime
が計算されるので、毎回ポリシーを置き換えて、その直後にカスタムリソースがちゃんと動く。といった構成です。
const expirationTime = new Date(Date.now() + 3 * 60 * 60 * 1000).toISOString();
onEvent.addToRolePolicy(new PolicyStatement({
effect: Effect.DENY,
actions: ['*'],
resources: ['*'],
conditions: {
DateGreaterThan: { 'aws:CurrentTime': expirationTime },
},
}));
デプロイするたびに、該当LambdaのIAMポリシーに変更差分がでてきますが、目をつぶっています。
IAM Statement Changes
┌───┬──────────┬────────┬────────┬────────────────────────────────────────────┬────────────────────────────────────────────┐
│ │ Resource │ Effect │ Action │ Principal │ Condition │
├───┼──────────┼────────┼────────┼────────────────────────────────────────────┼────────────────────────────────────────────┤
│ - │ * │ Deny │ * │ AWS:${Mail/SMTP/SmtpCredentialsGenerator/S │ "DateGreaterThan": { │
│ │ │ │ │ erviceRole} │ "aws:CurrentTime": "2024-07-02T17:15:38. │
│ │ │ │ │ │ 121Z" │
│ │ │ │ │ │ } │
├───┼──────────┼────────┼────────┼────────────────────────────────────────────┼────────────────────────────────────────────┤
│ + │ * │ Deny │ * │ AWS:${Mail/SMTP/SmtpCredentialsGenerator/S │ "DateGreaterThan": { │
│ │ │ │ │ erviceRole} │ "aws:CurrentTime": "2024-07-02T17:16:50. │
│ │ │ │ │ │ 146Z" │
│ │ │ │ │ │ } │
└───┴──────────┴────────┴────────┴────────────────────────────────────────────┴────────────────────────────────────────────┘
おわりに
今回は2つの小ネタの合わせ技でした。もちろんそれぞれ別の要素としても使えるので、もし要件があったらどうぞ。
そもそもLambdaでパスワードなどの機密情報を扱う是非は各組織でちゃんと認識しておいたほうがいいよねって話はある。
Lambdaの実行ログに間違ってもパスワードが生のまま流れる、なんてことはないように気を付けよう。
Discussion