CloudFrontでのアクセス制御付きコンテンツ配信

アクセス制限付きのコンテンツ配信

AWSではCloudFrontを使用してコンテンツ配信を行うケースは多いと思います。
CloudFrontでは、署名付きURL と 署名付きCookie を使用することで、権限を持つユーザーにのみコンテンツへのアクセス権限を与えることが可能です。
ユーザーに対しコンテンツのアクセスを制限する場合、コンテンツProxyを自分で用意したり、署名付きURLを発行したりするなどの選択肢があります。

例: オリジンで認証情報を検証し、コンテンツをプロキシする場合。
Authorization ヘッダーをオリジンに転送するように、CloudFront を設定します。

コンテンツをProxyする場合
コンテンツをProxyする場合

例: 署名付きURL を発行し、コンテンツ配信を CloudFront + S3 で実現する場合。

署名付きURLを使用する場合
署名付きURLを使用する場合

どちらの場合も、キャッシュ期間やURL有効期限を適切に決定する必要があります。
今回は後者を試してみます。

準備

以下を準備します。

  1. S3バケット
  2. CloudFront Key Management
  3. CloudFront Distribution

S3バケット

今回は、contents-deliveryという名前でバケットを作成しました。
パブリックアクセスはすべてブロックします。

パブリックアクセスブロック

署名に使用する鍵の生成

署名付きURLの生成に使用するキーペアを作成し、公開鍵をCloudFrontに登録します。
CloudFrontで使用する鍵の生成には、大きく2通りの選択肢があります。

  1. ルートユーザーとしてログインし IAMのセキュリティ認証情報 から生成する(非推奨)
  2. キーペアを作成し、公開鍵をCloudFront の Key Management に登録する(推奨)

署名付きURLについてのほとんどの記事では、1 で説明されているものが多く見受けられますが、公式では非推奨となっています。 docs.aws.amazon.com

今回は推奨方式で進めます。
キーペアを生成します。

$ openssl genrsa -out private_key.pem 2048
$ openssl rsa -pubout -in private_key.pem -out public_key.pem
$ pbcopy < public_key.pem

CloudFront の設定から、Key Management > Public keys > Add public key から鍵を登録します。
また、Key groups からグループを作成します(Distribution作成時に使用します)。

KeyGroup

これでキーペアの準備は完了です。

CloudFront Distribution

CloudFront の Distribution を作成します。
Origin Domain Name には、先程作成したS3バケットを指定します。
このとき、デフォルトで表示されているドメインに対して、s3-<region>のように、明示的にリージョンを指定することができます。
これにより、S3バケットと作成するDistributionのリージョン差異によって生じる反映遅延を短くすることができるため、即時反映が必要な場合は明示的に追記します。

また、Restrict Bucket Access を有効にし、Create a New Identity を選択すると、CloudFront の Origin Access Identity が生成されます。
これは、S3バケットなどのアクセス設定で、特定のIdentityにのみアクションを許可する(例えば、特定のCloudFront Distributionにのみ読み取りを許可する)ために使用されます。

Distribution設定1

この時点で、S3バケットバケットポリシーが更新され、以下のようになります。

