こんにちは、AWS グループの尾谷です。
re:Invent で KIRO のボーナス・クレジットをもらいました。
おかげさまで、年末年始は、ツールやゲームなどいろいろ作りながら、学ぶことができたのですが、CloudFront 署名付き URL ダウンロードサイトを作っている途中でボーナス・クレジットが有効期限切れになりました。

仕事始めまであと 2 日あったので「どうせなら最後まで作り終えたい!」と思い立ち、自分で調べて完成させました。
※ 消費したクレジットの内訳は こちらのブログ で紹介しています。
アプリケーションの概要
アプリの概要をご紹介します。
利用者(ファイルをダウンロードする人)
事前登録したユーザー名とパスワードでログインします。

事前登録した 10 文字の認証コードを入力し、[ダウンロードURLを発行] ボタンをクリックします。

15 分間有効な CloudFront 署名付き URL が発行され、リンクを開くとファイルがダウンロードされます。
URL をコピーからリンクをクリップボードにコピーすることもできます。

管理者(ファイルをアップロードする人)
管理者は、S3 管理コンソールを開き、downlods フォルダに 手動でファイルをアップロードします。(AWS CLI を使うでも問題ありません。)
そして、ファイル名とファイルパスを DynamoDB テーブルに手動登録します。

技術的な仕様
もう少し細かな技術的情報をまとめておきます。
DynamoDB テーブルの構造
DynamoDB には、ユーザー認証用と認証コードとファイルパスを紐付ける用の合計 2 つのテーブルを用意しました。
Users テーブル
ユーザー名とパスワードを登録します。
KIRO はパスワードを暗号化して、管理者も把握できないようにすべきと指摘しましたが、検証の本来の目的から逸脱するので、平文登録としました。
enabled は KIRO が提案してくれた機能です。
ユーザーを削除するのではなく、無効化するという考え方は勉強になりました。
{
"email": {
"S": "user@example.com"
},
"createdAt": {
"S": "2026-01-04T00:00:00Z"
},
"enabled": {
"BOOL": true
},
"password": {
"S": "Passw0rd!"
}
}
AuthCodes テーブル
10 桁の認証コード以外に、ダウンロード制限機能を実装したかったのですが、冬休みが終わってしまったので諦めました。
dlimit が最大ダウンロード回数で、S3 イベントを使って、ダウンロードごとに dcount をインクリメントしたかったのですが、未実装です。
{
"authCode": {
"S": "<10 桁の認証コード>"
},
"createdAt": {
"S": "2026-01-05T11:30:00Z"
},
"dlimit": {
"N": "5"
},
"dcount": {
"N": "0"
},
"enabled": {
"BOOL": true
},
"fileName": {
"S": "<オブジェクト名>"
},
"note": {
"S": "<メモ>"
},
"s3Key": {
"S": "<プレフィックスを含めたパス>"
}
}
アーキテクチャと処理フロー
全体的なアーキテクチャと処理フローをまとめました。
ステップごとに解説していきます。

❶ まず、ユーザーがブラウザに、CloudFront のドメイン名(https://dxx....cloudfront.net/)を入力するか、リンクをクリックします。
❷ CloudFront 経由で、オリジンの S3 に保存された HTML (index.html) ファイルが配信されます。
ルートフォルダはキャッシュさせる設定を入れました。そのため、index.html ファイルは CloudFront にキャッシュされます。

ユーザーがブラウザで、メールアドレスとパスワードを入力し、ログインボタンをクリックします。
値が入力されていない場合は、JavaScript が判定し、下部に「メールアドレスとパスワードは必須です。」を表示します。

❸ ユーザー名をパスワードが入力されていた場合は、JavaScript が、API Gateway のエンドポイントを/login パスで POST リクエストします。

❹ API Gateway に統合された Lambda 関数が、DynamoDB にメールアドレスをキーにクエリします。
レコードがヒットしたら、パスワードが同じ値か照合します。
同じ値なら、OK、値が違うなら NG をレスポンスします。

❺ JavaScript は OK がレスポンスされると、認証画面を表示します。

❺ NG がレスポンスされると、下部に「メールアドレスとパスワードは必須です。」を表示します。

❻ 認証コードを入力して [ダウンロードURLを発行] ボタンをクリックすると、JavaScript が、API Gateway のエンドポイントを POST リクエストします。
❼ API Gateway に統合された Lambda 関数が、DynamoDB に認証キーをキーにクエリします。
❽ レコードがヒットしたら、レコードに登録された URL から、CloudFront 署名付き URL を発行し、❾ ブラウザにレスポンスします。
レコードがヒットしなかったら、❾ NG をレスポンスします。

認証コードが空欄のままボタンをクリックすると、下部に「認証コードを入力してください。」を表示します。

CloudFront 署名付き URL の発行
座学では知っていましたが、CloudFront 署名付き URL の具体的な発行方法を理解していなかったので勉強になりました。
以下、ドキュメントを参照しながら仕組みを理解しました。
以下は、Lambda 関数に書き込んだ CloudFront の署名付き URL の発行コードの抜粋です。(node.js)
import { getSignedUrl } from "@aws-sdk/cloudfront-signer"; (中略) // CloudFront のファイルパスを生成 // CloudFront ドメインは環境変数に登録 const url = `https://${CLOUDFRONT_DOMAIN}${path2}`; // 署名の有効期限を現在時刻から 15 分後に設定 const expires = new Date(Date.now() + 15 * 60 * 1000); // getPrivateKeyPem メソッドで、Secret Manager から秘密鍵を取得 // Secret Manager のキー ID は環境変数から取得 const privateKeyPem = await getPrivateKeyPem(CLOUDFRONT_PRIVATE_KEY_SECRET_ID); // CloudFront のキーペア ID は環境変数から取得 const signedUrl = getSignedUrl({ url, keyPairId: CLOUDFRONT_KEY_PAIR_ID, dateLessThan: expires.toISOString(), privateKey: privateKeyPem, });
getSinerdUrl の使い方はドキュメントを参照ください。
CloudFront の設定
CloudFront で署名付き URL を利用する方法を記載します。
- まず、Mac で sshgen コマンドを使用して、秘密鍵と公開鍵を作成しました。
- 次に、CloudFront の管理コンソールで、キー管理にある「パブリックキー」を開き [パブリックキーを作成ボタン] をクリックしました。

- 名前を入力し、1 の手順で作成した公開鍵をコピペして、[パブリックキーを作成ボタン] をクリックしました。

- すぐ下にある [キーグループ] から、キーグループを作成しました。
- ビヘイビアに
/dounloads/*というパスを切って、[ビューワーのアクセスを制限する] を有効にし、4 で作成したキーグループを指定しました。
これにより、downlods パス以下は、署名付き URL でのみアクセスできようになりました。
https://dxx....cloudfront.net/downloads/ 以下にアクセスすると、以下のように Key のないとエラーが表示されるようになりました。

発行されるコードが不安定でハマりました。
/Downloads/* のビヘイビアは兎に角キャッシュしないように調整しました。

まとめ
以上が、KIRO が力尽きた後に CloudFront 署名付き URL ダウンロードサイトを自前で完成させた流れになります。
自前で構築するとスムーズに進まず、大幅に時間がかかりますが、バイブコーディングだと見逃すような辛みやノウハウが得られて良い経験になりました。
もう少し時間があれば、ダウンロード制限以外にもレートリミットを設定したり、4XX アラートを設定するなど、セキュリティ強化を行いたいです。
次に休みが取れたタイミングで着手したいと思います。