やりたいこと
- 認証が必要なWebアプリケーションをAWS上に構築する
- Amazon S3で画像ファイルを管理する
- 画像ファイルには認証済みユーザーのみがアクセス可能
- 独自URLで公開する
- 当然SSL化する
いろいろとググったところ、Amazon CloudFront の 署名付きURL (signed url) を使うと目的を達成できそう。
環境整備が面倒くさかったので手順を整理しておきます。
この記事では 署名付きURL の作成方法について取り上げます。Webアプリケーションの構築については範囲外です
手順
リソース管理用のS3バケットを準備する
画像ファイルを格納するS3のバケットを作成します。
今回は cloudfront-private-content
という名前にしました。
テストのために cat.jpg
と dog.jpg
をバケットにアップロードします。
CloudFrontでS3バケットを公開設定する
CloudFrontを迂回してアクセスできないように設定します。
Step 2: Create distribution の画面で以下の設定を行います。
Origin Settings
項目 | 設定値 | 説明 |
---|---|---|
Origin Domain Name | 作成したS3バケットを指定 | バケットのWebサイトホスティング設定は不要 |
Restrict Bucket Access | Yes | バケットへのアクセスをCloudFront経由に制限する |
Origin Access Identity | Create a New Identity | バケットアクセスのための認証アカウントを作成 |
Grant Read Permissions on Bucket | Yes, Update Bucket Policy | バケットのアクセス権限を変更し、CloudFrontによる読み取り権限を付与する |
Default Cache Behavior Settings
項目 | 設定値 | 説明 |
---|---|---|
Viewer Protocol Policy | Redirect HTTP to HTTPS | HTTPアクセスされたらHTTPSにリダイレクト |
Restrict Viewer Access (Use Signed URLs or Signed Cookies) | Yes | 署名付きURLあるいはCookieによるアクセスのみに制限する |
コンソールの CloudFront Distributions にて status が In Progress
から Deployed
に変わるまで結構時間がかかります。
ゆっくりコーヒーを飲む時間くらいはかかるので、余裕を見て作業しましょう。
2018-11-12追記
公開当初、この構成のキモである Restrict Viewer Access の設定が漏れてました。
今日、自分でこの記事を見て、「なんでこの設定で署名付きURL以外のアクセスが制限されるんだ?」って悩んでしまいました...
CloudFrontへ独自ドメインでアクセスする
Route 53 で先程作成した CloudFront Distribution へのaliasを作成します。
まず、CloudFrontのコンソールで Domain Name をコピーしておきます。
xxxxxxxxxxxxx.cloudfront.net
つづいて、Route53で Aレコード を追加します。
項目 | 設定値 |
---|---|
Name |
private-content .example.com |
Type | A - IPv4 address |
Alias | Yes |
Alias Target | xxxxxxxxxxxxx.cloudfront.net |
ふたたび CloudFrontのコンソールに戻り、Alternate Domain Names (CNAMEs) を更新します。
リストから作成したDistributionを選択し、Distribution Settings
ボタンをクリックします。
General
タブのEdit
ボタンをクリックします。
Alternate Domain Names (CNAMEs) にRoute53で登録したAレコードのNameを入力します。
Yes, Edit
ボタンをクリックし、変更を保存します。
独自ドメインをSSL化する
また Distribution Settings
> General
> Edit
ボタンをクリックします。
SSL Certificate で Custom SSL Certificate (example.com):
を選択したいところですが、まだアクティブになっていないと思います。(選択可能な証明書があればアクティブになる)
AWS Certificate Manager で証明書を作成し登録します。
Request or Import a Certificate with ACM
ボタンをクリックします。
AWS Certificate Managerが開きます。このときリージョンが バージニア北部 となっていることを確認してください。(そうでなければ変更する)
ドメイン名は *.example.com
として続行します。
今回、ドメインの管理もRoute53で行っているのでCNAMEの登録による確認で証明書が発行されます。
ここでも検証が完了するまで少し時間がかかりますが、状況 が 発行済み になるまで何度かリロードしてください。
(私の環境では、以降のCloudFrontのコンソールでなかなか証明書が一覧に表示されず、しばらく悩みました。
経験上、AWSコンソールでの作業はひとつづつ確実にこなすのが良いです。)
CloudFrontのコンソールに戻り、SSL Certificate で Custom SSL Certificate (example.com):
が選択可能であればチェックしてください。
(私の環境では一旦Editをキャンセルし、ブラウザのリロードを行い、再度Edit画面に戻ると選択可能になっていました。)
Custom SSL Certificate (example.com): のテキストボックスにフォーカスを合わせると、AWS Certificate Managerで発行した証明書が表示されますので、選択してください。
Yes, Edit
ボタンをクリックし、変更を保存します。
キーペアの作成
署名付きURLを生成するための公開鍵、秘密鍵、アクセスキーIDを取得します。
AWSコンソールのヘッダー右上、ユーザー名の部分をクリックして セキュリティ認証情報 を選択します。
ダイアログが表示される場合はContinue to Security Credentials
ボタンをクリックして続行します。
CloudFrontのキーペア にて 新しいキーペアの作成
をクリックし、公開鍵と秘密鍵をダウンロードします。
また、CloudFrontのキーペア に アクセスキーID
が表示されるので、これもコピーしておきます。
Node.jsでAmazon CloudFrontの署名付きURLを生成する
やっと本題。
今回は署名付きURLを生成する機能だけを実装します。
適当にNode.jsのプロジェクトフォルダを作成し、AWS-SDKとその他のパッケージをインストールします。
(その他のパッケージは署名付きURLの生成に直接関与するものではありません。AWS-SDKだけが必須です。)
$ mkdir cloudfront-secret-content
$ cd cloudfront-secret-content
$ npm init -y
$ npm install --save aws-sdk dotenv fs-extra moment
以下のように、コンソールに生成したURLを出力するコードを作成します。
require('dotenv').config();
const AWS = require('aws-sdk');
const fs = require('fs-extra');
const moment = require('moment');
// 対象となるリソースのURL
const target = 'https://private-content.example.com/cat.jpg';
/**
* AWS.CloudFront.Signer.getSignedUrlを呼び出す。
*
* Class: AWS.CloudFront.Signer — AWS SDK for JavaScript
* https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/CloudFront/Signer.html
* @param {String} keypairId
* @param {String} privateKey
* @param {Object} options
*/
function getSignedUrlAsync(keypairId, privateKey, options) {
return new Promise((resolve, reject) => {
// Signerインスタンスを生成
const signer = new AWS.CloudFront.Signer(keypairId, privateKey);
// URL生成
signer.getSignedUrl(options, (err, url) => {
if (err) {
reject(err);
}
resolve(url);
});
});
}
async function main() {
// private keyを読み込む
const privateKey = await fs.readFile(process.env.PRIVATE_KEY_FILE, { encoding: 'utf-8' });
// 期限を設定
// 現在日時から1日後まで有効とする
const expires = moment.utc().add(1, 'days').unix();
// URL生成
const url = await getSignedUrlAsync(
// キーペアのID
// AWSコンソールの以下の場所で確認可能
// セキュリティ認証情報 > CloudFront のキーペア > アクセスキーID
process.env.KEYPAIR_ID,
// 秘密鍵を渡す
privateKey,
{
// 対象となるCloudFrontのURL
url: target,
// 生成されるURLの期限 (UTCのunixtime)
expires
}
);
console.log(url);
}
main().then(() => {
console.log('done.');
}).catch((err) => {
console.error(err);
});
秘密鍵の保存場所とアクセスキーIDを .env
ファイルに記述します。
KEYPAIR_ID=xxxxxxxxxxxxxxxxxxxx
PRIVATE_KEY_FILE=./secret/pk-xxxxxxxxxxxxxxxxxxxx.pem
実行すると以下のような結果が返ります。
$ node index.js
https://private-content.example.com/cat.jpg?Expires=1540535260&Key-Pair-Id=XXXXXXXXXXXXXXXXXXXX&Signature=.....
done.
返されたURLにブラウザからアクセスし、画像が表示されることを確認してください。
また、QueryString部分をカットして 403
が返されること、cat.jpg
をdog.jpg
に変更してもアクセスできないことを確認します。