AWS WAFで、テナント別サブドメインごとに接続元IPアドレス制限をやってみた

AWS WAFで、テナント別サブドメインごとに接続元IPアドレス制限をやってみた

テナントが増えることにサブドメインを切る運用を考えているアプリケーションにおいて、テナントごとに接続元IPアドレス制限をかけたい場合は、AWS WAFのWeb ACLに、テナントの数だけルールを作成します。
Clock Icon2025.07.11

テナントが増えることにサブドメインを切る運用で、テナントごとにIP制限をかけたい

おのやんです。

みなさん、テナントが増えることにサブドメインを切る運用を考えているアプリケーションにおいて、テナントごとに接続元IPアドレス制限をかけたいと思ったことはありませんか?私はあります。

たとえば、大元のexample.comのドメインがあったとしましょう。ここに対してテナントが1個、2個と増えるごとに、tenant1.example.comtenant2.example.comとサブドメインを切っていきます。テナント1ではこのIP、テナント2ではこのIPからのみアクセスできるよう設定する、ということですね。

こちらを、今回はAWS WAF(以下、WAF)で設定する機会がありましたので、紹介します。

検証に際しての構成図

今回は、接続元テナントを2つのパブリックなAmazon EC2(以下、EC2)インスタンスに見立てて、ここからALBにアタッチされたサブドメインにアクセスします。その際、EC2インスタンスにアタッチされたElastic IPことに、xxxx.example.comへのアクセスを許可していきます。逆に条件を満たさないアクセスがブロックされるかどうかも検証します。

architecture

今回は、サブドメインごとに異なるデータを表示させるようなアプリの挙動は再現していません。同じALBに対して、複数のサブドメインを切って検証しています。

検証

ALBとパブリックEC2インスタンス・プライベートEC2インスタンスは、一枚のAWS CloudFornation(以下、CFn)テンプレートで作成しておきます。参考までに、テンプレートを作成しておきます。

CFnテンプレート
template.yaml
AWSTemplateFormatVersion: "2010-09-09"
Description: VPC, Security Groups, IAM, and EC2

# ============================================================#
# Input parameters
# ============================================================#

Parameters:
  SystemName:
    Description: System name of each resource names.
    Type: String
    Default: aws

  EnvName:
    Description: Environment name of each resource names.
    Type: String
    Default: test

#============================================================#
# Resources
#============================================================#

