ALBのアクセスをCloudFrontからだけに許可するように設定してみた

2022.09.19

今までALBの前にCloudFrontを置いた設定を行ったことが無かったのでやってみました。

やり方

CloudFront + S3の場合はOAIOACという設定をCloudFrontで行い、S3バケットポリシーを設定して制限できました。
ALBの場合は以下の方法があります。

  • CloudFrontでカスタムヘッダーをつけてALBで制限する方法
  • セキュリティグループにCloudFrontのマネージドプレフィックスを設定する方法

今回は上記2つの方法をやってみました。

CloudFrontでカスタムヘッダーをつけてALBで制限する方法

まずはCloudFrontとALBを作成していきます。
VPCとパブリックサブネットは作成済みのものを使用しています。
今回はCloudFormationを使用してCloudFront、ALB、EC2を作成してみました。

AWSTemplateFormatVersion: "2010-09-09"

Description: CloudFront ALB EC2

Parameters:
# ------------------------------------------------------------#
# Parameters
# ------------------------------------------------------------# 
  VolumeSize:
    Default: 8
    Type: Number

  Ec21InstanceType:
    Default: t2.micro
    Type: String

  Vpcid:
    Type: AWS::EC2::VPC::Id
    Description: Enter VPC ID

  PublicSubnet1:
    Type: AWS::EC2::Subnet::Id
    Description: Enter Subnet ID

  PublicSubnet2:
    Type: AWS::EC2::Subnet::Id
    Description: Enter Subnet ID

Resources:
# ------------------------------------------------------------#
# IAM
# ------------------------------------------------------------# 
  Ec2SsmRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument: 
        Version: "2012-10-17"
        Statement: 
          - Effect: Allow
            Principal: 
              Service: 
                - ec2.amazonaws.com
            Action: 
              - 'sts:AssumeRole'
      ManagedPolicyArns: 
        - arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore
      RoleName: EC2SsmRole

  Ec2IamInstanceProfile:
    Type: AWS::IAM::InstanceProfile
    Properties:
      InstanceProfileName: Ec2InstanceProfile
      Roles: 
        - !Ref Ec2SsmRole

# ------------------------------------------------------------#
# Security Group
# ------------------------------------------------------------# 
  AlbSg:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: for ALB
      GroupName: alb-sg
      SecurityGroupEgress: 
        - CidrIp: 0.0.0.0/0
          FromPort: -1
          IpProtocol: -1
          ToPort: -1
      SecurityGroupIngress: 
        - CidrIp: 0.0.0.0/0
          FromPort: 80
          IpProtocol: tcp
          ToPort: 80
      Tags: 
        - Key: Name
          Value: alb-sg
      VpcId: !Ref Vpcid

  Ec2Sg:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: for EC2
      GroupName: ec2-sg
      SecurityGroupEgress: 
        - CidrIp: 0.0.0.0/0
          FromPort: -1
          IpProtocol: -1
          ToPort: -1
      SecurityGroupIngress: 
        - FromPort: 80
          IpProtocol: tcp
          ToPort: 80
          SourceSecurityGroupId: !Ref AlbSg
      Tags: 
        - Key: Name
          Value: ec2-sg
      VpcId: !Ref Vpcid

# ------------------------------------------------------------#
# EC2
# ------------------------------------------------------------# 
  Ec2:
    Type: AWS::EC2::Instance
    Properties:
      BlockDeviceMappings: 
        - DeviceName: /dev/xvda
          Ebs:
            DeleteOnTermination: true
            Encrypted: true
            Iops: 3000
            VolumeSize: !Ref VolumeSize
            VolumeType: gp3
      IamInstanceProfile: !Ref Ec2IamInstanceProfile
      ImageId: ami-0f36dcfcc94112ea1
      InstanceType: !Ref Ec21InstanceType
      NetworkInterfaces: 
        - AssociatePublicIpAddress: true
          DeleteOnTermination: true
          DeviceIndex: 0
          GroupSet: 
            - !Ref Ec2Sg
          SubnetId: !Ref PublicSubnet1
      Tags:
        - Key: Name
          Value: ec2
      UserData: !Base64 |
        #!/bin/bash
        yum update -y
        yum upgrade -y
        yum install httpd -y
        systemctl enable httpd
        systemctl start httpd
        touch /var/www/html/index.html
        echo "CloudFront -> ALB -> EC2" > /var/www/html/index.html

