If your S3 bucket policy does not explicitly deny requests sent over plain HTTP, clients can read and write your objects over an unencrypted channel. Add a bucket policy statement that denies any request where aws:SecureTransport is false.
S3 supports both HTTP and HTTPS endpoints. By default, AWS does not stop a client from talking to your bucket over plain HTTP. That means object data, request parameters, and the contents of GET and PUT calls can travel across the network without TLS protection. This check flags any bucket whose policy fails to deny those non-SSL requests.
It is a small policy gap that quietly undermines encryption-in-transit guarantees you probably assume are already in place.
What this check detects
The s3_ssl_not_required check inspects each bucket policy and looks for a statement that explicitly denies requests when the connection is not using SSL/TLS. AWS exposes this through the aws:SecureTransport condition key, which evaluates to true for HTTPS requests and false for plain HTTP.
If no such deny statement exists, the bucket allows non-SSL access and the check fails. This applies whether the bucket has no policy at all or has a policy that simply never addresses transport security.
Note: S3 enforces encryption at rest separately from encryption in transit. A bucket with SSE-S3 or SSE-KMS enabled is still vulnerable here, because at-rest encryption does nothing to protect data while it moves between the client and the S3 endpoint.
Why it matters
When a request reaches S3 over HTTP, everything in that request is unencrypted on the wire. Depending on the call, that can include:
- The full body of objects being uploaded or downloaded
- Object keys, which often leak structure or sensitive identifiers like user IDs
- Query parameters and headers, including presigned URL signatures
An attacker positioned anywhere on the network path can read this traffic. That includes a compromised host inside your VPC, a malicious actor on a shared network, or someone intercepting traffic at an ISP or transit provider. This is a classic man-in-the-middle exposure, and it is entirely preventable.
The presigned URL case is worth calling out. If an application generates a presigned URL and a client fetches it over HTTP, the signature travels in plaintext. Anyone who captures it can replay the request until the URL expires.
Warning: Most compliance frameworks treat encryption in transit as mandatory. PCI DSS, HIPAA, SOC 2, and FedRAML all expect TLS for data moving over public or untrusted networks. A bucket that permits HTTP access can fail an audit even if no data has actually been exposed.
The business impact is straightforward: a single misconfigured bucket can turn a routine pen test or audit into a finding, and in the worst case it is the weak link that leaks customer data.
How to fix it
The fix is to attach a bucket policy statement that denies any S3 action when aws:SecureTransport is false. This does not require downtime and does not affect legitimate HTTPS clients.
Step 1: Write the deny statement
Here is the policy statement. Replace my-bucket with your bucket name.
{
"Sid": "DenyNonSSLRequests",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:*",
"Resource": [
"arn:aws:s3:::my-bucket",
"arn:aws:s3:::my-bucket/*"
],
"Condition": {
"Bool": {
"aws:SecureTransport": "false"
}
}
}
Both resource ARNs matter. The bare bucket ARN covers bucket-level operations like ListBucket, and the /* ARN covers object-level operations like GetObject and PutObject.
Step 2: Apply it via the CLI
If the bucket has no existing policy, you can apply a complete policy directly. First save the full document to a file, policy.json:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyNonSSLRequests",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:*",
"Resource": [
"arn:aws:s3:::my-bucket",
"arn:aws:s3:::my-bucket/*"
],
"Condition": {
"Bool": {
"aws:SecureTransport": "false"
}
}
}
]
}
Then apply it:
aws s3api put-bucket-policy \
--bucket my-bucket \
--policy file://policy.json
Danger: put-bucket-policy replaces the entire bucket policy, it does not merge. If your bucket already has a policy, fetch it first with aws s3api get-bucket-policy, add the deny statement to the existing Statement array, and re-apply the combined document. Overwriting a live policy with only the deny statement can break access for applications that rely on existing grants.
To pull the current policy before editing:
aws s3api get-bucket-policy \
--bucket my-bucket \
--query Policy \
--output text > current-policy.json
Step 3: Verify the fix
A non-SSL request should now be rejected. You can confirm by forcing an HTTP request to the path-style endpoint:
curl -s -o /dev/null -w "%{http_code}\n" \
http://my-bucket.s3.amazonaws.com/some-object
You should get a 403 response. The matching HTTPS request will behave normally according to your other permissions.
Console steps
If you prefer the console:
- Open the S3 console and select the bucket
- Go to the Permissions tab
- Scroll to Bucket policy and click Edit
- Add the deny statement to the JSON, keeping any existing statements intact
- Click Save changes
Fixing it in infrastructure as code
If you manage buckets with Terraform, the cleanest approach is a dedicated aws_s3_bucket_policy resource with the SSL deny baked in:
resource "aws_s3_bucket_policy" "deny_non_ssl" {
bucket = aws_s3_bucket.my_bucket.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "DenyNonSSLRequests"
Effect = "Deny"
Principal = "*"
Action = "s3:*"
Resource = [
aws_s3_bucket.my_bucket.arn,
"${aws_s3_bucket.my_bucket.arn}/*"
]
Condition = {
Bool = {
"aws:SecureTransport" = "false"
}
}
}
]
})
}
For CloudFormation, the equivalent lives under an AWS::S3::BucketPolicy resource:
{
"Type": "AWS::S3::BucketPolicy",
"Properties": {
"Bucket": { "Ref": "MyBucket" },
"PolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyNonSSLRequests",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:*",
"Resource": [
{ "Fn::GetAtt": ["MyBucket", "Arn"] },
{ "Fn::Sub": "${MyBucket.Arn}/*" }
],
"Condition": {
"Bool": { "aws:SecureTransport": "false" }
}
}
]
}
}
}
Tip: Wrap this statement in a Terraform module that every bucket in your organization must use. When the deny is part of a shared module rather than something each team copies by hand, new buckets are compliant by default and you never have to remember it again.
How to prevent it from coming back
Fixing one bucket is easy. Keeping every bucket compliant as your account grows is the real work. A few layers help here.
Policy as code in CI/CD
Catch the gap before the bucket exists. Tools like Checkov, tfsec, and OPA/Conftest can scan Terraform and CloudFormation in a pull request. Checkov ships a built-in rule for exactly this:
checkov -d . --check CKV_AWS_18,CKV2_AWS_6,CKV_AWS_53
The relevant check, CKV2_AWS_6 in Checkov, fails any bucket that lacks an SSL-enforcing policy. Wire it into your pipeline so the build fails when someone defines a bucket without the deny statement.
Detective controls with Config
AWS Config has a managed rule, s3-bucket-ssl-requests-only, that continuously evaluates every bucket and flags ones without HTTPS enforcement. Turn it on across all regions and route non-compliant findings to your alerting channel.
aws configservice put-config-rule \
--config-rule '{
"ConfigRuleName": "s3-bucket-ssl-requests-only",
"Source": {
"Owner": "AWS",
"SourceIdentifier": "S3_BUCKET_SSL_REQUESTS_ONLY"
},
"Scope": {
"ComplianceResourceTypes": ["AWS::S3::Bucket"]
}
}'
Service control policies
For organization-wide enforcement, a Service Control Policy can deny non-SSL S3 access at the account boundary, so it applies regardless of individual bucket policies. This is the strongest preventive control because it cannot be overridden by a team editing a single bucket.
Note: An SCP that denies s3:* on aws:SecureTransport: false covers all current and future buckets in the affected accounts. Test it against a non-production OU first, since SCPs apply broadly and a typo can lock out legitimate access.
Best practices
- Make the deny statement a default. Every bucket policy you write should include the
aws:SecureTransportdeny from the start. Bake it into modules and templates. - Layer enforcement. Use bucket policies for the per-bucket control, Config rules for detection, and an SCP for the organization-wide guarantee. No single layer should be your only line of defense.
- Pair it with at-rest encryption. SSL enforcement protects data in transit; default encryption with SSE-KMS protects it at rest. You want both, and neither substitutes for the other.
- Check presigned URL generation. Ensure your application code generates HTTPS presigned URLs and that clients never downgrade to HTTP.
- Audit existing buckets regularly. Run a periodic scan across all regions, not just the ones you think you use. Forgotten buckets in unused regions are common sources of findings.
The SSL deny statement is one of the cheapest security controls in AWS. It costs nothing, adds no latency to legitimate traffic, and closes a real exposure. Add it everywhere, enforce it in your pipeline, and back it with an organization policy so it stays in place.

