Amazon API GatewayのLambda AuthorizerにVPC Lambdaを使ってIP制限先のコンテンツを利用してみた
初めに
API GatewayのオーソライザとしてLambdaは利用可能ですがVPC Lambdaは利用可能だったけ?と思いドキュメントを読んでみたところ明示はされていなかったので試してみることにしました。
VPC Lambdaを選択されるケースの1つとして固定のIPが欲しいというのはある話ですので今回はLambdaがIP制限のかかっているコンテンツへのアクセスを行うパターンを試してみます。
構成
構成は以下のとおりです。
認証についてはなんちゃってコンテンツですがAuthorization
ヘッダに指定された値を元に{{ヘッダに指定された値}}.json
を取得しに行き、その値が意図しているものであれば成功という型になります。
成功した場合はHelloWorld
の関数が呼ばれますが、こちらは単純にhello world
を返却するだけのsam init
のテンプレートで作成されるデフォルトの関数となります。
HelloWorld
側は特にVPC Lambdaにする必要はないですが、普段SAMのアップデートでブログ書く際にわざわざVPC Lambdaで立てる機会もなくせっかくなのでVPC内に立てます。
コード
AWS SAMを利用して構築します。
SAMテンプレート
EC2は事前に立ててあるものを使うので含まれていません。
長いので畳んでおきます
AWSTemplateFormatVersion: '2010-09-09' Transform: AWS::Serverless-2016-10-31 Globals: Function: Timeout: 60 MemorySize: 128 Parameters: WebSgId: Type: String AuthHost: Type: String Resources: Api: Type: AWS::Serverless::Api Properties: StageName: dev Auth: DefaultAuthorizer: LambdaAuthorizer Authorizers: LambdaAuthorizer: FunctionArn: !GetAtt AuthFunction.Arn Identity: Headers: - Authorization HelloWorldFunction: Type: AWS::Serverless::Function Properties: CodeUri: hello_world/ Handler: app.lambda_handler Runtime: python3.9 Architectures: - x86_64 Events: HelloWorld: Type: Api Properties: Path: /hello Method: get RestApiId: !Ref Api VpcConfig: SubnetIds: - !Ref HelloFunctionSubnet SecurityGroupIds: - !Ref HelloFunctionSg AuthFunction: Type: AWS::Serverless::Function Properties: CodeUri: auth/ Handler: app.lambda_handler Runtime: python3.9 Environment: Variables: AUTH_HOST: !Ref AuthHost Architectures: - x86_64 VpcConfig: SubnetIds: - !Ref AuthorizerSubnet SecurityGroupIds: - !Ref AuthorizerSg #--------------------------- #--- VPC Contents #--------------------------- VPC: Type: AWS::EC2::VPC Properties: CidrBlock: !Sub 172.0.0.0/16 EnableDnsSupport: True EnableDnsHostnames: True Tags: - Key: Name Value: !Sub lambda-vpc IGW: Type: AWS::EC2::InternetGateway IGWAttachment: Type: AWS::EC2::VPCGatewayAttachment Properties: InternetGatewayId: !Ref IGW VpcId: !Ref VPC #-------- #--- Subnet #-------- AuthorizerSubnet: Type: AWS::EC2::Subnet Properties: VpcId: !Ref VPC AvailabilityZone: !Sub ${AWS::Region}a CidrBlock: 172.0.0.0/24 Tags: - Key: Name Value: lambda-authorizer-subnet-1a HelloFunctionSubnet: Type: AWS::EC2::Subnet Properties: VpcId: !Ref VPC AvailabilityZone: !Sub ${AWS::Region}a CidrBlock: 172.0.1.0/24 Tags: - Key: Name Value: lambda-hello-subnet-1a NatSubnet: Type: AWS::EC2::Subnet Properties: VpcId: !Ref VPC AvailabilityZone: !Sub ${AWS::Region}a CidrBlock: 172.0.128.0/24 Tags: - Key: Name Value: nat-subnet-1a #-------- #--- Route #-------- NatRouteTable: Type: AWS::EC2::RouteTable Properties: VpcId: !Ref VPC Tags: - Key: Name Value: nat-subnet-route NatRouteTableAssociation: Type: AWS::EC2::SubnetRouteTableAssociation Properties: RouteTableId: !Ref NatRouteTable SubnetId: !Ref NatSubnet ToInternetRoute: Type: AWS::EC2::Route Properties: RouteTableId: !Ref NatRouteTable DestinationCidrBlock: 0.0.0.0/0 GatewayId: !Ref IGW DependsOn: IGWAttachment AuthorizerRouteTable: Type: AWS::EC2::RouteTable Properties: VpcId: !Ref VPC Tags: - Key: Name Value: lambda-authorizer-subnet-route AuthorizerRouteTableAssociation: Type: AWS::EC2::SubnetRouteTableAssociation Properties: RouteTableId: !Ref AuthorizerRouteTable SubnetId: !Ref AuthorizerSubnet AuthorizerToPublicRoute: Type: AWS::EC2::Route Properties: RouteTableId: !Ref AuthorizerRouteTable DestinationCidrBlock: 0.0.0.0/0 NatGatewayId: !Ref NatGw #-------- #--- Security Group #-------- AuthorizerSg: Type: AWS::EC2::SecurityGroup Properties: GroupName: lambda-authorizer-sg GroupDescription: "-" VpcId: !Ref VPC Tags: - Key: Name Value: !Sub lambda-authorizer-sg HelloFunctionSg: Type: AWS::EC2::SecurityGroup Properties: GroupName: hello-function-sg GroupDescription: "-" VpcId: !Ref VPC Tags: - Key: Name Value: !Sub lambda-hello-function-sg #-------- #--- Etc #-------- NatGw: Type: AWS::EC2::NatGateway Properties: AllocationId: !GetAtt EIP.AllocationId SubnetId: !Ref NatSubnet Tags: - Key: Name Value: lambda-nat-1a EIP: Type: AWS::EC2::EIP Properties: Tags: - Key: Name Value: lambda-nat-eip ## 片付けをセットでやる為に疎通先のサーバの許可もこちらで定義しておく WebServerIngressFromNat: Type: AWS::EC2::SecurityGroupIngress Properties: IpProtocol: tcp FromPort: 80 ToPort: 80 CidrIp: !Sub ${EIP.PublicIp}/32 GroupId: !Ref WebSgId
API GatewayからLambda呼び出しはInvokeFunction
で行われるのでどちらの関数もセキュリティグループでのインバウンド許可は不要です。
テンプレート上には明示的にしてはないですが認証関数には以下のようなリソースベースのポリシーが割り当てられています。
認証関数
後述するWebサーバ側からjsonを取得して{"result": "success"}
の場合のみ許可します。
import os, urllib.request, json def lambda_handler(event, context): effect = 'Deny' try: res = urllib.request.urlopen( "http://{}/{}.json".format( os.environ["AUTH_HOST"], event['authorizationToken'] ) ) body = json.loads(res.read()) if "success" == body["result"]: effect = "Allow" except: pass return { 'principalId': 'user-auth', 'policyDocument': { 'Version': '2012-10-17', 'Statement': [ { 'Action': 'execute-api:Invoke', 'Effect': effect, 'Resource': [ event['methodArn'] ] } ] } }
HelloWorld関数
Hello Worldのテンプレートのデフォルトのコードです
import json def lambda_handler(event, context): return { "statusCode": 200, "body": json.dumps({ "message": "hello world", }), }
Webサーバ側
以下のようなコンテンツを用意しておきます。
$ tree . ├── fail-user.json └── success-user.json 0 directories, 2 files $ curl http://localhost/fail-user.json { "result": "fail" } $ curl http://localhost/success-user.json { "result": "success" }
デプロイ
通常通りsam deploy
を実行するだけなので割愛します。
動作確認
# 成功パターン $ curl https://xxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/hello -H "Authorization: success-user" {"message": "hello world"}% # 失敗パターン(存在しないファイル) $ curl https://xxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/hello -H "Authorization: hoge-user" {"Message":"User is not authorized to access this resource with an explicit deny"}% # 失敗パターン(存在するが認証失敗扱いパターン) $ curl https://xxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/hello -H "Authorization: fail-user" {"Message":"User is not authorized to access this resource with an explicit deny"}% # 失敗パターン(Authorizationヘッダ未存在) $ curl https://xxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/hello {"message":"Unauthorized"}%
Webサーバ側のアクセスログも確認しておきます。
52.197.128.xxx - - [28/Jul/2023:10:41:59 +0000] "GET /success-user.json HTTP/1.1" 200 28 "-" "Python-urllib/3.9" 52.197.128.xxx - - [28/Jul/2023:10:42:06 +0000] "GET /hoge-user.json HTTP/1.1" 404 196 "-" "Python-urllib/3.9" 52.197.128.xxx - - [28/Jul/2023:10:42:10 +0000] "GET /fail-user.json HTTP/1.1" 200 25 "-" "Python-urllib/3.9" # Authorizationヘッダが存在しないパターンAPI Gateway側で弾く為ログなし
想定通り今回作成したNAT Gatewayからアクセスが来ています。
セキュリティグループで制限をかけているので、別の環境からのEC2アクセスはタイムアウトとなります。
% curl http://43.xxx.xxx.xxx/ curl: (28) Failed to connect to 43.xxx.xxx.xxx port 80 after 75004 ms: Couldn't connect to server
終わりに
コンテンツ保護としてIP制限はよく使われる方法ですので非認可アクセスはLambdaまで到達させたくないなぁと思った場合にタイトルだけでもみてできるんだなということを知ってもらえればと思います。
今回はNAT Gatewayに通信を向けていますが、VPCとオンプレ上のネットワークをVPNで繋いでいてそちらに繋ぎたいから等のケースにも対応できそうですね。