許可されたソースIPアドレスからのみ Amazon S3 へのHTTPSアクセスを受け付ける [AWS CloudFormation テンプレート付き]

こんにちは、広野です。

S3に置いたファイルを会社等、特定のネットワークからのみアクセスさせたい、という要望は多いと思います。やり方はいろいろありますが、ここでは「許可されたソースIPアドレスからのみ Amazon S3 へのHTTPSアクセスを受け付ける」という機能をサーバーレスで実現する方法を1つ紹介したいと思います。

AWS ではもうコモディティ化したアーキテクチャだと思いますが、すぐにプロビジョニングできるように AWS CloudFormation テンプレートを付けます。若干の前提条件はありますが、要件が合えばパラメータだけ変えてそのままお使い頂けますし、合わない場合はカスタマイズ頂いてかまいません。

やりたいこと

  • インターネット上の許可されたソースIPアドレスからのアクセスのみ Amazon S3 に配置したオブジェクトへのアクセスを受け付ける。
  • 許可されたソースIPアドレスはCIDRで指定、ホワイトリストに登録する。本記事では、指定可能な条件は1つのみ。
  • ユーザがアクセスするURLは独自ドメインである。

実現方法

アーキテクチャ

  • ソースIPアドレスのチェックは AWS WAF で行います。
  • AWS WAF は Amazon CloudFront にアタッチします。
  • Amazon CloudFront 経由のアクセスのみ Amazon S3 へのアクセスを許可するよう、OAI (オリジンアクセスアイデンティティ) の設定を仕込みます。
  • ユーザのアクセス先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
Route 53 コンソールを使用してパブリックホストゾーンの作成、一覧表示、および削除を行います。

Amazon Route 53 エイリアスレコード

Amazon Route 53 に DNS レコードを登録するとき、エイリアスレコードという種類のレコードを登録することができます。エイリアスレコードは AWS 独自のレコードの種類で、エイリアスレコードを選択できるときは極力使用した方が良いです。

AWS リソースによっては IP アドレスが動的に変わることがあり、その都度 DNS レコードの変更を余儀なくされるようではサービスとして成り立ちません。エイリアスレコードでは DNS レコードを IP アドレスではなく、AWSがリソースに対して自動作成したFQDNで登録できるようにしたものです。それによって、前述の課題を解決しています。似たようなレコードで CNAME レコードがありますが、それよりもレスポンスが良いようです。

本記事のアーキテクチャでは、ユーザに公開する URL を Amazon CloudFront の FQDN に関連付けるために使用します。

エイリアスレコードと非エイリアスレコードの選択 - Amazon Route 53
Amazon Route 53 でエイリアスレコードを作成するかどうかを選択します。

AWS Certificate Manager (ACM)

AWS が SSL 証明書等を作成、管理してくれるサービスです。料金が安い、すぐに発行、自動更新あり、当然 AWS リソースへの関連付けも容易にできるという素晴らしいサービスです。利用しない手はありません。

本記事のアーキテクチャではユーザのアクセス先 URL を独自ドメインにするため、かつアクセスを暗号化するため HTTPS プロトコルを使用するために SSL 証明書が必要になります。ユーザのアクセスを直接受けるサービスは Amazon CloudFront になるため、ACM で管理している SSL 証明書を Amazon CloudFront に関連付けます。Amazon CloudFront に ACM の証明書を関連付けるときは、証明書がバージニア北部リージョン (us-east-1) で管理されている必要があります。

パブリック証明書をリクエストする - AWS Certificate Manager
パブリックに信頼できる証明書を ACM からリクエストする方法について説明します。

Amazon CloudFront

Amazon CloudFront は、CDN サービスです。CDN とは Contents Delivery Network の略で、世界中の多くの箇所にコンテンツのキャッシュを配置し、ユーザが近い位置のキャッシュにアクセスできるようにすることでレスポンスを速くするサービスです。そのため、ユーザは直接コンテンツそのものにアクセスするのではなく、Amazon CloudFront のキャッシュ配置先 (AWS ではエッジロケーションと言う) に自動的に「誘導される」動きをします。

Amazon CloudFront とは何ですか? - Amazon CloudFront
Amazon CloudFront ウェブサービスを使用して静的および動的なウェブコンテンツをエンドユーザーに高速に配信します。

Amazon CloudFront のアクセス先 URL はデフォルトでは cloudfront.net ドメインで自動生成されますが、独自ドメインを割り当てることができます。HTTPS アクセスをさせる際には、前述の ACM で管理している SSL 証明書が必要になります。独自ドメインの URL を有効にするよう、Route 53 エイリアスレコードを割り当てています。以下 AWS 公式ドキュメントには CNAME レコードを使用するよう記載されていますが、現在はエイリアスレコードも使用できます。また、IPv6にも対応できるようエイリアスレコードを2種類登録しています。

