bucketpilot/Docs

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 like ACCOUNT_ID in the starter policy.
  • An action that isn't in service:Action form, so s3:GetObjects is 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, an Effect that isn't Allow or Deny, or the same Sid used 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: "*" with Allow means 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:GetObject on arn:aws:s3:::my-bucket matches 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.

json
{
  "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.

json
{
  "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

json
{
  "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

json
{
  "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

json
{
  "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

json
{
  "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

json
{
  "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

json
{
  "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

json
{
  "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

json
{
  "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 consoleBucketPilot
A malformed policySaves, then shows AWS's error codeRefused before sending, with the statement and the fix named
Public accessA badge, after the factAsked before saving, with the opened actions listed
Statement naming another bucketSaved silentlyAsked — "saved here it does nothing"
Object rules on the bucket ARNSaved silentlyAsked — object rules need /*
A legacy construct that still worksNothingNoted, with the modern replacement named — no interruption
Previous versionsNoneEvery save kept, with notes, diff and one-click restore
Writing itJSONVisual builder or JSON
Who can editAnyone with console accessWorkspace 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.

© 2026 BucketPilot