NRIネットコム Blog

NRIネットコム社員が様々な視点で、日々の気づきやナレッジを発信するメディアです

AWS Amplify Hosting(AWS Amplify Console)にAmazon CloudFrontとAWS WAFを追加してIP制限を設定してみた - カスタムオリジンにIP制限、基本認証、SSL/TLS証明書を追加するAWS CloudFormationテンプレート

小西秀和です。
以前、次の記事でAWS Amplify Hosting(AWS Amplify Console)の構築方法について紹介しました。

AWSの静的ウェブサイトホスティングで入門するAWS Amplify(Console、CLI) - 構築編(Amplify Console)

しかし、AWS Amplify Hosting(AWS Amplify Console)では基本認証や証明書追加の機能はありますが、IP制限の機能がサポートされていません。

そのため、今回は内部がAmazon S3とAmazon CloudFrontで構成されていると推測されるAWS Amplify Hostingをカスタムオリジンと見なし、Amazon CloudFront、AWS WAF、Lambda@Edgeを使用してIP制限機能の追加と基本認証機能のオーバーライドを試してみたいと思います。

補足ですが、AWS Amplifyのコンポーネントは最近ではAWS Amplify CLI、AWS Amplify Libraries、AWS Amplify Hosting、AWS Amplify Studioに分類され、AWS Amplify HostingにはAWS Amplify CLIによるホスティングとAWS Amplify Consoleによるホスティングを含むような概念になっているようです。

参考:Web アプリケーション開発のいろはと AWS Amplify

ここでは以前の記事と同様にAWS Amplify Consoleの機能について言及しますが、AWS Amplifyによるホスティング機能をまとめてAWS Amplify Hostingと呼ぶようになっている傾向が強いため、用語としては「AWS Amplify Hosting」を使用しています。

※本記事および当執筆者のその他の記事で掲載されているソースコードは自主研究活動の一貫として作成したものであり、動作を保証するものではありません。使用する場合は自己責任でお願い致します。また、予告なく修正することもありますのでご了承ください。

今回の記事の内容は次のような構成になっています。

本記事で試す構成図

今回の構成ではAmazon S3とAmazon CloudFrontで構成されていると推測されるAWS Amplify Hostingをカスタムオリジンと見なし、Amazon CloudFront、AWS WAF、Lambda@Edgeと関連付けるので、Amazon CloudFrontが2段の構成になります。

また、AWS Amplify HostingのURLに直接アクセスされる可能性があるため、AWS Amplify Hostingで設定した基本認証のID・パスワードをBase64エンコードした文字列を追加するAmazon CloudFront側からカスタムヘッダーで送信することで、AWS Amplify Hostingへの直接アクセスを基本認証でブロックして、AWS CloudFormationで追加するAmazon CloudFrontからの通信を許可します。

一方でAmazon CloudFront側の基本認証はLambda@Edgeで実装し、AWS Amplify Hostingの基本認証情報とは別にID・パスワードを設定します。

Amazon CloudFront側のIP制限はAWS WAF Web ACLに入力したIPアドレスを許可するルールを作成して実現します。

今回の構成ではさらにAWS Certificate Manager(ACM)でSSL/TLS証明書も追加するAmazon CloudFrontに関連付けています。

AWS Amplify HostingにAmazon CloudFront、AWS WAF、Lambda@Edge、ACMを追加する構成例
AWS Amplify HostingにAmazon CloudFront、AWS WAF、Lambda@Edge、ACMを追加する構成例

AWS CloudFormationテンプレートとパラメータの例

AWS CloudFormationテンプレート(Amazon CloudFront、AWS WAF、Lambda@Edge、ACMをカスタムオリジンに関連付け)

このAWS CloudFormationテンプレートは、us-east-1リージョンにデプロイすることでAmazon CloudFront、AWS WAF、Lambda@Edge、ACMをカスタムオリジン(AWS Amplify Hosting、Amazon EC2など)に関連付けるものです。

CloudFrontOriginCustomHeaderNameAuthorizationCloudFrontOriginCustomHeaderValueBasic <基本認証用Base64文字列>を入力することでカスタムヘッダーを使用してAmazon CloudFrontの背後にあるカスタムオリジン(AWS Amplify Hosting、Amazon EC2など)で設定してある基本認証を通過します。

