こんにちは、広野です。
S3 に置いたファイルを会社等、特定のネットワークからのみアクセスさせたい、という要望は多いと思います。やり方はいろいろありますが、ここでは「許可されたソース IP アドレスからのみ Amazon S3 への HTTPS アクセスを受け付ける」という機能をサーバーレスで実現する方法を1つ紹介したいと思います。
AWS ではもうコモディティ化したアーキテクチャだと思いますが、すぐにプロビジョニングできるように AWS CloudFormation テンプレートを付けます。若干の前提条件はありますが、要件が合えばパラメータだけ変えてそのままお使い頂けますし、合わない場合はカスタマイズ頂いてかまいません。
2022.6.5 修正
- 当初 IPv4 アドレス 1つのみをソース IP アドレスとして設定可能としていましたが、IPv4, IPv6 それぞれを複数設定可能なようにテンプレートを修正しました。
- 本テンプレートはバージニア北部リージョンでのみ使用可能です。AWS WAF がそこでしかプロビジョニングできないため。テンプレートを分けて工夫すると、リージョンを分けることもできます。
2022.10.29 修正
- Amazon CloudFront からのみアクセスを受け付ける Amazon S3 バケットを作成するとき、従来は Origin Access Identity (OAI) をセットアップしていましたが、今後は Origin Access Control (OAC) が推奨になりました。本記事では OAC を使用するよう修正しました。
やりたいこと
- インターネット上の許可されたソースIPアドレスからのアクセスのみ Amazon S3 に配置したオブジェクトへのアクセスを受け付ける。
- 許可されたソース IP アドレスは CIDR で指定、ホワイトリストに登録する。IPv4、IPv6 両方を設定可能とする。
- ユーザがアクセスする URL は独自ドメインである。
実現方法
アーキテクチャ
- ソース IP アドレスのチェックは AWS WAF で行います。
- AWS WAF は Amazon CloudFront にアタッチします。
- Amazon CloudFront 経由のアクセスのみ Amazon S3 へのアクセスを許可するよう、OAC (オリジンアクセスコントロール) の設定を仕込みます。
- ユーザのアクセス先 URL が独自ドメインになるよう、Amazon CloudFront ドメインの URL に Amazon Route 53 で エイリアスレコードを追加します。
- アクセスするプロトコルは HTTPS になるので、独自ドメインの SSL 証明書をバージニア北部リージョンの AWS Certificate Manager で発行し、Amazon CloudFront に割り当てます。
解説
Amazon Route 53 パブリックホストゾーン
いわゆる権威 DNS サーバの機能だと思ってよいと思います。ドメイン指定事業者から取得した独自ドメインの権威 DNS サーバを Amazon Route 53 に登録することで機能し始めます。その状態で Amazon Route 53 に登録した DNS レコードはパブリックに伝播します。
Amazon Route 53 エイリアスレコード
Amazon Route 53 に DNS レコードを登録するとき、エイリアスレコードという種類のレコードを登録することができます。エイリアスレコードは AWS 独自のレコードの種類で、エイリアスレコードを選択できるときは極力使用した方が良いです。
AWS リソースによっては IP アドレスが動的に変わることがあり、その都度 DNS レコードの変更を余儀なくされるようではサービスとして成り立ちません。エイリアスレコードでは DNS レコードを IP アドレスではなく、AWS がリソースに対して自動作成した FQDN で登録できるようにしたものです。それによって、前述の課題を解決しています。似たようなレコードで CNAME レコードがありますが、それよりもレスポンスが良いようです。
本記事のアーキテクチャでは、ユーザに公開する URL を Amazon CloudFront の FQDN に関連付けるために使用します。
AWS Certificate Manager (ACM)
AWS が SSL 証明書等を作成、管理してくれるサービスです。料金が安い、すぐに発行、自動更新あり、当然 AWS リソースへの関連付けも容易にできるという素晴らしいサービスです。利用しない手はありません。
本記事のアーキテクチャではユーザのアクセス先 URL を独自ドメインにするため、かつアクセスを暗号化するため HTTPS プロトコルを使用するために SSL 証明書が必要になります。ユーザのアクセスを直接受けるサービスは Amazon CloudFront になるため、ACM で管理している SSL 証明書を Amazon CloudFront に関連付けます。Amazon CloudFront に ACM の証明書を関連付けるときは、証明書がバージニア北部リージョン (us-east-1) で管理されている必要があります。
Amazon CloudFront
Amazon CloudFront は、CDN サービスです。CDN とは Contents Delivery Network の略で、世界中の多くの箇所にコンテンツのキャッシュを配置し、ユーザが近い位置のキャッシュにアクセスできるようにすることでレスポンスを速くするサービスです。そのため、ユーザは直接コンテンツそのものにアクセスするのではなく、Amazon CloudFront のキャッシュ配置先 (AWS ではエッジロケーションと言う) に自動的に「誘導される」動きをします。
Amazon CloudFront のアクセス先 URL はデフォルトでは cloudfront.net ドメインで自動生成されますが、独自ドメインを割り当てることができます。HTTPS アクセスをさせる際には、前述の ACM で管理している SSL 証明書が必要になります。独自ドメインの URL を有効にするよう、Route 53 エイリアスレコードを割り当てています。以下 AWS 公式ドキュメントには CNAME レコードを使用するよう記載されていますが、現在はエイリアスレコードも使用できます。また、IPv6 にも対応できるようエイリアスレコードを2種類登録しています。
Amazon CloudFront へのアクセスプロトコルはセキュリティを考慮し HTTPS のみに限定しています。HTTP アクセスも許可できますが、現在のトレンドとしても HTTP でパブリックなインターネット内を通信することは推奨されていませんので。
AWS WAF
AWS WAF は Amazon CloudFront や Application Load Balancer など、特定の AWS リソースに対して アプリケーションファイアウォールの機能を持たせる (アタッチする) ことができるサービスです。ただアタッチするだけでは機能せず、ポリシーやルールを適切に設定することで機能し始め、逆に誤った設定をすると意味が無かったり、意図せぬアクセス障害を引き起こしたりすることがあります。
本記事のアーキテクチャでは、許可されたソース IP アドレスからのみ Amazon S3 へのアクセスを許可するために使用します。AWS WAF を直接 Amazon S3 にアタッチすることはできないので、Amazon CloudFront にアタッチします。
本記事のアーキテクチャとは異なり、Amazon S3 のバケットポリシーによってアクセス可能なソース IP アドレスを制御することはできるのですが、独自ドメインを使用するには Amazon CloudFront を使用することが設定しやすいこと、そうであればアクセス制御機能をフロント側に配置する方が効率的であること、Amazon S3 だけでアクセス制御を実装すると機能の制約で今後の拡張性が限られてしまうこと (AWS WAF の方が柔軟) などの理由から、AWS WAF でソース IP アドレス制御をする設計にしています。
AWS WAF には、IP セットと呼ばれる IP アドレス特有のパターンマッチングをかける設定が用意されており、それを活用します。
Amazon S3 と OAC
Amazon S3 は激安なオブジェクトストレージです。ファイルサーバのように CIFS や NFS でアクセスするのではなく、HTTP(S) でアクセスします。デフォルトではパブリックにファイルは公開されておらず、パブリックに、もしくは限定的に公開したい場合は適切に設定をする必要があります。アクセス制御に関する設定は最も大きい範囲でバケット(と呼ばれるフォルダのようなもの)単位で設定可能です。
本記事のアーキテクチャでは、OAC (Origin Access Control) という機能を利用して、Amazon S3 バケットへのアクセスを以下のように Amazon CloudFront 経由のみに限定許可するよう制御しています。
- Amazon S3 バケットへのアクセスを、Amazon CloudFront 経由のみに許可する。該当する Amazon CloudFront からのアクセスであれば S3 バケットのファイル読み取りを許可するバケットポリシーを Amazon S3 バケットに設定する。
- ソース IP アドレスによるアクセス許可判断を Amazon CloudFront にアタッチされた AWS WAF で行うため、上述のバケットポリシーにより、Amazon S3 バケットへのパブリックからの直接アクセスは許可しない。そうしないと ソース IP アドレス制御が機能しないバックドアができてしまうため。
実装 (AWS CloudFormation テンプレート)
前提事項
- 独自ドメインをドメイン指定事業者から取得済みである。
- 取得した独自ドメインを、Amazon Route 53 のパブリックホストゾーンに登録済みである。
つまり、AWS側でドメインのレコード管理ができる状態になっている。 - Amazon CloudFront のURLで使用するサブドメイン名が決定済みである。xxxxxx.example.com の xxxxx の部分。
独自ドメイン名、サブドメイン名ともに、AWS CloudFormation テンプレートのパラメータで指定する。 - バージニア北部リージョンの AWS Certificate Manager で、Amazon CloudFront のURLで使用する独自ドメインのFQDNが含まれるSSL証明書が発行済みである。
サブドメインが *(ワイルドカード)で Zone Apex (サブドメインがない example.com のこと) も含まれる万能証明書が準備されていると便利。
このSSL証明書の Certificate ID を AWS CloudFormation テンプレートのパラメータとして指定する。 - 使用したい Amazon S3 バケット名が決定済みである。
このバケット名を AWS CloudFormation テンプレートのパラメータとして指定する。 - アクセスを許可したいソースIPアドレス範囲が CIDR 形式で決定済みである。xx.xx.xx.xx/xx の形式。
それを AWS CloudFormation テンプレートのパラメータとして指定する。複数ある場合はカンマ区切りで入力する。
AWS CloudFormation テンプレート
前提事項参照の上、以下テンプレートでスタックを作成します。
AWSTemplateFormatVersion: 2010-09-09 Description: CloudFormation template that creates a S3 bucket, a CloudFront distribution and DNS records in Route 53 for the IP-based restricted access. # ------------------------------------------------------------# # Input Parameters # ------------------------------------------------------------# Parameters: BucketName: Type: String Description: S3 Bucket name for URL. Default: bucket_name MaxLength: 20 MinLength: 1 DomainName: Type: String Description: Domain name for URL. xxxxx.xxx (e.g. example.com) Default: example.com MaxLength: 40 MinLength: 5 AllowedPattern: "[^\\s@]+\\.[^\\s@]+" SubDomainName: Type: String Description: Sub domain name for URL. xxxxx.example.com Default: subdomain MaxLength: 20 MinLength: 1 SourceIpv4RangeList: Type: CommaDelimitedList Description: The comma delimited list of allowed source IPv4 address range (CIDR) except /0 to access this web site. (e.g. xxx.xxx.xxx.xxx/xx,yyy.yyy.yyy.yyy/yy) Default: "192.168.0.0/24" SourceIpv6RangeList: Type: CommaDelimitedList Description: The comma delimited list of allowed source IPv6 address range (CIDR) except /0 to access this web site. (e.g. 1111:0000:0000:0000:0000:0000:0000:0111/128) Default: "1111:0000:0000:0000:0000:0000:0000:0111/128" CertificateId: Type: String Description: ACM certificate ID. CloudFront supports ACM certificates only in us-east-1 region. Default: xxxxxxxx-xxxx-xxxxxxxxx-xxxxxxxxxxxx MaxLength: 36 MinLength: 36 Resources: # ------------------------------------------------------------# # S3 # ------------------------------------------------------------# S3Bucket: Type: AWS::S3::Bucket Properties: BucketName: !Ref BucketName PublicAccessBlockConfiguration: BlockPublicAcls: true BlockPublicPolicy: true IgnorePublicAcls: true RestrictPublicBuckets: true Tags: - Key: Cost Value: Example S3BucketPolicy: Type: AWS::S3::BucketPolicy Properties: Bucket: !Ref S3Bucket PolicyDocument: Version: "2012-10-17" Statement: - Action: - "s3:GetObject" Effect: Allow Resource: !Sub "arn:aws:s3:::${S3Bucket}/*" Principal: Service: cloudfront.amazonaws.com Condition: StringEquals: AWS:SourceArn: !Sub arn:aws:cloudfront::${AWS::AccountId}:distribution/${CloudFrontDistribution} DependsOn: - S3Bucket - CloudFrontDistribution # ------------------------------------------------------------# # CloudFront # ------------------------------------------------------------# CloudFrontDistribution: Type: AWS::CloudFront::Distribution Properties: DistributionConfig: Enabled: true Comment: CloudFront distribution for the IP-based restricted access Aliases: - !Sub ${SubDomainName}.${DomainName} HttpVersion: http2 IPV6Enabled: true PriceClass: PriceClass_200 DefaultCacheBehavior: TargetOriginId: !Sub S3Origin-${SubDomainName} ViewerProtocolPolicy: redirect-to-https AllowedMethods: - GET - HEAD - OPTIONS CachedMethods: - GET - HEAD CachePolicyId: !Ref CloudFrontCachePolicy OriginRequestPolicyId: !Ref CloudFrontOriginRequestPolicy Compress: true SmoothStreaming: false DefaultRootObject: index.html Origins: - Id: !Sub S3Origin-${SubDomainName} DomainName: !Sub ${S3Bucket}.s3.${AWS::Region}.amazonaws.com S3OriginConfig: OriginAccessIdentity: "" OriginAccessControlId: !GetAtt CloudFrontOriginAccessControl.Id ConnectionAttempts: 3 ConnectionTimeout: 10 ViewerCertificate: AcmCertificateArn: !Sub "arn:aws:acm:us-east-1:${AWS::AccountId}:certificate/${CertificateId}" MinimumProtocolVersion: TLSv1.2_2021 SslSupportMethod: sni-only WebACLId: !GetAtt WAFv2WebAcl.Arn Tags: - Key: Cost Value: Example DependsOn: - S3Bucket - CloudFrontOriginAccessControl - CloudFrontOriginRequestPolicy - CloudFrontCachePolicy - WAFv2WebAcl CloudFrontOriginAccessControl: Type: AWS::CloudFront::OriginAccessControl Properties: OriginAccessControlConfig: Description: !Sub CloudFront OAC IP-based restricted access Name: !Sub OriginAccessControl-${SubDomainName} OriginAccessControlOriginType: s3 SigningBehavior: always SigningProtocol: sigv4 CloudFrontCachePolicy: Type: AWS::CloudFront::CachePolicy Properties: CachePolicyConfig: Name: !Sub CachePolicy-${SubDomainName} Comment: CloudFront Cache Policy for the IP-based restricted internal access DefaultTTL: 3600 MaxTTL: 86400 MinTTL: 60 ParametersInCacheKeyAndForwardedToOrigin: CookiesConfig: CookieBehavior: none EnableAcceptEncodingBrotli: true EnableAcceptEncodingGzip: true HeadersConfig: HeaderBehavior: whitelist Headers: - Access-Control-Request-Headers - Access-Control-Request-Method - Origin QueryStringsConfig: QueryStringBehavior: none CloudFrontOriginRequestPolicy: Type: AWS::CloudFront::OriginRequestPolicy Properties: OriginRequestPolicyConfig: Name: !Sub OriginRequestPolicy-${SubDomainName} Comment: CloudFront Origin Request Policy for the IP-based restricted access CookiesConfig: CookieBehavior: none HeadersConfig: HeaderBehavior: whitelist Headers: - Access-Control-Request-Headers - Access-Control-Request-Method - Origin QueryStringsConfig: QueryStringBehavior: none # ------------------------------------------------------------# # WAF v2 # ------------------------------------------------------------# WAFv2WebAcl: Type: AWS::WAFv2::WebACL Properties: Name: !Sub WebACL-${SubDomainName} Description: WAF v2 WebACL for the IP-based restricted access DefaultAction: Block: {} Scope: CLOUDFRONT Rules: - Name: !Sub CustomRule-IPWhiteList-${SubDomainName} Action: Allow: {} Priority: 0 Statement: OrStatement: Statements: - IPSetReferenceStatement: Arn: !GetAtt WAFv2Ipv4WhiteList.Arn - IPSetReferenceStatement: Arn: !GetAtt WAFv2Ipv6WhiteList.Arn VisibilityConfig: CloudWatchMetricsEnabled: true MetricName: !Sub CustomRule-IPWhiteList-${SubDomainName} SampledRequestsEnabled: true VisibilityConfig: CloudWatchMetricsEnabled: true MetricName: !Sub WebACL-${SubDomainName} SampledRequestsEnabled: true Tags: - Key: Cost Value: Example DependsOn: - WAFv2Ipv4WhiteList - WAFv2Ipv6WhiteList WAFv2Ipv4WhiteList: Type: AWS::WAFv2::IPSet Properties: Name: !Sub IPv4WhiteList-${SubDomainName} Description: WAF v2 IPv4 white list for the IP-based restricted access IPAddressVersion: IPV4 Addresses: !Ref SourceIpv4RangeList Scope: CLOUDFRONT Tags: - Key: Cost Value: Example WAFv2Ipv6WhiteList: Type: AWS::WAFv2::IPSet Properties: Name: !Sub IPv6WhiteList-${SubDomainName} Description: WAF v2 IPv6 white list for the IP-based restricted access IPAddressVersion: IPV6 Addresses: !Ref SourceIpv6RangeList Scope: CLOUDFRONT Tags: - Key: Cost Value: Example # ------------------------------------------------------------# # Route 53 # ------------------------------------------------------------# Route53RecordA: Type: AWS::Route53::RecordSet Properties: HostedZoneName: !Sub ${DomainName}. Name: !Sub ${SubDomainName}.${DomainName}. Type: A AliasTarget: HostedZoneId: Z2FDTNDATAQYW2 DNSName: !GetAtt CloudFrontDistribution.DomainName DependsOn: CloudFrontDistribution Route53RecordAAAA: Type: AWS::Route53::RecordSet Properties: HostedZoneName: !Sub ${DomainName}. Name: !Sub ${SubDomainName}.${DomainName}. Type: AAAA AliasTarget: HostedZoneId: Z2FDTNDATAQYW2 DNSName: !GetAtt CloudFrontDistribution.DomainName DependsOn: CloudFrontDistribution # ------------------------------------------------------------# # Output Parameters # ------------------------------------------------------------# Outputs: #S3 BucketName: Value: !Ref S3Bucket #CloudFront SiteURL: Value: !Sub https://${SubDomainName}.${DomainName} CloudFrontOriginalDomain: Value: !GetAtt CloudFrontDistribution.DomainName
ソース IP アドレス範囲を 0.0.0.0/0 (IPv4) や ::/0 (IPv6) にすることはできないため、その必要がある場合は WAFv2WebAcl の Statement 部分にある条件を加工する必要があります。
まとめ
いかがでしたでしょうか?
詳細な解説まではできておらず恐縮ですが、各サービスの機能や設定については AWS 公式ドキュメントをお読み頂けたらと思います。
この記事がみなさまのお役に立てれば幸いです。