Bucket Policies & Access Fencing
By default, every EC2 credential in a project has full access to every bucket in that project. That is convenient: a new bucket works with the existing credentials without anyone having to touch permissions.
For some buckets that default is too broad — a bucket that exactly one application may write to and exactly one consumer may read from, for example. Instead of moving such a bucket into its own project, you can invert the default with a bucket policy: deny everything to everybody, except credentials that carry one of two named roles. All buckets and all credentials stay in the same project.
Which credentials a bucket policy can address
A bucket policy addresses roles, not individual access keys. The role of a credential is set in the portal when the credential is created (Credential role), see Create EC2 Credentials.
How access works by default
| Topic | Behaviour |
|---|---|
| Scope | A project is the container: all buckets and all EC2 credentials in it. |
| Default | Without a bucket policy, every credential of the project can read and write every bucket of the project — project-bound and user-bound alike. |
| Narrowing | Because access is allow-by-default, granting a right changes nothing. Only an explicit Deny in a bucket policy narrows access. |
| Granularity | One policy per bucket, per region. The same bucket in the other region needs its own policy, see Multi Region. |
| Visibility | The fence blocks access, not visibility: aws s3 ls without a bucket still lists the bucket name. |
Prerequisites
- A project with the object-storage quota profile and at least one bucket, see Configuration.
- An AWS CLI configured for the S3 endpoint and region, see AWS CLI & IAM Features.
- Two EC2 credentials in the project that carry a role, see step 1.
- The project ID, see step 2.
Step 1: Create credentials with a role
In the portal: Object Storage → S3 → select the project → Credentials → + Create Credentials, then fill in Credential role:
| Credential | Credential role |
|---|---|
| Write access | <bucket>-readwrite |
| Read access | <bucket>-readonly |
The role name is your authorisation scope
- The role name is free text and belongs to the credential. A different name means a new credential.
- Every credential in the project that carries the same role name is interchangeable as far as the policy is concerned — including user-bound credentials of other users.
- Do not use generic names such as
readwriteif different buckets need different access: one credential would then open every bucket whose policy lists that role. - Write the exact names down. You need them again to change or revoke the policy.
Step 2: Determine the project ID
The policy references credentials as ARNs:
arn:aws:iam::<PROJECT_ID>:role/<ROLE>
<PROJECT_ID> is the ID of your project, not its name. You can read it with the OpenStack client — openstack ec2 credentials list prints it in the Project ID column, see EC2 Credentials — or take it from the project overview in the portal.
Step 3: Write the policy
Set your values as shell variables and let the shell fill them in. Do not edit the ARNs by hand: a single wrong character in an ARN denies access to everybody, including the portal.
export bucket=my-bucket
export project=<PROJECT_ID>
# Note the unquoted EOF: ${bucket} and ${project} must be expanded.
cat <<EOF > bucket-policy.json
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyAllWithoutRole",
"Effect": "Deny",
"NotPrincipal": {
"AWS": [
"arn:aws:iam::${project}:role/${bucket}-readonly",
"arn:aws:iam::${project}:role/${bucket}-readwrite"
]
},
"Action": "s3:*",
"Resource": [
"arn:aws:s3:::${bucket}",
"arn:aws:s3:::${bucket}/*"
]
},
{
"Sid": "DenyNonReadActionsForReadonlyRole",
"Effect": "Deny",
"Principal": {
"AWS": "arn:aws:iam::${project}:role/${bucket}-readonly"
},
"NotAction": [
"s3:GetObject",
"s3:GetObjectVersion",
"s3:ListBucket",
"s3:ListBucketVersions",
"s3:GetBucketLocation"
],
"Resource": [
"arn:aws:s3:::${bucket}",
"arn:aws:s3:::${bucket}/*"
]
}
]
}
EOF
# Control: no ${...} may be left in the file
grep -n '\$' bucket-policy.json || echo "OK: all variables expanded"
What the two statements do:
DenyAllWithoutRole— everyone who is not one of the two roles is denied everything on this bucket and its objects. That includes all other credentials of the project, the portal, and anonymous requests.DenyNonReadActionsForReadonlyRole— the read-only role may only read (GetObject,ListBucket, plus the version and location lookups some clients need). Write and delete are denied to it.
An explicit Deny always wins over any Allow. If you do not need read-only access, delete the second statement and the -readonly entry in the first one.
Step 4: Rehearse on a scratch bucket
A mismatch between policy and credentials locks the bucket for everybody. Rehearse on a throwaway bucket in the same project and region first — it costs two minutes.
export prod_bucket=my-bucket
export bucket=${prod_bucket}-fence-test
aws s3 mb s3://${bucket}
# Run the heredoc from step 3 again (it uses ${bucket} and ${project}),
# then apply the policy to the scratch bucket:
aws s3api put-bucket-policy --bucket="${bucket}" --policy=file://bucket-policy.json
# 1) Read-write credential: export its keys first
export AWS_ACCESS_KEY_ID=<readwrite-access-key>
export AWS_SECRET_ACCESS_KEY=<readwrite-secret-key>
AWS_MAX_ATTEMPTS=1 aws s3 cp - s3://${bucket}/hello.txt <<<"hello"
# 2) Read-only credential: switch to its keys
export AWS_ACCESS_KEY_ID=<readonly-access-key>
export AWS_SECRET_ACCESS_KEY=<readonly-secret-key>
AWS_MAX_ATTEMPTS=1 aws s3 cp s3://${bucket}/hello.txt -
# 3) A credential without a role: switch to its keys (expect 403)
export AWS_ACCESS_KEY_ID=<roleless-access-key>
export AWS_SECRET_ACCESS_KEY=<roleless-secret-key>
AWS_MAX_ATTEMPTS=1 aws s3api head-bucket --bucket "${bucket}"
Check 3 is the actual proof. If a credential without a role still succeeds, stop and contact support — do not apply the policy to a productive bucket.
Clean up:
aws s3api delete-bucket-policy --bucket="${bucket}"
aws s3 rm s3://${bucket} --recursive
aws s3 rb s3://${bucket}
If the rehearsal locks itself
If check 1 fails with Access Denied, the principal in the policy does not match the credentials: wrong project ID, another project, or a typo in the role name. Leave the scratch bucket alone and do not apply the policy to the productive bucket. With no matching credential neither you nor the portal can remove the policy — see Revoke and lockout.
Step 5: Apply to the bucket
export bucket=${prod_bucket}
# Run the heredoc from step 3 again, then:
aws s3api put-bucket-policy --bucket="${bucket}" --policy=file://bucket-policy.json
# Control: the stored document should match your file
aws s3api get-bucket-policy --bucket="${bucket}" --query Policy --output text | jq .
Step 6: Verify
Run the three checks from step 4 against the productive bucket, each with its own credential as shown there. Expect: write and read with the read-write credential, read only with the read-only credential, and 403 for every other credential of the project.
Day-2 behaviour
- Portal: the S3 panel works with the credential selected under
Select Credentials. If that credential carries no role, browsing, uploading and the policy editor fail withAccess Denied. Give the credential your administrators use the<bucket>-readwriterole, or work with the CLI. - Other region: repeat steps 3 to 5 for the bucket in the second region.
- New buckets: there is no inheritance. A new bucket is open to the whole project until it gets its own policy.
- Versioning: the read-only role may read current and older object versions, nothing else.
- Changing a role: create the new credential, add the new role to the policy, switch the applications over, then remove the old role from the policy. Never in the reverse order.
Revoke and lockout
To revoke, remove the policy. This needs a credential that carries <bucket>-readwrite:
aws s3api delete-bucket-policy --bucket="${bucket}"
Afterwards the bucket is back to the project default: every credential of the project has full access again.
Lockout: nobody has access any more
Symptom: every credential of the project — including the portal — gets Access Denied on the bucket. Cause: the role names or the project ID in the policy match no existing credential, or the last credential carrying the role was deleted.
Recovery without support works as long as you know the role names from the policy. Create a new credential in the same project with exactly that role name (<bucket>-readwrite). The deny statement excludes this principal, so the new credential has access again. Use it to delete the policy, then recreate the credential you lost.
Recovery with support: if the role names are no longer known, open a ticket with the project ID, bucket name, region and the note that a DenyAllWithoutRole bucket policy locks the bucket. Support can remove the policy.
Prevention: create both credentials before applying the policy, rehearse on a scratch bucket, and never delete the read-write credential while the policy is in place.
Troubleshooting
| Symptom | Cause and fix |
|---|---|
Requests hang for about a minute, then 403 |
The CLI retries failed requests. export AWS_MAX_ATTEMPTS=1 fails fast — useful for the expected denials above. |
MalformedPolicy when applying |
${...} was not expanded: the heredoc must start with <<EOF, not with <<'EOF'. |
aws s3 rb fails |
The bucket is not empty. Delete the objects first (aws s3 rm --recursive, needs the role). |
The bucket still appears in aws s3 ls |
Expected. The fence denies content access, not the bucket name. |
| A second credential with the same role works | Expected. The role name is the scope, not the key. |