S3でVPCエンドポイント経由のアクセスのみに制限したけど特定のIAMユーザーはアクセスできるようにするバケットポリシーを作ってみた

2023.08.31

S3でVPCエンドポイントからのアクセスのみ許可するバケットポリシーを作成しましたが、マネジメントコンソールからもアクセスしたいといったことがあったので特定のIAMユーザーは許可するバケットポリシーを作成してみました。

リソース作成

バケットポリシー以外のリソースはCloudFormationで作成します。
構成図としては以下のようになります。

EC2からS3へのアクセスはVPCエンドポイント経由になるように設定を行います。
リソースの作成に使用したCloudFormationテンプレートは以下になります。

AWSTemplateFormatVersion: "2010-09-09"

Description: Test Stack

Parameters:
# ------------------------------------------------------------#
# Parameters
# ------------------------------------------------------------# 
  VPCCIDR:
    Default: 192.168.0.0/16
    Type: String

  PrivateSubnet01CIDR:
    Default: 192.168.0.0/24
    Type: String

  EC2VolumeSize:
    Default: 32
    Type: Number

  EC2VolumeIOPS:
    Default: 3000
    Type: Number

  EC2AMI:
    Default: ami-0310b105770df9334
    Type: AWS::EC2::Image::Id

  EC2InstanceType:
    Default: t3.micro
    Type: String

Resources:
# ------------------------------------------------------------#
# VPC
# ------------------------------------------------------------# 
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !Ref VPCCIDR
      EnableDnsSupport: true
      EnableDnsHostnames: true
      Tags: 
        - Key: Name
          Value: test-vpc

# ------------------------------------------------------------#
# Subnet
# ------------------------------------------------------------# 
  PrivateSubnet01:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: ap-northeast-1a
      CidrBlock: !Ref PrivateSubnet01CIDR
      Tags: 
        - Key: Name
          Value: test-subnet-private-1a
      VpcId: !Ref VPC

# ------------------------------------------------------------#
# RouteTable
# ------------------------------------------------------------# 
  PrivateRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: test-private-rtb

  PrivateRtAssociation1:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref PrivateRouteTable
      SubnetId: !Ref PrivateSubnet01

# ------------------------------------------------------------#
# Security Group
# ------------------------------------------------------------# 
  EC2SG:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: for EC2
      GroupName: test-sg-ec2
      SecurityGroupEgress: 
        - CidrIp: 0.0.0.0/0
          FromPort: -1
          IpProtocol: -1
          ToPort: -1
      Tags: 
        - Key: Name
          Value: test-sg-ec2
      VpcId: !Ref VPC

  VPCEndpointSG:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: for VPC Endpoint
      GroupName: test-sg-vpc-endpoint
      SecurityGroupEgress: 
        - CidrIp: 0.0.0.0/0
          FromPort: -1
          IpProtocol: -1
          ToPort: -1
      SecurityGroupIngress: 
        - CidrIp: 192.168.0.0/16
          FromPort: 443
          IpProtocol: tcp
          ToPort: 443
      Tags: 
        - Key: Name
          Value: test-sg-vpc-endpoint
      VpcId: !Ref VPC

# ------------------------------------------------------------#
# IAM
# ------------------------------------------------------------# 
  EC2IAMRole:
    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
        - arn:aws:iam::aws:policy/AmazonS3FullAccess
      RoleName: test-iam-role-ec2
      Tags:
        - Key: Name
          Value: test-iam-role-ec2

  EC2IAMInstanceProfile:
    Type: AWS::IAM::InstanceProfile
    Properties:
      InstanceProfileName: test-iam-instanceprofile-ec2
      Roles: 
        - !Ref EC2IAMRole

# ------------------------------------------------------------#
# EC2
# ------------------------------------------------------------# 
  EC2:
    Type: AWS::EC2::Instance
    Properties:
      BlockDeviceMappings: 
        - DeviceName: /dev/xvda
          Ebs:
            DeleteOnTermination: true
            Encrypted: true
            Iops: !Ref EC2VolumeIOPS
            VolumeSize: !Ref EC2VolumeSize
            VolumeType: gp3
      DisableApiTermination: false
      IamInstanceProfile: !Ref EC2IAMInstanceProfile
      ImageId: !Ref EC2AMI
      InstanceType: !Ref EC2InstanceType
      NetworkInterfaces: 
        - DeleteOnTermination: true
          DeviceIndex: 0
          GroupSet: 
            - !Ref EC2SG
          SubnetId: !Ref PrivateSubnet01
      Tags:
        - Key: Name
          Value: test-ec2

# ------------------------------------------------------------#
# VPC Endpoint
# ------------------------------------------------------------# 
  S3Endpoint:
    Type: AWS::EC2::VPCEndpoint
    Properties: 
      RouteTableIds: 
        - !Ref PrivateRouteTable
      ServiceName: !Sub com.amazonaws.${AWS::Region}.s3
      VpcEndpointType: Gateway
      VpcId: !Ref VPC

  SystemsManagerEndpoint:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      VpcEndpointType: Interface
      PrivateDnsEnabled: true
      ServiceName: !Sub com.amazonaws.${AWS::Region}.ssm
      VpcId: !Ref VPC
      SubnetIds:
        - !Ref PrivateSubnet01
      SecurityGroupIds:
        - !Ref VPCEndpointSG

  SystemsManagerMessageEndpoint:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      VpcEndpointType: Interface
      PrivateDnsEnabled: true
      ServiceName: !Sub com.amazonaws.${AWS::Region}.ssmmessages
      VpcId: !Ref VPC
      SubnetIds:
        - !Ref PrivateSubnet01
      SecurityGroupIds:
        - !Ref VPCEndpointSG

