3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

署名付きクッキーによるCloudFront配信コンテンツの保護

Posted at

やりたいこと

概要イメージです。
署名付きクッキー _やりたいこと.png

社内システムをAWS内に配置することを想定したケースで、S3バケットからCloudFrontにより配信するコンテンツ(HTML、JavaScriptソースや画像等のリソース)を認可されたアクセス以外から保護します。
プライベートコンテンツ、というようですね。
プライベートコンテンツ提供の概要 - Amazon CloudFront

認証APIはAPI Gatewayに配置し、CloudFrontで配信されるコンテンツ(HTML・JS等)とAPIはクロスオリジンかつクロスドメインになります。クッキーのSameSite=Noneを避けるため、Route53を利用してルートドメインを一致させます。
※実のところ、ルートドメインを別にして、SameSite=Noneの指定を試したのですが、クッキーが設定できませんでした。できるはずだと思っているのですが・・・。

やること

  • CloudFront OAI(オリジンアクセスアイデンティティ)によるS3アクセスの制限
  • CloudFront 署名付きクッキーによるコンテンツ保護
    • キーペア作成
    • 公開鍵をキーグループに登録
    • パスパターン別の動作(Behaviors)設定で信頼されたキーグループを登録
  • CloudFrontとAPI Gatewayのドメイン設定
  • CORSの設定
    • リクエスト
    • プリフライトレスポンス(OPTIONS)
    • 本レスポンス
  • Secrets Managerの利用
    • 秘密鍵をSecrets Managerに登録
    • IAMポリシーの作成
  • Lambda 署名付きクッキーの作成と設定
    • Lambda ロール設定
    • Lambda レイヤー設定
    • Lambda コードの作成(署名の作成とクッキーの設定)

CloudFront OAI(オリジンアクセスアイデンティティ)によるS3アクセスの制限

S3へのアクセスをCloudFrontからのみに制限します。
はじめてであればCloudFrontのオリジン設定でOAI作成とバケットポリシーの更新まで行うことができます。
CloudFront Dstributions → (ディストリビューションを選択) → Origins and Origion Groups → Origins Origin → (Originを選択、Edit)
OAI設定.png

OAI横のiボタンの説明によるとOAIは毎回作成ではなく再利用が推奨されています。
S3バケットポリシーにひとつのOAI設定をしておいて、複数のCloudFrontディストリビューションでそのOAIを利用することができますね。
おそらくS3バケット(多):(多)CloudFrontディストリビューションの紐付きを1つのOAIで設定できそうですが、少なくともプロジェクト、本番・開発・テスト等では分けておいた方がよさそうです。

オリジンアクセスアイデンティティを使用して Amazon S3 コンテンツへのアクセスを制限する - Amazon CloudFront

CloudFront 署名付きクッキーによるコンテンツ保護

CloudFrontでパスパターンによるアクセス保護を行います。デフォルトDefault(*)で認可なしではアクセスできないように設定しますが、ログインページは認可なしでアクセスさせる必要がありますのでlogin/配下はアクセス保護の対象外とします。
署名付きクッキー_パスパターン別アクセスイメージ.png

Default(*)にTrusted Key Groupを設定します。その上でアクセス保護の対象外とするパスパターンを追加して、優先順位(Precedence)をDefault(*)よりも上に設定します。こちらはTrusted Key Groupを設定しません。
下記、設定イメージです。(login配下に加えて、ルート直下のindex.htmlのみアクセス保護の対象外として、署名付きクッキーがない場合にlogin/index.htmlに遷移させるJavaScriptを埋め込みました)
URLパターン別設定.png

下記の手順で進めます。

  • キーペア作成
  • 公開鍵をキーグループに登録
  • パスパターン別の動作(Behaviors)設定で信頼されたキーグループを登録

キーペアの作成はAWSコンソール上ではなくローカルでopenssl等を使って行います。
下記の記事が参考になりました。
root ユーザー作業が不要に!Amazon CloudFront で署名付き URL/Cookie 向け公開鍵を IAM ユーザー権限で管理できるようになりました。 | DevelopersIO
署名付き URL と署名付き Cookie を作成できる署名者の指定 - Amazon CloudFront

