S3の特定フォルダへのクロスアカウントアクセスをIAM Roleで制限する

2024.02.18

はじめに

データアナリティクス事業本部ビッグデータチームのyosh-kです。
今回は、S3の特定フォルダへのクロスアカウントアクセスをIAM Roleで制限する実装をCloudFormationで試したいと思います。

前提

実現したいことは以下になります。

AWS Acccount Aで作成したIAM Role AはAWS Account BのIAM Role BへのAssumeRole権限のみを付与し、AWS Account BのIAM Role BでS3 Bucketの特定フォルダ配下のみにactionを制限します。内容としては以下記事とほぼ同様ですが、CloudFormationでの実装とフォルダ制限はなかったのでその辺りを検証していきたいと思います。

失敗した構成

最初は上図のように、Bucket policyでの制限を実装していたのですが、対象のS3 BucketではAWSマネージドキーを使用しており、AWSマネージドキーのキーポリシーにIAM Role Aに対するkms:Encryptkms:Decryptkms:GenerateDataKey権限を付与できないため、s3:GetObjects3:PutObjectの際にエラーとなることがわかりました。そのためこの構成で行う場合はカスタマーマネージドキーを設定する必要がありましたが、今回は、AWSマネージド型キーを用いた一つ前の図の構成で検証していきます。

AWS マネージド型キーのキーポリシー

{
    "Version": "2012-10-17",
    "Id": "auto-s3-2",
    "Statement": [
        {
            "Sid": "Allow access through S3 for all principals in the account that are authorized to use S3",
            "Effect": "Allow",
            "Principal": {
                "AWS": "*"
            },
            "Action": [
                "kms:Encrypt",
                "kms:Decrypt",
                "kms:ReEncrypt*",
                "kms:GenerateDataKey*",
                "kms:DescribeKey"
            ],
            "Resource": "*",
            "Condition": {
                "StringEquals": {
                    "kms:ViaService": "s3.ap-northeast-1.amazonaws.com",
                    "kms:CallerAccount": "<AWS_Account_B_ID>"
                }
            }
        },
        {
            "Sid": "Allow direct access to key metadata to the account",
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::<AWS_Account_B_ID>:root"
            },
            "Action": [
                "kms:Describe*",
                "kms:Get*",
                "kms:List*"
            ],
            "Resource": "*"
        }
    ]
}

実装

AWS Account AのIAM Role A

それでは実装になります。まずはIAM Role AをCloudFormationで実装します。

AWSTemplateFormatVersion: "2010-09-09"
Description: Creating Role

Resources:
  PracticeIamRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: "cm-kasama-practice-role-a"
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              AWS:
                - !Sub arn:aws:iam::<AWS Account ID>:user/<IAM User Name>
            Action: "sts:AssumeRole"
            Condition:
              Bool:
                "aws:MultiFactorAuthPresent": "true"
      Policies:
        - PolicyName: "AsuumeInAccountBRole"
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action:
                  - "sts:AssumeRole"
                Resource: "arn:aws:iam::<AWS Account ID B>:role/cm-kasama-practice-role-b"

私の場合はIAM UserからAssumeして使用するように設定しており、その際はMFAを設定しています。IAM Policyでは、AdministratorAccessのみを設定しています。AWS Account IDIAM User Nameは任意の値に置き換えて使用します。

AWS Account BのS3 Bucket

続いてS3 Bucketを実装します。

---
AWSTemplateFormatVersion: "2010-09-09"
Description:  S3 Bucket
Resources:
  CmKasamaPracticeBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: cm-kasama-practice
      AccessControl: Private
      BucketEncryption:
        ServerSideEncryptionConfiguration:
        - ServerSideEncryptionByDefault:
            SSEAlgorithm: aws:kms
      VersioningConfiguration:
        Status: Enabled
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true
  • Bucketはpublic accessを全てブロックするように設定します。
  • ACLはprivateに設定することでバケット所有者 (AWS アカウント)へのアクセスに制限します。
  • aws/s3 KMS キーを使用したデフォルトの暗号化を設定します。

AWS Account AのIAM Role B

AWSTemplateFormatVersion: "2010-09-09"
Description: Creating Role

Resources:
  PracticeIamRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: "cm-kasama-practice-role-b"
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              AWS:
                - !Sub arn:aws:iam::<AWS_Account_A_ID>:role/cm-kasama-practice-role-a
            Action: "sts:AssumeRole"
      Policies:
        - PolicyName: "AllowBucketListAction"
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: "Allow"
                Action:
                  - "s3:ListBucket"
                Condition:
                  StringLike:
                    s3:prefix:
                      - "src/*"
                Resource: "arn:aws:s3:::cm-kasama-practice"
        - PolicyName: "AllowObjectAction"
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: "Allow"
                Action:
                  - "s3:GetObject"
                  - "s3:PutObject"
                  - "s3:DeleteObject"
                Resource: "arn:aws:s3:::cm-kasama-practice/src/*"
  • 信頼ポリシーではIAM Role AがAssumeすることを許可します。
  • 許可ポリシーではS3 Bucketの特定prefix配下のみをListBucketで参照できるようにStringLikeで設定します。
  • 同じくS3 Bucketの特定prefix配下のみをGet,Put,DeleteObjectできるように設定します。

