Amazon S3
Require HTTPS and keep an S3 bucket inside your organization
One bucket policy with two baseline denies: no plain-HTTP requests, and no principals from outside your AWS organization.
Published · Updated .
The problem
Two guardrails belong on almost every bucket that holds internal data. The first refuses plain HTTP: S3 endpoints still accept it, and a misconfigured client or hand-built URL can send signed requests, session tokens and data unencrypted. The second refuses principals outside your AWS organization, so a future mistake, such as an Allow for the wrong account ID, can't expose the data.
A bucket has exactly one bucket policy, so both guardrails ship as one document, merged into the statements the bucket already has. Both are Denies and grant nothing; existing Allows keep working for callers inside the organization over HTTPS.
The policy
Bucket policy: baseline denies
Amazon S3 bucket policy. Attach to: The bucket policy of amzn-s3-demo-bucket, merged with its existing statements
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyInsecureTransport",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:*",
"Resource": [
"arn:aws:s3:::amzn-s3-demo-bucket",
"arn:aws:s3:::amzn-s3-demo-bucket/*"
],
"Condition": {
"Bool": {
"aws:SecureTransport": "false"
}
}
},
{
"Sid": "DenyPrincipalsOutsideOrganization",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:*",
"Resource": [
"arn:aws:s3:::amzn-s3-demo-bucket",
"arn:aws:s3:::amzn-s3-demo-bucket/*"
],
"Condition": {
"StringNotEqualsIfExists": {
"aws:PrincipalOrgID": "o-exampleorgid"
},
"BoolIfExists": {
"aws:PrincipalIsAWSService": "false"
}
}
}
]
}The form generators on this site don't edit bucket policy documents; copy it, then check it with IAM Access Analyzer as shown below.
Statement by statement
- aws:SecureTransport is always present in the request context, so a plain Bool test is correct. Principal "*" in a Deny is intentional: the rule applies to every caller, the bucket owner's administrators included. The two Resource entries cover bucket-level and object-level actions.
- Both conditions must be true for the Deny to apply. The first is true when the caller's organization ID differs from yours, and also when the key is missing, as it is for accounts outside any organization and for anonymous requests. The second is true for IAM principals and anonymous requests, and false when an AWS service acts as its own service principal, as S3 log delivery and CloudFront OAC do. Those calls are exempt, as AWS's data perimeter guidance recommends, because service principals belong to no organization.
Before you start
- AWS Organizations, with your organization ID (o-…) from the Organizations console or aws organizations describe-organization.
- A list of everyone who uses the bucket today: workloads in other member accounts, partners, and AWS services that deliver into it. Anyone outside the organization loses access the moment this policy applies.
- s3:GetBucketPolicy and s3:PutBucketPolicy, and a recovery path: the bucket owner's root user, or a privileged root session for member accounts, can still remove a policy that locks everyone out.
Deploy it step by step
- Export the current policy and merge. put-bucket-policy replaces the whole document, so start from what is there. If the command reports NoSuchBucketPolicy, the bucket has none and you can use the policy above as-is. Otherwise append the two statements to the existing Statement array and keep the Sids unique.
Commands (replace <placeholders>) aws s3api get-bucket-policy --bucket amzn-s3-demo-bucket --query Policy --output text > current.json - Look for callers the perimeter would cut off. If you record S3 data events in CloudTrail, group recent requests by caller account. Each account outside the organization needs a decision: move it in, give it access another way, or add an aws:PrincipalAccount exception to the condition. IAM Access Analyzer external access findings for the bucket list principals the current policy already admits.
- Validate the merged document and apply it. validate-policy checks grammar and flags suspicious patterns. It does not tell you who will be denied, which is why the previous step matters.
Commands (replace <placeholders>) aws accessanalyzer validate-policy --policy-type RESOURCE_POLICY --validate-policy-resource-type AWS::S3::Bucket --policy-document file://bucket-policy.json aws s3api put-bucket-policy --bucket amzn-s3-demo-bucket --policy file://bucket-policy.json
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 both statements before applying
simulate-custom-policy can evaluate the bucket policy together with a caller's identity policy (caller-allow.json, granting s3:GetObject). You supply the context values, so you can flip one at a time. Always pass the organization ID: with IfExists operators, leaving it out makes the caller look like an outsider.
aws iam simulate-custom-policy --policy-input-list file://caller-allow.json --resource-policy file://bucket-policy.json --caller-arn arn:aws:iam::111122223333:role/<role-name> --action-names s3:GetObject --resource-arns arn:aws:s3:::amzn-s3-demo-bucket/<test-key> --context-entries ContextKeyName=aws:SecureTransport,ContextKeyValues=false,ContextKeyType=boolean ContextKeyName=aws:PrincipalOrgID,ContextKeyValues=o-exampleorgid,ContextKeyType=string --query 'EvaluationResults[].EvalDecision' --output text
aws iam simulate-custom-policy --policy-input-list file://caller-allow.json --resource-policy file://bucket-policy.json --caller-arn arn:aws:iam::111122223333:role/<role-name> --action-names s3:GetObject --resource-arns arn:aws:s3:::amzn-s3-demo-bucket/<test-key> --context-entries ContextKeyName=aws:SecureTransport,ContextKeyValues=true,ContextKeyType=boolean ContextKeyName=aws:PrincipalOrgID,ContextKeyValues=o-exampleorgid,ContextKeyType=string --query 'EvaluationResults[].EvalDecision' --output textExpected: explicitDeny for the first call (HTTP) and allowed for the second (HTTPS, inside the organization). Removing the aws:PrincipalOrgID entry from the second call should turn it into explicitDeny. The simulator only evaluates the values you pass; it does not look up real organization membership.
Prove HTTP is refused with a request S3 would otherwise accept
An anonymous curl to an http:// URL is refused with or without this policy, so it proves nothing. A presigned URL does: it carries your signature, and the scheme isn't part of the SigV4 canonical request, so the same URL is valid over both schemes and only the bucket policy tells them apart. Use a short expiry and a non-sensitive object, since over HTTP the URL travels in clear text. The last command does the same with the CLI against the plain-HTTP endpoint.
URL=$(aws s3 presign s3://amzn-s3-demo-bucket/<test-key> --expires-in 60 --profile <profile>)
curl -s -o /dev/null -w '%{http_code}\n' "$URL"
curl -s "${URL/https:/http:}"
aws s3api list-objects-v2 --bucket amzn-s3-demo-bucket --max-keys 1 --endpoint-url http://s3.<region>.amazonaws.com --profile <profile>Expected: 200 over HTTPS. Over HTTP, an XML error body with Code AccessDenied, and the CLI call fails with AccessDenied. Because the signer is in the bucket owner's account, the message usually adds that the denial came from an explicit deny in a resource-based policy. If the HTTP request never reaches S3 (a proxy or firewall blocking port 80), the test is inconclusive.
Check the perimeter honestly
From outside the organization you can't observe much: without an Allow for that account the request was denied anyway, and S3 gives callers outside the owner's account or organization only a generic Access Denied. Verify from the owner's side instead. An external access analyzer with the organization as zone of trust reports buckets whose policies admit outside principals; this bucket should have none. CloudTrail data events in the owner's account can show denied requests from outside accounts, though AWS notes some access-denied entries are redacted or omitted.
aws accessanalyzer list-findings-v2 --analyzer-arn <org-analyzer-arn> --filter '{"resource": {"eq": ["arn:aws:s3:::amzn-s3-demo-bucket"]}, "status": {"eq": ["ACTIVE"]}}'Expected: An empty findings list for the bucket after the next analysis (AWS documents up to 30 minutes after a policy change). A remaining finding names the outside principal and the condition under which it has access; read it before archiving. Access Analyzer doesn't report access by AWS service principals, which this policy deliberately exempts.
Common mistakes and troubleshooting
Replacing instead of merging
Pasting these two statements in as the whole policy silently removes every existing Allow, such as a CloudFront OAC grant.
Writing the perimeter with NotPrincipal or without the service exemption
You can't use NotPrincipal to exempt AWS service principals, and a perimeter without the aws:PrincipalIsAWSService test also blocks service deliveries such as server access logs and CloudTrail. Those failures are quiet: logs simply stop arriving.
Adding an s3:TlsVersion floor that adds nothing or breaks clients
AWS documents TLS 1.2 and 1.3 as the versions S3 endpoints support, so a NumericLessThan 1.2 floor mostly restates what the endpoints already enforce. A 1.3 floor bites, but AWS notes TLS 1.3 isn't supported through AWS PrivateLink for S3 or Multi-Region Access Points, so it breaks clients on those paths.
Breaking partners and public content
Anonymous requests have no organization ID, so this policy also ends any public or presigned-URL-by-outsider access. Presigned URLs still work when the signer is inside the organization, because the request is evaluated as the signer.
Limitations and production considerations
- The service-principal exemption is broad: any AWS service principal passes the perimeter. Allow statements for services need their own aws:SourceArn or aws:SourceAccount conditions, and AWS's perimeter guidance adds an aws:SourceOrgID check, usually in a resource control policy, against confused-deputy access.
- A bucket policy protects one bucket. For every bucket in the organization, AWS's perimeter guidance uses resource control policies (RCPs).
- Org membership is a coarse boundary: a compromised role in any member account still passes. Combine it with least-privilege Allows and, for sensitive data, network conditions such as aws:SourceVpce.
Official AWS documentation
- Examples of Amazon S3 bucket policies
- Protecting data in transit with encryption (Amazon S3)
- AWS global condition context keys
- Building a data perimeter on AWS
- Establishing a data perimeter on AWS: allow only trusted identities to access company data
- Create a signed AWS API request (SigV4 canonical request)
- Troubleshoot access denied (403 Forbidden) errors in Amazon S3
- How IAM Access Analyzer findings work
- Amazon S3 CloudTrail events (cross-account delivery)
Account IDs, bucket and organization names are AWS documentation placeholders. Test in a non-production account before use (see the methodology).