{
    "Version": "2008-10-17",
    "Id": "PolicyForCloudFrontPrivateContent",
    "Statement": [
        {
            "Sid": "1",
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity E1YQQEHMTH97R7"
            },
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::contents-delivery/*"
        }
    ]
}

Restrict Viewer Access を有効にし、先程作成したKey Groupを指定します。
この設定により、対象のオリジンに対して署名付きURLまたは署名付きCookieを使用していないリクエストが拒否されるようになります。

Distribution設定2

これでアクセス制限付きコンテンツ配信をするためのCloudFrontの設定ができました。

署名付きURLを生成する

コードは以下のようになります。

package main

import (
    "crypto/rsa"
    "crypto/x509"
    "encoding/pem"
    "fmt"
    "io/ioutil"
    "os"
    "time"

    "github.com/aws/aws-sdk-go/service/cloudfront/sign"
)

const (
    keyID  = "K2INMPKFCORO4O"
    rawURL = "https://d1f21o78fmc0sh.cloudfront.net/sample/sample.jpg"
    ttl    = 3 * time.Minute
)

func main() {
    if err := run(); err != nil {
        fmt.Println(err)
        os.Exit(1)
        return
    }
}

func run() error {
    bs, err := ioutil.ReadFile("./private_key.pem")
    if err != nil {
        return err
    }

    key, err := readRsaPrivateKey(bs)
    if err != nil {
        return err
    }
    key.Precompute()

    if err := key.Validate(); err != nil {
        return err
    }

    url, err := issueURL(key, rawURL)
    if err != nil {
        return err
    }

    fmt.Printf("signed url: %s\n", url)
    return nil
}

func issueURL(key *rsa.PrivateKey, url string) (string, error) {
    signer := sign.NewURLSigner(keyID, key)
    signedURL, err := signer.Sign(url, time.Now().Add(ttl))
    if err != nil {
        return "", fmt.Errorf("failed to sign url: %w", err)
    }
    return signedURL, nil
}

func readRsaPrivateKey(bs []byte) (*rsa.PrivateKey, error) {
    block, _ := pem.Decode(bs)
    if block == nil {
        return nil, fmt.Errorf("invalid private key data")
    }

    switch block.Type {
    case "RSA PRIVATE KEY":
        key, err := x509.ParsePKCS1PrivateKey(block.Bytes)
        if err != nil {
            return nil, fmt.Errorf("failed to parse key: %w", err)
        }
        return key, nil
    case "PRIVATE KEY":
        ki, err := x509.ParsePKCS8PrivateKey(block.Bytes)
        if err != nil {
            return nil, fmt.Errorf("failed to parse key: %w", err)
        }
        key, ok := ki.(*rsa.PrivateKey)
        if !ok {
            return nil, fmt.Errorf("key is not rsa private key")
        }
        return key, nil
    default:
        return nil, fmt.Errorf("invalid private key type: %s", block.Type)
    }
}

実行すると署名付きURLが出力されます。

実際にアクセスしてみると、元のURLと署名付きURLで挙動の違いを確認できます。
画像や比較的容量の小さい音声ファイルなどは、署名付きURLを使用することでアクセス制限付きのコンテンツ配信を手軽に実装できそうです。

動画を配信する場合

HLS方式で動画を配信する場合、署名付きURLでは実現が困難になります。
画像や音声ファイルは単一のファイルに対して署名付きURLを発行することでアクセスを許可することができますが、HLSの場合はファイルが複数に分割されるため、複数ファイルに対する署名付きURLが必要になってしまうためです。
これに対して、署名付きCookieを使用することでアクセス制限付きHLS配信を行います。
有効期限付きCookieクライアントに返し、ファイル単位ではなくユーザー単位で対象全ファイルへのアクセスを許可できます。

サンプル動画の用意

適当なmp4ファイルを用意し、ffmpegでHLSに変換します。

$ ffmpeg -i contents/movie.mp4 -c:v copy -c:a copy -f hls -hls_time 5 -hls_playlist_type vod -hls_segment_filename "hls/movie%03d.ts" hls/movie.m3u8

複数のtsファイルとmovie.m3u8ファイルが作成されるので、すべてS3にアップロードします。
再生確認をしたい場合は、以下で確認できます。

$ ffplay hls/movie.m3u8

署名付きCookieの使用方法

追加のCloudFrontの設定はありません。
リクエストに乗せるCookie情報を取得します。

以下を参考にしました。 qiita.com

ポリシーを定義

{"Statement":[{"Resource":"https://d1f21o78fmc0sh.cloudfront.net/*","Condition":{"DateLessThan":{"AWS:EpochTime":1613823764}}}]}

base64

$ cat policy.json | openssl base64 | tr '+=/' '-_~'

署名されたbase64

$ cat policy.json | openssl sha1 -sign private_key.pem | openssl base64 | tr '+=/' '-_~'

これらの情報をリクエストヘッダーにセットすれば、HLSなどの複数ファイルを参照する形式のリクエストであっても、対象すべてのファイルにアクセスできるようになります。

署名付きCookieの動作確認

以下の手順で確認しました。

  1. ブラウザに上記のCookie情報をセット
  2. HLSで動画を再生するHTMLをS3にアップロード
  3. DistributionにHTML配信用のオリジンを追加し、/index.htmlアクセスに対して、2 でアップロードしたHTMLが参照されるように設定

ブラウザに設定するCookieは、以下のような値になります。

  • CloudFront-Policy=<base64>
  • CloudFront-Signature=<署名base64>
  • CloudFront-Key-Pair-Id=<キーペアID>

HTMLは以下を使用しました。

<script src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script>
<video id="movie" controls>

<script>
  if(Hls.isSupported()) {
    const video = document.getElementById('movie');
    const hls = new Hls({
      xhrSetup: xhr => {
        xhr.withCredentials = true;
      }
    })
    hls.loadSource('https://d1f21o78fmc0sh.cloudfront.net/sample/movie.m3u8');
    hls.attachMedia(video);
 }
</script>

CloudFrontのマルチオリジン設定を行った後上記HTMLを参照すると、問題なく動画が再生できました。

HLS再生
HLS再生
動画ファイルには以下を利用させていただきました。
https://pixabay.com/ja/videos/%E6%B5%B7-%E6%97%A5%E6%B2%A1-%E3%83%93%E3%83%BC%E3%83%81-%E6%B5%B7%E5%B2%B8-62249/

ローカル環境から検証する場合は、必要に応じてS3バケットやCloudFrontのCORS設定を行ってください。