# ------------------------------------------------------------#
# ALB
# ------------------------------------------------------------# 
  Alb:
    Type: AWS::ElasticLoadBalancingV2::LoadBalancer
    Properties:
      IpAddressType: ipv4
      Name: alb
      Scheme: internet-facing
      SecurityGroups:
        - !Ref AlbSg
      Subnets: 
        - !Ref PublicSubnet1
        - !Ref PublicSubnet2
      Tags: 
        - Key: Name
          Value: alb
      Type: application

  TargetGroup:
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    Properties:
      HealthCheckEnabled: true
      HealthCheckIntervalSeconds: 30
      HealthCheckPath: /
      HealthCheckPort: 80
      HealthCheckProtocol: HTTP
      HealthCheckTimeoutSeconds: 5
      HealthyThresholdCount: 5
      IpAddressType: ipv4
      Matcher:
        HttpCode: 200
      Name: targetgroup
      Port: 80
      Protocol: HTTP
      ProtocolVersion: HTTP1
      Tags: 
        - Key: Name
          Value: targetgroup
      Targets: 
        - Id: !Ref Ec2
          Port: 80
      TargetType: instance
      UnhealthyThresholdCount: 2
      VpcId: !Ref Vpcid

  AlbListener:
    Type: AWS::ElasticLoadBalancingV2::Listener
    Properties:
      DefaultActions:
        - FixedResponseConfig:
            ContentType: text/plain
            MessageBody: Access denied
            StatusCode: 403
          Type: fixed-response
      LoadBalancerArn: !Ref Alb
      Port: 80
      Protocol: HTTP

  AlbListenerRule:
    Type: AWS::ElasticLoadBalancingV2::ListenerRule
    Properties: 
      Actions: 
        - TargetGroupArn: !Ref TargetGroup
          Type: forward
      Conditions: 
        - Field: http-header
          HttpHeaderConfig:
            HttpHeaderName: Custom-Header
            Values: 
              - kobayashi-riku0226
      ListenerArn: !Ref AlbListener
      Priority: 1

# ------------------------------------------------------------#
# CloudFront
# ------------------------------------------------------------# 
  CloudFront:
    Type: AWS::CloudFront::Distribution
    Properties: 
      DistributionConfig:
        DefaultCacheBehavior:
          AllowedMethods:
            - GET
            - HEAD
          CachedMethods:
            - GET
            - HEAD
          CachePolicyId: 658327ea-f89d-4fab-a63d-7e88639e58f6
          TargetOriginId: Alb
          ViewerProtocolPolicy: allow-all
        Enabled: True
        HttpVersion: http1.1
        Origins: 
          - CustomOriginConfig:
              HTTPPort: 80
              OriginProtocolPolicy: http-only
            DomainName: !GetAtt Alb.DNSName
            Id: Alb
            OriginCustomHeaders: 
            - HeaderName: Custom-Header
              HeaderValue: kobayashi-riku0226
        PriceClass: PriceClass_200

Outputs:
# ------------------------------------------------------------#
# Outputs
# ------------------------------------------------------------# 
  CloudFrontDomain:
    Value: !GetAtt CloudFront.DomainName
    Export: 
      Name: CloudFrontDomain

  ALBDomain:
    Value: !GetAtt Alb.DNSName
    Export: 
      Name: AlbDNSName

以下のコマンドを実行してデプロイしていきます。

aws cloudformation create-stack --stack-name CloudFormationスタック名 --template-body file://CloudFormationテンプレートファイル名 --parameters ParameterKey=Vpcid,ParameterValue=作成済みのVPC ID ParameterKey=PublicSubnet1,ParameterValue=1つめのパブリックサブネットのID ParameterKey=PublicSubnet2,ParameterValue=2つめのパブリックサブネットのID --capabilities CAPABILITY_NAMED_IAM

