初めに
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は事前に立ててあるものを使うので含まれていません。
長いので畳んでおきます
template.yaml
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"}
の場合のみ許可します。
auth/app.py
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で繋いでいてそちらに繋ぎたいから等のケースにも対応できそうですね。