Demystifying Cross-Account S3 Access
Published: 2023-11-02 16:10:29

Multi-account setup in cloud offers several advantages like grouping of workloads based on business purpose and ownership, applying distinct security controls by environment, constraining access to sensitive data, reducing blast radius, supporting multiple IT operating models, managing costs and distributing AWS service quotas and API request rate limits. However, setting up multi-account environment requires understanding of services and its access across account boundaries.

For example, organizations are often required to grant external users or users from other accounts within their organization access to certain Amazon S3 buckets. In this article lets explore the possible options to enable cross account access to AWS S# buckets and their objects.

Depending on the use case there are couple of ways to grant cross-account access to objects:

Lets look at the scenario where IAM user “developer” is in account “A” and S3 bucket “test-app-bucket-1” is in account “B”


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mw0h9b7t5mashlkfvsef.png)

For better understanding and to practically observe this scenario, let's put CloudFormation Script to create user with required permissions in account “A”.

In this setup we'll define the following:-

1. Who can access the objects inside the bucket (using the Principal element)?
2. Objects they can access (using the Resource element).
3. How they can access the objects inside the bucket (using the Action element)?

**CloudFormation template in account "A"**

```
Description: CloudFormation Template for developer IAM user creation in account"A"
Parameters:
  ForEnvironment:
    Type: String
    Default: dev
    Description: Environment name for which this developer user is created
    AllowedValues:
    - sit
    - uat
    - prod
    - dev 
    - test
Resources:
#IAM user in account A#
  AccAdeveloperIAMUser:
    Type: 'AWS::IAM::User'
    Properties:
      UserName: !Join  # give a name to this user      
            - '-'
            - - 'developer'
              - !Ref ForEnvironment
              - "iam-user-01"


      Policies: # list of inline policy documents embedded in the user
        - PolicyName: !Join  # give a name to this user      
            - '-'
            - - 'developer'
              - !Ref ForEnvironment
              - "iam-policy-01"
          PolicyDocument: # JSON policy document
            Version: '2012-10-17'
            Statement:
            - Effect: Allow
              Action:
              - s3:PutObject
              - s3:GetObject
              - s3:ListBucketMultipartUploads
              - s3:DeleteObjectVersion
              - s3:ListBucketVersions
              - s3:ListBucket
              - s3:DeleteObject
              - s3:GetObjectVersion
              - "s3:ListAccessPoints"              
              Resource: "arn:aws:s3:::test-dev-app-bucket-1/*" 


Outputs:
  AccAdeveloperIAMUser:
    Description: ARN to be supplied in developer template for environments
    Value: !GetAtt AccAdeveloperIAMUser.Arn'
```
Now lets create an S3 bucket in account with required bucket policies to grant access to this user "Developer" from account "A".

```
AWSTemplateFormatVersion: '2010-09-09
Description: CloudFormation Template for S3 bucket in account "B"
Parameters:
  EnvName:
    Type: String
    Default: "dev"
    Description: The name of the S3 Bucket to be created for FrontEnd
    AllowedValues:
    - sit
    - uat
    - prod
    - dev     


Resources:
  S3Bucket1:
    Type: 'AWS::S3::Bucket'
    Properties:
      BucketName: !Join
            - '-'
            - - 'test'
              - !Ref EnvName
              - "app-bucket-1"
      VersioningConfiguration:
        Status: Enabled
      AccessControl: "Private"
      PublicAccessBlockConfiguration:
          BlockPublicAcls: "true"
          BlockPublicPolicy: "true"
          IgnorePublicAcls: "true"
          RestrictPublicBuckets: "true"
      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - ServerSideEncryptionByDefault:
              SSEAlgorithm: 'AES256'                


  NameBucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket: !Ref S3Bucket1
      PolicyDocument:
        Version: '2012-10-17'
        Statement:
        - Effect: Allow
          Principal:
            AWS:
            - !Ref SSIamRoleArn
            - !GetAtt IamUser.Arn           
          Action:
          - s3:GetObject
          - s3:PutObject
          - s3:ListBucket
          - s3:DeleteObject
          Resource:
          - !Join
            - ''
            - - 'arn:aws:s3:::'
              - !Ref S3Bucket1                  
          - !Join
            - ''
            - - 'arn:aws:s3:::'
              - !Ref S3Bucket1
              - /*


Outputs:
  S3Bucket1:
    Description: Name of the bucket created using this template.
    Value: !Ref S3Bucket1'
```
This configuration will allow user "Developer" from Account "A" to securely access an S3 bucket in account "B".

Now lets look at other scenario where an EC2 instance called "Jenkins-server" in account "A" needs to access S3 bucket "“test-dev-app-bucket-1" in account "B". Here the EC2 instance will have the required permissions to access S3 across account.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dfnbvnyyqtdlvg5r5ocr.png)

