Restrict an IAM user to a folder

A common use case for Impossible Cloud Storage is a shared bucket where each user (or team) can only see and work with their own folder. This guide shows the two policy patterns that handle this, both verified end-to-end.

The patterns rely on string conditions and the s3:prefix context key. For the operator reference, see String Conditions and s3:prefix.

Choose a pattern

Pattern
Use when

Hardcoded prefix per user or role

Each user or team has a fixed folder name that does not match their username. Easiest to read; one policy per folder.

Per-user folder via ${aws:username}

Every user gets a folder named after their username (their email). One policy serves all users.

Both patterns expose the user's full subtree under the allowed folder, including nested objects, and deny everything outside it.

Pattern A - Hardcoded prefix

Attach this inline policy to a user who should only see team-data/projectA/:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "AllowListProjectA",
            "Effect": "Allow",
            "Action": "s3:ListBucket",
            "Resource": "arn:aws:s3:::my-bucket",
            "Condition": {
                "StringLike": { "s3:prefix": "team-data/projectA/*" }
            }
        },
        {
            "Sid": "AllowReadWriteProjectA",
            "Effect": "Allow",
            "Action": ["s3:GetObject", "s3:PutObject"],
            "Resource": "arn:aws:s3:::my-bucket/team-data/projectA/*"
        }
    ]
}

The first statement scopes listing to the team-data/projectA/ folder. The second statement permits read and write on the objects inside it. Replace my-bucket with your bucket name and team-data/projectA/ with your folder.

Attach the policy via AWS CLI

Wait up to 90 seconds for the policy to propagate before testing.

Test the policy

List inside the allowed folder:

The response includes every object under team-data/projectA/, including nested keys such as team-data/projectA/sub/file.txt and team-data/projectA/deep/nested/file.txt. The result is paginated when there are more than 1000 keys.

Download an object:

Upload an object:

Confirm that listing a different folder is denied:

The response is 403 AccessDenied.

Pattern B - Per-user folder via ${aws:username}

Attach this single inline policy to every user who should be confined to user-data/<their-username>/:

${aws:username} resolves at request time to the caller's IAM username. On Impossible Cloud, the username is the email address used to create the user, so for a user [email protected] the pattern expands to user-data/[email protected]/*.

Both statements use the same variable, so each user automatically reads and writes only their own folder.

Test from each user

Sign in as [email protected] and run:

The response lists alice's full subtree.

Now try to list bob's folder from alice's credentials:

The response is 403 AccessDenied. Bob's folder is invisible to alice.

Combining with an IP restriction

To require both a specific source network and the prefix, add an IpAddress condition inside the same Condition block. Both conditions then must hold (AND).

A request from outside 203.0.113.0/24 is denied even if the prefix matches. See Policy Conditions for the full IP condition reference.

Block a sub-folder with explicit Deny

To allow a parent folder but block a sensitive sub-folder, add a second statement with Effect: Deny. Explicit Deny always wins over Allow.

Listing team-data/public/ succeeds. Listing team-data/secret/ returns 403 AccessDenied.

Gotchas

The matcher compares the request's --prefix parameter against the policy pattern as literal strings. The following situations catch first-time users.

The trailing slash matters

StringLike { s3:prefix: "team-data/projectA/*" } matches --prefix "team-data/projectA/" but does not match --prefix "team-data/projectA" (no trailing slash). The pattern requires the literal team-data/projectA/ prefix in the request value.

Always include the trailing slash on the request when the policy pattern includes it.

The user must always send --prefix

A request without --prefix is denied because s3:prefix is treated as absent and StringLike without IfExists denies on absent keys. For example:

The call above returns 403 AccessDenied.

If you need to allow listing without a prefix, use StringLikeIfExists:

The condition then passes when --prefix is missing, and the request is allowed.

Empty prefix is treated as absent

--prefix "" is the same as omitting the parameter. Non-IfExists conditions deny; IfExists conditions allow.

Nested keys are visible

Listing team-data/projectA/ returns every object below it, regardless of depth. To restrict a user to a single sub-folder, the policy pattern must target that sub-folder, for example team-data/projectA/public/*.

Usernames are emails

${aws:username} resolves to the full email used at user creation, including @ and any + tag. The pattern user-data/${aws:username}/* therefore expands to user-data/[email protected]/*. The matcher does not URL-decode the request value, so the request must use the same literal characters.

Common mistakes

Symptom
Cause
Fix

All requests return 403, even the allowed prefix.

Operator name typo (for example StringLikes or StringLIKE). The policy parses but never matches.

Use the exact operator names listed in String Conditions and s3:prefix.

The first list works, the next one is denied.

The first request included --prefix, the next did not. s3:prefix is then absent and the condition denies.

Always pass --prefix or switch the condition to StringLikeIfExists.

StringEquals { s3:prefix: "Team-Data/" } denies a request with --prefix "team-data/".

StringEquals is case-sensitive.

Use StringEqualsIgnoreCase if the case should not matter.

Adding an IpAddress condition broke an s3:ListBucket policy that was working.

Conditions inside one Condition block AND together. The request must now satisfy both.

Remove the IP condition, split into two statements, or update the allowed CIDR.

A policy using NumericEquals or DateGreaterThan denies every request.

These operators are not yet evaluated and behave as always-false.

Rewrite using a supported string or IP operator. See the operators-not-yet-evaluated section of String Conditions and s3:prefix.

See also

Last updated

Was this helpful?