I had a group with read-only S3 access and needed to add upload permissions without opening the door to every bucket in the account. Here's how I built the policy, and the two real errors — a bad statement name and a bucket-vs-object ARN mixup — that I had to work through before the upload finally succeeded.
Starting from read-only, and proving the gap
Before I touch a policy, I like to confirm the actual state of things rather than assume. The group I was working with — call it my S3 partial access group — only had read permissions. Members could list buckets and view objects, but nothing more. So the first thing I did was try to upload a file as a user in that group, fully expecting it to fail.
It did, and that's exactly the point. Watching the failure happen live, rather than just reading about what read-only access means, makes the fix that follows much more concrete. It also gives you a clean baseline: if something goes wrong later, you know it's not a leftover permission from somewhere else. You're starting from zero write access and building up from there.
This is a habit worth keeping even when you're confident you know the current state of a group's permissions. IAM policies accumulate over time, especially in shared accounts, and a quick failed-request check costs you thirty seconds but saves you from debugging the wrong problem later.
Finding the right group in IAM
With the failure confirmed, I headed into the IAM console. I used the console search rather than clicking through the sidebar — it's faster once you know roughly what you're looking for, and IAM's navigation has enough nested tabs that search saves real time.
I located the group in question and opened its Permissions tab to see what was actually attached. This step matters more than it sounds like it should. It's easy to assume a group has exactly the policy you remember giving it, but the Permissions tab is the source of truth. In this case, it confirmed what the failed upload had already told me: a single read-only policy, nothing else. That gave me a clean starting point for adding a new, narrowly scoped permission rather than editing an existing policy and risking unintended side effects on other permissions the group might need.
Writing the inline policy in JSON, not the visual editor
AWS gives you two ways to build a policy in the console: the visual editor, which walks you through service, actions, and resources with dropdowns, and the JSON editor, where you write the policy document directly. I went straight to JSON. The visual editor is fine for very simple grants, but as soon as you're being precise about resource ARNs and actions, it turns into a lot of clicking for something that's genuinely faster to type out.
The permission I needed was s3:PutObject, scoped to a specific bucket rather than every bucket in the account. I pulled the bucket's ARN from its Properties page in the S3 console and used that as the resource in the new statement. This is the part worth dwelling on: it would have been faster to just grant s3:PutObject on a wildcard resource, or a wildcard resource across all of S3, and call it done. But that's exactly the kind of shortcut that turns into a security problem later — a user or a compromised credential with write access to every bucket in the account, when they only ever needed to write to one.
Scoping the resource to a single bucket ARN takes maybe an extra ten seconds of copy-pasting. That's a trivial cost for meaningfully reducing what the policy can do if something goes wrong. I'd treat this as close to a default habit: write to the specific ARN first, and only broaden it if there's a real, articulated reason to.
A hyphen breaks the policy
Once I had the action and resource in place, I gave the statement a descriptive Sid — something like "S3 upload tutorial repo demo" — and named the policy itself. Saving threw a syntax error.
The cause turned out to be the hyphen I'd used in the statement name. IAM's policy validation rejected it outright. I hadn't run into this constraint before, and it's the kind of thing that's easy to miss because hyphens are so normal everywhere else in AWS naming — bucket names, resource tags, CLI flags. Statement IDs apparently don't follow the same rules. I want to flag this as something to double-check against AWS's current documentation before you treat it as a hard rule for every policy type going forward, since naming constraints like this can be specific to context and are worth confirming rather than taking on faith. But in this case, removing the hyphen and renaming the statement to something without special characters cleared the error immediately, and the policy — renamed to something simpler — saved without further complaint.
It's a small thing, but it's a good reminder that policy documents have their own syntax rules that don't always mirror the naming conventions you're used to elsewhere in AWS. If you hit an unexplained save failure, check the Sid before you go looking for anything more complicated.
Confirming inheritance, then failing again
With the policy actually saved and attached to the group, I went to the IAM user's own page to confirm the new permission was showing up in their effective permissions. It was — the user inherited the group's policy as expected, alongside the original read-only grant.
So I went back and retried the upload. It failed again.
This is the point where it would be tempting to assume something was cached, or that IAM just needed a minute to propagate, and to retry a few times hoping it resolves itself. I didn't want to do that. If a permission genuinely isn't working, retrying blindly just burns time and teaches you nothing. So instead of assuming, I went back into the policy and the error details to figure out what was actually happening.
Bucket ARN versus object ARN
The answer was in the resource ARN I'd used. I had granted s3:PutObject against the bucket's ARN directly — something in the form of the bucket name only, with no path underneath it. That ARN refers to the bucket as a resource, not to the objects that live inside it. And PutObject is fundamentally an object-level action: you're not writing to the bucket itself, you're writing to a key inside it.
AWS treats these as genuinely distinct resources for permission purposes, even though they look like the same thing when you're clicking around the console. Granting access to the bucket ARN gets you actions like ListBucket or GetBucketLocation — things that operate on the bucket as a whole. It does not get you PutObject, GetObject, or DeleteObject, because those need permission on the objects, which live one level down in the ARN hierarchy. This lines up with how AWS's S3 policy documentation generally describes bucket versus object permissions, though it's worth checking the current docs directly if you're writing a policy for anything beyond a quick fix, since resource-level nuances like this are exactly the kind of thing that's easy to get subtly wrong.
The fix was to append a wildcard to the ARN — turning the bucket-level reference into one that covers every object inside the bucket. That one change, adding the trailing wildcard segment, was the entire difference between a policy that looked correct and one that actually worked. I edited the resource line, saved the policy, and retried the upload. This time it went through cleanly.
Conclusion
Two failures, two fixes, and the upload finally worked — but neither failure was a fluke. A hyphen in a statement name is an easy thing to trip over the first time you write a policy by hand, and the bucket-versus-object ARN distinction is one of the most common ways people accidentally lock themselves out of S3 write access even when the action itself looks right. If you're maintaining IAM policies of your own, it's worth pulling up your existing S3 grants and checking whether any bucket-level ARNs are missing that trailing wildcard where object-level actions are involved — it's a quick scan that can save you the same round of trial and error.
Official AWS references
KEEP PRACTICING THE DECISION
Learn the concept, then test the edge case.
The Solutions Architect Associate course in TutorialRepo combines guided lessons, scenario questions, review, and course-aware explanations.