カテゴリー
【AWS使い方ガイド】CloudFront FunctionsでS3バケット内のリソースのアクセス認証を掛ける方法
※ 当ページには【広告/PR】を含む場合があります。
2024/10/07

以前の記事で、
おさらいになりますが、OACによるS3のアクセス制限はエンドユーザーからのS3の直接のアクセスからリソースを保護するための施策です。
814x426

つまるところ、S3バケットのルートに以下のようなテキストファイルがあったとして、
こんにちは!
このファイルへは、CloudFrontからは自由にアクセスできます。
$ curl -XGET https://xxxxxxxxxxxxx.cloudfront.net/greeting.txt
こんにちは!
しかし、S3バケットのリソースURLへ直接アクセスは制限されます。
$ curl -XGET https://xxxxxxxxxxxxxxxxx.s3.ap-northeast-1.amazonaws.com/greeting.txt
<?xml version="1.0" encoding="UTF-8"?>
<Error><Code>AccessDenied</Code><Message>Access Denied</Message><RequestId>*********</RequestId><HostId>************</HostId></Error>
こうすることでユーザーへのファイル供給をCloudFrontに一本化することはできますが、このままだと不特定多数のユーザーがアクセスすることになります。
今回はより進んだS3ファイルのアクセス制限をかけるための初歩として、
CloudFront Functions (Javascript Runtime 2.0)の話
まずは簡単にCF2の使い方に触れてみます。
中身はLambdaのー種類とはいえ、実際はCloudFrontの機能の一部で、利用するにもCloudFront側で設定する必要があります。
細かく見ていくと色々とありますが、CF2の開発者が特に考慮したいことは以下の3点です。
+ 実装には独自の「Javascript Runtime」を使う
+ 作成されるCF2インスタンスのロケーションはCFディストリビューションの場所と同じ
+ ビューアーリクエストとビューアーレスポンスの2つの関数タイプしかない
まず、Javascript Runtime(JR)については、v2.0になったおかげで、文法面で制約のあったv1.0からとても記述しやすくなりました。
簡単な例を示すと、ビューアーリクエストでヘッダを書き込みしたい場合、
async function handler(event) {
const request = event.request;
const clientIP = event.viewer.ip;
//クライアントのIP値をtrue-client-ipヘッダーとしてリクエストに追加する
request.headers['true-client-ip'] = {value: clientIP};
return request;
}
と、ほぼモダンなJavascriptコードとして実装できます。
また、CF2を使う大きなメリットとして、関数アタッチ先のCFディストリビューションと同じリージョンにすることができます。
L@Eだと"us-east-1"に作成する必要があり、他のリージョンに構築したサービスと機能連帯させるにはどうしても
リージョンまたぎ問題は、不要になったときの消し忘れだったり、デプロイバージョンに変わるARN値の指定変更作業だったり、何かとうっかりミスの原因にもなります。
CF2を使うことで、こういった細かな関数の管理も気にせずに利用できます。
ただし、依然としてオリジンリクエストとオリジンレスポンスの関数タイプはL@Eしか利用できないため、今後のCF2の機能アップデートに期待です。
リクエストイベント構造 〜 CF2のリクエストにはBody(ペイロード)が存在しない
個人的にはビューアーリクエスト型でCF2を使うことがほとんどですが、L@Eのイベントリクエスト型とは異なるため、CF2を採用するにはいくつかの注意点があります。
その最たるものが、CF2のイベントリクエスト型では、
CF2のリクエストイベントには、以下のプロパティフィールドが含まれています。
method:
リクエストされたHTTPメソッド(読み取り専用:値変更不可)
uri:
リクエストされたURL(の相対パス)
querystring:
リクエストされたクエリ文字列(のオブジェクト)
headers:
リクエストのHTTPヘッダー(を表すオブジェクト)
ただし、Cookieヘッダーは除外
cookies:
リクエストのCookie(を表すオブジェクト)
ヘッダーの内、Cookieヘッダーから取得されたもの
CF2でBodyが制限されているのは、内部メモリが2MBしかないため、それなりの大きさのデータが送り出されるとそもそも処理できるはずがない、という設計面を考慮した構造になっています。
よって、POSTやPUTメソッドで送り出されたデータペイロードをCF2で処理することは現状不可能であることは最初の内に頭に入れておく必要があります。
URLのクエリ文字列からパスコードを渡す方法
CF2の概要はこの辺にして、ここからはS3リソースのアクセス制限機能を実装して試してみましょう。
前準備として、
CloudFrontのダッシュボードから
[関数] > [関数を作成]
1013x336

適当な名前で関数をJR2.0で作成します。
602x632

作成した関数の中身を編集していきます。
[構築] > [関数コード]
[変更を保存]
704x758

ここで使うサンプルコードとして、以下のようなものを使います。
//認証に失敗した際のレスポンス
const response401 = {
statusCode: 401,
statusDescription: 'Unauthorized'
};
function token_verify(token, key) {
//認証トークンが無い場合
if (!token) {
throw new Error('No token supplied');
}
//認証できない場合
if (!_verify(token, key)) {
throw new Error('Signature verification failed');
}
}
//認証のロジック
function _verify(input, key) {
return input === key;
}
async function handler(event) {
let request = event.request;
//tokenヘッダーが存在しない場合には認証失敗で返す
if (!request.querystring.token) {
console.log("No token supplied in the querystring");
return response401;
}
const token = request.querystring.token.value;
//認証用の秘密鍵
const secretKey = 'HOGE';
try {
token_verify(token, secretKey);
}
catch (e) {
console.log(e);
return response401;
}
//レスポンスからのtokenヘッダー値を空にする
delete request.querystring.token;
return request;
}
CF2関数が未発行のままだと、テストも出来ないので、
[発行]
[関数を発行]
726x279