CloudFrontOriginCustomHeaderNameCloudFrontOriginCustomHeaderValueを変更すれば、基本認証以外のカスタムヘッダーを使用したカスタムオリジンの認証にも幅広く使用できるようにしています

一方でAmazon CloudFront側のLambda@Edgeを使用した基本認証は、LambdaEdgeBasicAuthIDにID、LambdaEdgeBasicAuthPWにパスワードを入力して設定します。

Amazon CloudFront側のIP制限は、WAFWebACLAllowIPListへプレフィックス表記の許可IPアドレスをカンマ区切りで入力します。

入力パラメータ例

  1. CloudFrontCustomOriginDomain: "demo.xxxxxxxxxxxxxx.amplifyapp.com"
  2. CloudFrontOriginCustomHeaderName: "Authorization"
  3. CloudFrontOriginCustomHeaderValue: "Basic SWFtOkltbWVyc2VkSW5BV1M=" # ID: Iam, Password: ImmersedInAWSの場合
  4. CloudFrontDistributionID: ""
  5. CloudFrontCachePolicyName: "CachingDisabled"
  6. CloudFrontOriginRequestPolicyName: "NONE"
  7. CloudFrontResponseHeaderPolicyName: "CORS-with-preflight-and-SecurityHeadersPolicy"
  8. WAFWebACLResourcePrefix: "WebHostIPRestrictionsForCustomOrigin"
  9. WAFWebACLAllowIPList: "XXX.XXX.XXX.XXX/16,YYY.YYY.YYY.YYY/24,ZZZ.ZZZ.ZZZ.ZZZ/32"
  10. LambdaEdgeBasicAuthFuncName: "WebHostBasicAuthLambdaEdgeForCustomOrigin"
  11. LambdaEdgeBasicAuthID: "Iam"
  12. LambdaEdgeBasicAuthPW: "Nobody"
  13. ACMCustomDomainName: "cfn-acm-edge-waf-cfnt-custom-origin.h-o2k.com"
  14. ACMHostedZoneId: "ZZZZZZZZZZZZZZZZZZZZZ"

※CloudFrontOriginCustomHeaderValueに入力する基本認証用のBase64文字列は次のUnix系コマンドで<ID>、<パスワード>にそれぞれ値を入れて実行すると取得できます。

  1. $ echo -n '<ID>:<パスワード>' | base64

テンプレート本体

