AWS Authentication: Principals in AWS IAM
Note: this article uses the boto3, the AWS Python SDK, as an example, but other SDKs have analogous features.
I’ve found that newcomers to AWS can sometimes get confused by what it means to have AWS credentials, and that people have notions of “logging into AWS” that don’t really correspond to the way AWS access works. This article aims to explain the basics of AWS authentication — that is, the way you gain an identity that you can use to access AWS services. It does not cover authorization, which is the granting of permissions to identity, that is, the domain of IAM policies, which is a separate topic.
All access to AWS is done by principals, which are either IAM Roles or IAM Users (which, despite the name, you should not use for humans). A principal gets credentials (access key id, secret access key, and for roles, session token). There is not a unique set of credentials for a principal; IAM Users can have two sets to allow for rotation (and can get short-lived credentials through STS.GetSessionToken), and IAM Roles only have short-lived credentials. Credentials are, when using boto3, represented by a Session.
Credentials can be located in a number of places. Environment variables,
~/.aws/credentials (for long-lived IAM User creds; credentials should not go in
~/.aws/config), a local metadata server on EC2, or even configured to be based on an AssumeRole call with other credentials. A boto3 Session that is created without specifying a config profile name will look in all these places to try to find credentials (if you give it the
profile_name keyword argument, it only looks in the config files). When you’re using an AWS compute service, you can configure it to assume a role for you, and make the credentials available to your code; this is why
session = boto3.Session() in a Lambda function (or EC2 instance, or ECS task, etc.) will get credentials for the role you’ve configured it to use.
An IAM principal has access granted to it by IAM policies. As stated above, this is authorization and is out of scope here, but it’s worth noting that the authorization model defines within-account access differently from cross-account access. The consequence here is that it is common to have identical or similar principals (e.g., IAM Roles) in multiple accounts, so that granting access in those accounts is simpler: the access is granted to the principal in the account, and the problem is shifted to becoming the right principal to access a given account, which is, in general, an easier problem to address.
IAM Roles require an outside source of identity; they are roles that another identity can assume. This can be an external identity provider, like Okta or ADFS. With AWS SSO, it’s the identity source for the AWS SSO instance (either an internal directory or an external provider). With a role assumed by a Lambda function, the source of identity is the Lambda service. But the source of identity can also be another role. That other role must itself have a source of identity, and so on. IAM Users, on the other hand, are their own identities. They have a username and password, which lets them be identities for humans, though AWS SSO is now the better way to provide that. We can therefore use IAM Users where there is no other source of identity available; primarily this is on-prem servers, or compute running in a different cloud provider.
An IAM Role has a trust policy that defines which identities/identity sources are allowed to assume it. The trust policy must grant permission for AssumeRole to succeed. They may trust an external identity provider (as a SAML provider, for STS.AssumeRoleWithSAML) to allow humans to assume them. AWS SSO roles specify the AWS SSO SAML provider, which allows humans to assume them, and this trust policy doesn’t depend on where the identity for AWS SSO is coming from (this is a benefit of AWS SSO). A role assumed by an IAM User or another IAM Role needs to specify either the other role itself, or the account that role is in. In fact, the permission needs to exist in both the trust policy on the role to be assumed, and the principal (Role/User) doing the assuming.
When granting cross-account access for role assumption, it’s generally the case that specifying the account in the trust policy is the right thing to do. The security boundary is the account; the trust policy has no control over who/what can log in as the user/assume the role in the remote account, so specifying that principal gives a bit of a false sense of added security over trusting the entire account.
AWS SSO adds a layer on top of this; when you log in to AWS SSO, you do not become an IAM principal. You are authenticated as an AWS SSO user, and this AWS SSO user has permission to assume IAM principals (IAM Roles, in particular) in AWS accounts. If you are assigned a permission set named Developer in a particular account, AWS SSO makes sure there’s an IAM Role in that account named after the permission set (it’ll be named
AWSReservedSSO_Developer_<random-tag>), and grants you permission to retrieve credentials for that IAM Role. Note this isn’t through STS.AssumeRole(WithSAML) (that’s called by AWS SSO internally), it’s through SSO.GetRoleCredentials, which takes a credential that represents your AWS SSO user, and returns the familiar AWS credentials that represent the IAM Role you’re requesting.
Hopefully this helps illuminate how access to AWS services works in terms of authentication. It’s not simple, and there are a lot of little details that can remain confusing. If you’ve got questions, drop me a line on Twitter.
If you’ve wondered what exactly the access key id, secret access key, and session token do, they are used to sign the HTTP requests you make to AWS services, verifying the principal accessing the service.
Many cloud services (e.g., GCP) make use of a bearer token for authentication, where your entire credential is this token, and it is sent in its entirety to the service. While in theory this lets an attack snoop on your request, steal your token, and use it to access services, HTTPS goes a long ways towards preventing this. But it does mean that you have a higher exposure, are subject to replay attacks, etc. versus a secret that is not sent out.
With AWS, you have a secret (the secret access key) that never gets included in the request. Instead, you use it to create a cryptographic signature for the request, and it is this signature that is included in the request. AWS verifies the signature to check that you are indeed in possession of the secret key (and recently in possession, as there’s a timestamp involved).
One of the advantages of this is that it enables pre-signed URLs; the signature gets embedded as query parameters, and because the signature is specific to the exact API call (including parameters), and time-limited, the URL is something that can be shared without sharing the actual access granted to the credentials themselves. There aren’t really standardized mechanisms to allow this with bearer tokens.
While SigV4 signing is embedded in all the AWS SDKs, it is not always exposed in a way that can be used independently. This is a problem as using IAM authentication for API Gateway requires SigV4 signatures on requests that can’t be made through the SDK. This leads to third party implementations like aws-requests-auth for Python.