Resources:
  #============================================================#
  # VPC and subnets
  #============================================================#

  AWSTestVPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 10.1.0.0/16
      EnableDnsSupport: true
      EnableDnsHostnames: true
      InstanceTenancy: default
      Tags:
        - Key: Name
          Value: !Sub ${SystemName}-${EnvName}-vpc
        - Key: Env
          Value: !Sub ${EnvName}

  AWSTestPublicSubnet1a:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: ap-northeast-1a
      CidrBlock: 10.1.0.0/24
      VpcId: !Ref AWSTestVPC
      Tags:
        - Key: Name
          Value: !Sub ${SystemName}-${EnvName}-public-subnet-1a
        - Key: Env
          Value: !Sub ${EnvName}

  AWSTestPublicSubnet1c:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: ap-northeast-1c
      CidrBlock: 10.1.1.0/24
      VpcId: !Ref AWSTestVPC
      Tags:
        - Key: Name
          Value: !Sub ${SystemName}-${EnvName}-public-subnet-1c
        - Key: Env
          Value: !Sub ${EnvName}

  AWSTestProtectedSubnet1a:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: ap-northeast-1a
      CidrBlock: 10.1.2.0/24
      VpcId: !Ref AWSTestVPC
      Tags:
        - Key: Name
          Value: !Sub ${SystemName}-${EnvName}-protected-subnet-1a
        - Key: Env
          Value: !Sub ${EnvName}

  AWSTestProtectedSubnet1c:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: ap-northeast-1c
      CidrBlock: 10.1.3.0/24
      VpcId: !Ref AWSTestVPC
      Tags:
        - Key: Name
          Value: !Sub ${SystemName}-${EnvName}-protected-subnet-1c
        - Key: Env
          Value: !Sub ${EnvName}

  AWSTestPrivateSubnet1a:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: ap-northeast-1a
      CidrBlock: 10.1.4.0/24
      VpcId: !Ref AWSTestVPC
      Tags:
        - Key: Name
          Value: !Sub ${SystemName}-${EnvName}-private-subnet-1a
        - Key: Env
          Value: !Sub ${EnvName}

  AWSTestPrivateSubnet1c:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: ap-northeast-1c
      CidrBlock: 10.1.5.0/24
      VpcId: !Ref AWSTestVPC
      Tags:
        - Key: Name
          Value: !Sub ${SystemName}-${EnvName}-private-subnet-1c
        - Key: Env
          Value: !Sub ${EnvName}

  #============================================================#
  # Internet gateway and NAT gateway
  #============================================================#

  AWSTestIgw:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: Name
          Value: !Sub ${SystemName}-${EnvName}-igw
        - Key: Env
          Value: !Sub ${EnvName}

  AWSTestIgwAttachment:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      InternetGatewayId: !Ref AWSTestIgw
      VpcId: !Ref AWSTestVPC

  AWSTestEIP:
    Type: AWS::EC2::EIP
    Properties:
      Tags:
        - Key: Name
          Value: !Sub ${SystemName}-${EnvName}-eip
        - Key: Env
          Value: !Sub ${EnvName}

  AWSTestEIPPublic1:
    Type: AWS::EC2::EIP
    Properties:
      Tags:
        - Key: Name
          Value: !Sub ${SystemName}-${EnvName}-eip-public-1
        - Key: Env
          Value: !Sub ${EnvName}

  AWSTestEIPPublic2:
    Type: AWS::EC2::EIP
    Properties:
      Tags:
        - Key: Name
          Value: !Sub ${SystemName}-${EnvName}-eip-public-2
        - Key: Env
          Value: !Sub ${EnvName}

  AWSTestNgw:
    Type: AWS::EC2::NatGateway
    Properties:
      AllocationId: !GetAtt AWSTestEIP.AllocationId
      SubnetId: !Ref AWSTestPublicSubnet1a
      Tags:
        - Key: Name
          Value: !Sub ${SystemName}-${EnvName}-ngw
        - Key: Env
          Value: !Sub ${EnvName}

  #============================================================#
  # Route tables
  #============================================================#

  AWSTestPublicRtb:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref AWSTestVPC
      Tags:
        - Key: Name
          Value: !Sub ${SystemName}-${EnvName}-public-rtb
        - Key: Env
          Value: !Sub ${EnvName}

  AWSTestProtectedRtb:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref AWSTestVPC
      Tags:
        - Key: Name
          Value: !Sub ${SystemName}-${EnvName}-protected-rtb
        - Key: Env
          Value: !Sub ${EnvName}

  AWSTestPrivateRtb:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref AWSTestVPC
      Tags:
        - Key: Name
          Value: !Sub ${SystemName}-${EnvName}-private-rtb
        - Key: Env
          Value: !Sub ${EnvName}

  #============================================================#
  # Routes
  #============================================================#

  AWSTestPublicRoute:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref AWSTestPublicRtb
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref AWSTestIgw

  AWSTestProtectedRoute:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref AWSTestProtectedRtb
      DestinationCidrBlock: 0.0.0.0/0
      NatGatewayId: !Ref AWSTestNgw

  AWSTestPrivateRoute:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref AWSTestPrivateRtb
      DestinationCidrBlock: 0.0.0.0/0
      NatGatewayId: !Ref AWSTestNgw

  #============================================================#
  # Route Tables Subnet Association
  #============================================================#

  AWSTestPublicRtbAssociation1a:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref AWSTestPublicSubnet1a
      RouteTableId: !Ref AWSTestPublicRtb

  AWSTestPublicRtbAssociation1c:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref AWSTestPublicSubnet1c
      RouteTableId: !Ref AWSTestPublicRtb

  AWSTestProtectedRtbAssociation1a:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref AWSTestProtectedSubnet1a
      RouteTableId: !Ref AWSTestProtectedRtb

  AWSTestProtectedRtbAssociation1c:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref AWSTestProtectedSubnet1c
      RouteTableId: !Ref AWSTestProtectedRtb

  AWSTestPrivateRtbAssociation1a:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref AWSTestPrivateSubnet1a
      RouteTableId: !Ref AWSTestPrivateRtb

  AWSTestPrivateRtbAssociation1c:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref AWSTestPrivateSubnet1c
      RouteTableId: !Ref AWSTestPrivateRtb

  #============================================================#
  # IAM Role for EC2 instances
  #============================================================#

  AWSTestEC2Role:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub ${SystemName}-${EnvName}-ec2-role
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - ec2.amazonaws.com
            Action:
              - sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore

  AWSTestEC2InstanceProfile:
    Type: AWS::IAM::InstanceProfile
    Properties:
      InstanceProfileName: !Sub ${SystemName}-${EnvName}-ec2-role
      Path: /
      Roles:
        - Ref: AWSTestEC2Role

  #============================================================#
  # Security Groups
  #============================================================#

  AWSTestSgVPCEndpoint:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: !Sub ${SystemName}-${EnvName}-sg-vpc-endpoint
      GroupName: !Sub ${SystemName}-${EnvName}-sg-vpc-endpoint
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 443
          ToPort: 443
          CidrIp: 10.1.0.0/16
      VpcId: !Ref AWSTestVPC
      Tags:
        - Key: Name
          Value: !Sub ${SystemName}-${EnvName}-sg-vpc-endpoint
        - Key: Env
          Value: !Sub ${EnvName}

  AWSTestSgALB:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: !Sub ${SystemName}-${EnvName}-sg-alb
      GroupName: !Sub ${SystemName}-${EnvName}-sg-alb
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          CidrIp: 0.0.0.0/0
      VpcId: !Ref AWSTestVPC
      Tags:
        - Key: Name
          Value: !Sub ${SystemName}-${EnvName}-sg-alb
        - Key: Env
          Value: !Sub ${EnvName}

  AWSTestSgEC2Private:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: !Sub ${SystemName}-${EnvName}-sg-ec2-private
      GroupName: !Sub ${SystemName}-${EnvName}-sg-ec2-private
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          SourceSecurityGroupId: !Ref AWSTestSgALB
      VpcId: !Ref AWSTestVPC
      Tags:
        - Key: Name
          Value: !Sub ${SystemName}-${EnvName}-sg-ec2-private
        - Key: Env
          Value: !Sub ${EnvName}

  AWSTestSgRDS:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: !Sub ${SystemName}-${EnvName}-sg-rds
      GroupName: !Sub ${SystemName}-${EnvName}-sg-rds
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 3306
          ToPort: 3306
          SourceSecurityGroupId: !Ref AWSTestSgEC2Private
      VpcId: !Ref AWSTestVPC
      Tags:
        - Key: Name
          Value: !Sub ${SystemName}-${EnvName}-sg-rds
        - Key: Env
          Value: !Sub ${EnvName}

  AWSTestSgEC2Public:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: !Sub ${SystemName}-${EnvName}-sg-ec2-public
      GroupName: !Sub ${SystemName}-${EnvName}-sg-ec2-public
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 22
          ToPort: 22
          CidrIp: 0.0.0.0/0
        - IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          CidrIp: 0.0.0.0/0
      VpcId: !Ref AWSTestVPC
      Tags:
        - Key: Name
          Value: !Sub ${SystemName}-${EnvName}-sg-ec2-public
        - Key: Env
          Value: !Sub ${EnvName}

  #============================================================#
  # EC2 Launch Template
  #============================================================#

  AWSTestLaunchTemplate:
    Type: AWS::EC2::LaunchTemplate
    Properties:
      LaunchTemplateName: !Sub ${SystemName}-${EnvName}-ec2-template
      LaunchTemplateData:
        ImageId: ami-039e8f15ccb15368a
        InstanceType: t2.micro
        BlockDeviceMappings:
          - DeviceName: /dev/xvda
            Ebs:
              VolumeType: gp2
              VolumeSize: 20
              DeleteOnTermination: true
        DisableApiTermination: false
        UserData:
          Fn::Base64: !Sub |
            #!/bin/bash
            sudo yum install httpd -y
            sudo systemctl start httpd
            sudo systemctl enable httpd

  #============================================================#
  # EC2 Instances
  #============================================================#

  AWSTestEC2Private1a:
    Type: AWS::EC2::Instance
    Properties:
      LaunchTemplate:
        LaunchTemplateId: !Ref AWSTestLaunchTemplate
        Version: 1
      SubnetId: !Ref AWSTestPrivateSubnet1a
      SecurityGroupIds:
        - !Ref AWSTestSgEC2Private
      IamInstanceProfile: !Ref AWSTestEC2InstanceProfile
      DisableApiTermination: false
      Tags:
        - Key: Name
          Value: !Sub ${SystemName}-${EnvName}-ec2-private-1a
        - Key: Env
          Value: !Sub ${EnvName}

  AWSTestEC2Private1c:
    Type: AWS::EC2::Instance
    Properties:
      LaunchTemplate:
        LaunchTemplateId: !Ref AWSTestLaunchTemplate
        Version: 1
      SubnetId: !Ref AWSTestPrivateSubnet1c
      SecurityGroupIds:
        - !Ref AWSTestSgEC2Private
      IamInstanceProfile: !Ref AWSTestEC2InstanceProfile
      DisableApiTermination: false
      Tags:
        - Key: Name
          Value: !Sub ${SystemName}-${EnvName}-ec2-private-1c
        - Key: Env
          Value: !Sub ${EnvName}

  AWSTestEC2Public1a:
    Type: AWS::EC2::Instance
    Properties:
      LaunchTemplate:
        LaunchTemplateId: !Ref AWSTestLaunchTemplate
        Version: 1
      SubnetId: !Ref AWSTestPublicSubnet1a
      SecurityGroupIds:
        - !Ref AWSTestSgEC2Public
      IamInstanceProfile: !Ref AWSTestEC2InstanceProfile
      DisableApiTermination: false
      Tags:
        - Key: Name
          Value: !Sub ${SystemName}-${EnvName}-ec2-public-1a
        - Key: Env
          Value: !Sub ${EnvName}

  AWSTestEC2Public1c:
    Type: AWS::EC2::Instance
    Properties:
      LaunchTemplate:
        LaunchTemplateId: !Ref AWSTestLaunchTemplate
        Version: 1
      SubnetId: !Ref AWSTestPublicSubnet1c
      SecurityGroupIds:
        - !Ref AWSTestSgEC2Public
      IamInstanceProfile: !Ref AWSTestEC2InstanceProfile
      DisableApiTermination: false
      Tags:
        - Key: Name
          Value: !Sub ${SystemName}-${EnvName}-ec2-public-1c
        - Key: Env
          Value: !Sub ${EnvName}

  #============================================================#
  # EIP Associations
  #============================================================#

  AWSTestEIPAssociation1:
    Type: AWS::EC2::EIPAssociation
    Properties:
      AllocationId: !GetAtt AWSTestEIPPublic1.AllocationId
      InstanceId: !Ref AWSTestEC2Public1a

  AWSTestEIPAssociation2:
    Type: AWS::EC2::EIPAssociation
    Properties:
      AllocationId: !GetAtt AWSTestEIPPublic2.AllocationId
      InstanceId: !Ref AWSTestEC2Public1c

  #============================================================#
  # Application Load Balancer
  #============================================================#

  AWSTestALB:
    Type: AWS::ElasticLoadBalancingV2::LoadBalancer
    Properties:
      Name: !Sub ${SystemName}-${EnvName}-alb
      Type: application
      Scheme: internet-facing
      IpAddressType: ipv4
      Subnets:
        - !Ref AWSTestPublicSubnet1a
        - !Ref AWSTestPublicSubnet1c
      SecurityGroups:
        - !Ref AWSTestSgALB
      Tags:
        - Key: Name
          Value: !Sub ${SystemName}-${EnvName}-alb
        - Key: Env
          Value: !Sub ${EnvName}

  AWSTestTargetGroup:
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    Properties:
      Name: !Sub ${SystemName}-${EnvName}-tg
      Port: 80
      Protocol: HTTP
      VpcId: !Ref AWSTestVPC
      TargetType: instance
      Targets:
        - Id: !Ref AWSTestEC2Private1a
          Port: 80
        - Id: !Ref AWSTestEC2Private1c
          Port: 80
      HealthCheckEnabled: true
      HealthCheckIntervalSeconds: 30
      HealthCheckPath: /
      HealthCheckPort: traffic-port
      HealthCheckProtocol: HTTP
      HealthCheckTimeoutSeconds: 5
      HealthyThresholdCount: 2
      UnhealthyThresholdCount: 5
      Tags:
        - Key: Name
          Value: !Sub ${SystemName}-${EnvName}-tg
        - Key: Env
          Value: !Sub ${EnvName}

  AWSTestALBListener:
    Type: AWS::ElasticLoadBalancingV2::Listener
    Properties:
      DefaultActions:
        - Type: forward
          ForwardConfig:
            TargetGroups:
              - TargetGroupArn: !Ref AWSTestTargetGroup
                Weight: 1
      LoadBalancerArn: !Ref AWSTestALB
      Port: 80
      Protocol: HTTP

  #============================================================#
  # VPC Endpoints for SSM
  #============================================================#

  AWSTestVPCEndpointSSM:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      VpcId: !Ref AWSTestVPC
      ServiceName: !Sub com.amazonaws.${AWS::Region}.ssm
      VpcEndpointType: Interface
      SubnetIds:
        - !Ref AWSTestPrivateSubnet1a
        - !Ref AWSTestPrivateSubnet1c
      SecurityGroupIds:
        - !Ref AWSTestSgVPCEndpoint
      PolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal: '*'
            Action:
              - ssm:UpdateInstanceInformation
              - ssm:SendCommand
              - ssm:ListCommandInvocations
              - ssm:DescribeInstanceInformation
              - ssm:GetDeployablePatchSnapshotForInstance
              - ssm:GetDefaultPatchBaseline
              - ssm:GetManifest
              - ssm:GetParameter
              - ssm:GetParameters
              - ssm:ListAssociations
              - ssm:ListInstanceAssociations
              - ssm:PutInventory
              - ssm:PutComplianceItems
              - ssm:PutConfigurePackageResult
              - ssm:UpdateAssociationStatus
              - ssm:UpdateInstanceAssociationStatus
            Resource: '*'

  AWSTestVPCEndpointSSMMessages:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      VpcId: !Ref AWSTestVPC
      ServiceName: !Sub com.amazonaws.${AWS::Region}.ssmmessages
      VpcEndpointType: Interface
      SubnetIds:
        - !Ref AWSTestPrivateSubnet1a
        - !Ref AWSTestPrivateSubnet1c
      SecurityGroupIds:
        - !Ref AWSTestSgVPCEndpoint
      PolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal: '*'
            Action:
              - ssmmessages:CreateControlChannel
              - ssmmessages:CreateDataChannel
              - ssmmessages:OpenControlChannel
              - ssmmessages:OpenDataChannel
            Resource: '*'

  AWSTestVPCEndpointEC2Messages:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      VpcId: !Ref AWSTestVPC
      ServiceName: !Sub com.amazonaws.${AWS::Region}.ec2messages
      VpcEndpointType: Interface
      SubnetIds:
        - !Ref AWSTestPrivateSubnet1a
        - !Ref AWSTestPrivateSubnet1c
      SecurityGroupIds:
        - !Ref AWSTestSgVPCEndpoint
      PolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal: '*'
            Action:
              - ec2messages:AcknowledgeMessage
              - ec2messages:DeleteMessage
              - ec2messages:FailMessage
              - ec2messages:GetEndpoint
              - ec2messages:GetMessages
              - ec2messages:SendReply
            Resource: '*'