代替ドメイン名 (CNAME) を追加することによるカスタム URL の使用 - Amazon CloudFront
代替ドメイン名 (CNAME) を追加して、デフォルトで CloudFront から割り当てられるドメイン名の代わりに一意のドメイン名を使用する方法、および、代替ドメイン名を移動したり削除したりする方法を説明します。

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 アドレス特有のパターンマッチングをかける設定が用意されており、それを活用します。

IP セットおよび正規表現パターンセット - AWS WAF、AWS Firewall Manager、および AWS Shield Advanced
AWS WAF は、ルールでそれらを参照することで、使用するセットに、より複雑な情報を保存します。これらのセットにはそれぞれ名前があり、作成時に Amazon リソースネーム (ARN) が割り当てられます。これらのセットは、ルールステートメント内から管理でき、コンソールのナビゲーションペインから単独でアクセスして管理...

Amazon S3 と OAI

Amazon S3 は激安なオブジェクトストレージです。ファイルサーバのように CIFS や NFS でアクセスするのではなく、HTTP(S) でアクセスします。デフォルトではパブリックにファイルは公開されておらず、パブリックに、もしくは限定的に公開したい場合は適切に設定をする必要があります。アクセス制御に関する設定は最も大きい範囲でバケット(と呼ばれるフォルダのようなもの)単位で設定可能です。

本記事のアーキテクチャでは、OAI (Origin Access Identity) という機能を利用して、Amazon S3 バケットへのアクセスを以下のように Amazon CloudFront 経由のみに限定許可するよう制御しています。

  • Amazon S3 バケットへのアクセスを、Amazon CloudFront 経由のみに許可する。該当する Amazon CloudFront リソースに ID を持たせ、その ID からのアクセスであれば S3 バケットのファイル読み取りを許可するバケットポリシーを Amazon S3 バケットに設定する。
  • ソース IP アドレスによるアクセス許可判断を Amazon CloudFront にアタッチされた AWS WAF で行うため、上述のバケットポリシーにより、Amazon S3 バケットへのパブリックからの直接アクセスは許可しない。そうしないと ソース IP アドレス制御が機能しないバックドアができてしまうため。
オリジンアクセスアイデンティティ (OAI) を使用して Amazon S3 コンテンツへのアクセスを制限する - Amazon CloudFront
Amazon CloudFront オリジンアクセスアイデンティティ(OAI) で、Amazon S3 コンテンツへのアクセスを制限します。

実装 (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
  SourceIpRange:
    Type: String
    Description: Source IP Address (CIDR) able to access this site. (e.g. xxx.xxx.xxx.xxx/xx)
    Default: 0.0.0.0/0
    MaxLength: 32
    MinLength: 10
  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:
              AWS: !Sub "arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity ${CloudFrontOriginAccessIdentity}"
    DependsOn:
      - S3Bucket
      - CloudFrontOriginAccessIdentity

# ------------------------------------------------------------#
# 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
        Origins:
          - Id: !Sub S3Origin-${SubDomainName}
            DomainName: !GetAtt S3Bucket.DomainName
            S3OriginConfig:
              OriginAccessIdentity: !Sub "origin-access-identity/cloudfront/${CloudFrontOriginAccessIdentity}"
            ConnectionAttempts: 3
            ConnectionTimeout: 10
        ViewerCertificate:
          AcmCertificateArn: !Sub "arn:aws:acm:us-east-1:${AWS::AccountId}:certificate/${CertificateId}"
          MinimumProtocolVersion: TLSv1.2_2019
          SslSupportMethod: sni-only
        WebACLId: !GetAtt WAFv2WebAcl.Arn
      Tags:
        - Key: Cost
          Value: Example
    DependsOn:
      - S3Bucket
      - CloudFrontOriginAccessIdentity
      - CloudFrontOriginRequestPolicy
      - CloudFrontCachePolicy
      - WAFv2WebAcl
  CloudFrontOriginAccessIdentity:
    Type: AWS::CloudFront::CloudFrontOriginAccessIdentity
    Properties:
      CloudFrontOriginAccessIdentityConfig:
        Comment: CloudFront OAI for the IP-based restricted access
  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:
            IPSetReferenceStatement:
              Arn: !GetAtt WAFv2IPWhiteList.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: WAFv2IPWhiteList
  WAFv2IPWhiteList:
    Type: AWS::WAFv2::IPSet
    Properties:
      Name: !Sub IPWhiteList-${SubDomainName}
      Description: WAF v2 IP white list for the IP-based restricted access
      IPAddressVersion: IPV4
      Addresses:
        - !Ref SourceIpRange
      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アドレス範囲を複数持ちたい場合は、WAFv2IPWhiteListの部分をカスタマイズすれば設定可能です。

まとめ

いかがでしたでしょうか?

詳細な解説まではできておらず恐縮ですが、各サービスの機能や設定についてはAWS公式ドキュメントをお読み頂けたらと思います。

この記事がみなさまのお役に立てれば幸いです。

タイトルとURLをコピーしました