1. 概要
インターネット経由でのAmazon Virtual Private Cloud (VPC)内のリソースへのアクセスについて、特定のIPアドレスからのアクセスは認証無しで許可し、それ以外のIPアドレスからのアクセスには認証を要求したいというケースがあります。
その実現手段の一つとして、Amazon CloudFrontとAWS Lambda@Edgeを用いて、特定のIPアドレス以外からのアクセスに対してAmazon Cognito認証を要求する方法を試してみました。
2. 試作した構成
試作した構成を下記に示します。
図1:CloudFrontとLambda@Edgeを用いてCognito認証を行う構成上記の構成に示されているコンポーネントを説明します。
- CloudFront: CloudFrontはエッジロケーションを用いるコンテンツ配信サービスです。ここではCloudFrontのオリジンはApplication Load Balancers (ALB)とします。
- Lambda@Edge: Lambda@Edgeは、CloudFrontのエッジロケーションにおいて、Lambdaで定義した任意のコードを実⾏できる機能です。ここでは、リクエスト元のIPアドレスをが許可されたものかを確認し、それ以外のIPアドレスに対して、Amazon Cognitoから提供されるJWTトークンを検証することにより認証を処理するコードを実行します。
- Amazon S3: Lambda@Edge関数のソースコードはS3バケットに保存されます。
- AWS Systems Maneger (SSM): 認証無しでアクセスを許可するIPアドレスのリストをParameter Storeに保存します。
- Amazon Cognito: ユーザープールを作成し、ユーザー認証を管理します。
- Application LoadBalancer(ALB): EC2へのリクエストの負荷分散に使用します。
- Amazon EC2: ここではアクセス先のVPC内のリソースとしてEC2を使用し、nginxを実行します。他のサービスやアプリケーションでも構いません。
3. Lambda@Edgeを利用する理由
CloudFrontとLambda@Edgeを用いて認証処理を実装することには、以下のメリットが考えられます。
- Lambda@EdgeによりAWSエッジロケーションで認証処理を実行するため、高いパフォーマンスを期待できる。
- 不正なリクエストをエッジロケーションで拒否できるため、セキュリティの強化と、オリジンの負荷軽減ができる。
- 様々なCloudFrontのオリジンに対して応用できる。VPC内のリソースへのアクセスだけでなく、S3の静的コンテンツへのアクセスへの認証にも利用可能。
- 既存アプリケーションに変更を加えずに認証処理を追加することが可能。
- Cognito認証以外の認証方法に変更することが容易。
一方で、通常のAWS Lambdaを利用して認証処理を実装する方が、開発・運用コストを抑えられることや、AWSリージョン内で実行される他サービスとの統合が容易になることも考えられます。
4. 構築手順
構築手順を説明します。
- VPCおよびVPC内のリソースを構築する
- Amazon Cognitoのユーザープールとユーザーを作成する
- Amazon CloudFrontを作成する
- Lambda@Edge関数を作成する
- 動作確認をする
ステップ1:VPCおよびVPC内のリソースを構築する
アクセス先のVPCおよびVPC内のリソースを作成します。VPC内のリソースの構成は自由です。上記構成では、EC2上でWebアプリケーションを実行して、ALBで負荷分散することを想定しています。
ステップ2:Amazon Cognitoのユーザープールとユーザーを作成する
Amazon Cognitoでユーザープールを作成した後、ユーザープールにユーザーを作成します。
まずはユーザー認証の動作確認に用いるメールアドレスとパスワードを設定しておきます。
ステップ3:CloudFrontを作成する
CloudFrontディストリビューションを作成します。CloudFrontのオリジンとして何を選ぶかで設定が変わります。今回の構成では、CloudFront経由でALBにアクセスするように設定します。
ALBはインターネット向けでも内部向けでも構いません。インターネット向けのALBを使用する場合には、CloudFrontがリクエストにカスタムHTTPヘッダーを追加するようにして、ALBでそのカスタムHTTPヘッダーが含まれるリクエストのみアクセスを許可するルールを設定します。
ステップ4:Lambda関数をデプロイしてCloudFrontと関連付ける
Lambda@Edge関数を作成してCloudFrontと関連付けを実施します。前のステップでCloudFrontを作成済みなので、Lambda@Edge関数を作成して、トリガーを設定します。
また、この構成では認証無しでアクセスを許可するIPアドレスのリストをパラメータとしてSSMに格納します。そのため、Lambda@Edge関数がS3に格納されるソースコードとSSMに格納したIPアドレスのリストにアクセスできるように、Lambda@Edge関数にIAMロールを与えます。
参考として、IAMロールを作成するCloudFormationテンプレートの一部を掲載します。
CognitoAuthorizerFunctionRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Principal:
Service:
- lambda.amazonaws.com
- edgelambda.amazonaws.com
Action:
- sts:AssumeRole
Path: "/"
Policies:
- PolicyName: CognitoAuthorizerFunctionPolicy
PolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action:
- 's3:GetObject'
Resource: !Sub arn:aws:s3:::${EnvironmentName}-lambdacode/*
- Effect: Allow
Action:
- cognito-idp:AdminGetUser
Resource: "*"
- Effect: Allow
Action:
- logs:CreateLogGroup
- logs:CreateLogStream
- logs:PutLogEvents
Resource: arn:aws:logs:*:*:*
- Effect: Allow
Action:
- ssm:GetParameter
Resource:
- !Sub 'arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/myapp/allowed_ips'
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
Lambda@Edgeに登録するコードは、Amazon CloudFrontとAWS Lambda@Edgeを使用した認証処理のAWSサンプルコードを参考にしてください。サンプルコードに、認証無しでアクセスを許可するIPアドレスのリストを取得して、リクエスト元IPアドレスが含まれているかをチェックする処理を追加します。追加するコードを示します。
const allowedIPsParameter = '/myapp/allowed_ips';
async function getAllowedIPs() {
try {
const data = await ssm.getParameter({
Name: config.allowedIPsParameter,
WithDecryption: true
}).promise();
return data.Parameter.Value.split(",").map(ip => ip.trim());
} catch (error) {
console.error('Error fetching allowed IPs from SSM:', error);
return [];
}
}
async function isIPAllowed(clientIP) {
const allowedIPs = await getAllowedIPs();
return allowedIPs.includes(clientIP);
}
exports.handler = (event, context, callback) => {
const cfrequest = event.Records[0].cf.request;
const headers = cfrequest.headers;
console.log('getting started');
console.log('USERPOOLID=' + USERPOOLID);
console.log('region=' + region);
console.log('pems=' + pems);
const clientIP = cfrequest.clientIp;
if (await isIPAllowed(clientIP)) {
return cfrequest;
}
<以降はサンプルコード通りなので省略>
};
ステップ5:動作確認をする
動作確認のために、認証無しでアクセスを許可されたIPアドレスと許可されていないIPアドレスのそれぞれから、CloudFrontのURLにアクセスします。
許可されたIPアドレスに対してはCognito認証は実行されず、コンテンツに直接アクセスできることを確認します。
許可されていないIPアドレスに対してのみCognito認証が実行されることを確認します。
- クライアントのIPアドレスが許可されたIPリストに含まれている場合: クライアントは認証無しで、ウェブサイトに直接アクセスできます。
-
クライアントのIPアドレスが許可されたIPリストに含まれていない場合: クライアントはアクセス用ユーザー名とパスワードを求められます。
-
ユーザー名またはパスワードのいずれかが不正な場合:
401 Unauthorized
エラーが返却されます。
-
ユーザー名またはパスワードのいずれかが不正な場合:
5. まとめ
アクセス元のIPアドレスで認証有無を選択する方法として、AWS CloudFrontとLambda@Edgeを用いて、特定のIPアドレス以外にはCognito認証を実施する方法を試しました。LambdaまたはLambda@Edgeを用いた認証の応用例としてもご参考になれば幸いです。