# ============================================================#
# Output Parameters
# ============================================================#

Outputs:
  # ============================================================#
  # VPC Outputs
  # ============================================================#

  AWSTestVPC:
    Value: !Ref AWSTestVPC
    Export:
      Name: !Sub ${SystemName}-${EnvName}-vpc

  AWSTestPublicSubnet1a:
    Value: !Ref AWSTestPublicSubnet1a
    Export:
      Name: !Sub ${SystemName}-${EnvName}-public-subnet-1a

  AWSTestPublicSubnet1c:
    Value: !Ref AWSTestPublicSubnet1c
    Export:
      Name: !Sub ${SystemName}-${EnvName}-public-subnet-1c

  AWSTestProtectedSubnet1a:
    Value: !Ref AWSTestProtectedSubnet1a
    Export:
      Name: !Sub ${SystemName}-${EnvName}-protected-subnet-1a

  AWSTestProtectedSubnet1c:
    Value: !Ref AWSTestProtectedSubnet1c
    Export:
      Name: !Sub ${SystemName}-${EnvName}-protected-subnet-1c

  AWSTestPrivateSubnet1a:
    Value: !Ref AWSTestPrivateSubnet1a
    Export:
      Name: !Sub ${SystemName}-${EnvName}-private-subnet-1a

  AWSTestPrivateSubnet1c:
    Value: !Ref AWSTestPrivateSubnet1c
    Export:
      Name: !Sub ${SystemName}-${EnvName}-private-subnet-1c

  # IAM Outputs
  AWSTestEC2Role:
    Value: !Ref AWSTestEC2Role
    Export:
      Name: !Sub ${SystemName}-${EnvName}-ec2-role

  AWSTestEC2InstanceProfile:
    Value: !Ref AWSTestEC2InstanceProfile
    Export:
      Name: !Sub ${SystemName}-${EnvName}-ec2-instance-profile

  # Security Group Outputs
  AWSTestSgEC2Private:
    Value: !Ref AWSTestSgEC2Private
    Export:
      Name: !Sub ${SystemName}-${EnvName}-sg-ec2-private

  AWSTestSgALB:
    Value: !Ref AWSTestSgALB
    Export:
      Name: !Sub ${SystemName}-${EnvName}-sg-alb

  AWSTestSgRDS:
    Value: !Ref AWSTestSgRDS
    Export:
      Name: !Sub ${SystemName}-${EnvName}-sg-rds

  AWSTestSgVPCEndpoint:
    Value: !Ref AWSTestSgVPCEndpoint
    Export:
      Name: !Sub ${SystemName}-${EnvName}-sg-vpc-endpoint

  # ============================================================#
  # EC2 Outputs
  # ============================================================#

  AWSTestEC2Private1a:
    Value: !Ref AWSTestEC2Private1a
    Export:
      Name: !Sub ${SystemName}-${EnvName}-ec2-private-1a

  AWSTestEC2Private1c:
    Value: !Ref AWSTestEC2Private1c
    Export:
      Name: !Sub ${SystemName}-${EnvName}-ec2-private-1c

  AWSTestEC2Public1a:
    Value: !Ref AWSTestEC2Public1a
    Export:
      Name: !Sub ${SystemName}-${EnvName}-ec2-public-1a

  AWSTestEC2Public1c:
    Value: !Ref AWSTestEC2Public1c
    Export:
      Name: !Sub ${SystemName}-${EnvName}-ec2-public-1c

  AWSTestEIPPublic1:
    Value: !Ref AWSTestEIPPublic1
    Export:
      Name: !Sub ${SystemName}-${EnvName}-eip-public-1

  AWSTestEIPPublic2:
    Value: !Ref AWSTestEIPPublic2
    Export:
      Name: !Sub ${SystemName}-${EnvName}-eip-public-2

  # ============================================================#
  # Application Load Balancer Outputs
  # ============================================================#

  AWSTestALB:
    Value: !Ref AWSTestALB
    Export:
      Name: !Sub ${SystemName}-${EnvName}-alb

  AWSTestTargetGroup:
    Value: !Ref AWSTestTargetGroup
    Export:
      Name: !Sub ${SystemName}-${EnvName}-target-group

  AWSTestALBDNSName:
    Value: !GetAtt AWSTestALB.DNSName
    Export:
      Name: !Sub ${SystemName}-${EnvName}-alb-dns-name

  # ============================================================#
  # VPC Endpoint Outputs
  # ============================================================#

  AWSTestVPCEndpointSSM:
    Value: !Ref AWSTestVPCEndpointSSM
    Export:
      Name: !Sub ${SystemName}-${EnvName}-vpc-endpoint-ssm

  AWSTestVPCEndpointSSMMessages:
    Value: !Ref AWSTestVPCEndpointSSMMessages
    Export:
      Name: !Sub ${SystemName}-${EnvName}-vpc-endpoint-ssmmessages

  AWSTestVPCEndpointEC2Messages:
    Value: !Ref AWSTestVPCEndpointEC2Messages
    Export:
      Name: !Sub ${SystemName}-${EnvName}-vpc-endpoint-ec2messages 

