CloudFront の署名付き URL を使ってみるメモ

CloudFront の署名付き URL を使ってみたメモ。

S3 の署名付き URL

署名付き URL といえば S3 の方もよく聞きます。いずれも同じように有効期限付きの署名付き URL を作成して認証を通すというのは変わりませんが、S3 の方は AWS IAM Role の一時クレデンシャルで署名します。その署名付き URL で可能な操作は元のアイデンティティに付与されたポリシーが適用されるため s3:GetObject などで許可したいバケットへの操作を設定しておく必要があります。また、署名の対象に Canonical URI が含まれるため元の URL ごとに署名が必要です。

https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/userguide/using-presigned-url.html https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/API/sig-v4-authenticating-requests.html

CloudFront の署名付き URL

CloudFront の署名付き URL は異なります。署名鍵にはあらかじめ作成しておいた RSA 鍵を使います。ペアとなる公開鍵を CloudFront に登録し、ビヘイビアに関連付けます。公開鍵の登録時に鍵IDが払い出されます。URL には署名とともにこの鍵IDもクエリストリングで付与します。Cloudfront はそれを元に鍵IDに対応する公開鍵で署名を検証します。

https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/private-content-signed-urls.html

ポリシー

署名付き URL には次のものが指定されたポリシーが必要です。

  • URL(オプション)
  • 有効期限
  • 開始日時(オプション)
  • IP アドレスレンジ(オプション)

これらを次のような JSON で指定します。ここでは見やすさのために空白や改行を入れていますが実際には空白や改行は取り除く必要があります。

{
    "Statement": [
        {
            "Resource": "https://example.com/aaa/bbb/ccc",
            "Condition": {
                "DateLessThan": {
                    "AWS:EpochTime": 1696049757
                },
                "DateGreaterThan": {
                    "AWS:EpochTime": 1696049457
                },
                "IpAddress": {
                    "AWS:SourceIp": "192.168.0.0/24"
                }
            }
        }
    ]
}

Statement が配列ですがこれは1つしか指定できません。また、IPv6 形式の IP アドレスは指定できません。URL にはワイルドカードが使用可能なので複数の URL に対して有効な署名も作成できます。むしろ URL はオプションなので未指定にもできます。

これを Base64(URL Safe にするため +=/-_~ に置換)したものをクエリストリングで指定します。また、署名はこの JSON に対して作成します。

ポリシーは省略も可能です。その場合は次のような既定のポリシーが指定されたものとみなされます。

  • 特定の URL のみに有効(ワイルドカード不可)
  • 有効期限の指定は必須
  • 開始日時やIPアドレスレンジは指定できない

これはドキュメントでは既定のポリシーと呼ばれています。一方で前述のように JSON で指定するものはカスタムポリシーと呼ばれています。既定のポリシーでもちろんポリシーの署名は必要です。ただポリシーの JSON(の Base64)を URL に含める必要が無いため、カスタムポリシーと比べれば URL が短くて済みます。

署名の作成

署名はポリシーの JSON に対して作成します。AWS SDK でも秘密鍵を指定して署名できますが、次の要領で自前でも作成可能です。

https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/private-content-linux-openssl.html

試しに Node.js で署名を作成してみました。

import * as fs from 'fs/promises';
import * as crypto from 'crypto';
import * as querystring from 'querystring';

function sign(policy, privateKey) {
    const signer = crypto.createSign('RSA-SHA1');
    signer.update(policy);
    return signer.sign(privateKey);
}

function urlSafeBase64(buf) {
    return buf.toString('base64')
        .replaceAll('+', '-')
        .replaceAll('=', '_')
        .replaceAll('/', '~');
}

const policy = JSON.stringify({/* ポリシー */});

const privateKey = await fs.readFile(/* 秘密鍵*/);

const keyPairId = 'K2JCJMDEHXQW5F'; // 鍵ID

const signature = sign(policy, privateKey);

const qs = querystring.stringify({
    'Expires': expires, // 有効期限、ポリシーと一致する必要あり
    'Policy': urlSafeBase64(Buffer.from(policy)),
    'Signature': urlSafeBase64(signature),
    'Key-Pair-Id': keyPairId, // 鍵ID
});

これを元の URL のクエリストリングに付与すれば OK です。

さいごに

カスタムポリシーなら複数 URL へのワイルドカードが指定できるので、大量の URL を含むようなレスポンスでそれらすべての URL に署名付けたいときなどにも1つの署名でカバーできるので便利そうです。

最初のリクエストと、それに対するレスポンスに含まれる URL へのリクエストとで、ソース IP アドレスが一致することが確かであれば、最初のリクエストのソース IP アドレスをそのままポリシーの IP アドレスレンジに含めるような使い方も出来そうです。ただ、グローバル IP アドレスが複数あるようなNAT だと先行するリクエストと後続するリクエストでソース IP アドレスが変わることもあるだろうのでかなり限定的な環境でしか利用できないでしょうか。

また、署名は Cookie にも入れられます。例えば S3 に画像を保存しているようなシステムで、ログイン時に Set-Cookie で署名で付けるようにしておけば、未ログインでも URL に直接アクセスすれば画像だけは見えてしまう、みたいな問題も避けられそうです。