Remediation Guides

How to Fix S3.5: Require TLS with an aws:SecureTransport Bucket Policy

26 September 2026 8 min read

Security Hub control S3.5 fails when an S3 general purpose bucket has no policy statement that explicitly denies requests sent over plain HTTP. To fix it, add a Deny statement covering all principals and all s3:* actions on the bucket and its objects when aws:SecureTransport is false, merge it into the existing policy, and apply it with put-bucket-policy.

The aws:SecureTransport bucket policy statement itself is about a dozen lines of JSON. The part that needs care is everything around it: put-bucket-policy replaces the whole policy, so a careless paste can delete the grants your applications rely on, and any client still using an http:// endpoint starts receiving 403 errors.

What the scanner is actually detecting

Tool Finding title What it evaluates
AWS Security Hub CSPM (shown as Security Hub in older consoles and reports) [S3.5] S3 general purpose buckets should require requests to use TLS Each AWS::S3::Bucket through the AWS Config rule s3-bucket-ssl-requests-only. Severity Medium, change triggered, no parameters.
AWS Config managed rule s3-bucket-ssl-requests-only (identifier S3_BUCKET_SSL_REQUESTS_ONLY) NON_COMPLIANT when a bucket policy allows HTTP requests.
CIS AWS Foundations Benchmark (via Security Hub) 2.1.1 in v3.0.0 and v5.0.0 (2.1.2 in v1.4.0) The same S3.5 check, mapped to CIS.

None of these tools send traffic to your bucket. AWS Config reads the bucket policy through the AWS API, which is how cloud security posture management (CSPM) checks work in general. The control description asks for a policy that covers all requests (Action: s3:*) using the aws:SecureTransport condition key, and the AWS Knowledge Center notes that policies which allow HTTPS but do not explicitly deny HTTP might not comply. Security Hub also maps S3.5 to PCI DSS v4.0.1 requirement 4.2.1 and NIST SP 800-53 SC-8.

Real-world risk, stated honestly

Amazon S3 accepts both HTTP and HTTPS. A request sent over HTTP crosses the network unencrypted, so object contents, metadata and headers can be read or altered by anyone positioned on the path. AWS recommends allowing only HTTPS for exactly that reason.

The exposure is narrower than the finding may suggest. The AWS CLI uses TLS by default, so HTTP traffic to S3 usually comes from older applications with hard-coded http:// endpoints, appliances and backup tools, or public objects linked with http:// URLs. The Deny statement is a guardrail that turns a silent cleartext transfer into a visible error. It does not change who can access the bucket: a bucket that is public over HTTPS stays public, which is a separate problem covered in our guide to finding public storage buckets.

How to confirm it

List the buckets that currently fail S3.5 in a Region:

aws securityhub get-findings --region us-east-1 --filters '{
  "ComplianceSecurityControlId":[{"Value":"S3.5","Comparison":"EQUALS"}],
  "ComplianceStatus":[{"Value":"FAILED","Comparison":"EQUALS"}],
  "RecordState":[{"Value":"ACTIVE","Comparison":"EQUALS"}]}' 
  --query "Findings[].Resources[].Id" --output text

Or ask AWS Config directly. Rule names vary by deployment, so look up the exact name first:

aws configservice describe-config-rules 
    --query "ConfigRules[?contains(ConfigRuleName,'ssl-requests-only')].ConfigRuleName"

aws configservice get-compliance-details-by-config-rule 
    --config-rule-name RULE_NAME_FROM_ABOVE --compliance-types NON_COMPLIANT 
    --query "EvaluationResults[].EvaluationResultIdentifier.EvaluationResultQualifier.ResourceId"

Inspect one bucket. A NoSuchBucketPolicy error means the bucket has no policy at all; otherwise this prints any matching Deny statement:

aws s3api get-bucket-policy --bucket amzn-s3-demo-bucket --query Policy --output text 
  | jq '[.Statement] | flatten | .[] | select(.Effect=="Deny" and .Condition.Bool["aws:SecureTransport"]=="false")'

Then prove that HTTP works today. The CLI signs the request as usual but sends it to an http:// endpoint in the bucket’s Region; if objects come back, the bucket accepts cleartext requests:

aws s3api list-objects-v2 --bucket amzn-s3-demo-bucket --max-items 1 
    --endpoint-url http://s3.us-east-1.amazonaws.com

Before enforcing anything, find out who actually uses HTTP. In S3 server access logs, the Cipher Suite and TLS version fields contain – for requests that did not use TLS. For CloudTrail data events, AWS suggests a CloudWatch alarm on tlsDetails.tlsVersion NOT EXISTS to catch HTTP access attempts.

How to fix it

The statement to add

This is the statement from the AWS documentation. Keep the condition value as the string “false”, as AWS’s examples do. The aws:SecureTransport key is always present in the request context, so a plain Bool operator is enough. Outside the commercial partition, change the ARN prefix (for example arn:aws-us-gov or arn:aws-cn).

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

AWS CLI: merge into the existing policy

B=amzn-s3-demo-bucket

# 1. Save the current policy (this file is also your rollback copy)
aws s3api get-bucket-policy --bucket "$B" --query Policy --output text > current.json

# 2. Append the Deny statement and keep every existing statement
jq --arg b "$B" '.Statement = ([.Statement] | flatten) + [{
  "Sid": "DenyInsecureTransport",
  "Effect": "Deny",
  "Principal": "*",
  "Action": "s3:*",
  "Resource": ["arn:aws:s3:::($b)", "arn:aws:s3:::($b)/*"],
  "Condition": {"Bool": {"aws:SecureTransport": "false"}}
}]' current.json > new.json