Amazon Route 53では、筆者が持っているパブリックホストゾーンのドメインのCNAMEレコードとして、tenant1tenant2をそれぞれ設定しておきます。宛先は、上記のテンプレートで作成したALBのドメインです。

スクリーンショット 2025-07-11 0.37.15

次に、WAFで許可するためのIP setsを作成します。ここはテナントごとに設定するので、今回はEC2インスタンスのElastic IPごとに1つのIP setsを作っていきます。

スクリーンショット 2025-07-11 0.44.35

このIP setsを使って、WAFのルールを設定してきます。ALBには、WAFのWeb ACLを関連づけ、そこにルールを作成します。JSONで表記すると、こんな感じになります。

{
    "Name": "aws-test-tenant1-allow",
    "Priority": 0,
    "Action": {
        "Allow": {}
    },
    "VisibilityConfig": {
        "SampledRequestsEnabled": true,
        "CloudWatchMetricsEnabled": true,
        "MetricName": "aws-test-tenant1-allow"
    },
    "Statement": {
        "AndStatement": {
            "Statements": [
                {
                    "ByteMatchStatement": {
                        "FieldToMatch": {
                            "SingleHeader": {
                                "Name": "host"
                            }
                        },
                        "PositionalConstraint": "EXACTLY",
                        "SearchString": "tenant1.example.com",
                        "TextTransformations": [
                            {
                                "Type": "NONE",
                                "Priority": 0
                            }
                        ]
                    }
                },
                {
                    "IPSetReferenceStatement": {
                        "ARN": "arn:aws:wafv2:ap-northeast-1:************:regional/ipset/aws-test-ipset-1/********-****-****-****-************"
                    }
                }
            ]
        }
    }
}

