Securing AWS S3 Buckets: Best Practices and Common Pitfalls

Welcome, developers and tech enthusiasts! Today, we’re diving deep into a critical topic for anyone building on AWS: Securing S3 Buckets. Amazon S3 is the backbone of cloud storage for countless applications, from static websites to massive data lakes. Its simplicity and scalability are incredible, but this power comes with a significant responsibility: security. Misconfigured S3 buckets have historically been a source of major data breaches, leading to financial penalties, reputation damage, and loss of trust.

In this post, we’re not just going to talk about S3 security; we’re going to demonstrate common pitfalls and then walk through how to secure your buckets using both the AWS Command Line Interface (CLI) and Terraform for Infrastructure as Code (IaC). This guide complements our YouTube video and the GitHub repository containing all the source code for the demonstrations.

Understanding S3 and the Risk of Public Exposure

At its core, an S3 bucket is a highly scalable, virtual folder in the cloud where you store ‘objects’ like files, images, and documents. S3 is designed for 11 nines of durability, meaning your data is incredibly resilient. However, durability doesn’t automatically mean privacy or security.

The fundamental issue we often see is unintended public exposure. Imagine a digital safe, but someone accidentally leaves the door wide open for the whole internet to peek inside. This happens when bucket policies, Access Control Lists (ACLs), or S3 Block Public Access settings are misconfigured, allowing anonymous users to read, or even write to, your sensitive data.

Demonstrating the Danger: Creating an Insecure S3 Bucket (AWS CLI)

To really drive this point home, let’s start by creating an intentionally insecure S3 bucket using the AWS CLI. We’ll simulate a common mistake: explicitly granting public read access. Our demonstration script, 01_create_insecure_bucket.sh, does the following:

  1. Provisions a new S3 bucket.
  2. Applies a permissive bucket policy with a Principal: * and s3:GetObject rule, allowing anyone on the internet to retrieve objects.
  3. Uploads a sample text file and configures its ACL to public-read.
  4. Explicitly disables S3’s powerful Block Public Access settings for this bucket to ensure our public policy and ACL take effect.