CloudFrontとAPI Gatewayのドメイン設定

「やること」に書いたようにCloudFrontとAPI Gatewayのルートドメインを一致させます。
下記にようにCloudFrontに独自ドメインを設定し、API Gatewayをそのサブドメインにする等するとよいでしょう。

ドメイン
CloudFront example.com
API Gateway api.example.com

CloudFrontの独自ドメイン設定は下記記事が参考になります。
Route53を用いてCloudFrontに独自ドメインを設定する - Qiita
お名前.comからRoute53へネームサーバーを変更する時に迷子になる人へ - Qiita

API Gatewayのサブドメイン設定については単独の記事にしました。
Amazon API Gatewayを独自サブドメインで使う - Qiita

CORSの設定

クロスオリジンになりますので、CORSの設定が必要になります。
プリフライトリクエスト(OPTIONS)はMockを利用しますので、レスポンスヘッダの設定はAPI Gatewayの設定として行います。
本リクエストでは動的にクッキーを作成して返すため「Lambda プロキシ統合」を使用し、レスポンスヘッダはLambdaの戻り値に設定します。

CORS設定 - リクエスト

クッキーを返すためにリクエストでwithCredentialsを指定します。
axiosを利用すると下記のようになります。

js
  const res = await axios.post(
    "https://api.example.com/login/login",
    { ...body },
    { withCredentials: true },
  );

CORS設定 - プリフライトレスポンス(OPTIONS)

API Gateway → リソース → アクション → CORSの有効化 の操作でレスポンスヘッダの初期設定できます。
Access-Control-Allow-Origin'*'ではなく明示的にCloudFrontのオリジンを設定しましょう。
リクエストのwithCredentialsの指定に対応して、Access-Control-Allow-Credentialsの設定も必要になります。

レスポンスヘッダ設定の編集は「メソッドレスポンス」よりヘッダの追加・削除、ヘッダ値の設定は「統合レスポンス」より行います。
OPTIONSメソッド設定.png
OPTIONSレスポンスヘッダ2.png
Access-Control-Allow-MethodにはOPTIONSと本リクエストのメソッドを設定します。ただ、私の試したところではこのヘッダ自体を削除しても動作しました。

CORS設定 - 本レスポンス

API Gateway → リソース → アクション → メソッドの作成 より本リクエストのメソッドを作成します。
動的にクッキーを作成して返すために「Lambdaプロキシ統合」を使用します。
Lamdaプロキシ統合の設定.png

Lambdaプロキシ統合を使用するとLambdaの戻り値にレスポンスヘッダを設定することになります。
API Gateway で Lambda プロキシ統合を設定する - Amazon API Gateway

Access-Control-Allow-OriginAccess-Control-Allow-Credentialsを設定します。

python3
    return {
	  'statusCode': 200,
	  'headers': {
	    'Access-Control-Allow-Origin': 'https://api.example.com',
	    'Access-Control-Allow-Credentials': True,
	  },
	  'body': body,
	}

Secrets Managerの利用

上で作成したキーペアの秘密鍵をシークレットマネージャーに登録し、Lambdaから取得します。
Lambdaからシークレットマネージャーを使用するロールに追加するIAMポリシーを作成します。

秘密鍵をSecrets Managerに登録

下記の記事が参考になります。
AWS Secrets ManagerでEC2キーペア秘密鍵の安全な受け渡しを管理してみる | DevelopersIO

シークレットの作成が完了すると、シークレットの個別画面にLambdaからのシークレットの取得のサンプルコードが表示されます。

IAMポリシーの作成

IAM → ポリシー → ポリシーの作成
サービスにSecrets Managerを指定し、アクションの読み込みを選択します。
リソースは指定したほうがセキュアです。上で作成したシークレットのARNを指定します。
IAMポリシーの作成.png

Lambda 署名付きクッキーの作成と設定

Lambdaの処理で署名付きクッキーを作成して返します。Lambdaの言語はPython3を採用します。
現時点(2021年6月)では、boto3やbotocoreに署名や署名付きクッキーを作成する処理がありません。下記のコードを参考に作成します。
CloudFront — Boto3 Docs documentation
Python script that generates signed cookies to control access to CloudFront content
この参考コードの中で使われているcryptographyの利用にはLambdaレイヤーの追加が必要になります。