やっていることは、2つの条件式をANDで組み合わせて、そのときのみ許可する、というものです。

IP制限を行いたいため、WebACL全体では デフォルトはBlock にしています。

スクリーンショット 2025-07-11 0.52.33

ANDで繋げる1つ目の条件式は、HTTPリクエストヘッダーのHostヘッダーフィールドです。ここがtenant1.example.comに完全に一致する条件を指定しています。

スクリーンショット 2025-07-11 1.25.36

ANDで繋げる2つ目の条件式は、接続元IPアドレスです。ここは、先ほど作成したIP setsで指定できます。

スクリーンショット 2025-07-11 0.55.54

上記の2つを共に満たすときのみ、許可(Allow)するルールとして、最後設定しています。

スクリーンショット 2025-07-11 0.57.51

上記で見せたのはtenant1.example.comです。tenant2.example.com分のルールも作ると、Web ACLのルールはこんな感じになります。

スクリーンショット 2025-07-11 0.59.20

上記が設定されていれば、各サブドメインへのアクセスは、各サブドメインごとに許可された特定のIPアドレスからしかアクセスできないようになっているはずです。

検証

検証します。まず1つ目のEC2インスタンス内に入ってみます。このEC2インスタンスのElastic IPは1.2.3.4なので、tenant1.example.comへのアクセスのみ可能です。意図した通りに、特定のIPからのみ許可されていますね。

