Securing Cloud Storage Buckets
Misconfigured object storage โ AWS S3 buckets, Azure Blob containers, Google Cloud Storage buckets โ remains one of the most common sources of publicly disclosed data exposure, precisely because the failure mode requires no exploit at all: a bucket left readable (or writable) to anyone on the internet is a data breach the moment a scanner finds it, not after some sophisticated attack chain. This guide covers hardening object storage across the three major providers, aligned to each provider's own CIS Foundations Benchmark and to NIST SP 800-53 Rev. 5.
Scope: AWS S3, Azure Blob Storage, and Google Cloud Storage (GCS). The underlying principles โ deny public access by default, prefer policy over ACLs, encrypt everywhere, log everything, restrict by network โ apply equally to any object storage service, including smaller/regional providers.
Why Storage Buckets Are a Distinct Risk Category
| Risk | Why It's Different From a Typical Server Misconfiguration |
|---|---|
| Public exposure requires zero exploitation | A bucket with public read access is a breach the instant it's found โ no vulnerability, no exploit chain, just a misconfigured permission |
| Discoverable at internet scale | Automated scanners continuously enumerate predictable bucket-naming patterns (company name, product name, common suffixes) across all major providers |
| Write access is worse than read access | A publicly writable bucket can be used to plant malware, deface hosted content, or โ for buckets referenced by build/deploy pipelines โ achieve supply-chain compromise |
| Defaults have shifted, but legacy resources haven't | Providers have made "private by default" the modern baseline, but buckets created years ago under older defaults, or via Terraform/CloudFormation templates copied from outdated examples, often predate the safer defaults |
| Third-party and cross-account access sprawl | Buckets are frequently shared with analytics vendors, backup providers, or partner accounts โ each grant is a standing trust relationship that outlives the reason it was created |
1. Deny Public Access by Default
Every major provider now offers an account/organization-wide switch that overrides individual bucket misconfigurations โ enable it first, before reviewing anything else.
AWS S3 โ Block Public Access, set at both the account level and per-bucket:
aws s3control put-public-access-block \
--account-id 123456789012 \
--public-access-block-configuration \
BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true
Azure Storage โ disable "Allow Blob public access" at the storage account level:
az storage account update \
--name mystorageaccount --resource-group myrg \
--allow-blob-public-access false
GCS โ enforce Public Access Prevention, ideally as an organization policy so it can't be disabled per-project:
gcloud storage buckets update gs://my-bucket --public-access-prevention
Org-level enforcement: constraints/storage.publicAccessPrevention.
2. Prefer Policies Over ACLs โ and Disable ACLs Entirely Where Possible
Legacy ACLs (per-object, per-grantee) are difficult to audit at scale and easy to misapply. Modern practice is to disable them entirely and manage access through a single, reviewable resource policy.
AWS: Set Object Ownership to Bucket owner enforced โ this disables ACLs for the bucket entirely, making the bucket policy the sole access-control mechanism:
aws s3api put-bucket-ownership-controls \
--bucket my-bucket \
--ownership-controls Rules=[{ObjectOwnership=BucketOwnerEnforced}]
Least-privilege bucket policy example โ explicit principal, explicit actions, explicit deny for everything else (implicit by omission, reinforced by Block Public Access):
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowSpecificRoleReadWrite",
"Effect": "Allow",
"Principal": { "AWS": "arn:aws:iam::123456789012:role/AppServiceRole" },
"Action": ["s3:GetObject", "s3:PutObject"],
"Resource": "arn:aws:s3:::my-bucket/*"
},
{
"Sid": "DenyInsecureTransport",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:*",
"Resource": ["arn:aws:s3:::my-bucket", "arn:aws:s3:::my-bucket/*"],
"Condition": { "Bool": { "aws:SecureTransport": "false" } }
}
]
}
Azure: Use Azure RBAC role assignments (Storage Blob Data Reader/Contributor) scoped to the specific container rather than Shared Access Signatures with broad, long-lived permissions. Where SAS tokens are unavoidable, scope them narrowly and set the shortest practical expiry.
GCS: Use uniform bucket-level access (disables per-object ACLs) combined with IAM Conditions to scope grants precisely:
gcloud storage buckets update gs://my-bucket --uniform-bucket-level-access
3. Encrypt at Rest and Enforce Encryption in Transit
All three providers encrypt at rest by default today, but "default" isn't the same as "enforced and verified" โ and transit encryption still needs an explicit deny rule.
| Provider | At-Rest Default | Recommended Upgrade | Enforce TLS |
|---|---|---|---|
| AWS S3 | SSE-S3 (AES-256) | SSE-KMS with a customer-managed key for sensitive data, enabling key rotation and access-logged key usage | Bucket policy Deny on aws:SecureTransport: false (example above) |
| Azure Blob | Microsoft-managed keys | Customer-managed keys (CMK) via Key Vault for regulated data | "Require secure transfer for REST API operations" set to enabled |
| GCS | Google-managed keys | Customer-managed encryption keys (CMEK) via Cloud KMS | GCS enforces TLS for all API access by default โ no separate toggle needed |
To require SSE-KMS on every upload (rejecting unencrypted or wrong-key uploads) on S3, add a bucket policy statement denying s3:PutObject unless the s3:x-amz-server-side-encryption header matches your expected value.
4. Versioning, Object Lock, and Ransomware Resilience
Versioning alone protects against accidental overwrite/deletion; combined with immutability controls, it protects against a compromised identity deliberately deleting or encrypting data.
- AWS S3: Enable Versioning, then layer S3 Object Lock (Governance or Compliance mode) for a defined retention period on data that must survive a compromised-credential scenario. Enable MFA Delete on the bucket so permanently removing a version requires a second factor, not just IAM permissions.
- Azure: Enable blob versioning and configure immutable blob storage policies (time-based retention or legal hold) on containers holding data that must be tamper-proof.
- GCS: Enable Object Versioning, and apply a Bucket Lock on a retention policy so the retention period itself cannot be shortened or removed, even by a project owner.
These controls are a direct, practical mitigation against ransomware and insider-threat scenarios where the attacker has valid write/delete credentials โ the point isn't preventing access, it's preventing destruction.
5. Restrict by Network, Not Just by Identity
Layer network-level controls so that even a leaked credential with valid IAM permissions can't be used from an arbitrary internet location.
AWS: Restrict bucket access to traffic through a specific VPC Endpoint via a bucket policy condition:
{
"Effect": "Deny",
"Principal": "*",
"Action": "s3:*",
"Resource": ["arn:aws:s3:::my-bucket", "arn:aws:s3:::my-bucket/*"],
"Condition": { "StringNotEquals": { "aws:sourceVpce": "vpce-1a2b3c4d" } }
}
Azure: Configure Storage account firewall and virtual networks to allow access only from selected VNets/subnets or specific public IP ranges, and use Private Endpoints for traffic that should never traverse the public internet at all.
GCS: Apply VPC Service Controls to create a service perimeter around the project, preventing data exfiltration even via a valid but externally-used credential.
6. Logging, Monitoring, and Automated Drift Detection
Configuration drifts. A bucket that was correctly locked down at creation can become public again through a later, unreviewed change โ automated detection is what catches that before a scanner does.
| Provider | Access Logging | Change/Audit Logging | Automated Exposure Detection |
|---|---|---|---|
| AWS | S3 Server Access Logs, or CloudTrail data events for object-level API calls | CloudTrail management events | AWS Config rule s3-bucket-public-read-prohibited/-write-prohibited; GuardDuty S3 Protection; Security Hub |
| Azure | Storage Analytics logging / diagnostic settings to Log Analytics | Azure Activity Log | Microsoft Defender for Storage; Azure Policy definitions denying public access |
| GCS | Cloud Audit Logs (Data Access logs) | Cloud Audit Logs (Admin Activity) | Security Command Center; Org Policy Constraints enforced continuously, not just at creation |
Independent of provider-native tooling, open-source multi-cloud scanners (ScoutSuite, Prowler) are worth running on a schedule as a second opinion against your own cloud-native posture tools โ see Data Security Posture Management for the broader discovery-and-classification approach these fit into.
7. Cross-Account and Third-Party Access
Shared buckets accumulate trust relationships that outlive their original purpose.
- Never use a wildcard (
"Principal": "*") in a bucket policy without a strong, explicitConditionnarrowing it โ a wildcard principal with a weak or missing condition is functionally a public bucket. - Scope cross-account grants with
aws:PrincipalOrgID(AWS) or equivalent tenant-restriction conditions so a compromised external account can't be substituted for the intended one. - Maintain an inventory of every third party with standing bucket access (backup vendors, analytics platforms, CI/CD systems) and recertify it on the same cadence as any other third-party access review โ a grant made for a since-cancelled vendor relationship is pure unmanaged risk.
- For S3 buckets fronting a CDN (static website hosting, asset delivery), prefer Origin Access Control with CloudFront over a public bucket โ the bucket itself stays private; only the CDN can read it.
Hardening Checklist
| Control | Priority | Verify With |
|---|---|---|
| Account/org-level public access block enabled | Critical | aws s3control get-public-access-block / Azure Policy / org constraint |
| Per-bucket public access block enabled | Critical | Bucket-level Block Public Access / container public-access setting |
| ACLs disabled in favor of bucket policy / IAM (Bucket Owner Enforced) | High | Object Ownership setting |
| Least-privilege policy โ no unscoped wildcard principals | Critical | Policy review / access analyzer |
| Encryption at rest with a customer-managed key for sensitive data | High | Bucket/container encryption settings |
| TLS enforced (deny insecure transport) | High | Bucket policy aws:SecureTransport deny / "Require secure transfer" |
| Versioning enabled | High | Versioning status |
| Object Lock / immutability configured for critical data | Medium | Retention policy / Object Lock configuration |
| MFA Delete enabled (AWS) | Medium | aws s3api get-bucket-versioning |
| Network restriction (VPC endpoint / firewall / Service Controls) | High | Policy condition / firewall rules |
| Access logging and audit logging enabled, forwarded to SIEM | High | Logging configuration + SIEM ingestion check |
| Automated public-exposure scanning running continuously | Critical | AWS Config / Defender for Storage / Security Command Center status |
| Third-party/cross-account grants inventoried and recertified | Medium | Access review log |
CIS Foundations Benchmark Alignment
Each provider's CIS Foundations Benchmark includes a dedicated storage section covering the same core controls: block public access, encryption at rest, access logging, and versioning.
| Benchmark | Storage Section Covers |
|---|---|
| CIS AWS Foundations Benchmark | S3 bucket public access blocking, encryption (SSE/KMS), access logging, versioning/MFA Delete |
| CIS Microsoft Azure Foundations Benchmark | Storage account public access, secure transfer requirement, encryption, diagnostic logging |
| CIS Google Cloud Platform Foundation Benchmark | Bucket IAM (not ACLs), uniform bucket-level access, public access prevention, logging |
NIST SP 800-53 Rev. 5 Control Mapping
| Control | Title | Applied Here |
|---|---|---|
| AC-3 / AC-6 | Access Enforcement / Least Privilege | Bucket policy/IAM scoping, disabling ACLs (Sections 1โ2) |
| AC-4 | Information Flow Enforcement | Network restriction via VPC endpoints/Service Controls (Section 5) |
| SC-8 | Transmission Confidentiality/Integrity | Enforced TLS (Section 3) |
| SC-28 | Protection of Information at Rest | Encryption at rest, customer-managed keys (Section 3) |
| CM-6 | Configuration Settings | Public access block as an enforced baseline configuration |
| AU-2 / AU-6 | Audit Events / Audit Review | Access and audit logging, SIEM forwarding (Section 6) |
| CA-7 | Continuous Monitoring | Automated exposure scanning and drift detection (Section 6) |
| CP-9 | System Backup | Versioning and Object Lock as tamper-resistant retention (Section 4) |
| SR-3 / SR-4 | Supply Chain Controls / Provenance | Third-party and cross-account access inventory (Section 7) |
References
- AWS โ Security Best Practices for Amazon S3
- AWS โ Blocking Public Access to S3
- Microsoft โ Security Recommendations for Blob Storage
- Google Cloud โ Cloud Storage Security Best Practices
- CIS AWS Foundations Benchmark
- CIS Microsoft Azure Foundations Benchmark
- CIS Google Cloud Platform Foundation Benchmark
- NIST SP 800-53 Rev. 5 โ Security and Privacy Controls
- Encryption
- Data Security Posture Management