テンプレートの説明

CloudFrontでカスタムヘッダーを設定するには229行目にあるOriginsの中で設定します。
以下のようにヘッダー名と値を設定しています。
今回は「Custom-Header」というヘッダー名で値に「kobayashi-riku0226」というものを入れています。

        Origins: 
          - CustomOriginConfig:
              HTTPPort: 80
              OriginProtocolPolicy: http-only
            DomainName: !GetAtt Alb.DNSName
            Id: Alb
            OriginCustomHeaders: 
            - HeaderName: Custom-Header
              HeaderValue: kobayashi-riku0226

ALBでのアクセス制限は195行目にあるリスナールールのConditionsで設定します。
また、固定レスポンスとして403を返すためにDefaultActionsで設定をしています。

  AlbListener:
    Type: AWS::ElasticLoadBalancingV2::Listener
    Properties:
      DefaultActions:
        - FixedResponseConfig:
            ContentType: text/plain
            MessageBody: Access denied
            StatusCode: 403
          Type: fixed-response
      LoadBalancerArn: !Ref Alb
      Port: 80
      Protocol: HTTP

  AlbListenerRule:
    Type: AWS::ElasticLoadBalancingV2::ListenerRule
    Properties: 
      Actions: 
        - TargetGroupArn: !Ref TargetGroup
          Type: forward
      Conditions: 
        - Field: http-header
          HttpHeaderConfig:
            HttpHeaderName: Custom-Header
            Values: 
              - kobayashi-riku0226

動作確認

スタックの作成が完了したら以下のコマンドを実行してALBのDNS名とCloudFrontのDNS名を確認します。

aws cloudformation describe-stacks --stack-name スタック名 --query 'Stacks[0].Outputs[*].OutputValue'

実行後、2つの結果が出力されるのでそれぞれブラウザでアクセスしてみます。
以下の結果はALBのDNS名(末尾が「ap-northeast-1.elb.amazonaws.com」になっているもの)にアクセスした結果になります。

結果は画面に「Access denied」と表示されることが確認できます。
curlコマンドを使用してリクエストヘッダーに「Custom-Header: kobayashi-riku0226」指定して実行するとALBで制御されていることがわかります。

curl -H "Custom-Header: kobayashi-riku0226" http://alb-894067198.ap-northeast-1.elb.amazonaws.com -i

次にCloudFrontのDNS名(末尾が「cloudfront.net」になっているもの)にアクセスしてみます。
結果は画面に「CloudFront -> ALB -> EC2」と表示されEC2までアクセスできていることが確認できます。
この設定で気を付けることはカスタムヘッダーを外部に漏らさないようにしなければならないことです。
以下のAWSのドキュメントにも記載されていますが、カスタムヘッダーが外部に漏れた場合にCloudFrontを経由しないでオリジンに直接アクセスできるようになってしまいます。
Application Load Balancers へのアクセスを制限する

このユースケースは、カスタムヘッダー名と値の機密性維持を信頼しています。ヘッダー名と値が機密でない場合、他の HTTP クライアントは、Application Load Balancer に直接送信するリクエストにヘッダー名や値を含める可能性があります。これにより、リクエストをしていない時に、リクエストが CloudFront から送信されたかのように Application Load Balancer を動作させる可能性があります。これを防ぐためには、カスタムヘッダー名と値を機密にしておきます。

セキュリティグループにCloudFrontのマネージドプレフィックスを設定する方法

こちらの方法はALBに設定するセキュリティグループにCloudFrontのマネージドプレフィックスを設定してアクセスを制限する方法になります。

マネージドプレフィックスとは複数のCIDRをリストとして管理するものです。
これを利用することでセキュリティグループやルートテーブルで1つずつルールを設定する手間を省くことができます。
こちらもCloudFormationを使用して設定してみました。

AWSTemplateFormatVersion: "2010-09-09"

Description: CloudFront ALB EC2