This setup creates a perfectly vulnerable target, demonstrating exactly what not to do in a production environment. Here’s a snippet of the critical bucket policy:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": "*",
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::$BUCKET_NAME/*"
    }
  ]
}

You can find the full script in the aws-cli/ directory of our GitHub repository.

Verifying the Vulnerability (AWS CLI)

With our insecure bucket now deployed, let’s verify its vulnerability and witness the potential for data exposure. The 02_verify_public_access.sh script first inspects the bucket’s configuration, checking:

  • Public Access Block settings (expecting them to be all false).
  • Bucket policy (confirming the public Principal: * and s3:GetObject statement).
  • Object’s ACL (verifying AllUsers have READ permission).

The most impactful verification comes next: we’ll use curl to directly access the sample file via its public S3 URL. If everything is misconfigured as intended, curl will successfully download the file, revealing its contents.

curl "https://$BUCKET_NAME.s3.$AWS_REGION.amazonaws.com/$SAMPLE_FILE_NAME"
# Expected output: "This is publicly accessible data in the insecure bucket!"

This simple act dramatically illustrates how easily sensitive data can be compromised when S3 buckets are left unsecured, making it a critical lesson in cloud security.

The Pillars of S3 Security: Defense Strategies

Now that we’ve seen the danger, let’s talk about defense. Securing S3 buckets relies on a few fundamental pillars:

  1. S3 Block Public Access: These are four settings that, when all set to true, prevent any public access through ACLs or bucket policies, overriding even conflicting configurations. This is your primary defense.
  2. Server-Side Encryption: Ensures your data is encrypted at rest. Whether it’s SSE-S3 (AWS-managed keys), SSE-KMS (your keys managed by KMS), or SSE-C (customer-provided keys), encrypting data at rest is a non-negotiable best practice.
  3. Versioning: Provides crucial protection against accidental deletions or malicious overwrites, allowing you to recover previous versions of objects.
  4. Access Logging: Records every request made to your bucket, creating an invaluable audit trail for monitoring, security analysis, and compliance.

Together, these form a robust foundation for S3 security.

Implementing Security Best Practices (AWS CLI)

Let’s put these security pillars into practice. We’ll use the 03_secure_bucket.sh AWS CLI script to transform our dangerously exposed bucket into a bastion of security. The script performs these actions:

  1. Enables all four S3 Block Public Access settings, effectively shutting down any avenues for public access.
  2. Removes the previously applied public bucket policy.
  3. Enables Server-Side Encryption by default using AES256 for all new objects.
  4. Activates Versioning, so every change to an object creates a new version.
  5. Configures Access Logging to a dedicated, highly secured log bucket, ensuring a robust audit trail.

Here’s a look at how we enable S3 Block Public Access:

{
  "BlockPublicAcls": true,
  "IgnorePublicAcls": true,
  "BlockPublicPolicy": true,
  "RestrictPublicBuckets": true
}

This comprehensive set of commands quickly and effectively secures the bucket. The full script can be found in the GitHub repository.

Confirming Security Effectiveness (AWS CLI)

With our security measures applied, it’s vital to confirm their effectiveness. The 04_verify_secured_bucket.sh script systematically checks each security control. We expect to see all four Public Access Block settings as true, default Server-Side Encryption enabled, Versioning as Enabled, and access logging correctly configured.

To give us the ultimate assurance, we’ll repeat the curl test from before. This time, we fully expect it to fail with a 403 Forbidden error, definitively proving that our bucket is no longer publicly accessible. This step provides tangible proof of our successful security hardening.

curl "https://$BUCKET_NAME.s3.$AWS_REGION.amazonaws.com/$SAMPLE_FILE_NAME"
# Expected output: (Empty or a 403 Forbidden error from curl)

Infrastructure as Code (IaC) with Terraform

While AWS CLI is fantastic for scripting ad-hoc changes or integrating into CI/CD pipelines, for managing your cloud infrastructure consistently and repeatably, Infrastructure as Code (IaC) is the way to go. This is where tools like Terraform shine.

Terraform allows you to define your AWS resources, including S3 buckets and all their security settings, using declarative configuration files. This means your infrastructure state is version-controlled, auditable, and easily replicated across environments. Instead of manually clicking through the AWS console or writing imperative shell scripts, you define the desired end-state, and Terraform ensures it’s met. This approach drastically reduces the risk of human error and ensures that every bucket deployed follows your organization’s security best practices by default. It’s security built into your deployment pipeline.

Terraform Demonstration: Insecure vs. Secure

Our demonstration code includes two distinct Terraform configurations: one for an intentionally insecure S3 bucket and another for a fully secured one. We use a helper script, terraform_init_apply_destroy.sh, to manage their deployment.

Insecure Terraform Configuration Example

The terraform/insecure/main.tf configuration explicitly omits public access blocks and crucially defines an aws_s3_bucket_policy resource that grants s3:GetObject to Principal: *. It also foregoes default encryption, versioning, and logging.

resource "aws_s3_bucket_policy" "insecure_policy" {
  bucket = aws_s3_bucket.insecure_bucket.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect    = "Allow"
        Principal = "*" # Allows ANYONE
        Action    = "s3:GetObject"
        Resource  = "${aws_s3_bucket.insecure_bucket.arn}/*"
      },
    ]
  })
}

Secure Terraform Configuration Example

Conversely, our terraform/secure/main.tf configuration defines all the best practices we discussed:

  • aws_s3_bucket_public_access_block with all settings true.
  • aws_s3_bucket_server_side_encryption_configuration for AES256.
  • aws_s3_bucket_versioning set to Enabled.
  • aws_s3_bucket_logging directing logs to a separate, secured log bucket.
  • An example Lifecycle Policy for cost management and data retention.
resource "aws_s3_bucket_public_access_block" "secure_bucket_public_access_block" {
  bucket                  = aws_s3_bucket.secure_bucket.id
  block_public_acls       = true
  ignore_public_acls      = true
  block_public_policy     = true
  restrict_public_buckets = true
}

resource "aws_s3_bucket_server_side_encryption_configuration" "secure_bucket_encryption" {
  bucket = aws_s3_bucket.secure_bucket.id
  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm = "AES256"
    }
  }
}

Deploying these configurations side-by-side vividly demonstrates how IaC allows you to enforce security standards from the very beginning of your infrastructure lifecycle.

All Terraform code and the helper script are available in the terraform/ directory of the GitHub repository.

Beyond the Core Pillars: Advanced S3 Security Best Practices

Beyond these core pillars, there are additional best practices to elevate your S3 security posture:

  • Least Privilege: Ensure IAM users, roles, and services only have the exact permissions they need to interact with your S3 buckets. Avoid wildcard Allow * statements unless absolutely necessary and scoped tightly.
  • Multi-Factor Authentication (MFA) Delete: For critical buckets, require an MFA code to permanently delete objects or suspend versioning. This adds an extra layer of protection against accidental or malicious data loss.
  • Regularly Review Policies: Periodically audit your bucket policies, object ACLs, and related IAM policies. AWS Config or third-party tools can help automate this. Security is not a one-time setup; it’s a continuous process of vigilance and adaptation.

Recap and Conclusion

Let’s quickly recap the essential S3 security practices we’ve covered today:

  • Always enable S3 Block Public Access at both the account and bucket level to prevent any unintended public exposure.
  • Enforce server-side encryption for data at rest (SSE-S3, SSE-KMS, or SSE-C).
  • Enable versioning to safeguard against accidental deletions or malicious modifications.
  • Configure S3 access logging to a separate, secure bucket for a comprehensive audit trail.
  • Adhere to the principle of least privilege in all your IAM policies.

By following these guidelines, you can significantly enhance the security posture of your data stored in AWS S3 and protect against common pitfalls that lead to data breaches.

We’ve explored the risks of public buckets, seen live demonstrations of creating and securing them with the AWS CLI, and understood the power of Infrastructure as Code with Terraform for maintaining consistent security. Remember, the security of your data is paramount.

Watch the Full Video Tutorial:

Explore the Code on GitHub:

The code samples used in this demonstration are available in our GitHub repository. We encourage you to fork it, experiment, and implement these best practices in your own AWS environments!

>> Get the S3 Security Demonstrator Code on GitHub <<

Start securing your S3 buckets today! If you found this blog post and video helpful, please give it a thumbs up, share it with your colleagues, and subscribe to our channel for more in-depth technical tutorials. Your support helps us create more valuable content for the community. Thanks for reading, and stay secure!

Posted in

Leave a Reply