Cloud & infrastructure

Amazon S3 for Azure admins: secure buckets with IAM and bucket policies

Map Amazon S3 buckets, IAM policies, bucket policies, Block Public Access and presigned URLs to Azure Blob Storage, Azure RBAC and SAS, then build a locked down bucket with the AWS CLI.

12 min read
On this page

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 StorageAmazon S3Notes
Storage accountAWS account plus bucket settingsBucket-level settings such as encryption, versioning and Block Public Access cover what you'd set on a storage account
ContainerBucketObjects sit directly in the bucket; "folders" are key prefixes
BlobObjectAddressed by bucket name and key
Azure RBAC role assignmentIAM identity-based policyAttached to a user, group or role in AWS
No direct equivalentBucket policyResource-based JSON policy on the bucket
AllowBlobPublicAccess and container access levelS3 Block Public Access and bucket policyBoth default to no public access
Container ACL levels (Private, Blob, Container)ACLs, disabled by Bucket owner enforcedAWS recommends keeping ACLs disabled
Microsoft-managed keysSSE-S3On by default in both
Customer-managed keys in Key VaultSSE-KMS with an AWS KMS keyKey policy also governs access
User delegation SASPresigned URLTime-limited, signed by an identity
Hot, cool, cold, archive tiersStorage classesSee 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:

  1. Every request starts as an implicit deny.
  2. An explicit Deny in 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.
  3. 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).
  4. 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:

SettingEffect
BlockPublicAclsRejects requests that set a public ACL
IgnorePublicAclsIgnores any public ACLs that already exist
BlockPublicPolicyRejects bucket or access point policies that grant public access
RestrictPublicBucketsLimits 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 BucketOwnerEnforced

Outside 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=Enabled

AWS 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.json

After 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 3600

The 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 tierClosest S3 storage classMinimum duration
HotS3 Standard (default)None in either
CoolS3 Standard-IA30 days in both
ColdS3 Glacier Instant Retrieval90 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-TieringS3: 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-7f3a2c

Then 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 use bucket-owner-full-control.
  • Access denied only on KMS-encrypted objects: the caller lacks kms:Decrypt or kms: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

Questions people ask

What is the Azure equivalent of an S3 bucket?

There isn't an exact one. Objects sit directly in a bucket, like blobs in a container, but the bucket is also where you set public access blocking, encryption defaults, versioning and a resource policy, which in Azure live on the storage account. Treat a bucket as a container that carries its own account-level settings.

Is an S3 bucket policy the same as an Azure RBAC role assignment?

No. A bucket policy is a JSON resource-based policy attached to the bucket that names principals, actions and conditions. Azure RBAC assigns a role to a principal at a scope. Within one AWS account, an allow in either the IAM identity policy or the bucket policy is enough, and an explicit deny in any policy wins.

Are new S3 buckets public by default?

No. New buckets, access points and objects don't allow public access, ACLs are disabled by the Bucket owner enforced setting, and new objects are encrypted with SSE-S3. Public access is only possible if someone changes Block Public Access and adds a public policy.

What is the S3 equivalent of an Azure SAS token?

A presigned URL. It grants time-limited access using the credentials of the identity that signed it. The S3 console allows up to 12 hours and the AWS CLI up to 7 days, similar in purpose to a user delegation SAS signed with Microsoft Entra credentials.

Amazon S3AWS IAMAzure Blob StorageAzure RBAC
  1. A practical Azure landing zone for small and mid-size companies

    Set up Azure management groups, subscriptions, hub-and-spoke networking, Azure Policy, RBAC and budgets the right way from day one, scaled down from Microsoft's landing zone architecture.

  2. Autopilot device preparation vs classic Autopilot: choosing the right one

    Compare Windows Autopilot device preparation and classic Windows Autopilot on join types, modes, app limits, registration, ESP and reporting, and pick the right one for each device population.

  3. AVD pooled host pool with FSLogix profiles on Azure Files (Entra Kerberos)

    Build an Azure Virtual Desktop pooled host pool with Microsoft Entra joined session hosts and FSLogix profile containers stored on Azure Files, using Microsoft Entra Kerberos instead of domain controllers.