AWS Cross-Account S3 Access with CodeBuild Explained

Think of AWS accounts like two different houses:

Your CodeBuild project lives in the tools-account, but it needs to put files into an S3 bucket inside the dev-account.

By default, AWS accounts do NOT trust each other.

So you need:

  1. Someone in tools-account who is allowed to act (IAM Role)
  2. Permission from dev-account saying "I allow that external role to enter my bucket" (Bucket Policy)

Step-by-step flow


1. CodeBuild service starts

AWS CodeBuild itself is just a service.

It needs an identity to act as.

That identity is:

CodeBuildRole

This is why you have this trust relationship:

{
  "Effect": "Allow",
  "Principal": {
      "Service": "codebuild.amazonaws.com"
  },
  "Action": "sts:AssumeRole"
}

This means:

"Hey AWS, the CodeBuild service is allowed to temporarily become this role."

Without this trust policy:


2. CodeBuild "wears" the role

When the build starts:

"Can I assume CodeBuildRole?"

AWS checks the trust relationship.

If allowed:

✓ AWS gives temporary credentials

Now CodeBuild is effectively acting as:

arn:aws:iam::<tools-account-id>:role/CodeBuildRole

3. CodeBuild tries to access S3 bucket in another account

Now CodeBuild tries:

aws s3 sync ...

or

PutObject
GetObject

AWS now checks TWO things:


First check → IAM Role permissions (tools-account)

AWS checks:

"Does CodeBuildRole itself allow these S3 actions?"

Your role policy says:

{
  "Action": [
      "s3:PutObject",
      "s3:GetObject",
      "s3:DeleteObject"
  ],
  "Resource": [
      "arn:aws:s3:::bucket",
      "arn:aws:s3:::bucket/*"
  ]
}

So:

✓ The role IS ALLOWED to attempt these actions.

But that's not enough yet.


Second check → Bucket policy (dev-account)

Now AWS asks the bucket owner:

"Do YOU allow this external role from another account?"

That is your bucket policy:

{
  "Principal": {
      "AWS": "arn:aws:iam::<tools-account-id>:role/CodeBuildRole"
  }
}

This means:

"I, the bucket owner, trust this role from the tools-account."

Now both sides agree:

Side Says
IAM Role "I am allowed to access bucket"
Bucket "I allow this external role"

So access succeeds.


SUPER IMPORTANT CONCEPT

For cross-account access:

You usually need permission from BOTH SIDES.

Think of it like entering a private apartment:

Both must approve.


Why is bucket policy necessary?

Because the bucket is in ANOTHER AWS account.

Without bucket policy:

"I have permission to access the bucket"

BUT...

The bucket owner says:

"I never allowed you."

So AWS denies access.


What happens if bucket policy is removed?

Even though CodeBuildRole has:

s3:GetObject
s3:PutObject

the request fails with:

AccessDenied

because cross-account resource access requires the resource owner to allow it.


Easy analogy

Imagine:

"Can enter library"

But the library belongs to another school.

That school must ALSO say:

"We allow students from your school."

That second permission is the bucket policy.


Why is CodeBuildRole necessary?

Because CodeBuild service itself has no permissions.

AWS services don't magically get admin access.

The role is the identity CodeBuild uses.


What happens if CodeBuildRole doesn't exist?

Then CodeBuild has:

Build fails immediately.


Why both trust policy and permissions policy?

These are DIFFERENT things.


Trust Policy = "Who can become me?"

{
  "Principal": {
      "Service": "codebuild.amazonaws.com"
  }
}

This answers:

"Who is allowed to assume this role?"

Answer:

✓ CodeBuild service


Permissions Policy = "What can I do?"

{
  "Action": [
      "s3:PutObject"
  ]
}

This answers:

"Once someone becomes this role, what are they allowed to do?"

Answer:

✓ Access S3 bucket