Lambda ロール設定

Lambda → 関数 → (関数名を選択) → 設定 → アクセス権限 → 実行ロール → (ロール名を選択) → 「ポリシーをアタッチします」
ポリシーのアタッチ.png
ポリシー名は作成時につけた名前です。

この記事では扱いませんが、認証(Cognito等)やDBアクセスのためのポリシーも必要がに応じて追加します。
予めポリシーを追加したロールを作成しておき、Lambdaに設定しても構いません。この場合、AWSLambdaBasicExecutionRole(CloudWatch Logs書き込み)ポリシーを追加しておきましょう。

Lambda レイヤー設定

暗号化ライブラリcryptographyを利用可能とするレイヤーを作成して、Lambdaに追加します。
下記の記事が参考になります。
Lambdaでcryptographyを使う(Python3.8 + cryptography) - Qiita
AWS Lambda Layersでライブラリを共通化 - Qiita

Lambda コードの作成(署名の作成とクッキーの設定)

署名とポリシーを作成します。
get_secret()はシークレット個別画面のサンプルコードを参考に作成します。

Python3
  origin = 'https://example.com'
  resource_url = origin + '/*'
  expire_at = datetime.now() + timedelta(days=1)
  secret_name = 'xxx'
  key_pair_id = 'xxx'

  priv_key = get_secret(secret_name)   # rsa_signer()内で使用
  def rsa_signer(message):
    private_key = serialization.load_pem_private_key(
      priv_key,
      password=None,
      backend=default_backend()
    )
    return private_key.sign(message, padding.PKCS1v15(), hashes.SHA1())

  def get_cookies(rsa_signer, key_pair_id, resource_url, expire_at):
    cf_signer = CloudFrontSigner(key_pair_id, rsa_signer)
    policy = cf_signer.build_policy(resource_url, expire_at).encode('utf8')
    policy_64 = cf_signer._url_b64encode(policy).decode('utf8')
    signature = rsa_signer(policy)
    signature_64 = cf_signer._url_b64encode(signature).decode('utf8')

    return {
      'CloudFront-Policy': policy_64,
      'CloudFront-Signature': signature_64,
      'CloudFront-Key-Pair-Id': key_pair_id,
  }
  cookies = get_cookies(rsa_signer, key_pair_id, resource_url, expire_at)

クッキーをヘッダーに設定します。上で説明したCORS用のヘッダも合わせて設定します。

Python3
  domain = 'example.com'
  return {
    'statusCode': 200,
    'headers': {
        'Access-Control-Allow-Origin': origin,
        'Access-Control-Allow-Credentials': True,
    },
    'multiValueHeaders': {
      'Set-Cookie': [
          'CloudFront-Policy=%s; domain=%s; path=/;Secure;HttpOnly' % (cookies['CloudFront-Policy'], domain),
          'CloudFront-Signature=%s; domain=%s; path=/;Secure;HttpOnly' % (cookies['CloudFront-Signature'], domain),
          'CloudFront-Key-Pair-Id=%s; domain=%s; path=/;Secure;HttpOnly' % (cookies['CloudFront-Key-Pair-Id'], domain),
      ],
    },
    'body': json.dumps('success'),
  }

おわりに

・SameSite=Noneで別ドメインでのクッキー設定ができなかった
・Access-Control-Allow-Methodはどういう時に必要なのか
など、心残り、理解不足なところがありますが、なんとかやりたいことはできました。

数多く嵌りましたが、特に苦労したのは下記あたりですかね。
・署名作成の参考コードは私がこの課題に挑戦していた当時はなかなか検索にかからなかった
・Lambdaから動的にヘッダーを返すための「Lambdaプロキシ統合」になかなかたどり着けなかった

署名作成の参考コードは今検索したら日本語の記事が見つかりました。コード以外にもいろいろ丁寧に書いてくれています。(この記事が当時あれば。。)
CloudFront 署名付きURLと署名付きCookieをおさらいしてPythonで試してみた | DevelopersIO

TODO:Lambdaのひと通りのコードサンプルをgithubにあげようと思います。

3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?