1.2.3.4からのアクセス
$ curl tenant1.onoyama.classmethod.info
<html><body><h1>It works!</h1></body></html>

$ curl tenant2.onoyama.classmethod.info
<html>
<head><title>403 Forbidden</title></head>
<body>
<center><h1>403 Forbidden</h1></center>
</body>
</html>

次に2つ目のEC2インスタンス内に入ってみます。このEC2インスタンスのElastic IPは5.6.7.8なので、tenant2.example.comへのアクセスのみ可能です。こちらも、意図した通りに特定のIPからのみ許可されていますね。

5.6.7.8からのアクセス
$ curl tenant1.onoyama.classmethod.info
<html>
<head><title>403 Forbidden</title></head>
<body>
<center><h1>403 Forbidden</h1></center>
</body>
</html>

$ curl tenant2.onoyama.classmethod.info
<html><body><h1>It works!</h1></body></html>

Web ACLのルールで細かい制御も可能

特に、テナントが増えるごとにサブドメインを切る運用の場合は、テナントが増えるごとに、Web ACLのルールとIP setsが増えていくことになります。そのため、サブドメイン自体もAmazon Route 53のパブリックホストゾーンではなく、CMANEレコードとして追加していくのがいいと思います。ホストゾーン自体に料金がかかりますし、各サブドメインが指すリソースは一緒なので、CNAMEレコードで十分管理できる範疇かな、という印象です。

この記事がどなたかの参考になれば幸いです。では!

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.