This use case is suitable to centralize permission management when providing cross-account access to multiple services. Cross-account IAM roles simplifies provisioning cross-account access to S3 objects that are stored in multiple S3 buckets. So that we don't have to manage multiple bucket policies for S3 buckets. This IAM role based access also allows cross-account access to objects owned or uploaded by another AWS account or AWS services.

Following script will create an EC2 instance with required role association in Account "A"

```
AWSTemplateFormatVersion: '2010-09-09
Description: CloudFormation Template for ec2 creation in Account A
Parameters:
  EnvName:
    Type: String
    Default: "dev"
    Description: The name of the S3 Bucket to be created for FrontEnd
    AllowedValues:
    - sit
    - uat
    - prod
    - dev         


  KeyPairName:
    Description: Name of an existing EC2 KeyPair to enable SSH access to the instance
    Type: AWS::EC2::KeyPair::KeyName
    ConstraintDescription: must be the name of an existing EC2 KeyPair. 


  SubnetInstance:
    Type: AWS::EC2::Subnet::Id
    Description: Subnet ID for  Instance    


  VPCIdEC2:
    Type: AWS::EC2::VPC::Id
    Description: VPC ID for  Instance   



Resources:
  EC2SecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupName: "instance-sg-01"
      GroupDescription: "security group attached to  ec2 instance"
      VpcId: !Ref VPCIdEC2
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: '22'
          ToPort: '22'
          CidrIp:  172.16.0.0/16       


  EC2Instance:
    Type: 'AWS::EC2::Instance'
    Properties:
      ImageId: "ami-0015c3ec5dfe53255"         
      InstanceType: "t3.small"        
      KeyName: !Ref KeyPairName
      SubnetId: !Ref SubnetInstance
      SecurityGroupIds:
        - Ref: EC2SecurityGroup
      BlockDeviceMappings:
        - DeviceName: /dev/xvda
          Ebs:
            VolumeSize: 50
            Encrypted: 'true'
            VolumeType: "gp3"
      IamInstanceProfile: !Ref InstanceProfile  
      LaunchTemplate:
          LaunchTemplateId: !Ref InstanceLaunchTemplate
          Version: "1"
      Tags:
        - Key: Name
          Value: !Join
              - '-'
              - - 'kbc-be'
                - !Ref EnvName
                - "-Inst-01"       
        - Key: Schedule
          Value: -server
        - Key: environment
          Value: !Ref EnvName          
                  


  InstanceSSMRole: 
    Type: 'AWS::IAM::Role'
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - ec2.amazonaws.com
            Action:
              - 'sts:AssumeRole'
      Path: /
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AmazonEC2RoleforSSM
        - arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore  


  CrossAccAssumeRole:
    Type: 'AWS::IAM::Role'
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - ec2.amazonaws.com
            Action:
              - 'sts:AssumeRole'
      Path: /
      Policies:
        - PolicyName: root
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action: '*'
                Resource: '*'       


  InstanceProfile: 
    Type: 'AWS::IAM::InstanceProfile'
    Properties:
      Path: /
      Roles:
        - !Ref InstanceSSMRole 
        - !Ref CrossAccAssumeRole'
```

Now lets create a role which will be assumed by account "A" EC2 instance. Following is the script which will create required role in account "B" and an S3 bucket for test.

```
RoleAccountB:
    Type: 'AWS::IAM::Role'
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
        - Action:
          - s3:ListAllMyBuckets
          Effect: Allow
          Resource:
          - arn:aws:s3:::*
        - Action:
          - s3:ListBucket
          - s3:GetBucketLocation
          Effect: Allow
          Resource: arn:aws:s3:::AccountBBucketName
        - Effect: Allow
          Action:
          - s3:GetObject
          - s3:PutObject
          Resource: arn:aws:s3:::AccountBBucketName/* 


  AccountBS3Bucket1:
    Type: 'AWS::S3::Bucket'
    Properties:
      BucketName: !Join
            - '-'
            - - 'test'
              - !Ref EnvName
              - "app-bucket-1"
      VersioningConfiguration:
        Status: Enabled
      AccessControl: "Private"
      PublicAccessBlockConfiguration:
          BlockPublicAcls: "true"
          BlockPublicPolicy: "true"
          IgnorePublicAcls: "true"
          RestrictPublicBuckets: "true"
      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - ServerSideEncryptionByDefault:
              SSEAlgorithm: 'AES256' :
```

Now role in account assumes the role in Account B so that EC2 "Jenkins-server" in Account A can perform the required S3 operations.

This configuration covers most of the use cases however architectures are evolving every day. For example many organizations prefer to put restrictive rules around editing bucket policies. AWS has another solution called S3 access points which enforce granular access to S3 objects with flexible cross-account access without having to edit bucket policies. I'll cover this topic in my subsequent Articles.

-- Reference- Amazon web service