Write a Simple IAM Policy

IAM policies can get very complex, but today we will start with basics.

Prerequisites

The Lesson

In previous IAM labs we created an IAM user and an IAM role. The user is an identity, and the role is a persona. They are two of the three most important IAM primitives in AWS. And as you sit there counting fingers, that means we have one more to cover.

In both cases we pre-assigned permissions. For the IAM user it was an AdministratorAccess policy, and for the role it was the SSMManagedInstanceCore policy. We briefly glanced at the permissions but didn’t spend much time on them.

This week we will write our own IAM policy covering the core elements. AWS IAM policies are where any of you who actually work in AWS for a living will spend a large percentage of your time, for better or worse. They are incredibly granular and powerful, but also very easy to mess up. They also, of course, have nuances which can lead to non-obvious privilege escalation.

SPONSOR SHOUTOUT! Sonrai Security

✍️The Best IAM Policy is One You Don’t Have to Write

Writing IAM policies can be a breeze in the beginning.

But once you have thousands of identities and multiple development teams, it’s easy to lose control.  

That’s where the Cloud Permissions Firewall comes in. 

Rather than writing individual policies for every identity in your cloud, we give you one global policy that removes excess sensitive permissions from every human and machine identity. 

Zombie (unused) identities are quarantined from use, and unused regions and cloud services are disabled, leaving them useless to attackers.

Even better, the policies are automatically updated when identities need new access, granted through a simple ChatOps approval workflow.

There is absolutely no way we can cover all of IAM policies in a 15-minute lab, so this week we will focus on core concepts and capabilities.

Since I’ve been using these labs to cover general security rather than just AWS, let’s add one more IAM definition to the mix:

  • Entitlement: Mapping of an identity (or persona) to an authorization/permission.

An IAM permissions policy is a list of actions which are allowed or denied. We attach a policy to an IAM user or role, and it becomes an entitlement specifying what they can do.

In the information security world we use the terms authentication and authorization to describe the two components of an identity and access management system.

  • Authentication: Logging in. Proving you are who you say you are. Security nerds call this AuthN just to be annoying.

  • Authorization: Taking an action. Being allowed to do something. Security nerds call this AuthZ to make sure they are never invited back to any parties.

A Few AWS IAM Policy Fundamentals

AWS has the single most mature permissions system I have ever seen on any platform. I started using AWS before IAM was available to customers, and it’s been wild watching it evolve. With great power comes great complexity, and there are a lot of layers to peel back as we progress, but today we will limit ourselves to basics.

Our scope today will be identity based policies. These are policies which attach to an IAM user, group, or role; they grant or deny permissions. AWS also supports other policy types, including resource based policies, which we will get into later. Identity based policies allows users and roles to do things.

There are two subtypes of identity based policies:

  • Inline policies attach directly to the user/group/role and only work for the thing they are attached to. You write them for that user and cannot reuse them anyplace else. These were horrific to manage at scale, so AWS added…

  • Managed policies are central policies you write once and can attach wherever you want. If you update a managed policy, that change applies instantly to everything using it. There are two types of managed policies:

    • AWS managed policies are standard ones AWS writes and maintains, which customers can use.

    • Customer managed policies are ones you write yourself, which only exist in your AWS Account.

Policies are written in JSON (JavaScript Object Notation), a relatively human-readable format (for certain values of ‘human’). Like most computer stuff, JSON is strict about syntax and structure, so you’ll find a JSON checker helpful at times (narrator: at all times).

The AWS console has a visual policy editor, or you can write raw JSON. I teach both but have done this enough that I hand-write most of my policies.

Well, that’s a half-lie. I usually start with either an AWS managed policy or one I’ve used before, then edit it. Sometimes I even use ChatGPT, and then assume Skynet has backdoor access to all my AWS accounts. I, for one, welcome our new AI overlords.

In future labs we will go over how AWS evaluates all the different policies, because there are important real-world implications. Today the most important things to know are:

  • IAM is default deny. If you don’t attach any policies, the principal can’t do anything.

  • You can attach multiple policies to any IAM principal.

  • The total set of what you can do is then a logical combination, prioritizing deny. That means you can do everything which a) you are allowed and b) not denied. This means a deny statement in any attached policy overrides any matching allow statements.

    • Learn it, know it, love it: a single deny wins over all allows.

  • Policies support “*” wildcards. This is one of those power/responsibility things, because we need them in some places, but they can get you in the kinds of headlines PR and legal teams really hate.