ファイル名:WebHostCFnAddCloudfrontWafEdgeAndAcmToCustomOrigin.yml

  1. AWSTemplateFormatVersion: '2010-09-09'
  2. Description: 'CFn Template for a stack that creates ACM, Lambda@Edge, WAF, and Custom Origin+CloudFront Hosting.'
  3. Parameters:
  4. CloudFrontCustomOriginDomain: #【CloudFront用】Amazon CloudFrontに設定するCustomOriginのURL
  5. Type: String
  6. Default: "demo.xxxxxxxxxxxxxx.amplifyapp.com"
  7. CloudFrontOriginCustomHeaderName: #【CloudFront用】Amazon CloudFrontのオリジンカスタムヘッダーに設定するヘッダー名
  8. Type: String
  9. Default: "Authorization" # 基本認証を想定してデフォルトはAuthorization
  10. CloudFrontOriginCustomHeaderValue: #【CloudFront用】Amazon CloudFrontのオリジンカスタムヘッダーに設定するヘッダー値
  11. Type: String
  12. Default: "Basic SWFtOkltbWVyc2VkSW5BV1M=" # 基本認証を想定して「Basic <Base64文字列>」。例は「echo -n "Iam:ImmersedInAWS" | base64」を実行した場合のBase64文字列。
  13. Description: 'The following command creates a base64 string: echo -n "ID:Password" | base64'
  14. CloudFrontCachePolicyName: #【CloudFront用】Amazon CloudFrontのキャッシュポリシーの指定。マネージドポリシーからの選択式。
  15. Type: String
  16. Default: CachingDisabled
  17. AllowedValues:
  18. - NONE
  19. - Amplify
  20. - CachingDisabled
  21. - CachingOptimized
  22. - CachingOptimizedForUncompressedObjects
  23. - Elemental-MediaPackage
  24. CloudFrontOriginRequestPolicyName: #【CloudFront用】Amazon CloudFrontのオリジンリクエストポリシー。マネージドポリシーからの選択式。
  25. Type: String
  26. Default: NONE
  27. AllowedValues:
  28. - NONE
  29. - AllViewer
  30. - AllViewerAndCloudFrontHeaders-2022-06
  31. - AllViewerExceptHostHeader
  32. - CORS-CustomOrigin
  33. - CORS-S3Origin
  34. - Elemental-MediaTailor-PersonalizedManifests
  35. - UserAgentRefererHeaders
  36. CloudFrontResponseHeaderPolicyName: #【CloudFront用】Amazon CloudFrontのレスポンスヘッダーポリシー。マネージドポリシーからの選択式。
  37. Type: String
  38. Default: CORS-with-preflight-and-SecurityHeadersPolicy
  39. AllowedValues:
  40. - NONE
  41. - SimpleCORS
  42. - CORS-With-Preflight
  43. - SecurityHeadersPolicy
  44. - CORS-and-SecurityHeadersPolicy
  45. - CORS-with-preflight-and-SecurityHeadersPolicy
  46. ACMCustomDomainName: #【ACM用】ACM証明書を発行する対象ドメイン名
  47. Type: String
  48. Default: cfn-acm-edge-waf-cfnt-custom-origin.h-o2k.com
  49. ACMHostedZoneId: #【ACM用】ACM証明書を発行する対象ドメイン名を管理するRoute53のホストゾーンID
  50. Type: String
  51. Default: ZZZZZZZZZZZZZZZZZZZZZ
  52. WAFWebACLResourcePrefix: #【WAFWebACL用】AWS WAF WebACLのリソースにつけるPrefix
  53. Type: String
  54. Default: WebHostIPRestrictionsForCustomOrigin
  55. WAFWebACLAllowIPList: #【WAFWebACL用】AWS WAF WebACLのルールでIP制限をするCIDR
  56. #Type: CommaDelimitedList
  57. Type: String
  58. Default: "XXX.XXX.XXX.XXX/16,YYY.YYY.YYY.YYY/24,ZZZ.ZZZ.ZZZ.ZZZ/32"
  59. LambdaEdgeBasicAuthFuncName: #【Lambda@Edge用】基本認証をするLambda@Edgeの関数名
  60. Type: String
  61. Default: WebHostBasicAuthLambdaEdgeForCustomOrigin
  62. LambdaEdgeBasicAuthID: #【Lambda@Edge用】基本認証をするLambda@Edgeで認証するID。AWS Secrets Managerシークレットとして保管する。
  63. Type: String
  64. Default: Iam
  65. LambdaEdgeBasicAuthPW: #【Lambda@Edge用】基本認証をするLambda@Edgeで認証するパスワード。AWS Secrets Managerシークレットとして保管する。
  66. Type: String
  67. Default: Nobody
  68. CloudFrontDistributionID: #【共通】各リソースの関連付けに使用するAmazon CloudFrontのDistribution ID。
  69. #ALambda@Edgeで使用するAWS Secrets Managerシークレットの一意識別で使用する。
  70. Type: String #※スタックの初回Create処理でAmazon CloudFrontを作成してから、2回目のUpdate処理で入力する。
  71. Default: ""
  72. Mappings:
  73. CloudFrontCachePolicyIds:
  74. NONE:
  75. Id: ""
  76. Amplify:
  77. Id: 2e54312d-136d-493c-8eb9-b001f22f67d2
  78. CachingDisabled:
  79. Id: 4135ea2d-6df8-44a3-9df3-4b5a84be39ad
  80. CachingOptimized:
  81. Id: 658327ea-f89d-4fab-a63d-7e88639e58f6
  82. CachingOptimizedForUncompressedObjects:
  83. Id: b2884449-e4de-46a7-ac36-70bc7f1ddd6d
  84. Elemental-MediaPackage:
  85. Id: 08627262-05a9-4f76-9ded-b50ca2e3a84f
  86. CloudFrontOriginRequestPolicyIds:
  87. NONE:
  88. Id: ""
  89. AllViewer:
  90. Id: 216adef6-5c7f-47e4-b989-5492eafa07d3
  91. AllViewerAndCloudFrontHeaders-2022-06:
  92. Id: 33f36d7e-f396-46d9-90e0-52428a34d9dc
  93. AllViewerExceptHostHeader:
  94. Id: b689b0a8-53d0-40ab-baf2-68738e2966ac
  95. CORS-CustomOrigin:
  96. Id: 59781a5b-3903-41f3-afcb-af62929ccde1
  97. CORS-S3Origin:
  98. Id: 88a5eaf4-2fd4-4709-b370-b4c650ea3fcf
  99. Elemental-MediaTailor-PersonalizedManifests:
  100. Id: 775133bc-15f2-49f9-abea-afb2e0bf67d2
  101. UserAgentRefererHeaders:
  102. Id: acba4595-bd28-49b8-b9fe-13317c0390fa
  103. CloudFrontResponseHeaderPolicyIds:
  104. NONE:
  105. Id: ""
  106. CORS-and-SecurityHeadersPolicy:
  107. Id: e61eb60c-9c35-4d20-a928-2b84e02af89c
  108. CORS-With-Preflight:
  109. Id: 5cc3b908-e619-4b99-88e5-2cf7f45965bd
  110. CORS-with-preflight-and-SecurityHeadersPolicy:
  111. Id: eaab4381-ed33-4a86-88ca-d9558dc6cd63
  112. SecurityHeadersPolicy:
  113. Id: 67f7725c-6f97-4210-82d7-5512b31e9d03
  114. SimpleCORS:
  115. Id: 60669652-455b-4ae9-85a4-c4c02393f86c
  116. Conditions:
  117. IsAllowIPList:
  118. !Not [!Equals [!Ref WAFWebACLAllowIPList, ""]]
  119. IsCloudFrontDistributionID: #入力パラメータにAmazon CloudFrontのDistribution IDがある場合はTrueでLambda@Edgeを作成、ない場合はFalseでLambda@Edge作成をスキップ。
  120. !Not [!Equals [!Ref CloudFrontDistributionID, ""]]
  121. Resources:
  122. #CloudFrontをエイリアスレコードとしてRoute53ホストゾーンに登録するRoute53レコードセット
  123. Route53RecordSetGroup:
  124. Type: AWS::Route53::RecordSetGroup
  125. DependsOn:
  126. - CloudFrontDistribution
  127. Properties:
  128. HostedZoneId:
  129. !Ref ACMHostedZoneId
  130. RecordSets:
  131. - Name: !Ref ACMCustomDomainName
  132. Type: A
  133. #CloudFrontをエイリアスレコードとして登録する場合はエイリアスターゲットのHostedZoneIdが次の固定値となる
  134. #参考:https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/quickref-route53.html
  135. AliasTarget:
  136. HostedZoneId: Z2FDTNDATAQYW2
  137. DNSName: !GetAtt CloudFrontDistribution.DomainName
  138. #ACM証明書の発行
  139. CertificateManagerCertificate:
  140. Type: AWS::CertificateManager::Certificate
  141. Properties:
  142. DomainName: !Ref ACMCustomDomainName
  143. DomainValidationOptions:
  144. - DomainName: !Ref ACMCustomDomainName
  145. HostedZoneId: !Ref ACMHostedZoneId
  146. ValidationMethod: DNS #カスタムドメインの検証はRoute53のDNSを使用する方法で実施する
  147. #AWS WAF WebACLの作成
  148. WAFv2WebACL:
  149. Type: AWS::WAFv2::WebACL
  150. Properties:
  151. Name: !Sub "${WAFWebACLResourcePrefix}-WebACL"
  152. Scope: CLOUDFRONT
  153. DefaultAction:
  154. Block: {}
  155. VisibilityConfig:
  156. SampledRequestsEnabled: true
  157. CloudWatchMetricsEnabled: true
  158. MetricName: !Sub "${WAFWebACLResourcePrefix}-WebACL-Metric"
  159. Rules:
  160. - Name: !Sub "${WAFWebACLResourcePrefix}-WebACL-Rule"
  161. Action:
  162. Allow: {}
  163. Priority: 0
  164. Statement:
  165. IPSetReferenceStatement:
  166. Arn: !GetAtt WAFv2IPSet.Arn
  167. VisibilityConfig:
  168. SampledRequestsEnabled: true
  169. CloudWatchMetricsEnabled: true
  170. MetricName: !Sub "${WAFWebACLResourcePrefix}-WebACL-Rule-Metric"
  171. WAFv2IPSet:
  172. Type: AWS::WAFv2::IPSet
  173. Properties:
  174. Name: !Sub "${WAFWebACLResourcePrefix}-IPSet"
  175. Scope: CLOUDFRONT
  176. IPAddressVersion: IPV4
  177. Addresses: !If [IsAllowIPList, !Split [ ",", !Ref WAFWebACLAllowIPList ], []]
  178. #Lambda@Edgeの作成
  179. LambdaEdgeBasicAuth:
  180. Type: AWS::Lambda::Function
  181. DependsOn:
  182. - SecretsManagerSecret
  183. - LambdaEdgeBasicAuthRole
  184. Properties:
  185. FunctionName: !Ref LambdaEdgeBasicAuthFuncName
  186. Runtime: python3.8
  187. MemorySize: 128 #Lambda@Edgeのクォータ最大値を設定
  188. Timeout: 5 #Lambda@Edgeのクォータ最大値を設定
  189. Role: !GetAtt LambdaEdgeBasicAuthRole.Arn
  190. Handler: index.lambda_handler
  191. Code:
  192. ZipFile: |
  193. # 基本認証を実施するLambda@Edgeの関数
  194. import json
  195. import boto3
  196. import base64
  197. # ID、パスワードを保管し、認証に使用するAWS Secrets Managerを扱うクライアントを作成
  198. asm_client = boto3.client('secretsmanager', region_name='us-east-1')
  199. # エラーの場合のレスポンスを定義
  200. err_response = {
  201. 'status': '401',
  202. 'statusDescription': 'Unauthorized',
  203. 'body': 'Authentication Failed.',
  204. 'headers': {
  205. 'www-authenticate': [
  206. {
  207. 'key': 'WWW-Authenticate',
  208. 'value': 'Basic realm="Basic Authentication"'
  209. }
  210. ]
  211. }
  212. }
  213. def lambda_handler(event, context):
  214. try:
  215. print('event:')
  216. print(event)
  217. # イベントからCloudFrontのリクエストを取得
  218. request = event['Records'][0]['cf']['request']
  219. # イベントからCloudFrontのDistribution IDを取得
  220. cf_dist_id = event['Records'][0]['cf']['config']['distributionId']
  221. # リクエストからヘッダーを取得
  222. headers = request['headers']
  223. if (headers.get('authorization') != None):
  224. # ヘッダーにauthorizationがあれば認証を試行する
  225. # headers['authorization'][0]['value']の中身は「Basic <base64でエンコードされた[ID:Password]の文字列>」
  226. # authorizationの内容を分解してID、パスワードを取り出す
  227. target_credentials_str = headers['authorization'][0]['value'].split(
  228. " ")
  229. target_credentials = base64.b64decode(
  230. target_credentials_str[1]).decode().split(":")
  231. target_id = target_credentials[0]
  232. target_pw = target_credentials[1]
  233. # 取得したCloudFrontのDistribution IDおよび基本認証で入力されたIDでシークレットを取得する
  234. response = asm_client.get_secret_value(
  235. SecretId='CloudFrontBasicAuth/' +
  236. cf_dist_id + '/' + str(target_id)
  237. )
  238. # シークレットが取得でき、格納されている文字列が基本認証で入力されたパスワードと一致すれば認証成功としてリクエストを返却する。
  239. # それ以外の場合はエラーレスポンスを返す。
  240. if (response.get('SecretString') != None):
  241. secret_string = json.loads(response['SecretString'])
  242. if (secret_string.get('Password') == target_pw):
  243. return request
  244. else:
  245. return err_response
  246. else:
  247. return err_response
  248. else:
  249. return err_response
  250. except Exception as e:
  251. print("Exception:")
  252. print(e)
  253. return err_response
  254. LambdaEdgeBasicAuthVersion: #呼出元スタックでAmazon CloudFrontと関連付けるためのLambdaEdgeバージョンを作成する
  255. Type: AWS::Lambda::Version
  256. DependsOn:
  257. - LambdaEdgeBasicAuth
  258. Properties:
  259. FunctionName: !Ref LambdaEdgeBasicAuth
  260. Description: 'Basic Auth Lambda Edge for Amazon CloudFront'
  261. LambdaEdgeBasicAuthRole: #基本認証用Lambda@Edgeに適用するIAMロールとIAMポリシーの設定
  262. Type: AWS::IAM::Role
  263. Properties:
  264. RoleName: !Sub 'IAMRole-${LambdaEdgeBasicAuthFuncName}'
  265. Path: /
  266. AssumeRolePolicyDocument:
  267. Version: 2012-10-17
  268. Statement:
  269. - Effect: Allow
  270. Principal:
  271. Service:
  272. - edgelambda.amazonaws.com
  273. - lambda.amazonaws.com
  274. Action:
  275. - sts:AssumeRole
  276. ManagedPolicyArns:
  277. - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
  278. Policies:
  279. - PolicyName: !Sub 'IAMPolicy-${LambdaEdgeBasicAuthFuncName}'
  280. PolicyDocument:
  281. Version: '2012-10-17'
  282. Statement:
  283. - Effect: Allow
  284. Action:
  285. - iam:CreateServiceLinkedRole
  286. Resource:
  287. - '*'
  288. - Effect: Allow
  289. Action:
  290. - lambda:GetFunction
  291. - lambda:EnableReplication
  292. Resource:
  293. - !Sub 'arn:aws:lambda:us-east-1:${AWS::AccountId}:function:${LambdaEdgeBasicAuthFuncName}'
  294. - Effect: Allow
  295. Action:
  296. - cloudfront:UpdateDistribution
  297. Resource:
  298. !If
  299. - IsCloudFrontDistributionID
  300. - !Sub 'arn:aws:cloudfront::${AWS::AccountId}:distribution/${CloudFrontDistributionID}'
  301. - !Sub 'arn:aws:cloudfront::${AWS::AccountId}:distribution/*'
  302. - PolicyName: SecretsManagerGetSecretValue
  303. PolicyDocument:
  304. Version: '2012-10-17'
  305. Statement:
  306. - Effect: Allow
  307. Action:
  308. - secretsmanager:GetSecretValue
  309. Resource:
  310. !If
  311. - IsCloudFrontDistributionID
  312. - !Sub 'arn:aws:secretsmanager:us-east-1:${AWS::AccountId}:secret:CloudFrontBasicAuth/${CloudFrontDistributionID}/*'
  313. - !Sub 'arn:aws:secretsmanager:us-east-1:${AWS::AccountId}:secret:CloudFrontBasicAuth/DUMMY/*'
  314. SecretsManagerSecret: #基本認証に使用するID、パスワードを格納するAWS Secrets Managerシークレットの作成
  315. Type: 'AWS::SecretsManager::Secret'
  316. Properties:
  317. Name:
  318. !If
  319. - IsCloudFrontDistributionID
  320. - !Sub CloudFrontBasicAuth/${CloudFrontDistributionID}/${LambdaEdgeBasicAuthID}
  321. - !Sub CloudFrontBasicAuth/DUMMY/${LambdaEdgeBasicAuthID}
  322. SecretString: !Sub '{"Password":"${LambdaEdgeBasicAuthPW}"}'
  323. Description: "SecretsManagerSecret of LambdaEdgeBasicAuth"
  324. CloudFrontDistribution:
  325. Type: AWS::CloudFront::Distribution
  326. DependsOn:
  327. #ACM証明書が発行されてからCloudFrontに関連付けるためDependsOnを設定
  328. - CertificateManagerCertificate
  329. #AWS WAF WebACLが作成されてからCloudFrontに関連付けるためDependsOnを設定
  330. - WAFv2WebACL
  331. #基本認証用Lambda@Edgeバージョンが作成されてからCloudFrontに関連付けるためDependsOnを設定
  332. - LambdaEdgeBasicAuth
  333. Properties:
  334. DistributionConfig:
  335. #CloudFrontのエイリアスにACM証明書を発行したドメイン名を設定する
  336. Aliases:
  337. - !Ref ACMCustomDomainName
  338. #CloudFrontにACM証明書を設定する
  339. ViewerCertificate:
  340. SslSupportMethod: sni-only
  341. MinimumProtocolVersion: TLSv1.2_2021
  342. AcmCertificateArn: !Ref CertificateManagerCertificate #返却値はACM証明書のARN
  343. WebACLId: !GetAtt WAFv2WebACL.Arn
  344. HttpVersion: http2
  345. Origins:
  346. - DomainName: !Ref CloudFrontCustomOriginDomain
  347. Id: StaticWebsiteHostingCustomOrigin
  348. CustomOriginConfig:
  349. OriginProtocolPolicy: "https-only"
  350. OriginCustomHeaders:
  351. - HeaderName: !Ref CloudFrontOriginCustomHeaderName
  352. HeaderValue: !Ref CloudFrontOriginCustomHeaderValue
  353. Enabled: true
  354. DefaultCacheBehavior:
  355. AllowedMethods: [GET, HEAD, OPTIONS]
  356. TargetOriginId: StaticWebsiteHostingCustomOrigin #オリジンをターゲットに指定する
  357. ForwardedValues:
  358. QueryString: false
  359. ViewerProtocolPolicy: redirect-to-https
  360. CachePolicyId: !FindInMap [ CloudFrontCachePolicyIds, !Ref CloudFrontCachePolicyName , Id ]
  361. OriginRequestPolicyId: !FindInMap [ CloudFrontOriginRequestPolicyIds, !Ref CloudFrontOriginRequestPolicyName , Id ]
  362. ResponseHeadersPolicyId: !FindInMap [ CloudFrontResponseHeaderPolicyIds, !Ref CloudFrontResponseHeaderPolicyName , Id ]
  363. #DefaultTTL: 86400
  364. #MaxTTL: 31536000
  365. #MinTTL: 60
  366. #Compress: true
  367. LambdaFunctionAssociations:
  368. !If
  369. - IsCloudFrontDistributionID
  370. - [
  371. {
  372. EventType: viewer-request,
  373. IncludeBody: "true",
  374. LambdaFunctionARN: !Ref LambdaEdgeBasicAuthVersion
  375. }
  376. ]
  377. - !Ref AWS::NoValue
  378. DefaultRootObject: index.html
  379. CustomErrorResponses:
  380. - {
  381. ErrorCachingMinTTL: 10,
  382. ErrorCode: 400,
  383. ResponseCode: 200,
  384. ResponsePagePath: /,
  385. }
  386. - {
  387. ErrorCachingMinTTL: 10,
  388. ErrorCode: 404,
  389. ResponseCode: 200,
  390. ResponsePagePath: /,
  391. }
  392. Outputs:
  393. Region:
  394. Value:
  395. !Ref AWS::Region
  396. ACMCustomDomainURL:
  397. Value:
  398. !Join ["", ["https://", !Ref ACMCustomDomainName]]
  399. Description: "Web hosting URL with Certificate"
  400. CloudFrontDistributionID:
  401. Value:
  402. !Ref CloudFrontDistribution
  403. CloudFrontDomainName:
  404. Value:
  405. !GetAtt CloudFrontDistribution.DomainName
  406. CloudFrontSecureURL:
  407. Value:
  408. !Join ["", ["https://", !GetAtt CloudFrontDistribution.DomainName]]
  409. LambdaEdgeCompleteSetupSkiped:
  410. Value:
  411. !If [IsCloudFrontDistributionID, "false", "true"]

