If you know Azure Blob Storage, Amazon S3 is mostly familiar: a bucket holds objects the way a container holds blobs, IAM identity policies play the role of Azure RBAC assignments, presigned URLs replace SAS tokens, and storage classes replace access tiers. The two big differences are that S3 adds a resource-based bucket policy that can grant or deny access on its own, and that public exposure is controlled by S3 Block Public Access at the organization, account and bucket level. Securing a bucket means keeping Block Public Access and Bucket owner enforced on, granting access through IAM roles and a tight bucket policy, and denying non-TLS and out-of-organization requests.
Who this is for and what you will have
This guide is for Azure administrators who have been handed AWS accounts, often after a merger or a multicloud decision, and need to understand and secure S3 without relearning storage from scratch.
By the end you will be able to:
- Translate S3 concepts into the Azure Storage terms you already know.
- Explain how IAM identity policies and bucket policies combine, including across accounts.
- Create a bucket with ACLs disabled, all public access blocked, versioning on and default encryption set.
- Write a bucket policy that enforces TLS and restricts access to your AWS organization.
- Grant a role read-only access and share one object temporarily.
- Read S3 access denied messages and fix the cause.
If the AWS accounts are joining a Microsoft-centric estate, the companion guide on AWS IAM Identity Center SSO with Entra ID covers sign-in, and the enterprise Azure cloud migration playbook covers the wider migration process.
Concept map: Azure Storage to Amazon S3
| Azure Blob Storage | Amazon S3 | Notes |
|---|---|---|
| Storage account | AWS account plus bucket settings | Bucket-level settings such as encryption, versioning and Block Public Access cover what you'd set on a storage account |
| Container | Bucket | Objects sit directly in the bucket; "folders" are key prefixes |
| Blob | Object | Addressed by bucket name and key |
| Azure RBAC role assignment | IAM identity-based policy | Attached to a user, group or role in AWS |
| No direct equivalent | Bucket policy | Resource-based JSON policy on the bucket |
AllowBlobPublicAccess and container access level | S3 Block Public Access and bucket policy | Both default to no public access |
| Container ACL levels (Private, Blob, Container) | ACLs, disabled by Bucket owner enforced | AWS recommends keeping ACLs disabled |
| Microsoft-managed keys | SSE-S3 | On by default in both |
| Customer-managed keys in Key Vault | SSE-KMS with an AWS KMS key | Key policy also governs access |
| User delegation SAS | Presigned URL | Time-limited, signed by an identity |
| Hot, cool, cold, archive tiers | Storage classes | See the table later in this guide |
Naming and namespace
A storage account name becomes part of a public DNS name (<account>.blob.core.windows.net). S3 general purpose bucket names behave similarly: each must be unique across all AWS accounts in the partition. Names are 3 to 63 characters of lowercase letters, numbers, periods and hyphens, must start and end with a letter or number, and can't look like an IP address. You can't rename a bucket or move it to another Region after creation. AWS also warns that after you delete a bucket in the shared namespace, another account can create one with the same name and receive requests meant for yours, so empty unused buckets rather than deleting them. AWS now offers an account regional namespace, where names end in -<account-id>-<region>-an and only your account can ever own them.
Avoid periods in bucket names unless the bucket only hosts a static website; virtual-hosted-style HTTPS certificates don't work with them.
How S3 decides whether a request is allowed
Azure RBAC is additive: a principal's effective access is the union of its role assignments, minus any deny assignments. AWS evaluation looks similar within one account but adds a resource policy:
- Every request starts as an implicit deny.
- An explicit
Denyin any applicable policy (identity policy, bucket policy, service control policy, resource control policy, VPC endpoint policy, permissions boundary or session policy) ends the evaluation with a deny. - Same account: if either the identity-based policy or the bucket policy allows the action, it's allowed (subject to any boundaries and organization policies).
- Cross account: the requester's identity policy in their account and the bucket policy in yours must both allow the action.
Two consequences matter for Azure admins. First, a bucket with no bucket policy still lets any IAM identity in the owning account in, as long as that identity's IAM policy allows it, much like a data role assigned at subscription scope applies to every storage account beneath it. Second, a bucket policy can grant access to another account or a service without touching any IAM user, which has no Azure Storage equivalent and is where most accidental exposure starts.
Public access: Block Public Access and Object Ownership
S3 Block Public Access has four settings you can apply at the account, bucket and access point level, and through AWS Organizations as a single on-or-off policy:
| Setting | Effect |
|---|---|
BlockPublicAcls | Rejects requests that set a public ACL |
IgnorePublicAcls | Ignores any public ACLs that already exist |
BlockPublicPolicy | Rejects bucket or access point policies that grant public access |
RestrictPublicBuckets | Limits a bucket with a public policy to AWS service principals and the owning account |
S3 applies the most restrictive combination across levels. AWS recommends turning on all four at the account level, and also on each bucket to comply with AWS Security Hub control S3.8, and only relaxing them for a bucket that genuinely needs public access, such as a static website. At the organization level the four settings can only be enabled or disabled together.
The Azure parallel is AllowBlobPublicAccess=false on the storage account, which overrides any container-level anonymous access setting.
Object Ownership controls ACLs. The default for new buckets, Bucket owner enforced, disables ACLs so the bucket owner owns every object and only policies grant access. The alternatives, Bucket owner preferred and Object writer, re-enable ACLs and should be reserved for legacy cross-account upload patterns. With ACLs disabled, uploads that specify any ACL other than bucket-owner-full-control fail with AccessControlListNotSupported.
Encryption
Since 5 January 2023 every new S3 object is encrypted with SSE-S3 at no extra cost, matching Azure Storage, where encryption with Microsoft-managed keys is always on and can't be disabled. For your own key control, use SSE-KMS with an AWS KMS key (the equivalent of a customer-managed key in Key Vault) and enable an S3 Bucket Key to reduce KMS request costs. With SSE-KMS, callers also need kms:GenerateDataKey to upload and kms:Decrypt to download, and the key policy must allow them. That's a second authorization layer Azure admins often miss.
Step-by-step: build a locked-down bucket
The commands use the AWS CLI. Run aws sts get-caller-identity first to confirm which account and role you're using; it's the AWS equivalent of checking az account show.
1. Create the bucket with ACLs disabled
aws s3api create-bucket \
--bucket contoso-finance-reports-7f3a2c \
--region eu-west-1 \
--create-bucket-configuration LocationConstraint=eu-west-1 \
--object-ownership BucketOwnerEnforcedOutside us-east-1 you must pass LocationConstraint; without it the bucket is created in us-east-1.
2. Block all public access on the bucket
aws s3api put-public-access-block \
--bucket contoso-finance-reports-7f3a2c \
--public-access-block-configuration "BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true"Apply the same four settings at the account level as well, so a future bucket can't be made public by accident.
3. Turn on versioning
aws s3api put-bucket-versioning --bucket contoso-finance-reports-7f3a2c --versioning-configuration Status=EnabledAWS recommends waiting 15 minutes after first enabling versioning before issuing PUT or DELETE requests, because you may see intermittent 404 NoSuchKey errors until it propagates.
4. Set default encryption with a KMS key
aws s3api put-bucket-encryption --bucket contoso-finance-reports-7f3a2c --server-side-encryption-configuration '{
"Rules": [
{
"ApplyServerSideEncryptionByDefault": {
"SSEAlgorithm": "aws:kms",
"KMSMasterKeyID": "<KMS-Key-ARN>"
},
"BucketKeyEnabled": true
}
]
}'S3 doesn't validate the key ID in this call, so check the ARN carefully. The key must be a symmetric key in the same Region as the bucket. Skip this step if SSE-S3 meets your requirements.
5. Add a bucket policy that enforces TLS and your organization
Save the following as policy.json. The first statement denies plain HTTP; the second denies any principal outside your AWS organization:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "RestrictToTLSRequestsOnly",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:*",
"Resource": [
"arn:aws:s3:::contoso-finance-reports-7f3a2c",
"arn:aws:s3:::contoso-finance-reports-7f3a2c/*"
],
"Condition": { "Bool": { "aws:SecureTransport": "false" } }
},
{
"Sid": "DenyAccessFromOutsideOrg",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:*",
"Resource": [
"arn:aws:s3:::contoso-finance-reports-7f3a2c",
"arn:aws:s3:::contoso-finance-reports-7f3a2c/*"
],
"Condition": { "StringNotEquals": { "aws:PrincipalOrgID": "o-aa111bb222" } }
}
]
}aws s3api put-bucket-policy --bucket contoso-finance-reports-7f3a2c --policy file://policy.jsonAfter applying the organization deny, test any AWS service that reads from or writes to this bucket, such as log delivery, and add an exception if it's blocked. If a bad bucket policy locks everyone out, sign in as the AWS account root user (or, for an AWS Organizations member account, launch a privileged root session from IAM) and delete the policy.
6. Grant a role read-only access
Attach an identity-based policy to the IAM role (or the IAM Identity Center permission set) that should read the data. Note that listing applies to the bucket ARN and reading applies to the object ARNs:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ListBucket",
"Effect": "Allow",
"Action": "s3:ListBucket",
"Resource": "arn:aws:s3:::contoso-finance-reports-7f3a2c"
},
{
"Sid": "ReadObjects",
"Effect": "Allow",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::contoso-finance-reports-7f3a2c/*"
}
]
}This is the S3 version of assigning Storage Blob Data Reader at container scope. In Azure the same grant looks like this:
az role assignment create \
--role "Storage Blob Data Reader" \
--assignee-object-id "<object-id>" \
--assignee-principal-type "User" \
--scope "/subscriptions/<subscription-id>/resourceGroups/<resource-group>/providers/Microsoft.Storage/storageAccounts/<storage-account>/blobServices/default/containers/<container>"Remember that in Azure, Owner, Contributor and Storage Account Contributor don't grant data access through Entra ID, but they can list account keys and read everything with Shared Key. In AWS, any IAM principal with broad s3:* permissions in the owning account can read every bucket that has no deny. Both deserve the same scrutiny.
7. Share one object temporarily
aws s3 presign s3://contoso-finance-reports-7f3a2c/2026/q3-summary.pdf --expires-in 3600The URL carries the permissions of whoever signed it, so sign with a role that can only read what you intend to share. The CLI allows up to seven days; the S3 console allows up to 12 hours. The Azure counterpart is a user delegation SAS, which Microsoft recommends over account-key SAS because it's signed with Entra credentials.
Storage classes and access tiers
| Azure tier | Closest S3 storage class | Minimum duration |
|---|---|---|
| Hot | S3 Standard (default) | None in either |
| Cool | S3 Standard-IA | 30 days in both |
| Cold | S3 Glacier Instant Retrieval | 90 days in both |
| Archive (offline, rehydrate first) | S3 Glacier Flexible Retrieval or Deep Archive (restore first) | Azure 180 days; S3 90 or 180 days |
| Smart tier (moves blobs between hot, cool and cold) | S3 Intelligent-Tiering | S3: none |
Standard-IA, One Zone-IA and Glacier Instant Retrieval bill a minimum object size of 128 KB. S3 One Zone-IA stores data in a single Availability Zone, so use it only for data you can re-create.
Verify the configuration
aws s3api get-bucket-ownership-controls --bucket contoso-finance-reports-7f3a2c
aws s3api get-bucket-policy --bucket contoso-finance-reports-7f3a2cThen review the bucket in IAM Access Analyzer for S3, which reports buckets whose policies or ACLs grant access to the public or to accounts outside your organization. Finally, test with the read-only role: listing and downloading should work, uploading should fail, and a request over http:// should be denied.
Troubleshooting access denied errors
For requests within the same account or organization, S3 now says which policy type denied access. Most messages look like User <arn> is not authorized to perform <action> on "<resource>" because <context>.
- "because no identity-based policy allows the s3:GetObject action": the caller's IAM policy is missing an allow. Add it to the role or permission set.
- "with an explicit deny in a resource-based policy": a bucket policy statement denies the caller, often the TLS or organization condition. Check the caller's organization and protocol.
- "because no service control policy allows" or "with an explicit deny in a service control policy": an AWS Organizations policy is blocking the action, and the message includes the policy ARN for explicit denies.
AccessControlListNotSupported(400): a client is uploading with an ACL to a bucket with ACLs disabled. Remove the ACL from the request or usebucket-owner-full-control.- Access denied only on KMS-encrypted objects: the caller lacks
kms:Decryptorkms:GenerateDataKey, or the KMS key policy doesn't allow them. - Generic "Access Denied" from another account: enhanced messages aren't returned for cross-account requests outside your organization. Check both the caller's identity policy and your bucket policy.
- "405 Method Not Allowed" on put-bucket-policy: the caller isn't in the bucket owner's account.
On the Azure side, the matching symptom is AuthorizationPermissionMismatch after a role assignment; allow up to 10 minutes for blob data role changes to take effect.
Closing checklist
- Account-level and bucket-level Block Public Access on, all four settings.
- Object Ownership set to Bucket owner enforced; no ACL-based grants.
- Versioning enabled; default encryption set, with SSE-KMS and a Bucket Key if you need key control.
- Bucket policy denies non-TLS requests and principals outside your organization.
- Access granted through IAM roles or permission sets with least-privilege actions and resource ARNs.
- Presigned URLs signed by narrowly scoped roles with short expiry.
- IAM Access Analyzer for S3 reviewed for public or external access.
References
- Blocking public access to your Amazon S3 storage
- Controlling ownership of objects and disabling ACLs
- Configuring default encryption
- Examples of Amazon S3 bucket policies
- Policy evaluation for requests within a single account
- Cross-account policy evaluation logic
- General purpose bucket naming rules
- Sharing objects with presigned URLs
- Understanding and managing Amazon S3 storage classes
- Troubleshoot access denied (403 Forbidden) errors in Amazon S3
- AWS CLI create-bucket, put-public-access-block, put-bucket-versioning, put-bucket-policy
- Authorize access to blobs using Microsoft Entra ID
- Assign an Azure role for access to blob data
- Remediate anonymous read access to blob data
- Grant limited access with shared access signatures
- Access tiers for blob data
- Azure Storage encryption for data at rest