Have you ever read something like this in the AWS documentation?
“When you attach policy to a user, the user is the implicit principal. When you attach a permission policy to an IAM role, the principal identified in the role’s trust policy gets the permissions.”
I spent a while trying to wrap my mind around offhand statements like that, with a lot of guesswork and without much luck.
First off, IAM stands for Identity and Access Management.
It turns out, Amazon has one decent overview of IAM. That page is talking about it within the context of AWS Lambda, but it introduces some of the concepts. Still, it starts a bit in the weeds. Let me try to take it from the top.
What is IAM?
When you interact with Amazon Web Services, either as a developer building AWS apps, or an end user whose app is running in or calling into AWS, you need permission. Amazon largely defaults to giving you no permission, except for the account you used to sign up for AWS in the first place.
So, let’s say I signed up for AWS as
- I can log in to the AWS console as a developer, using that login, and set up some services.
- I can configure that e-mail address and the associated password (or token, which we’ll get to later) for the AWS command-line tools. That is, I can run a terminal command to (let’s say) deploy a new AWS Lambda function, and it will authenticate to AWS as
email@example.com order to deploy the Lambda to my account.
- I can run a Web app or other client that calls the Lambda function, so long as it logs in to Amazon as
firstname.lastname@example.org. If I want to have different users in the app, I can implement my own user management however I want, but all users would still need to use
email@example.com authenticate to AWS in order to call the Lambda function. If I want to talk to DynamoDB, same story. Out of the box, only
firstname.lastname@example.org do it.
OK, so this works… sort of. It’s especially lousy for a Web app, because nobody else knows the password or has the token for
email@example.com, so I have to hardcode it into the app. And anybody can inspect the code. Doesn’t seem likely that my Amazon account will stay mine for long…
The more you think about it, the more these problems multiply. I can hardcode my account and password in my app in order to call a Lambda and to call DynamoDB, but what if the Lambda wants to call DynamoDB? Then do I hardcode it into the Lambda function too? Oh, and the Lambda wants to log to CloudWatch, and so on.
These are the kind of problems that IAM is intended to solve. It has items that help with all of these:
- Users — Within your Amazon account, you can define additional users. Then you can grant them certain permissions and so on. For instance, I can create a new user who can publish Lambda functions. Then when I want to use a command-line tool to publish a Lambda, I’ll use the name and password or token for that user. If it’s somehow compromised, I can just shut it off. I can also lock it down to only certain tasks, such as deploying Lambdas, so someone who took it over couldn’t set up other Amazon services. These users are still within my Amazon account — they don’t have a full account of their own. I could create IAM users for other developers on my team, so we could all work within a single account, rather than each requiring our own. Users can get their permissions by belonging to Groups, for ease of management.
- Roles — When one Amazon service needs to access other Amazon services, it can assume a Role. That is, it’s not a user with a specific name and password. It’s just a group of permissions. So if a Lambda function wants to write to a DynamoDB database and CloudWatch logs, you can assign it a role that grants those permissions. When the Lambda function executes, it will have the permissions associated with that role. If somehow someone manipulates the Lambda function into trying to create a new EC2 instance, it won’t work, because the role doesn’t grant that permission.
- Policies — A policy is a group of permissions. So you might have a “ReadDynamoDB” policy that allows gets, queries, and scans for all DynamoDB tables (or just one). You might have a “WriteCloudWatch” policy that allows creating log streams and writing logs to all log streams (or just one). You might then set up a “RunMyLambda” role, which has both the ReadDynamoDB policy and the WriteCloudWatch policy, and then if you assign that role to a Lambda function, it could both read from DynamoDB and write to CloudWatch. (But not create EC2 instances.)
- AWS-managed Policies vs. Customer-managed Policies — AWS includes a long list of policies by default. There is, for instance,
AmazonDynamoDBReadOnlyAccess(can read table data as well as DynamoDB configuration data, logs, etc.), and many more. Those are AWS-managed Policies. Generally speaking, I have not found them especially useful. For instance, there’s not a DynamoDB policy that allows you to read and write table data without also being able to reconfigure the database. So, you can create your own policies (Customer-managed policies). That’s what I mostly do, except it’s a bit arcane. The main problem is discovering exactly what permissions you need to include in the policy — for instance, which specific permissions are required to allow read and write access to table data in DynamoDB? It turns out there are about 7, but you have to pick them out of a big list of all available DynamoDB permissions.
Cognito and other ID Providers
AWS has another service that deals with users and accounts, and that’s Cognito. It’s very different, though. The goal of Cognito is to handle user-management within an application: signing up users, logging users in and out, giving users tokens they can use to authenticate to AWS, etc.
It sounds similar, except at the end of the day, a Cognito user has no AWS permissions. You can call into the Amazon API Gateway, for instance, and provide a Cognito token. The API Gateway can validate the token, proving that you are who you say you are. But you still don’t have permissions to call a Lambda running behind the API Gateway.
There are two ways to handle that:
- The API Gateway can grant the same IAM permissions to every valid Cognito user. If you have a valid Cognito token, you get to call the Lambda function behind the API Gateway. If not, you don’t. (Cognito is not the only way to authenticate to the API Gateway, for what it’s worth.)
- You can use Federated Identities. With Cognito, or Facebook, or Google, or other identity providers, IAM can map a valid account from there into a particular IAM role. So, for instance, maybe
firstname.lastname@example.org map to the “BigAdministrator” IAM role, while
email@example.com map to the “JustRunALambdaFunction” role. This becomes a bit of a rabbit hole — you can use SAML or OpenID Connect providers, you can authenticate on the client side or server side, you can use federated identities within IAM or within Cognito, the billing scheme may differ depending on your decisions, and etc.
Due to the complexity, I have tried to avoid federated identities, though of course in some cases it’s required.
Passwords and Tokens
You can have passwords at several levels — your master AWS account, IAM users within the account, Cognito users in a particular Cognito pool, and etc. Generally, though, Amazon tries to avoid sending password over the wire.
For instance, when you create an IAM user, it generates a “secret access key” that you can use instead of a password to authenticate as that user. For instance, you could put that in a configuration file to run command-line AWS tools as that user. The user may or may not also have a password to log in to the AWS console.
When you log in a Cognito user, Cognito responds with several JSON Web Token tokens. When you want to call AWS services, you don’t send your password again, instead you use one of the tokens to authenticate.
Therefore, when interacting with the AWS API, you’ll often need something other than a password, and as long as you authenticate successfully, ultimately the execution path will probably end up assuming an IAM role.