参考

デプロイ

まずはAWS Account AでIAM Role Aをデプロイします。cliコマンドでもデプロイできますが、今回はAWS Management Consoleからデプロイします。

CloudFormationの画面からスタックの作成を選択し、新しいソースを作成します。

テンプレートは作成したyamlファイルを指定し、スタックを作成しました。問題なくIAM Roleが作成されています。

次にAWS Account BでS3 Bucketを作成します。 同様にAWS Management Consoleから新規スタックを作成します。 問題なく成功しました。

最後にAWS Account BでIAM Role Bを作成します。 こちらも問題なくCloudFormationから作成できました。

検証

それでは検証していきます。まずは検証用に参照権限のあるsrcフォルダと参照権限のないtestフォルダを作成し、それぞれにtxtファイルを格納します。

aws cliで操作するので、configファイルを設定します。

@~ % cat ~/.aws/config
[default]
region = ap-northeast-1
output = json

[profile cm-kasama-practice-role-a]
role_arn = arn:aws:iam::<AWS_Account_A_ID>:role/cm-kasama-practice-role-a
source_profile = default
region = ap-northeast-1
mfa_serial = arn:aws:iam::<AWS_Account_ID>:mfa/<IAM_USER_NAME>


[profile cm-kasama-practice-role-b]
role_arn = arn:aws:iam::<AWS_Account_B_ID>:role/cm-kasama-practice-role-b
source_profile = cm-kasama-practice-role-a



@~ %

まずはListBucketでアクセス制限されているかを確認します。

@ Downloads % aws s3 ls s3://cm-kasama-practice/ --profile cm-kasama-practice-role-b
Enter MFA code for arn:aws:iam:::mfa/cm-: 

An error occurred (AccessDenied) when calling the ListObjectsV2 operation: Access Denied
@ Downloads % aws s3 ls s3://cm-kasama-practice/test/ --profile cm-kasama-practice-role-b

An error occurred (AccessDenied) when calling the ListObjectsV2 operation: Access Denied
@ Downloads % aws s3 ls s3://cm-kasama-practice/test/test.txt --profile cm-kasama-practice-role-b

An error occurred (AccessDenied) when calling the ListObjectsV2 operation: Access Denied
@ Downloads % aws s3 ls s3://cm-kasama-practice/src/ --profile cm-kasama-practice-role-b
2024-02-18 12:01:05          0 
2024-02-18 12:17:00          4 src.txt
@ Downloads % aws s3 ls s3://cm-kasama-practice/src/src.txt --profile cm-kasama-practice-role-b
2024-02-18 12:17:00          4 src.txt
@ Downloads %

src/配下以外はアクセスできないことを確認できました。

次にObjectへのアクセスです。

@ tmp % aws s3 cp s3://cm-kasama-practice/test/test.txt . --profile cm-kasama-practice-role-b
fatal error: An error occurred (403) when calling the HeadObject operation: Forbidden
@ tmp % aws s3 cp s3://cm-kasama-practice/src/src.txt . --profile cm-kasama-practice-role-b
download: s3://cm-kasama-practice/src/src.txt to ./src.txt     

@ tmp % aws s3 cp ./tmp.md s3://cm-kasama-practice/test/ --profile cm-kasama-practice-role-b
upload failed: ./tmp.md to s3://cm-kasama-practice/test/tmp.md An error occurred (AccessDenied) when calling the PutObject operation: Access Denied
@ tmp % aws s3 cp ./tmp.md s3://cm-kasama-practice/src/ --profile cm-kasama-practice-role-b
upload: ./tmp.md to s3://cm-kasama-practice/src/tmp.md        

@ tmp % aws s3 rm s3://cm-kasama-practice/test/test.txt --profile cm-kasama-practice-role-b
delete failed: s3://cm-kasama-practice/test/test.txt An error occurred (AccessDenied) when calling the DeleteObject operation: Access Denied
@ tmp % aws s3 rm s3://cm-kasama-practice/src/src.txt --profile cm-kasama-practice-role-b
delete: s3://cm-kasama-practice/src/src.txt
@ tmp %

GetObjectPutObjectDeleteObjectともにsrc/配下にアクセス制限されていることを確認できました。

最後に

この検証するまではBucket policyで制限できるのではと思っていたのですが、AWSマネージド型キーを使用すると難しいということを学べたので、検証した甲斐がありました。