構築手順

  1. 次の記事で説明してある手順を参考にAWS Amplify Hostingによる静的ウェブサイトを作成し、基本認証のID、パスワードを設定する。
    AWSの静的ウェブサイトホスティングで入門するAWS Amplify(Console、CLI) - 構築編(Amplify Console)
    以下の情報はAWS CloudFormationテンプレートのパラメータとして使用するのでメモしておく。
    ・作成したAWS Amplify Hostingのドメイン(例:<App Name>.xxxxxxxxxxxxxx.amplifyapp.com)
    ・Unix系OSで次のコマンドを実行して出力されるBase64文字列(<ID>、<パスワード>にはAWS Amplify Hostingに設定した基本認証のID、パスワードを入力する)
     echo -n '<ID>:<パスワード>' | base64

  2. us-east-1リージョンで「Amazon CloudFront、AWS WAF、Lambda@Edgeをカスタムオリジンに関連付け」のAWS CloudFormationテンプレートにカスタムオリジン(AWS Amplify Hosting)のドメインや基本認証用Base64文字列などをパラメータに入力した上で作成(初回実行)のデプロイをする。
    ※初回実行ではCloudFrontDistributionIDは入力しない。
    初回作成後にOutputフィールドへCloudFrontDistributionIDが出力されるのでメモしておく。

  3. us-east-1リージョンで「Amazon CloudFront、AWS WAF、Lambda@Edgeをカスタムオリジンに関連付け」のAWS CloudFormationテンプレートにCloudFrontDistributionIDのパラメータを入力した上で更新(2回目実行)のデプロイをする。
    入力されたCloudFrontDistributionIDに基づいてLambda@Edgeで使用するAWS Secrets Managerシークレットや権限が変更されてAmazon CloudFront側の基本認証が設定される。

  4. 結果確認
    正しく実行されていればパラメータで指定した許可IPアドレスからACM証明書を発行する対象ドメインにhttpsでアクセスすると基本認証のダイアログが表示され、Amazon CloudFront側の基本認証としてパラメータで指定したIDとパスワードで認証を通過します。
    CloudFrontOriginCustomHeaderNameAuthorizationCloudFrontOriginCustomHeaderValueBasic <基本認証用Base64文字列>を入力していれば、Amazon CloudFront側からカスタムオリジン(AWS Amplify Hosting)には前述した基本認証用Base64文字列がAuthorizationのカスタムヘッダーで送信され、カスタムオリジン側の基本認証を通過します。
    WAFWebACLAllowIPListパラメータで指定した許可IPアドレス以外からアクセスすると「403 ERROR」でアクセス拒否されます。
    正しく動作しない場合は呼出元AWS CloudFormationスタックのイベント内容、AWS CloudTrailのログから原因を特定して不具合を修正します。

