Amazon S3

Limit a team to its own prefix in a shared S3 bucket

Give a team read, write and list access under teams/analytics/ in a shared bucket, with optional console navigation.

Published · Updated .

The problem

Many organizations run one data bucket per environment and give each team a key prefix: teams/analytics/, teams/finance/ and so on. Nothing in S3 enforces those boundaries. A folder in the console is just a shared key prefix, and a role with s3:GetObject on bucket/* can read every team's data.

Isolation therefore comes from two separate mechanisms: object permissions scoped by the key path in the resource ARN, and listing permissions scoped by the s3:prefix condition key, because listing is an operation on the bucket, not on objects. This example shows both and explains how the S3 console's way of listing interacts with them.

The policies

Identity policy: one team, one prefix

IAM identity-based policy. Attach to: The analytics team's IAM role (or IAM Identity Center permission set as an inline policy)

Identity policy: one team, one prefix · IAM identity-based policyOpen in Inline Policy Generator
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ListOwnPrefix",
      "Effect": "Allow",
      "Action": "s3:ListBucket",
      "Resource": "arn:aws:s3:::amzn-s3-demo-bucket",
      "Condition": {
        "StringLike": {
          "s3:prefix": "teams/analytics/*"
        }
      }
    },
    {
      "Sid": "ReadWriteOwnObjects",
      "Effect": "Allow",
      "Action": [
        "s3:GetObject",
        "s3:PutObject",
        "s3:DeleteObject",
        "s3:AbortMultipartUpload"
      ],
      "Resource": "arn:aws:s3:::amzn-s3-demo-bucket/teams/analytics/*"
    }
  ]
}

Statement by statement

  • Listing is allowed only when the request carries a prefix parameter that starts with teams/analytics/. The * wildcard also matches nothing, so a prefix of exactly teams/analytics/ is covered. A list request with no prefix parameter has no s3:prefix value at all, so the condition doesn't match and the request is implicitly denied; other teams' key names stay hidden.
  • Object permissions are scoped by the ARN path, because GetObject and PutObject requests don't carry a prefix parameter; the object key itself is the resource. s3:PutObject also covers multipart uploads; AbortMultipartUpload lets the team clean up failed ones. DeleteObject removes the current version only; on a versioned bucket it adds a delete marker, and permanently removing versions would need s3:DeleteObjectVersion, which is deliberately not granted.

Optional add-on: navigate to the prefix in the S3 console

IAM identity-based policy. Attach to: The same role, only if the team uses the S3 console

Optional add-on: navigate to the prefix in the S3 console · IAM identity-based policyOpen in Inline Policy Generator
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ConsoleBucketPicker",
      "Effect": "Allow",
      "Action": [
        "s3:ListAllMyBuckets",
        "s3:GetBucketLocation"
      ],
      "Resource": "*"
    },
    {
      "Sid": "ConsoleWalkToTeamFolder",
      "Effect": "Allow",
      "Action": "s3:ListBucket",
      "Resource": "arn:aws:s3:::amzn-s3-demo-bucket",
      "Condition": {
        "StringEquals": {
          "s3:prefix": [
            "",
            "teams/"
          ],
          "s3:delimiter": "/"
        }
      }
    }
  ]
}

Statement by statement

  • The console lists all buckets with ListBuckets (no resource to scope) and calls GetBucketLocation before talking to a bucket. This reveals bucket names in the account, not their contents.
  • When you open a bucket, the console lists it with an empty prefix and the delimiter /, then repeats with teams/ when you open that folder. Allowing exactly those two prefixes, and only with the delimiter, lets users click down to teams/analytics/. The trade-off: they see top-level folder names, the other teams' folder names under teams/, and any objects stored directly at those two levels.

Before you start

  • A bucket whose keys follow the teams/<team>/ convention, in the same account as the team's role.
  • No bucket policy statement that grants the same role wider access; an Allow in the bucket policy adds to this identity policy rather than being limited by it.

Deploy it step by step

  1. Write the prefix exactly as clients send it. Decide on the trailing slash and keep it everywhere: in the s3:prefix value, in the object ARN, and in the paths your tools use. teams/analytics/* doesn't match teams/analytics-archive/, which is what you want, but it also doesn't match a client that lists teams/analytics without the slash.
  2. Validate, then attach. Run validate-policy on the main policy. Attach the console add-on as a separate policy so you can grant it only to people who need it.
    Commands (replace <placeholders>)
    aws accessanalyzer validate-policy --policy-type IDENTITY_POLICY --policy-document file://policy.json
    aws iam put-role-policy --role-name <team-role-name> --policy-name AnalyticsPrefixAccess --policy-document file://policy.json
  3. Tell the team how to list. Clients must always pass the prefix. aws s3 ls s3://amzn-s3-demo-bucket/teams/analytics/ works; aws s3 ls s3://amzn-s3-demo-bucket/ does not (unless the console add-on is attached, which allows the root listing with a delimiter). SDK code that calls ListObjectsV2 without Prefix fails the same way. For console users, share a link that opens the teams/analytics/ folder directly.

Verify it without changing anything

These checks only read state, simulate, or use dry runs, so they are safe to run in any account. Exact error wording varies by service and over time.

Simulate listing with and without the team prefix

s3:prefix only exists when the request supplies it, so the simulator needs it as a context entry. Compare the team's prefix, another team's prefix, and no prefix at all.

Commands (replace <placeholders>)
aws iam simulate-custom-policy --policy-input-list file://policy.json --action-names s3:ListBucket --resource-arns arn:aws:s3:::amzn-s3-demo-bucket --context-entries ContextKeyName=s3:prefix,ContextKeyValues=teams/analytics/,ContextKeyType=string --query 'EvaluationResults[].EvalDecision' --output text
aws iam simulate-custom-policy --policy-input-list file://policy.json --action-names s3:ListBucket --resource-arns arn:aws:s3:::amzn-s3-demo-bucket --context-entries ContextKeyName=s3:prefix,ContextKeyValues=teams/finance/,ContextKeyType=string --query 'EvaluationResults[].EvalDecision' --output text
aws iam simulate-custom-policy --policy-input-list file://policy.json --action-names s3:ListBucket --resource-arns arn:aws:s3:::amzn-s3-demo-bucket --query 'EvaluationResults[].EvalDecision' --output text

Expected: allowed for teams/analytics/, and implicitDeny for teams/finance/ and for the call with no prefix.

Simulate object access inside and outside the prefix

Checking object access with the simulator avoids test uploads into another team's area. The look-alike prefix teams/analytics-archive/ is the case that catches a missing trailing slash.

Commands (replace <placeholders>)
aws iam simulate-custom-policy --policy-input-list file://policy.json --action-names s3:GetObject s3:PutObject --resource-arns arn:aws:s3:::amzn-s3-demo-bucket/teams/analytics/q3.csv --query 'EvaluationResults[].[EvalActionName,EvalDecision]' --output text
aws iam simulate-custom-policy --policy-input-list file://policy.json --action-names s3:GetObject s3:PutObject --resource-arns arn:aws:s3:::amzn-s3-demo-bucket/teams/analytics-archive/q3.csv --query 'EvaluationResults[].[EvalActionName,EvalDecision]' --output text

Expected: Both actions return allowed for teams/analytics/q3.csv and implicitDeny for teams/analytics-archive/q3.csv.

List for real as the team role

These two calls only read. The first lists the team's own prefix; the second omits the prefix, which the policy refuses.

Commands (replace <placeholders>)
aws s3api list-objects-v2 --bucket amzn-s3-demo-bucket --prefix teams/analytics/ --max-keys 5 --profile <team-profile>
aws s3api list-objects-v2 --bucket amzn-s3-demo-bucket --max-keys 5 --profile <team-profile>

Expected: The first call returns keys under teams/analytics/ (or an empty result if there are none). The second fails with an AccessDenied error; for a same-account caller S3 usually explains that no identity-based policy allows the s3:ListBucket action.

Common mistakes and troubleshooting

Putting the prefix condition on object actions

A statement that allows s3:GetObject with an s3:prefix condition never matches, because GetObject requests carry no prefix parameter. Object access is scoped by the Resource path, listing by the condition.

The console opens the bucket but shows an error

Without the add-on, opening the bucket sends a list request with an empty prefix, which the main policy refuses. Either attach the add-on or give users a direct link to the folder. Add teams/ to the allowed console prefixes too, or users can't click from the bucket root into the team folder.

Listing works, downloads fail

If the bucket uses SSE-KMS with a customer managed key, the team role also needs kms:Decrypt on the key for reads and kms:GenerateDataKey for uploads. Listing never touches KMS, so it keeps working and hides the problem.

Versioned buckets and "Show versions"

Listing versions is a separate action, s3:ListBucketVersions, and reading an old version needs s3:GetObjectVersion. Neither is granted here, so the console's version view fails for this role. Add them with the same prefix condition and path if the team needs them.

Limitations and production considerations

  • This is an identity-side boundary. Anyone who can change the team's role policies or the bucket policy can widen it. For a hard boundary, add a Deny to the bucket policy, or use S3 Access Points with one access point per team.
  • The console add-on reveals folder and object names at the bucket root and under teams/. If even names are sensitive, skip the add-on and use direct links or the CLI.
  • One policy per team doesn't scale to dozens of teams. Policy variables such as ${aws:PrincipalTag/team} in the Resource and the s3:prefix value can replace the literal team name, but only if the tag is controlled centrally, otherwise anyone who can tag their own role picks their own prefix.

Official AWS documentation

Account IDs, bucket and organization names are AWS documentation placeholders. Test in a non-production account before use (see the methodology).