# ------------------------------------------------------------#
# S3
# ------------------------------------------------------------# 
  S3Bucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Sub bucket-${AWS::AccountId}-${AWS::Region}

今回は検証用の為、EC2が使用するIAMロールにS3FullAccessを設定しています。
実際使用する際は必要なアクションだけに絞ったIAMポリシーを設定することをお勧めします。

デプロイは以下のコマンドで行います。

aws cloudformation create-stack --stack-name CloudFormationスタック名 --template-body file://CloudFormationテンプレートファイル名 --capabilities CAPABILITY_NAMED_IAM

デプロイが完了するとEC2にセッションマネージャーで接続することが出来ます。
接続方法は以下のドキュメントに記載されているのでご確認ください。
セッションを開始する

接続後、以下のコマンドを実行するとS3へのアクセスが確認できます。

aws s3 ls s3://S3バケット名

ここまでの手順ではまだS3にファイルを置いていないので上記のコマンドを実行しても何も表示されません。
なので、ファイルを以下のコマンドでアップロードしてみます。

cd
touch test.txt
aws cp ./test.txt s3://S3バケット名
aws s3 ls s3://S3バケット名

cpコマンドでS3にtest.txtをコピーしています。
lsコマンドを実行するとファイルが確認できます。
また、バケットポリシーで制限をしていないためマネジメントコンソールからも確認することが可能です。

バケットポリシーを設定する

ここから本題です。
特定の VPC エンドポイントからアクセスを許可するバケットポリシーは以下のようになります。
特定の VPC エンドポイントへのアクセスの制限

{
   "Version": "2012-10-17",
   "Id": "Policy1415115909152",
   "Statement": [
     {
       "Sid": "Access-to-specific-VPCE-only",
       "Principal": "*",
       "Action": "s3:*",
       "Effect": "Deny",
       "Resource": ["arn:aws:s3:::S3バケット名",
                    "arn:aws:s3:::S3バケット名/*"],
       "Condition": {
         "StringNotEquals": {
           "aws:SourceVpce": "VPCエンドポイントID"
         }
       }
     }
   ]
}

上記のバケットポリシーを設定するとマネジメントコンソールからアクセスができなくなります。
実際に設定して確認してみます。
EC2で以下のコマンドを実行してください。
policy.jsonには上記のバケットポリシーを記載してください。

aws s3api put-bucket-policy --bucket S3バケット名 --policy file://policy.json

コマンド実行後、マネジメントコンソールから該当のS3にアクセスすると以下のようになります。

EC2からのアクセスを確認してみます。
以下のlsコマンドを実行します。

aws s3 ls s3://S3バケット名

実行するとアップロードしたtest.txtが確認できます。
もし、この時点で権限エラーが発生した場合はバケットポリシーに問題があるためEC2からコマンドでバケットポリシーを更新するか、以下のドキュメントの手順に従ってルートユーザーでバケットポリシーを更新してください。
Amazon S3 バケットへのすべてのユーザーのアクセスを誤って拒否しました。アクセスを回復するにはどうしたらいいですか?

特定のIAMユーザーを許可するバケットポリシーを作成してみます。
ユーザーの制御にはaws:PrincipalArnというグローバル条件キーを使用します。
こちらの条件キーを使用することでリクエスト元のIAMユーザーのARNとバケットポリシーで設定したARNで比較を行うことが可能になります。
つまりConditionの中でaws:PrincipalArnを設定することで特定のIAMユーザーはアクセスを許可させることが可能になります。
実際に設定するバケットポリシーは以下のようになります。

{
   "Version": "2012-10-17",
   "Id": "Policy1415115909152",
   "Statement": [
     {
       "Sid": "Access-to-specific-VPCE-only",
       "Principal": "*",
       "Action": "s3:*",
       "Effect": "Deny",
       "Resource": ["arn:aws:s3:::S3バケット名",
                    "arn:aws:s3:::S3バケット名/*"],
       "Condition": {
         "StringNotEquals": {
           "aws:SourceVpce": "VPCエンドポイントID",
           "aws:PrincipalArn": "IAMユーザーARN"
         }
       }
     }
   ]
}

実際に上記のバケットポリシーを設定してみます。
設定はEC2から行います。
policy.jsonを上記のバケットポリシーに修正して以下のコマンドを実行します。

aws s3api put-bucket-policy --bucket S3バケット名 --policy file://policy.json

実行後、アクセス確認のために以下のコマンドでS3バケット内を確認します。

aws s3 ls s3://S3バケット名

EC2からは正常にtest.txtが確認できます。
次にaws:PrincipalArnで設定したIAMユーザーでマネジメントコンソールからアクセスしてみます。
ARNで指定したIAMユーザーの場合は以下のように正常にS3内のオブジェクトが確認できます。

次にaws:PrincipalArnで設定していないIAMユーザーでアクセスします。
許可されていないユーザーの場合は以下のように権限エラーが発生します。

さいごに

aws:PrincipalArnはIAMユーザー以外にもIAMロールのARNでも評価させることが可能なのでスイッチロールさせている環境にも使用することが可能です。