Bucket policies
Edit a policy with a visual builder or raw JSON — checked before AWS sees it, and every version kept.
Two ways to edit
Open a bucket, then the Policy tab. There are two editors and they work on the same document, so you can move between them freely:
- •The visual builder — statements as questions ("who?", "what can they do?", "to which objects?"). No JSON to get right.
- •The JSON editor — the raw document, with autocomplete, formatting, and a diff view against what's currently live on S3.
Policies apply to S3 and S3-compatible buckets. Server drives don't have them (they're a filesystem, not a bucket), and the tab is hidden there rather than shown empty.
Checked before AWS sees it
A bucket policy is a document AWS either accepts or rejects with a single line like MalformedPolicy: Invalid principal in policy. That tells you something is wrong and not what to change.
BucketPilot reads your policy before it's sent and refuses to submit one AWS would turn down, naming the exact problem in the statement it's in:
- •A principal that isn't a 12-digit AWS account, an IAM or role ARN, an AWS service, or
*. This catches the most common mistake of all — leaving a placeholder likeACCOUNT_IDin the starter policy. - •An action that isn't in
service:Actionform, sos3:GetObjectsis caught as the typo it is. - •A resource that isn't an S3 ARN — a bare bucket name won't do.
- •Missing
Principal,Action,Resource,Version, anEffectthat isn'tAlloworDeny, or the sameSidused twice.
If AWS does refuse something we didn't predict, you get AWS's own words plus the line it pointed at — not "save failed".
Warnings: valid, but probably not what you meant
Some policies are perfectly legal and still don't do what the person writing them expects. AWS accepts these without comment. BucketPilot asks first, tells you what will actually happen, and saves it if that's what you want:
- •"This policy opens the bucket to the internet."
Principal: "*"withAllowmeans anyone, with no sign-in — right for a public website, expensive otherwise. The actions being opened up are named. - •A statement pointed at a different bucket. Saved here, it does nothing.
- •Object permissions granted on the bucket itself.
s3:GetObjectonarn:aws:s3:::my-bucketmatches no objects — object rules need the/*form. This is the usual cause of "I set the policy and it still says access denied". - •An action from another service, which can never match anything on the bucket.
None of these block you. You're told once, in plain language, and you decide. And you're only asked about what your edit introduces — a bucket that has been public for years doesn't re-ask every time you change an unrelated statement.
Separately, some things are simply worth knowing. A statement using a legacy CloudFront Origin Access Identity (OAI) is correct and keeps working; it's noted quietly, with the modern replacement named (Origin Access Control), and it never interrupts a save.
Every version kept
Each save is stored with an optional note, so the Policy tab has a History you can read, compare, and restore from — including a final snapshot taken automatically before a policy is deleted.
That means a policy change is reversible in one click. Changing a bucket policy is one of the easier ways to lock yourself out of your own data, and "what did it say yesterday?" is not a question S3 can answer.
Policy recipes
Ten policies worth having, written out in full. Replace my-bucket with your bucket — everything else is copy-and-paste. Each one is checked by the editor before it reaches AWS, so a typo in the account number or a missing /* is caught here rather than after the fact — and every example on this page is run through that same validator on every release, so a recipe we publish can never be one our own editor would reject.
Serve the bucket through CloudFront (Origin Access Control)
The modern way: CloudFront reaches the bucket, nobody else does, and the SourceArn condition pins it to one distribution so another account's CloudFront can't read your objects.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowCloudFrontOAC",
"Effect": "Allow",
"Principal": { "Service": "cloudfront.amazonaws.com" },
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::my-bucket/*",
"Condition": {
"StringEquals": {
"AWS:SourceArn": "arn:aws:cloudfront::111122223333:distribution/E1EXAMPLE"
}
}
}
]
}⚠️ Without the SourceArn condition this grants every CloudFront distribution in the world read access to your objects. The condition is the security, not the principal.
The legacy Origin Access Identity (OAI) equivalent
Still valid, still working, and superseded. If you have this, nothing is broken — BucketPilot points out the newer form and leaves it alone.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity E1EXAMPLE"
},
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::my-bucket/*"
}
]
}A public static website
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "PublicRead",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::my-bucket/*"
}
]
}⚠️ This is the one BucketPilot stops to confirm, because it means anyone on the internet, with no sign-in and no rate limit. It also needs the bucket's Block Public Access settings to allow policies — AWS blocks this by default, and a policy that saves but does nothing is the most confusing outcome of all.
Give another AWS account read access
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "CrossAccountRead",
"Effect": "Allow",
"Principal": { "AWS": "arn:aws:iam::111122223333:role/analytics" },
"Action": ["s3:GetObject", "s3:ListBucket"],
"Resource": ["arn:aws:s3:::my-bucket", "arn:aws:s3:::my-bucket/*"]
}
]
}⚠️ Both ARNs are required and they are not interchangeable: s3:ListBucket acts on the bucket, s3:GetObject acts on objects. Naming only one is why "I granted access and it still says denied" happens. Access also has to be granted on their side, in that role's IAM policy — a bucket policy alone is half the handshake.
Require HTTPS
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyInsecureTransport",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:*",
"Resource": ["arn:aws:s3:::my-bucket", "arn:aws:s3:::my-bucket/*"],
"Condition": { "Bool": { "aws:SecureTransport": "false" } }
}
]
}A Deny to "*" is not public access — it's the opposite, and BucketPilot doesn't confuse the two.
Require encryption on upload
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyUnencryptedUploads",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::my-bucket/*",
"Condition": {
"StringNotEquals": { "s3:x-amz-server-side-encryption": "aws:kms" }
}
}
]
}⚠️ This rejects uploads that don't ask for KMS explicitly. If the bucket has default encryption on, most uploads still send no header and will be refused — use Null on s3:x-amz-server-side-encryption instead if you only want to stop deliberately unencrypted writes.
Lock the bucket to a VPC endpoint
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyOutsideVpce",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:*",
"Resource": ["arn:aws:s3:::my-bucket", "arn:aws:s3:::my-bucket/*"],
"Condition": { "StringNotEquals": { "aws:SourceVpce": "vpce-0abc123456789" } }
}
]
}⚠️ This locks out everything that isn't inside that VPC — including you, the console, and BucketPilot. Add an ArnNotEquals exception for an admin role before you save it, or you will need account-root access to undo it.
Allow only certain IP addresses
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyOutsideOffice",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:*",
"Resource": ["arn:aws:s3:::my-bucket", "arn:aws:s3:::my-bucket/*"],
"Condition": { "NotIpAddress": { "aws:SourceIp": ["203.0.113.0/24", "198.51.100.42/32"] } }
}
]
}⚠️ Same warning as the VPC one, and one more: BucketPilot's own workers copy from an AWS network, not from your office. An IP allowlist will stop backup and migration jobs unless you allow them too.
Read-only access to one folder
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ListOnlyThatPrefix",
"Effect": "Allow",
"Principal": { "AWS": "arn:aws:iam::111122223333:role/reports-reader" },
"Action": "s3:ListBucket",
"Resource": "arn:aws:s3:::my-bucket",
"Condition": { "StringLike": { "s3:prefix": ["reports/*"] } }
},
{
"Sid": "ReadOnlyThatPrefix",
"Effect": "Allow",
"Principal": { "AWS": "arn:aws:iam::111122223333:role/reports-reader" },
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::my-bucket/reports/*"
}
]
}The two statements do different jobs: the first lets them see what's in reports/, the second lets them read it. Without the s3:prefix condition, the first one lists your whole bucket.
Stop anyone deleting objects except one role
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyDeleteExceptAdmin",
"Effect": "Deny",
"Principal": "*",
"Action": ["s3:DeleteObject", "s3:DeleteObjectVersion"],
"Resource": "arn:aws:s3:::my-bucket/*",
"Condition": {
"ArnNotEquals": { "aws:PrincipalArn": "arn:aws:iam::111122223333:role/storage-admin" }
}
}
]
}⚠️ A policy is not a backup. This stops the accidental delete; it does not stop the policy being edited by whoever can edit policies. Pair it with versioning and a backup job.
Compared with the S3 console
| S3 console | BucketPilot | |
|---|---|---|
| A malformed policy | Saves, then shows AWS's error code | Refused before sending, with the statement and the fix named |
| Public access | A badge, after the fact | Asked before saving, with the opened actions listed |
| Statement naming another bucket | Saved silently | Asked — "saved here it does nothing" |
| Object rules on the bucket ARN | Saved silently | Asked — object rules need /* |
| A legacy construct that still works | Nothing | Noted, with the modern replacement named — no interruption |
| Previous versions | None | Every save kept, with notes, diff and one-click restore |
| Writing it | JSON | Visual builder or JSON |
| Who can edit | Anyone with console access | Workspace members with write access to that bucket |
The rule we hold ourselves to: BucketPilot never blocks a policy AWS would accept. Anything we're not certain AWS refuses is a warning you can save through, because it's your bucket.