Parameters:
# ------------------------------------------------------------#
# Parameters
# ------------------------------------------------------------# 
  VolumeSize:
    Default: 8
    Type: Number

  Ec21InstanceType:
    Default: t2.micro
    Type: String

  Vpcid:
    Type: AWS::EC2::VPC::Id
    Description: Enter VPC ID

  PublicSubnet1:
    Type: AWS::EC2::Subnet::Id
    Description: Enter Subnet ID

  PublicSubnet2:
    Type: AWS::EC2::Subnet::Id
    Description: Enter Subnet ID

Resources:
# ------------------------------------------------------------#
# IAM
# ------------------------------------------------------------# 
  Ec2SsmRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument: 
        Version: "2012-10-17"
        Statement: 
          - Effect: Allow
            Principal: 
              Service: 
                - ec2.amazonaws.com
            Action: 
              - 'sts:AssumeRole'
      ManagedPolicyArns: 
        - arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore
      RoleName: EC2SsmRole

  Ec2IamInstanceProfile:
    Type: AWS::IAM::InstanceProfile
    Properties:
      InstanceProfileName: Ec2InstanceProfile
      Roles: 
        - !Ref Ec2SsmRole

# ------------------------------------------------------------#
# Security Group
# ------------------------------------------------------------# 
  AlbSg:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: for ALB
      GroupName: alb-sg
      SecurityGroupEgress: 
        - CidrIp: 0.0.0.0/0
          FromPort: -1
          IpProtocol: -1
          ToPort: -1
      SecurityGroupIngress: 
        - SourcePrefixListId: pl-58a04531
          FromPort: 80
          IpProtocol: tcp
          ToPort: 80
      Tags: 
        - Key: Name
          Value: alb-sg
      VpcId: !Ref Vpcid

  Ec2Sg:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: for EC2
      GroupName: ec2-sg
      SecurityGroupEgress: 
        - CidrIp: 0.0.0.0/0
          FromPort: -1
          IpProtocol: -1
          ToPort: -1
      SecurityGroupIngress: 
        - FromPort: 80
          IpProtocol: tcp
          ToPort: 80
          SourceSecurityGroupId: !Ref AlbSg
      Tags: 
        - Key: Name
          Value: ec2-sg
      VpcId: !Ref Vpcid

# ------------------------------------------------------------#
# EC2
# ------------------------------------------------------------# 
  Ec2:
    Type: AWS::EC2::Instance
    Properties:
      BlockDeviceMappings: 
        - DeviceName: /dev/xvda
          Ebs:
            DeleteOnTermination: true
            Encrypted: true
            Iops: 3000
            VolumeSize: !Ref VolumeSize
            VolumeType: gp3
      IamInstanceProfile: !Ref Ec2IamInstanceProfile
      ImageId: ami-0f36dcfcc94112ea1
      InstanceType: !Ref Ec21InstanceType
      NetworkInterfaces: 
        - AssociatePublicIpAddress: true
          DeleteOnTermination: true
          DeviceIndex: 0
          GroupSet: 
            - !Ref Ec2Sg
          SubnetId: !Ref PublicSubnet1
      Tags:
        - Key: Name
          Value: ec2
      UserData: !Base64 |
        #!/bin/bash
        yum update -y
        yum upgrade -y
        yum install httpd -y
        systemctl enable httpd
        systemctl start httpd
        touch /var/www/html/index.html
        echo "CloudFront -> ALB -> EC2" > /var/www/html/index.html

# ------------------------------------------------------------#
# ALB
# ------------------------------------------------------------# 
  Alb:
    Type: AWS::ElasticLoadBalancingV2::LoadBalancer
    Properties:
      IpAddressType: ipv4
      Name: alb
      Scheme: internet-facing
      SecurityGroups:
        - !Ref AlbSg
      Subnets: 
        - !Ref PublicSubnet1
        - !Ref PublicSubnet2
      Tags: 
        - Key: Name
          Value: alb
      Type: application

  TargetGroup:
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    Properties:
      HealthCheckEnabled: true
      HealthCheckIntervalSeconds: 30
      HealthCheckPath: /
      HealthCheckPort: 80
      HealthCheckProtocol: HTTP
      HealthCheckTimeoutSeconds: 5
      HealthyThresholdCount: 5
      IpAddressType: ipv4
      Matcher:
        HttpCode: 200
      Name: targetgroup
      Port: 80
      Protocol: HTTP
      ProtocolVersion: HTTP1
      Tags: 
        - Key: Name
          Value: targetgroup
      Targets: 
        - Id: !Ref Ec2
          Port: 80
      TargetType: instance
      UnhealthyThresholdCount: 2
      VpcId: !Ref Vpcid

  AlbListener:
    Type: AWS::ElasticLoadBalancingV2::Listener
    Properties:
      DefaultActions:
        - TargetGroupArn: !Ref TargetGroup
          Type: forward
      LoadBalancerArn: !Ref Alb
      Port: 80
      Protocol: HTTP

