Fix S3 Access Denied (403 Forbidden) Errors in AWS
Amazon S3 returns a plain “Access Denied” or HTTP 403 for any request that fails authorization, and it does not tell you which policy caused the denial. S3 evaluates IAM policies, bucket policies, Block Public Access, object ownership, KMS key permissions, and VPC endpoint policies. Any one of them can independently block access. This guide walks you through identifying and fixing the actual cause.
Quick answer
S3 Access Denied errors come from one of these sources:
- IAM policy: the calling principal lacks the required S3 action in their identity policy
- Bucket policy: an explicit Deny in the bucket policy, or no Allow for a cross-account request
- Block Public Access: a setting blocks access patterns that look public (anonymous or broad-principal)
- Object Ownership / ACL state: ACLs are disabled or the object is owned by a different account
- SSE-KMS permissions: the object is encrypted with a KMS key and the caller lacks
kms:Decryptorkms:GenerateDataKey - VPC endpoint policy: a Gateway or Interface endpoint policy restricts which buckets or actions are allowed
- SCP / RCP: AWS Organizations controls restrict the action at the account level
- Access point policy: an access point is in the request path and its policy does not cover the action
- Presigned URL: the URL has expired or the session that created it has ended
Same-account and cross-account requests behave differently. Within the same account, one Allow (either IAM or bucket policy) is usually enough if no Deny exists. Across accounts, both sides must explicitly allow the action.
The fastest debugging path: identity → same account or cross-account → explicit Deny → missing Allow → Block Public Access → object ownership → encryption → network controls.
Check these first:
- Who is making the request? (
aws sts get-caller-identity) - Is the bucket in the same AWS account as the caller?
- Does the bucket policy contain an explicit Deny?
- Does the IAM policy include the right action and the correct ARN format?
- Is Block Public Access enabled at the account or bucket level?
- Who owns the objects: the bucket owner or another account?
- Are objects encrypted with a customer-managed KMS key?
- Is the request going through a VPC endpoint?
What “Access Denied” actually means in S3
Most AWS services return an AccessDenied error that names the missing permission. S3 is different. S3 returns a flat Access Denied with no explanation, by design, so that private bucket existence is not leaked to unauthorized callers.
This makes debugging harder than a typical IAM AccessDenied error, where the error message usually names the specific action and resource.
Think of S3 access like getting into a secure building that has multiple checkpoints. Each checkpoint operates independently. Passing the front desk (IAM policy) does not guarantee you pass the floor scanner (bucket policy). Getting through both doesn’t help if the elevator requires a separate key card (KMS permissions). You need to clear every relevant checkpoint, and any single one can send you back.
S3 has its own authorization layer that evaluates several policies simultaneously:
- Your IAM policy must allow the action
- The bucket policy may allow or deny the action independently
- Block Public Access filters certain request patterns before policy evaluation even begins
- KMS permissions apply on top of S3 permissions if the object is encrypted with a customer-managed key
- VPC endpoint policies restrict what S3 actions are allowed through the endpoint
Whether one Allow is enough depends on the account relationship.
Same-account access is more permissive. If you own the bucket and the IAM principal, one Allow (either IAM policy or bucket policy) is usually sufficient, as long as no explicit Deny exists anywhere.
Cross-account access is stricter. If Account B wants to access a bucket in Account A, both the IAM policy in Account B and the bucket policy in Account A must allow the action. Without both, the request is denied by default.
S3 intentionally gives you nothing when it denies a request. If it said “this bucket doesn’t exist,” an attacker could enumerate bucket names. If it said “you need s3:GetObject,” it would confirm the bucket exists. The blank “Access Denied” is a deliberate security choice, which is why debugging requires systematic investigation rather than reading an error message.
Understanding same-account vs cross-account resolves the majority of S3 403 errors.
How S3 access control actually works
S3 evaluates several policy layers in every request. Here is what each one controls and when it causes a 403.
| Layer | What it controls | Can independently deny? | Common symptom |
|---|---|---|---|
| IAM identity policy | Which S3 actions the calling principal may perform | Yes | Works in console but fails via app role; missing action name |
| Bucket policy | Who can access the bucket and under what conditions | Yes | Explicit Deny overrides all allows; cross-account reads fail without it |
| Block Public Access | Blocks policies/ACLs that grant broad or anonymous access | Yes | Static website returns 403; public bucket policy silently not applied |
| ACL / Object Ownership | Per-object or per-bucket grants to AWS accounts | Yes (when ACLs are active) | Object owned by a different account; legacy ACL assumptions fail |
| KMS key policy | Who can use the KMS key to decrypt or encrypt objects | Yes | Access Denied on encrypted objects despite valid S3 permissions |
| VPC endpoint policy | Which S3 actions and buckets are allowed through the endpoint | Yes | Works outside VPC; fails from EC2 or Lambda inside VPC |
| SCP / RCP | Account-level restrictions applied by AWS Organizations | Yes | All S3 access blocked in a specific account or OU |
| Access point policy | Access control for requests through an S3 access point | Yes | 403 specifically when using an access point ARN |
An explicit “Effect”: “Deny” in any policy — IAM, bucket policy, SCP — overrides every Allow everywhere else. It doesn’t matter if ten other policies say “yes.” One Deny says “no” and that’s final. Always search for Deny statements before assuming your Allows are the problem.
Same-account vs cross-account S3 access#
| Scenario | What must allow access | Common failure |
|---|---|---|
| Same account, no bucket policy | IAM policy alone is sufficient | IAM policy missing the action or using wrong ARN format |
| Same account, bucket policy exists | Either IAM or bucket policy can allow; any explicit Deny blocks | Explicit Deny in bucket policy overriding a valid IAM Allow |
| Cross-account read (bucket in Account A, caller in Account B) | Bucket policy in A must allow the Account B principal AND IAM policy in B must allow the action | One side is missing, usually the bucket policy in Account A |
| Cross-account upload, object owned by Account B | Account B user owns uploaded objects by default; Account A cannot read them | Missing bucket-owner-full-control ACL or BucketOwnerEnforced not set |
| Cross-account via role assumption | The assumed role must have S3 permissions; bucket policy must allow that role’s ARN | Role ARN in bucket policy does not match the assumed role ARN exactly |
S3 now returns enhanced access denied messages for same-account and same-organization requests that indicate which policy type caused the denial. Check CloudTrail for these.
Fast diagnosis checklist
Work through these steps in order. Most S3 Access Denied errors are found by step 6.
Step 1: Confirm caller identity.
aws sts get-caller-identityVerify the account ID, user ID, and ARN. A frequent trap: your CLI profile uses a different role than your application does. Make sure these match.
Step 2: Confirm whether the bucket is in the same account.
aws s3api get-bucket-location --bucket my-bucketIf this returns Access Denied, you may lack s3:GetBucketLocation, or the bucket belongs to a different account.
Step 3: Check CloudTrail for the exact denial.
CloudTrail logs S3 data events with errorCode=AccessDenied. Same-account denials now include enhanced messages indicating which policy type caused them.
aws cloudtrail lookup-events \
--lookup-attributes AttributeKey=ResourceName,AttributeValue=my-bucket \
--query 'Events[?contains(CloudTrailEvent, `AccessDenied`)]'CloudTrail logs management events (bucket creation, policy changes) by default, but S3 data events (GetObject, PutObject, and their failures) require a separate trail or event data store configuration. If you see no results in CloudTrail for a 403 on an object, data events may not be enabled. Enable them on the trail for the bucket in question.
Step 4: Check the IAM policy.
aws iam simulate-principal-policy \
--policy-source-arn arn:aws:iam::111122223333:role/my-role \
--action-names s3:GetObject \
--resource-arns "arn:aws:s3:::my-bucket/path/to/object"Run separate simulations for s3:ListBucket with the bucket ARN (arn:aws:s3:::my-bucket) and for object actions with the object ARN (arn:aws:s3:::my-bucket/*).
Step 5: Check the bucket policy.
aws s3api get-bucket-policy --bucket my-bucketLook for any "Effect": "Deny" statements. Check whether Principal and Resource in Allow statements actually match your caller ARN and the object path you are requesting.
Step 6: Check Block Public Access settings.
# Account-level settings
aws s3control get-public-access-block --account-id 111122223333
# Bucket-level settings
aws s3api get-public-access-block --bucket my-bucketIf BlockPublicPolicy or RestrictPublicBuckets is true and your bucket policy uses "Principal": "*", the policy is silently blocked.
Step 7: Check object ownership and ACL state.
aws s3api get-bucket-ownership-controls --bucket my-bucketIf ObjectOwnership is BucketOwnerEnforced, all ACLs are disabled and the bucket owner owns all objects. If objects were uploaded by a different account without bucket-owner-full-control, the bucket owner cannot read them.
Step 8: Check whether SSE-KMS is involved.
aws s3api head-object --bucket my-bucket --key path/to/object \
--query '[ServerSideEncryption, SSEKMSKeyId]'If SSEKMSKeyId is present, the object is encrypted with a KMS key. Confirm your principal has kms:Decrypt for reads or kms:GenerateDataKey for writes. See Customer Managed Keys for how to update KMS key policies.
Step 9: Check the VPC endpoint policy if traffic goes through a VPC.
aws ec2 describe-vpc-endpoints \
--filters Name=service-name,Values=com.amazonaws.us-east-1.s3A restrictive VPC endpoint policy can block access to specific buckets or actions even when both IAM and bucket policies allow them.
Step 10: Check Organizations controls.
If the account is under AWS Organizations, check whether any Service Control Policy (SCP) or Resource Control Policy (RCP) restricts S3 access for the account or OU. SCPs require management account access to review.
Step 11: Check presigned URL validity.
If you are using a presigned URL:
- Check whether the URL has passed its expiry (default: 1 hour)
- Check whether the role session that signed the URL has expired
- Check whether a bucket policy Deny was added after the URL was created
- Verify the client clock is within 15 minutes of AWS time
See S3 Signed URLs for how presigned URL signing and expiry works.
Step 12: Check access point, CloudFront, or website endpoint path.
If your request goes through an S3 access point, a CloudFront distribution, or an S3 static website endpoint, the authorization path differs from a direct bucket request. A CloudFront Origin Access Control (OAC) requires the bucket policy to allow the specific CloudFront distribution ARN as the principal. If the bucket policy still references an old OAI, access fails.
Common root causes of S3 Access Denied
| Root cause | What it looks like | How to verify | How to fix |
|---|---|---|---|
| Missing IAM Allow | Works in console; fails via app role | simulate-principal-policy | Add the missing action to the IAM policy |
| Explicit Deny in IAM or bucket policy | Fails even with an Allow present | Read all policies for "Effect": "Deny" | Remove or narrow the Deny statement |
| Wrong resource ARN: bucket vs object | s3:ListBucket returns 403 instead of 404; object actions fail with correct bucket policy | Check whether policy ARN ends in /* or not | Use bucket ARN for s3:ListBucket; use bucket/* for object actions |
Missing s3:ListBucket | aws s3 ls returns 403; s3:GetObject may work | Simulate s3:ListBucket on the bucket ARN | Add s3:ListBucket scoped to the bucket ARN |
| Block Public Access blocking public-style policy | Bucket policy with "Principal": "*" is silently not applied | Check get-public-access-block for BlockPublicPolicy=true | Disable BlockPublicPolicy and RestrictPublicBuckets on the bucket |
| Cross-account bucket policy missing | Cross-account reads fail even with IAM policy in place | get-bucket-policy returns no policy or policy lacks the external principal | Add bucket policy allowing the external account or role ARN |
| Object owned by another account | Bucket owner cannot read the object | head-object shows owner metadata from a different account | Require bucket-owner-full-control on uploads, or set BucketOwnerEnforced |
| ACLs disabled but code assumes ACL grants | ACL-based grants silently ignored | get-bucket-ownership-controls shows BucketOwnerEnforced | Switch to bucket policy for access control |
| SSE-KMS permission missing | Access Denied specifically on encrypted objects, not others | head-object shows a KMS key ID | Add kms:Decrypt or kms:GenerateDataKey to IAM role; update KMS key policy |
| Requester Pays bucket | Request fails with RequestorPaysBucket or 403 | get-bucket-request-payment shows Requester | Add --request-payer requester to CLI commands |
| VPC endpoint policy restriction | Works outside VPC; fails from EC2 or Lambda | Check endpoint policy for bucket or action restrictions | Update endpoint policy to allow the bucket and action |
| SCP / RCP restriction | All S3 access in an account blocked; even admin users fail | Review SCPs applied to the account’s OU | Update SCP in Organizations management account |
| Access point policy missing action | 403 specifically when using an access point ARN | Read the access point policy | Add the missing action to the access point policy |
| CloudFront OAI/OAC mismatch | CloudFront returns 403; direct S3 URL also returns 403 | Compare bucket policy Principal against the CloudFront distribution ARN | Update bucket policy to reference the current OAC ARN |
| Presigned URL expired | 403 on a URL that worked previously | Check X-Amz-Expires and X-Amz-Date query parameters | Regenerate the URL; extend --expires-in if the session permits |
| Session that created presigned URL expired | URL 403 before its stated expiry | URL was created by a role session with a short max duration | Use long-lived IAM user credentials to sign, or regenerate frequently |
| Wrong principal or stale credentials | Works with one profile; fails with another | sts get-caller-identity confirms the ARN | Rotate credentials or re-assume the correct role |
How to fix same-account S3 Access Denied
Within the same account, S3 access usually requires one Allow: either in the IAM policy or the bucket policy, as long as no explicit Deny exists. The most common cause is a missing IAM permission or a wrong ARN format in the policy.
The most common fix: add the missing action with the correct ARN format.
Bucket-level actions (s3:ListBucket, s3:GetBucketLocation) need the bucket ARN. Object-level actions (s3:GetObject, s3:PutObject, s3:DeleteObject) need the object ARN with /*. Use two separate statements:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["s3:ListBucket", "s3:GetBucketLocation"],
"Resource": "arn:aws:s3:::my-bucket"
},
{
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:PutObject"],
"Resource": "arn:aws:s3:::my-bucket/*"
}
]
}A policy with only "Resource": "arn:aws:s3:::my-bucket/*" will fail on ListBucket. A policy with only "Resource": "arn:aws:s3:::my-bucket" will fail on GetObject and PutObject.
Block Public Access only affects access patterns that look public: “Principal”: ”*” without account restrictions. A properly scoped IAM policy that names your role ARN is not affected by Block Public Access. If your IAM policy is correct and the bucket has no explicit Deny, Block Public Access is not your problem.
If objects are encrypted with a customer-managed KMS key, the IAM policy alone is not enough. The role also needs kms:Decrypt in the KMS key policy. Run head-object to confirm the key ID, then review the key policy via aws kms get-key-policy.
See S3 IAM vs Bucket Policies for a detailed comparison of when to use each policy type.
How to fix cross-account S3 Access Denied
Cross-account S3 access requires explicit allows on both sides. Neither the bucket policy alone nor the IAM policy alone is sufficient.
Think of it like two locked doors. The bucket owner holds the key to the first door (bucket policy) and Account B holds the key to the second door (IAM policy). You need both keys to get through. Having only one key gets you nowhere.
Scenario: Account A owns the bucket. Account B has the role that needs access.
Bucket policy in Account A:
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::222233334444:role/reader-role"
},
"Action": ["s3:GetObject", "s3:ListBucket"],
"Resource": [
"arn:aws:s3:::my-shared-bucket",
"arn:aws:s3:::my-shared-bucket/*"
]
}]
}IAM policy on reader-role in Account B:
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:ListBucket"],
"Resource": [
"arn:aws:s3:::my-shared-bucket",
"arn:aws:s3:::my-shared-bucket/*"
]
}]
}Both must be present. If Account A’s bucket policy grants access but Account B’s role has no S3 IAM policy, the request is denied. If Account B has an IAM policy but Account A has no bucket policy, S3’s default denies the cross-account request regardless.
When Account B uploads an object to Account A’s bucket, Account B owns that object by default. Account A cannot read it even though it owns the bucket. Fix this one of two ways:
- Require
bucket-owner-full-controlACL on uploads: Account B includes—acl bucket-owner-full-controlin upload commands, which transfers full control to Account A. - Enable
BucketOwnerEnforced: Account A setsObjectOwnership=BucketOwnerEnforced. This disables ACLs and forces bucket-owner ownership for all objects regardless of who uploaded them.
For understanding how role assumption works in cross-account scenarios, see AWS STS and Temporary Credentials.
S3 Access Denied vs IAM AccessDenied
These are two different errors from two different authorization layers. Distinguishing them speeds up debugging significantly.
| Error type | Where it comes from | What details you get | How to debug |
|---|---|---|---|
IAM AccessDenied | IAM service evaluation layer, before the request reaches S3 | Error message names the action and ARN: “User is not authorized to perform s3:PutObject on resource …” | IAM Policy Simulator; CloudTrail errorCode=AccessDenied with errorMessage |
S3 Access Denied (403) | S3’s own evaluation: bucket policy, Block Public Access, ACL, object ownership | Generic “Access Denied” with no policy details | CloudTrail S3 data events; enhanced access denied messages for same-account requests |
KMS AccessDeniedException | KMS key authorization during SSE-KMS decrypt or encrypt | Named as kms:Decrypt or kms:GenerateDataKey not authorized | CloudTrail KMS events; check key policy and grants |
For guidance on IAM-layer permission errors that also affect S3, see Fixing IAM AccessDenied Errors. For how to read authorization failures in audit logs, see CloudTrail Log Types.
When to use this page
This page covers:
- AWS CLI, SDK, or application requests returning S3 Access Denied or HTTP 403
- Cross-account bucket access failures: works in one account but not another
- Public bucket or static website returning 403
- Presigned URL failures, including URLs that expire unexpectedly early
- CI/CD pipelines that can read from S3 but fail on writes
- CloudFront or application paths where S3 is the backing store
- VPC-internal workloads that cannot reach S3 despite having IAM permissions
If your error involves credential authentication failing (signature mismatch, token expired, bad access key), see Troubleshooting Authentication Errors.
Real scenario: CI/CD pipeline can read S3 but can’t write
A CI/CD pipeline reads build artifacts from S3 successfully but fails when uploading new artifacts:
An error occurred (AccessDenied) when calling the PutObject operation:
Access DeniedThe pipeline uses an IAM role cicd-pipeline-role.
Investigation:
Check the IAM policies on the role:
aws iam list-attached-role-policies --role-name cicd-pipeline-role
aws iam get-policy-version \
--policy-arn arn:aws:iam::111122223333:policy/cicd-s3-policy \
--version-id v1 \
--query 'PolicyVersion.Document'The policy returned:
{
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:ListBucket", "s3:GetBucketLocation"],
"Resource": [
"arn:aws:s3:::artifacts-bucket",
"arn:aws:s3:::artifacts-bucket/*"
]
}s3:PutObject is missing. The pipeline was originally built for read-only access and was later given write responsibility without the policy being updated.
Check the bucket policy for explicit denies:
aws s3api get-bucket-policy --bucket artifacts-bucketNo bucket policy exists (NoSuchBucketPolicy). This is a same-account bucket, so the IAM policy alone controls access.
Fix:
Update the IAM policy with two separate statements — bucket-level and object-level actions need different ARN forms:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["s3:ListBucket", "s3:GetBucketLocation"],
"Resource": "arn:aws:s3:::artifacts-bucket"
},
{
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:PutObject"],
"Resource": "arn:aws:s3:::artifacts-bucket/*"
}
]
}s3:ListBucket applies to the bucket ARN. s3:GetObject and s3:PutObject apply to the object ARN with /*.
Presigned URL Access Denied
Presigned URLs give time-limited access to S3 objects without requiring the requester to have AWS credentials. A 403 from a presigned URL usually has one of these causes.
Think of a presigned URL like a valet ticket. It’s only valid as long as the valet (the role session that signed the URL) is still on duty. If the valet clocks out early, the ticket becomes worthless even if it says it’s valid until midnight.
The URL has expired. Default expiry is 1 hour. Generate a new URL with a longer --expires-in value:
aws s3 presign s3://my-bucket/my-object --expires-in 86400The signing session expired before the URL expiry. If the URL was generated by an IAM role, the URL cannot outlive the role’s session token. IAM role sessions have a maximum duration of 12 hours. An --expires-in of 24 hours does not override a session that expires in 1 hour.
The signing credentials were revoked. If the IAM user or role that created the URL was deleted or had credentials rotated, the URL immediately returns 403 regardless of its stated expiry. See AWS STS and Temporary Credentials for session lifecycle details.
A bucket policy Deny was added after URL creation. An explicit Deny in the bucket policy overrides the presigned URL’s authorization.
Clock skew. AWS allows up to 15 minutes of clock skew between client and AWS. Beyond that, signature validation fails.
If you need presigned URLs that last longer than a role session, generate them using an IAM user’s long-term access keys rather than a role session token. The tradeoff is that long-term access keys carry more risk if they are leaked. For most use cases, regenerating presigned URLs on a schedule is safer than relying on long-term keys.
Common mistakes
- Confusing bucket ARN and object ARN:
s3:ListBucketneedsarn:aws:s3:::bucket-name.s3:GetObjectands3:PutObjectneedarn:aws:s3:::bucket-name/*. Using only one form causes silent failures for the other action type. - Assuming an IAM Allow overrides a bucket Deny: it does not. An explicit Deny anywhere (IAM policy, bucket policy, SCP) blocks the request regardless of any Allow present.
- Forgetting KMS permissions on SSE-KMS encrypted objects: S3 permissions are necessary but not sufficient. The caller also needs
kms:Decrypton the key. Runhead-objectfirst to confirm encryption before chasing IAM or bucket policy issues. - Ignoring object ownership on cross-account uploads: without
bucket-owner-full-controlorBucketOwnerEnforced, objects uploaded by an external account are not readable by the bucket owner. - Assuming presigned URLs outlive the session that created them: role session expiry caps the URL’s effective lifetime. Generate from long-lived credentials if you need long-lived URLs.
- Testing via the console but failing via the app role: console access often runs under a high-privilege admin role. Your app role may have much narrower permissions. Always simulate with the app role ARN, not personal credentials.
- Forgetting Requester Pays: Requester Pays buckets require
—request-payer requesterin CLI and SDK calls. Without it, the request returns 403 regardless of IAM permissions. - Forgetting VPC endpoint policy restrictions: a Gateway endpoint policy that allows only a specific bucket list will deny access to any other bucket, even from an EC2 instance with a fully permissive IAM policy.
Summary
- S3 403 errors don’t explain themselves. Use CloudTrail, the IAM Policy Simulator, and work through each policy layer systematically.
- Same-account: one Allow (IAM or bucket policy) is usually enough, if no Deny exists. Cross-account: both sides must explicitly Allow.
- Block Public Access blocks policies that grant broad or anonymous access, not IAM-authenticated requests with specific principals.
s3:ListBucketuses the bucket ARN;s3:GetObjectands3:PutObjectuse the object ARN with/*. Both are usually needed in the same policy.- SSE-KMS adds a second authorization layer. S3 permissions alone are not enough for KMS-encrypted objects.
- Presigned URL 403s are almost always expiration or session lifetime. Check the URL creation timestamp and the signing role’s session duration.
Frequently asked questions
Why do I get S3 Access Denied even though my IAM policy allows GetObject?
Several layers can deny access independently of your IAM policy. An explicit Deny in the bucket policy overrides any IAM Allow. Block Public Access blocks access patterns that look public even for IAM-authenticated users. For cross-account access, both the IAM policy and the bucket policy must allow the action. One side alone is not enough. KMS key permissions and VPC endpoint policies can also independently deny access.
Can Block Public Access cause 403 errors for my S3 static website?
Yes. Hosting a public static website requires s3:GetObject to be granted to "Principal": "*" in the bucket policy. Block Public Access settings BlockPublicPolicy and RestrictPublicBuckets will prevent this policy from applying. You must disable those two settings on the bucket to allow a public bucket policy for a static website.
Why does cross-account S3 access still fail after I added an IAM policy?
For cross-account S3 access, you need allows on both sides. The bucket owner account must have a bucket policy that explicitly allows the external principal. The requester account must have an IAM policy that allows the S3 actions on the bucket ARN and object ARN. If either side is missing, the request is denied. Adding an IAM policy in the requester account without a matching bucket policy in the bucket owner account is the most common mistake.
Can SSE-KMS cause S3 Access Denied?
Yes. When an object is encrypted with a KMS key (SSE-KMS), accessing it requires both S3 permissions and KMS key permissions. The requester needs kms:Decrypt for reads and kms:GenerateDataKey for writes. AWS-managed S3 keys (aws/s3) are automatically accessible to any principal in the same account with S3 permissions. Customer-managed keys require explicit permission in the KMS key policy.
Why does a presigned URL return 403 before its stated expiry time?
Presigned URLs are tied to the credentials of whoever created them. If the URL was created using an IAM role session, the URL stops working when that session expires, even if the URL expiry is set further in the future. IAM role sessions have a maximum duration of 12 hours. Other causes: the signing credentials were revoked, a bucket policy with an explicit Deny was added after the URL was created, or the client clock is more than 15 minutes out of sync with AWS time.