削除手順

AWS CloudFormationではLambda@EdgeバージョンとAmazon CloudFrontの相互関係のある関連付け解除を順序制御することはしないので一度にすべてのスタックを削除することはできません。
そのため、削除する場合は次の手順のようにAmazon CloudFrontとLambda@Edgeバージョンの関連付けを解除した後、AWS CloudFormationスタックを削除する必要があります。

  1. AWS CloudFormationスタックのパラメータでAmazon CloudFrontのDistribution IDを空にしてUpdate処理をする。
    呼出元AWS CloudFormationスタックのパラメータ「CloudFrontDistributionID」を空にしてスタックを更新する。
  2. AWS CloudFormationスタックを削除する
  3. AWS CloudFormationスタックの削除が失敗するようであれば、Lambda@Edgeを残してAWS CloudFormationスタックを削除し、後からLambda@Edgeバージョンを個別に削除する。


参考:
Web アプリケーション開発のいろはと AWS Amplify
Route 53 template snippets - AWS CloudFormation
Tech Blog with related articles referenced

まとめ

今回はus-east-1リージョンへ作成したAmazon CloudFront、AWS WAF、Lambda@EdgeへAWS Amplify Hostingをカスタムオリジンとして関連付けてIP制限機能の追加と基本認証機能のオーバーライドを試しました。