# ------------------------------------------------------------#
# CloudFront
# ------------------------------------------------------------# 
  CloudFront:
    Type: AWS::CloudFront::Distribution
    Properties: 
      DistributionConfig:
        DefaultCacheBehavior:
          AllowedMethods:
            - GET
            - HEAD
          CachedMethods:
            - GET
            - HEAD
          CachePolicyId: 658327ea-f89d-4fab-a63d-7e88639e58f6
          TargetOriginId: Alb
          ViewerProtocolPolicy: allow-all
        Enabled: True
        HttpVersion: http1.1
        Origins: 
          - CustomOriginConfig:
              HTTPPort: 80
              OriginProtocolPolicy: http-only
            DomainName: !GetAtt Alb.DNSName
            Id: Alb
        PriceClass: PriceClass_200

Outputs:
# ------------------------------------------------------------#
# Outputs
# ------------------------------------------------------------# 
  CloudFrontDomain:
    Value: !GetAtt CloudFront.DomainName
    Export: 
      Name: CloudFrontDomain

  ALBDomain:
    Value: !GetAtt Alb.DNSName
    Export: 
      Name: AlbDNSName

CloudFrontでカスタムヘッダーをつけてALBで制限する方法と同じコマンドでデプロイ、DNS名の確認をします。

aws cloudformation create-stack --stack-name CloudFormationスタック名 --template-body file://CloudFormationテンプレートファイル名 --parameters ParameterKey=Vpcid,ParameterValue=作成済みのVPC ID ParameterKey=PublicSubnet1,ParameterValue=1つめのパブリックサブネットのID ParameterKey=PublicSubnet2,ParameterValue=2つめのパブリックサブネットのID --capabilities CAPABILITY_NAMED_IAM
aws cloudformation describe-stacks --stack-name スタック名 --query 'Stacks[0].Outputs[*].OutputValue'

テンプレート説明

一番上のテンプレートとほとんど同じ内容ですが、59行目のALBのセキュリティグループでCloudFrontのマネージドプレフィックスリストを設定しています。
SourcePrefixListIdでマネージドプレフィックスリストのIDを指定します。

  AlbSg:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: for ALB
      GroupName: alb-sg
      SecurityGroupEgress: 
        - CidrIp: 0.0.0.0/0
          FromPort: -1
          IpProtocol: -1
          ToPort: -1
      SecurityGroupIngress: 
        - SourcePrefixListId: pl-58a04531
          FromPort: 80
          IpProtocol: tcp
          ToPort: 80
      Tags: 
        - Key: Name
          Value: alb-sg
      VpcId: !Ref Vpcid

動作確認

ALBのDNS名とCloudFrontのDNS名にブラウザからアクセスしてみます。
ALBの場合は以下の画像のようにタイムアウトになりアクセスに失敗します。

CloudFrontの場合は以下のように接続に成功します。

この設定で気を付けることは以下のドキュメントの通りCloudFrontのマネージドプレフィックスは重みが55なので55個分のルールが設定されるのと同等になります。
デフォルトのセキュリティグループのルール数だと60個までしかルールが設定できないので60個以上設定したい場合は上限緩和で対応する必要があります。
AWS マネージドプレフィックスリストのウェイト

さいごに

設定はどちらも難しくはないですが、カスタムヘッダーを漏洩させないように管理するよりはマネージドプレフィックスリストを設定する方が良いのかなとは思いました。