次にテストを行います。
[テスト]
[JSONを編集]
625x633

以下のテストスニペットを貼り付けて、
[保存]
{
"version": "1.0",
"context": {
"eventType": "viewer-request"
},
"viewer": {
"ip": "1.2.3.4"
},
"request": {
"method": "GET",
"uri": "/index.html",
"headers": {},
"cookies": {},
"querystring": {
"token": {
"value": "aaaaa"
}
}
}
}
では
[関数をテスト]
617x684

テストの結果は、
401 Unauthorized
クエリ文字列で
token=aaaaa
では同じ要領で、JSONスニペットで
token=HOGE
546x555

これも期待通りに、認証してアクセスを通してくれていることが分かります。
これでCF2の動作確認が終わったので、この関数をCFディストリビューションへ紐付けします。
対象のCFディストリビューションを選択し、
[ビヘイビア] > [ビヘイビアを作成]
971x867

ビヘイビアの他の設定項目はそのままデフォルト値のままにしておいて、
[関数の関連付け]
[ビューワーリクエスト]
CloudFront Functions
[ビヘイビアを作成]
CFディストリビューションの変更が反映されデプロイされるのを待って、早速認証が有効になったか、外部からアクセスを試してみます。
まずは、認証トークンを含むクエリがなかったり、間違っていたりする場合の応答を確認します。
$ curl -i -XGET https://xxxxxxxxxx.cloudfront.net/greeting.txt
HTTP/2 401
...
$ curl -i -XGET https://xxxxxxxxxx.cloudfront.net/greeting.txt?token=aaaaa
HTTP/2 401
...
ちゃんと
401
ではちゃんとパスコードが一致した場合はどうでしょうか。
$ curl -i -XGET https://xxxxxxxxxx.cloudfront.net/greeting.txt?token=HOGE
HTTP/2 200
content-type: text/plain
...
こんにちは!
今度はちゃんとファイルにアクセス出来ています。
ヘッダーからパスコードを渡す方法
先程のやり方だと、認証情報をもったクエリ文字列のパラメーターが第三者に読み取られる可能性もあり、あまりセキュアな方法とは言えません。
ヘッダーに認証用のトークンを付与する仕組みには、
「Authorization」には主にBasic認証とBearer認証の2つが使われることが多いですが、今回のように自前で認証用のロジックを構築するために「Authorization」を使ってしまうと、別に使っている認証用のサービスやライブラリー(Bearerの場合がほとんど)と競合する恐れもあります。
AuthorizationがどのサービスやAPIと競合するかどうかに配慮しながら開発するくらいなら、独自に認証用のヘッダーを作るほうが懸命と言えます。
ということで、
CF2関数の設定手順は先程の節と全く同じですので、同じ説明は割愛し、CF2関数の中身だけを以下に交換します。
//認証に失敗した際のレスポンス
const response401 = {
statusCode: 401,
statusDescription: 'Unauthorized'
};
function token_verify(token, key) {
//認証トークンが無い場合
if (!token) {
throw new Error('No token supplied');
}
//認証できない場合
if (!_verify(token, key)) {
throw new Error('Signature verification failed');
}
}
//認証のロジック
function _verify(input, key) {
return input === key;
}
async function handler(event) {
let request = event.request;
//X-Auth-Tokenヘッダーが存在しない場合には認証失敗で返す
if (!request.headers['x-auth-token']) {
console.log("No Authrization supplied in the headers");
return response401;
}
const token = request.headers['x-auth-token'].value;
//認証用の秘密鍵
const secretKey = 'HOGE';
try {
token_verify(token, secretKey);
}
catch (e) {
console.log(e);
return response401;
}
//レスポンスからのX-Auth-Tokenヘッダー値を空にする
delete request.headers['x-auth-token'];
return request;
}
先程のコードから変わったところは、リクエストオブジェクトからのヘッダーを値を引き出すのに、
request.headers['x-auth-token']
注意すべきなのは、コード内で利用するヘッダー名は小文字に変換する必要があります。 つまり、
X-Auth-Token
x-auth-token
他は至って先程のクエリ文字列の場合でやったことを同じです。
ではこのCF2関数をCFディストリビューションに割り当てて、外部からアクセスを試してみましょう。
#👇トークンが異なるためアクセス拒否
$ curl -i -XGET -H 'X-Auth-Token:aaaaaaaa' https://xxxxxxxxxx.cloudfront.net/greeting.txt
HTTP/2 401
...
#👇トークンが一致
$ curl -i -XGET -H 'X-Auth-Token:HOGE' https://xxxxxxxxxx.cloudfront.net/greeting.txt
HTTP/2 200
content-type: text/plain
...
こんにちは!
いい感じにアクセス制限が出来ていることが分かります。
まとめ
今回は2種類のCloudFront Functionを使って、カスタム認証を行う基礎的な実装を紹介してみました。
まだパスコードによる復号・暗号化などの高度な仕組みについては触れていませんが、S3リソースの保護する方法の基礎的なやり方はご理解いただけたかと思います。
記事を書いた人
ナンデモ系エンジニア
主にAngularでフロントエンド開発することが多いです。 開発環境はLinuxメインで進めているので、シェルコマンドも多用しております。 コツコツとプログラミングするのが好きな人間です。
カテゴリー