結果として今回の構成で追加したAmazon CloudFront側のSSL/TLS証明書(AWS Certificate Manager)・基本認証(Lambda@Edge)・IP制限(AWS WAF)が機能し、Authorizationカスタムヘッダーで設定した基本認証用Base64文字列によってカスタムオリジン(AWS Amplify Hosting)の基本認証を通過してHTMLコンテンツが表示されることを確認できました。

また、今回の構成はAWS Amplify Hosting(AWS Amplify Console)の内部にあると想定されるAmazon CloudFrontとAWS CloudFormationによって追加したAmazon CloudFrontで2段のAmazon CloudFront構成のため、レスポンスは遅くなりましたが、特に不具合はなくHTMLコンテンツが表示されました(※動作保証ではありません。特にJavaScriptやバックエンドとのAPI通信などが絡んでくると不具合が出る可能性はあります。)。

ただ、本来であればAWS Amplify Hosting側でAWS WAFのIP制限などの機能がサポートされることが理想的です。
今後もAWS Amplify Hostingの改善を楽しみにしながら、AWS CloudFormation、AWS Amplify、AWS CDKなどのデプロイ関連サービスや静的ウェブホスティングに関するアップデートについて様々なパターンを試してみたいと考えています。

Written by Hidekazu Konishi
Hidekazu Konishi (小西秀和), a Japan AWS Top Engineer and a Japan AWS All Certifications Engineer

執筆者小西秀和

Japan AWS Top Engineer, Japan AWS All Certifications Engineer(AWS認定全冠)として、知識と実践的な経験を活かし、AWSの活用に取り組んでいます。
NRIネットコムBlog: 小西 秀和: 記事一覧
Amazon.co.jp: 小西 秀和: books, biography, latest update
Personal Tech Blog | [B! Bookmark]