AWS S3 Access Control: IAM Policy vs Bucket Policy Precedence Explained with VPC Restrictions


37 views

When AWS evaluates permissions for S3 access, it follows a strict hierarchy:

  1. Explicit DENY: Any explicit deny in either policy overrides all allows
  2. IAM Policy: User/role permissions are evaluated first
  3. Bucket Policy: Resource-based permissions are evaluated second

For your specific case of restricting access to a VPC while maintaining granular IAM permissions, here's the optimal approach:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Deny",
            "Principal": "*",
            "Action": "s3:*",
            "Resource": [
                "arn:aws:s3:::xyz",
                "arn:aws:s3:::xyz/*"
            ],
            "Condition": {
                "StringNotEquals": {
                    "aws:SourceVpc": "vpc-12345678"
                }
            }
        }
    ]
}

Your existing IAM policy grants specific permissions to users, while the bucket policy adds a network-layer restriction. The key points:

  • The bucket policy uses DENY to block all non-VPC traffic
  • IAM policies still control which actions users can perform
  • No permission duplication needed

To verify the setup works as intended:

aws s3 ls s3://xyz --vpc-endpoint-id vpce-12345678  # Should work
aws s3 ls s3://xyz                                  # Should fail
aws s3 cp test.txt s3://xyz/ --vpc-endpoint-id vpce-12345678  # Should work for users with put permissions

When combining these policies:

  • Never use conflicting DENY rules in both policies
  • Avoid overlapping ALLOW rules that might bypass restrictions
  • Remember that bucket policies have a 20KB size limit

For time-bound VPC access combined with IAM permissions:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Deny",
            "Principal": "*",
            "Action": "s3:*",
            "Resource": [
                "arn:aws:s3:::xyz",
                "arn:aws:s3:::xyz/*"
            ],
            "Condition": {
                "StringNotEquals": {
                    "aws:SourceVpc": "vpc-12345678"
                },
                "DateGreaterThan": {
                    "aws:CurrentTime": "2023-12-31T23:59:59Z"
                }
            }
        }
    ]
}



When both IAM policies and bucket policies exist, AWS evaluates them differently:

  1. Explicit Deny: Any explicit "Deny" in either policy overrides all "Allow" statements
  2. IAM Policy: Evaluates permissions granted to the principal (user/role)
  3. Bucket Policy: Evaluates resource-based permissions

For your scenario combining IAM user permissions with VPC restrictions, here's the complete solution:

// Bucket Policy (VPC restriction)
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "RestrictToVPC",
            "Effect": "Deny",
            "Principal": "*",
            "Action": "s3:*",
            "Resource": [
                "arn:aws:s3:::xyz",
                "arn:aws:s3:::xyz/*"
            ],
            "Condition": {
                "StringNotEquals": {
                    "aws:SourceVpc": "vpc-12345678"
                }
            }
        }
    ]
}
  • The bucket policy's Deny statement will override any IAM Allow when the VPC condition isn't met
  • For requests coming from the allowed VPC, the IAM policy's permissions will apply normally
  • Using s3:* in the bucket policy's Deny statement ensures comprehensive VPC protection

Verify your setup with these AWS CLI commands:

# Test from within allowed VPC
aws s3 ls s3://xyz --vpc-endpoint-id vpce-12345678

# Test from outside (should fail)
aws s3 ls s3://xyz

If access isn't working as expected:

1. Check IAM policy attachment to users
2. Verify VPC ID in bucket policy condition
3. Ensure no conflicting Deny statements exist
4. Validate network routing for VPC endpoints

When dealing with multiple accounts, combine both policies like this:

// IAM Policy (cross-account user)
{
    "Version": "2012-10-17",
    "Statement": {
        "Effect": "Allow",
        "Action": ["s3:GetObject"],
        "Resource": "arn:aws:s3:::other-account-bucket/*"
    }
}

// Bucket Policy (in target account)
{
    "Version": "2012-10-17",
    "Statement": {
        "Effect": "Allow",
        "Principal": {"AWS": "arn:aws:iam::SOURCE_ACCOUNT:user/USERNAME"},
        "Action": ["s3:GetObject"],
        "Resource": "arn:aws:s3:::other-account-bucket/*",
        "Condition": {"StringEquals": {"aws:SourceVpc": "vpc-12345678"}}
    }
}