As I mentioned, there are other policies which interact with identity policies, but the rule of logical combination with deny prioritized applies across the whole spectrum.

Simple Policy Walkthrough

Here is what an identity policy looks like. This is a version of the policy we will use today:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "s3:GetObject",
                "s3:PutObject"
            ],
            "Resource": "arn:aws:s3:::rmogull-slaw/*"
        }
    ]
}

This is a pretty minimal policy with just the core elements.

  • Version: This defines the policy language version. "2012-10-17" is the current version; this is required.

  • Statement: This is the main element of the policy. It’s an array of multiple policy statements. (Anything in [] is an array). Each statement must have an effect, action, and resource.

  • Effect: Whether the statement results in an allow or a deny.

  • Action: Lists the specific actions (API calls) allowed or denied. In this policy:

    • "s3:GetObject": Grants permission to retrieve objects from the specified bucket.

    • "s3:PutObject": Allows the user to upload or modify objects in the bucket.

  • Resource: Specifies the resources the statement covers.

    • Here, "arn:aws:s3:::rmogull-slaw/*" means the policy applies to all objects (/*) in the rmogull-slaw bucket.

    • An ARN (Amazon Resource Name) uniquely identifies AWS resources. In this case it specifies my S3 bucket.

Every IAM policy starts with these bits. The version, at least one statement, the effect (either allow or deny), what action is allowed or denied, and what resources are in scope.

If you haven’t worked with JSON before, here’s a concise summary of the syntax from ChatGPT:

JSON (JavaScript Object Notation) is a lightweight data-interchange format, easily readable by humans and parsable by machines. Here are its basic syntax rules:

  • Data is in Name/Value Pairs: A field name (in double quotes) followed by a colon and a value, e.g., "name": "Rich".

  • Data Types: JSON supports strings, numbers, objects (JSON object), arrays, booleans (true/false), and null.

  • Objects: Enclosed in curly braces {}, containing a series of name/value pairs, e.g., {"firstName": "Rich", "lastName": "Mogull"}.

  • Arrays: A list of values enclosed in square brackets [], e.g., ["AWS", "Cloud Security", "IAM"].

  • String: Text enclosed in double quotes, e.g., "example string".

  • Number: Integer or floating point, e.g., 25 or 3.14.

  • Boolean: true or false.

  • Null: Represents a null value.

It’s key to remember that JSON files must have text only, with no functions or comments. JSON is commonly used for transmitting data in web applications and for configuration files.

Today’s Key Points:

  • IAM policies are the heart of AWS security. They define who or what is allowed to perform which actions.

    • There are a lot of different policies. Today’s lab focuses on identity based policies, the ones we apply to users and roles.

  • The permission is called an authorization (AuthZ), and assigning a permission to a principal (like a user) is called an entitlement.

  • AWS IAM policies are a logical summation of all applied policies, where any deny statement overrides all allow statements. Even though you are logged in as a full admin user, if you attached a second policy with “deny RunInstance” you wouldn’t be able to run an instance.

  • Policies are written in JSON with a strict syntax.

The Lab

We will use the visual policy editor to write an IAM policy, then come back and review its JSON. Our policy will allow a user access to read our CloudTrail logs from S3. Steps:

  • Get the ARN of our CloudTrail bucket in S3.

  • Write a customer managed policy with the visual policy editor in the console.

  • Review the JSON.

In a future lab we will show you how to test the policy, along with policy combinations.

Tip: The visual policy editor can help you figure out which actions you need, but over time you will want to use the API documentation, which will give you a better idea of which actions need to be combined to achieve your goal. Personally I use the command line interface or Python coding documentation more.

I also will sometimes start with an AWS managed policy and then adjust it to my requirements. AWS managed policies nearly always have too many permissions because AWS doesn't know what resources to list… that’s all on you to copy/paste and adjust!

Video Walkthrough

Step by Step

Today we will write a policy that allows read or write access to a specific S3 bucket. S3 is object storage, a place to put files, and we keep them in things called buckets. To write the policy we will need the ARN (Amazon Resource Name) of the bucket, so this lab starts by finding that.

First, got to https://console.aws.amazon.com and sign in. Then go to S3.

You will have 1 (possibly 2) buckets. Pick the one with CloudTrail in the name and click it. I have 2 because I previously clicked something in CloudFormation that we aren’t using. You probably only have the one bucket (I’ll delete the extra later, but it is a good example of how accounts won’t always look exactly the same).

Then click Properties. This will pull up a bunch of info, including the ARN:

Now click aws in the upper-left and OPEN A NEW BROWSER TAB!!! We will return to this tab to grab the ARN a bit later.

In the new tab, go to IAM:

On the left side, click Policies:

Here you’ll see the 1169 AWS Managed policies, but we will create our own. Click Create policy:

This brings us to the Visual Policy Editor. Personally I work mostly with JSON, usually in a text editor outside the console, but the visual editor is a great place to start and see how a policy is built, so that’s what we will use today.

Our first step is to pick the service we want to write the statement for. Any policy can have multiple statements, up to the policy size limit, which is based on the number of characters. A managed policy has a limit of 6,144 characters.

Select S3:

This will “open up” the rest of the page, and we can start building our policy. We will write one which allows read and write access to objects in our bucket. One interesting tidbit is that it won’t allow listing the contents of the bucket, so to read an object the policy user needs to know exactly which object to look at.

Picking the service starts to fill in the “action” part of the statement. Actions are the actual API calls to allow or deny. The two we will use today are GetObject, which allows you to read a file, and PutObject, which allows you to write a file. The Read part of this policy is relatively normal for a CloudTrail bucket, but you won’t normally want to allow the Write policy. We won’t attach this policy to any principle so we will not actually creating an entitlement, and we will tighten up the policy in a future lab (soon). If you click on the arrow by List, your screen should look like this. Don’t click anything else yet, just notice the highlighted parts: the Effect, and how the permissions/API calls show.

Click Read and select GetObject. As a reminder, we will create a version of the policy I pasted at the top of this post. The moment you click that box it’s added to your policy.

Personally I find hunting through those lists to be laborious. Also, although it lists all the API calls, it doesn’t tell me which I want. This is a good place to start this early in our educational journey, but this will be one of the last times we use this part of the user interface. However, if you already have an idea of the action you want, you can use the Filter Actions box. Once you start typing something, it will pull up all matching actions for that service.

Let’s type in “putob”, since we already know we want PutObject, watch as it pops right up, then select it.

So far we have our Effect and our Action (well, comma-separated actions, but the policy itself uses the singular). Now we need to add our Resource. By default the visual editor pushes you to only add specific resources, not All, and shows a warning. Click Add ARN to pull up a modal where we can paste in our ARN.

Now swap to your other browser tab to get your S3 bucket ARN. Click on the little copy icon to pull the ARN.

Now swap back to the IAM tab and paste in the ARN (yes, you could also use any of the other fields, but I’m a creature of habit, and this is how I like to do it):

Oops! We have an error! And yeah, forcing you to make this error was deliberate. Here’s what’s going on: we specified 2 API calls that work on Objects (files). GetObject and PutObject, but we specified a bucket, rather than an object, as the resource! This produced an error, because a bucket isn’t an object (a directory is not a file). The fix? just add “/*” to the end of the path — this means “all objects in this bucket”. (Down the road we’ll play with paths in buckets, which is exactly like a directory path, with ‘/’ and everything.)

Scroll back up and click JSON, and this looks almost identical to my example up in the Lesson part of this post:

Cool, huh? Relatively recently they added pick lists to the JSON box, so you can search for the elements you want and build out policies there, instead of using the drop-down and checkboxes we just worked with.

Scroll down and click Next:

Give it the name CloudtrailReadWrite and then a really good description. This is critical because the next person to see this policy might not recognize a policy named “asdfTest” with a description of “asdf”. Which, looks suspiciously like one of my GitHub commit comments.

Go with “Allow read and write access to our main CloudTrail bucket.” That’s a good one. Then click Create policy.

Now your policy will show up quickly and easily under the Filter by Type dropdown if you select Customer managed.

Key Points

  • The visual policy editor is a great tool for helping learn how to build IAM policies.

  • Always review the JSON so you don’t miss anything.

  • Be very careful with wildcards, especially in the Resource section.

  • AWS managed policies can be a good start, but since they never have resources restrictions, you will want to copy and scope them down before use.

-Rich

Reply

or to participate.