# 3. Check the merged policy
aws accessanalyzer validate-policy --policy-type RESOURCE_POLICY 
    --validate-policy-resource-type AWS::S3::Bucket 
    --policy-document file://new.json

# 4. Apply it
aws s3api put-bucket-policy --bucket "$B" --policy file://new.json

If step 1 fails with NoSuchBucketPolicy, write {“Version”:”2012-10-17″,”Statement”:[]} to current.json and continue. If it fails with AccessDenied, stop: you cannot see the existing policy, and applying new.json would overwrite it. Review any ERROR or SECURITY_WARNING findings from step 3 before step 4, and if a statement named DenyInsecureTransport already exists, inspect it instead of adding a second one.

S3 console

Open the bucket, choose Permissions, and under Bucket policy choose Edit. Add the statement to the existing Statement array rather than replacing the text, then choose Save changes.

CloudFormation and Terraform

A bucket has a single policy, so add the statement to the template that already owns it instead of creating a second policy resource:

DataBucketPolicy:
  Type: AWS::S3::BucketPolicy
  Properties:
    Bucket: !Ref DataBucket
    PolicyDocument:
      Version: "2012-10-17"
      Statement:
        - Sid: DenyInsecureTransport
          Effect: Deny
          Principal: "*"
          Action: "s3:*"
          Resource:
            - !GetAtt DataBucket.Arn
            - !Sub "${DataBucket.Arn}/*"
          Condition:
            Bool:
              aws:SecureTransport: "false"

Terraform’s aws_s3_bucket_policy resource also manages the complete document, so the same rule applies there.

Buckets that serve a static website

S3 website endpoints do not support HTTPS. Adding this statement takes down a site served from http://bucket.s3-website-Region.amazonaws.com, including a CloudFront distribution that uses that website endpoint as its origin. AWS’s documented HTTPS options are CloudFront or AWS Amplify Hosting in front of the bucket. Until you migrate, treat the bucket as a documented exception.

How to verify the fix and rescan

  1. Rerun the get-bucket-policy and jq check above; it should print the DenyInsecureTransport statement.
  2. Rerun the –endpoint-url http:// test. It should now fail with AccessDenied, while the same command without –endpoint-url still succeeds.
  3. S3.5 is change triggered, so the control should switch to PASSED once AWS Config records the new policy; rerun the get-findings query to confirm. If you deployed the managed rule yourself, you can force an evaluation with aws configservice start-config-rules-evaluation –config-rule-names s3-bucket-ssl-requests-only.
  4. For a few days, watch server access logs or CloudTrail for 403 responses on requests without a TLS version. Those are the clients you still need to fix.

What can break and how to roll back

  • HTTP clients. Applications, appliances and scripts configured with http:// endpoints get AccessDenied, whatever their IAM permissions.
  • Public links. Because the principal is “*”, anonymous http:// downloads of public objects fail too.
  • Website endpoints, as described above.
  • A policy that was replaced rather than merged. Losing statements for CloudFront, cross-account access or log delivery breaks those integrations immediately.
# Restore the saved policy
aws s3api put-bucket-policy --bucket "$B" --policy file://current.json

# If the bucket had no policy before the change
aws s3api delete-bucket-policy --bucket "$B"

If a mistake in the policy locks everyone out, the root user of the bucket owner’s account can still run GetBucketPolicy, PutBucketPolicy and DeleteBucketPolicy, unless a VPC endpoint policy or an AWS Organizations policy blocks it.

Common false positive reasons

  • An Allow statement with “aws:SecureTransport”: “true”. It looks equivalent, but AWS documents that such a policy does not comply with the rule. Add the explicit Deny.
  • A Deny that is narrower than the AWS example. Statements limited to a few actions, only the object ARN, or specific principals may not satisfy a check that expects all requests (s3:*). Match the documented statement.
  • Stale results. The control only re-evaluates when AWS Config records a change. If Config is not recording S3 buckets in that Region, a fixed bucket never flips to PASSED.
  • “Nobody uses HTTP here.” That is not a false positive: S3.5 inspects the policy, not traffic.
  • Website buckets that must stay on HTTP. This is a real exception. Suppress the finding in Security Hub with a written justification and a migration date rather than ignoring it.

FAQ

Can I use an Allow statement with aws:SecureTransport set to true instead?

No. The AWS Knowledge Center gives exactly that as a non-compliant example. The rule looks for an explicit Deny on HTTP requests.

Will Block Public Access reject a policy with Principal “*”?

Not for this statement. Block Public Access evaluates policies that grant access, and a Deny grants none. If the console reports a conflict, check your edit for an accidental Allow.

Will this break the AWS CLI?

Not in a default setup. The AWS CLI documentation states that it uses TLS by default. Only clients deliberately pointed at http:// endpoints are affected.

Does S3.5 cover directory buckets or access points?

No. The control targets general purpose buckets (AWS::S3::Bucket), and AWS Config managed rules only support general purpose buckets when evaluating S3.

Tracking this finding across many buckets and accounts

At scale, S3.5 is best fixed in the templates and pipelines that create buckets, with the aggregated Security Hub control status as your evidence. If you consolidate findings in SITEY, a self-hosted vulnerability management platform that imports results from 16 scanners, first confirm that your cloud posture source is among them; duplicates are merged per scanner, not across scanners, and its AI triage suggests false positives with evidence while a human decides. Its per-finding retest covers only Nessus, Acunetix and Burp, so for this control the Security Hub or AWS Config re-evaluation remains the proof of closure.

Sources

SITEY closes the loop, not just the report.Discover, validate, fix and verify in your own infrastructure.

See pricing