Identity is everywhere, whether we're talking about your GitHub id, your Twitter handle, or your email address. A strong notion of identity is important in information systems, particularly where security and compliance is involved, and a good identity system supports access control, trust delegation, and audit trail.
AWS provides a number of services for managing identity, and today we'll be looking at their main service in this area: IAM - Identity and Access Management.
Let's take a look at the building blocks that IAM provides.
First of all, there's the root user. This is how you'll log in when
you've first set up your AWS account. This identity is permitted to do anything
and everything to any resource you create in that account, and - like the unix
root
user - you should really avoid using it for day to day work.
As well as the root user, IAM supports other users. These are separate identities which will typically be used by the people in your organisation. Ideally, you'll have just one user per person; and only one person will have access to that user's credentials - sharing usernames and passwords is bad form. Users can have permissions granted to them by the use of policies.
Policies are JSON documents which, when attached to another entity, dictate what those entities are allowed to do.
Just like a unix system, we also have groups. Groups pull together lists of users, and any policies applied to the group are available to the members.
IAM also provides roles. In the standard AWS icon set, an IAM Role is represented as a hard hat. This is fairly appropriate, since other entities can "wear" a role, a little like putting on a hat. You can't log directly into a role - they can't have passwords - but users and instances can assume a role, and when they do so, the policies assoicated with that role dictate what they're allowed to do.
Finally we have tokens. These are sets of credentials you can hold, either permanent or temporary. If you have a token you can present these to API calls to prove to them who you are.
IAM works across regions, so any IAM entity you create is available everywhere. Unlike other AWS services, IAM itself doesn't cost anything - though obviously anything created in your account by an IAM user will incur costs in the same way as if you've done it yourself.
In a typical example we may have three members of staff: Alice, Bob and Carla.
Alice is the person who runs the AWS account, and to stop her using the root
account for day to day work, she can create herself an IAM user, and assign it
one of the default IAM Policies: AdministratorAccess
.
As we said earlier, IAM Policies are JSON documents. The AdministratorAccess
policy looks like this:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "*",
"Resource": "*"
}
]
}
The Version
number here establishes which version of the JSON policy schema we're
using and this will likely be the same across all of your policies. For the
purpose of this discussion it can be ignored.
The Statement
list is the interesting bit: here, we're saying that anyone
using this policy is permitted to call any Action
(the *
is a wildcard
match), on any Resource
. Essentially this holder of this policy has the same
level of access as the root account, which is what Alice wants, because she's
in charge.
Bob and Carla are part of Alice's team. We want them to be able to make changes
to most of the AWS account, but we don't want to let them manipulate users -
otherwise they might disable Alice's account, and she doesn't want that! We
can create a group called PowerUsers
to put Bob and Carla in, and assign
another default policy to that group, PowerUserAccess
, which looks like this:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"NotAction": "iam:*",
"Resource": "*"
}
]
}
Here you can see that we're using a NotAction
match in the Statement
list.
We're saying that users with this policy are allowed to access all actions that
don't match the iam:*
wildcard. When we give this policy to Bob and Carla,
they're no longer able to maniuplate users with IAM, either in the console, on
the CLI or via API calls.
This, though, presents a problem. Now Bob and Carla can't make changes to their own users either! They won't be able to change their passwords, for a start, which isn't great news.
So, we want to allow PowerUser
s to perform certain IAM activities, but only
on their own users - we shouldn't let Bob change Carla's password. IAM provides
us with a way to do that. See, for example, this ManageOwnCredentials
policy:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"iam:*LoginProfile",
"iam:*AccessKey*",
"iam:*SSHPublicKey*"
],
"Resource": "arn:aws:iam::<account number>:user/${aws:username}"
},
{
"Effect": "Allow",
"Action": [
"iam:ListAccount*",
"iam:GetAccountSummary",
"iam:GetAccountPasswordPolicy",
"iam:ListUsers"
],
"Resource": "*"
}
]
}
The important part of this policy is the ${aws:username}
variable expansion.
This is expanded when the policy is evaluated, so when Bob is making calls
against the IAM service, that variable is expanded to bob
.
There's a great set of example policies for administering IAM resources in the IAM docs, and these cover a number of other useful scenarios.
You can increase the level of security in your IAM accounts by requiring users to make use of a multi-factor authentication token.
Your password is something that you know. An MFA token adds a possession factor: it's something that you have. You're then only granted access to the system when both of these factors are present.
If someone finds out your password, but they don't have access to your MFA token, they still won't be able to get into the system.
There are instructions on how to set up MFA tokens in the IAM documentaion. For most types of user, a "virtual token" such as the Google Authenticator app is sufficient.
Once this is set up, we can prevent non-MFA users from accessing certain policies by adding this condition to IAM policy statements:
"Condition": {
"Bool": {
"aws:MultiFactorAuthPresent": "true"
}
}
As an aside, several other services permit the use of MFA tokens (they may refer to it as 2FA) - enabling MFA where available is a good practise to get into. I use it with my Google accounts, with Github, Slack, and Dropbox.
If your app needs to write to an S3 bucket, or use DynamoDB, or otherwise make AWS API calls, you may have AWS access credentials hard-coded in your application config. There is a better way!
In the Roles section of the IAM console, you can create a new AWS Service Role, and choose the "Amazon EC2" type. On creation of that role, you can attach policy documents to it, and define what that role is allowed to do.
As a real life example, we host application artefacts as package repositories in an S3 bucket. We want our EC2 instances to be able to install these packages, and so we create a policy which allows our instances read-only access to our S3 bucket.
When we create new EC2 instances, we can attach our new role to it. Code running on the instance can then request temporary tokens associated with the new server role.
These tokens are served by the Instance Metadata Service. They can be used to call actions on AWS resources as dictated by the policies attached to the role.
[ DIAGRAM HERE ]
The diagram shows the flow of requests. At step 1, the application connects to the instance metadata service with a request to assume a role. In step 2, the metadata service returns a temporary access token back to the application. In step 3, the application connects to S3 using that token.
The offical AWS SDKs are all capable of obtaining credentials from the Metadata Service without you needing to worry about it. Refer to the documentation for details.
The benefit of this approach is that if your application is compromised and your AWS tokens leak out, these can only be used for a short amount of time before they'll expire, reducing the amount of damage that can be caused in this scenario. With hard-coded credentials you'd have to rotate these yourself.
One other use of roles is really useful if you use multiple AWS accounts. It's considered best practise to use separate AWS accounts for different environments (eg. live and test). In our consultancy work, we work with a number of customers, who each have four or more accounts, so this is invaluable to us.
In our main account (in this example, account ID 00001), we create a group for
our users who are allowed to access customer accounts. We create a policy
for that group, AssumeRoleCustomer
, that looks like this:
# AssumeRoleCustomer
{
"Version": "2012-10-17",
"Statement": {
"Effect": "Allow",
"Action": "sts:AssumeRole",
"Resource":
”arn:aws:iam::00005:role/ScaleFactoryUser"
}
}
In this example, our customer's account is ID 00005, and they have a role in that
account called ScaleFactoryUser
. This AssumeRoleCustomer
policy permits
our users to call sts:AssumeRole
to take on the ScaleFactoryUser
role in
the customer's account.
sts:AssumeRole
is an API call which will return a temporary token for the
role specified in the resource, which we can then use in order to behave as
that role.
Of course, the other account (00005) also needs a policy to allow this, and so we set up a Trust Relationship Policy:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::00001:root"
},
"Action": "sts:AssumeRole",
"Condition": {
"Bool": {
"aws:MultiFactorAuthPresent": "true"
}
}
}
]
}
This policy allows any entity in the 00001 account to call sts:AssumeRole
in
our account, as long as it is using an MFA token (remember we saw that
conditional in the earlier example?).
Having set that up, we can now log into our main account in the web console,
click our username in the top right of the console and choose "Switch Role". By
filling in the account number of the other account (0005
), and the name of the
role we want to assume (ScaleFactoryUser
) the web console calls
sts:AssumeRole
in the background, and uses that to start accessing the
customer account.
Role assumption doesn't have to be cross-account, by the way. You can also
allow users to assume roles in the same account - and this can be used to allow
unprivileged users occasional access to superuser privileges, in the same way
you might use sudo
on a unix system.
When we're talking about identity, It's important to know the difference between the two "auth"s: authentication and authorization.
Authentication is used to establish who you are. So, when we use a username and password (and optionally an MFA token) to connect to the web console, that's authentication at work.
This is distinct from Authorization which is used to establish what you can do. IAM policies control this.
In IAM, these two concepts are separate. It is possible to configure an Identity Provider (IdP) which is external to IAM, and use that for authentication. Users authenticated against the external IdP can then be assigned IAM roles which control the authentication part of the story.
IdPs can be either SAML or using OpenID Connect. Google Apps (or are we calling
it G-Suite now?) can be set up as a SAML provider, and I followed this blog post with some success.
I can now jump straight from my Google account into my AWS console, taking on a
role I've called GoogleSSO
, without having to give any other credentials.
I hope I've given you a flavour of some of the things you can do with IAM. If you're still logging in with the root account, if you're not using MFA, or if you're hard-coding credentials in your application config, you should now be armed with the information you need to level up your security practise.
In addition to that, you may benefit from using role assumption, cross-account access, or an external IdP. As a bonus hint, you should also look into CloudTrail logging, so that your Alice can keep an eye on what Bob and Carla are up to!
However you're spending the rest of this year, I wish you all the best.