I have a personal AWS account that I use for a handful of things. One aspect of it that always bothered me is the use of long-lived credentials: I had a jonathan
IAM user with a long-lived access key and secret, and then if I wanted to, e.g., upload a backup to S3, I would use those credentials to assume a role with the necessary permissions before performing the task.
Having IAM users is generally considered poor security hygiene. In my case, if the access key and secret were to be stolen, they could be used (no MFA) to waste a bunch of my money, e.g. mining cryptocurrency in EC2. But what’s the alternative access strategy without a lot of extra complexity?
It turns out that one great alternative is AWS SSO Identity and Access Management (IAM) Identity Center (previously known as SSO). The name is confusing, but the service is amazing for my use case. Here’s how access to my AWS account is set up now:
~/.aws
.https://<subdomain>.awsapps.com
) and then choose a “role” 1.aws sso login
, which opens a browser to initiate the same login flow mentioned above.It’s pretty fantastic! Over the past few years, I saw things that led me to believe IAMIC could probably be used for this, but it was never spelled out in terms that made it clear that it would (1) make sense for my rinky-dink personal account, (2) allow me to get rid of my long-lived secrets, (3) not require any other external services, and (4) not introduce any real complexity beyond IAM. Well, it does check all these boxes, and below I’ll give a quick tour of my setup.
I’ll start by listing what about this setup can’t be Terraformed (as of March 2024):
Once you’ve clicked the magic button in the web console, it will create some sort of instance such that the following Terraform code will work (docs for data.aws_ssoadmin_instances):
data "aws_ssoadmin_instances" "identity_store" {}
locals {
identity_store_id = one(data.aws_ssoadmin_instances.identity_store.identity_store_ids)
identity_store_arn = one(data.aws_ssoadmin_instances.identity_store.arns)
}
Also in the web UI, you’ll see a link to your login page, something like https://some-identifier.awsapps.com/start. Take note of this; we’ll use it to log in later.
Then we can define users (TF, docs) and permission sets (TF, docs). Think of permission sets as IAM roles. In fact, IAMIC does create IAM roles for them behind the scenes, with trust policies so the IAMIC user can assume the roles.
For example, here’s my user:
resource "aws_identitystore_user" "jonathan" {
identity_store_id = local.identity_store_id
display_name = "Jonathan"
user_name = "jonathan"
name {
given_name = "Jonathan"
family_name = "Bergknoff"
}
emails {
primary = true
value = "..."
}
}
Aside: specify an email address. It’s optional, and maybe not necessary if you’re using an external identity provider, but in my case I omitted it at first and then had no way to log in. To fix it, I added an email to the TF configuration and re-applied, and then was able to set a password using “forgot password” from the login page. I think if you specify an email address up front, you’ll get a welcome email and a smoother onboarding experience.
Here’s a read-only permission set, and an “account assignment” so that my user can use it in my organization management account:
resource "aws_ssoadmin_permission_set" "read_only" {
name = "ReadOnly"
description = "Read-only access"
instance_arn = local.identity_store_arn
}
resource "aws_ssoadmin_managed_policy_attachment" "read_only_managed_policy" {
instance_arn = local.identity_store_arn
managed_policy_arn = "arn:aws:iam::aws:policy/ReadOnlyAccess"
permission_set_arn = aws_ssoadmin_permission_set.read_only.arn
}
data "aws_caller_identity" "current_user" {}
locals {
management_account_id = data.aws_caller_identity.current_user.account_id
}
resource "aws_ssoadmin_account_assignment" "jonathan_read_only_management_account" {
instance_arn = local.identity_store_arn
permission_set_arn = aws_ssoadmin_permission_set.read_only.arn
principal_id = aws_identitystore_user.jonathan.user_id
principal_type = "USER"
target_id = local.management_account_id
target_type = "AWS_ACCOUNT"
}
You can imagine that, in a multi-account setup, this account assignment system lets us do cool things like defining one permission set and then using it to grant those permissions in several accounts.
Not pictured: we can also create groups (collections of users), and attach custom policies to permission sets, but this is enough to give a sense of how this works.
As mentioned before, IAMIC translates the permission set into IAM role(s) in the accounts where you’re using it. After applying the configuration above, an IAM role AWSReservedSSO_ReadOnly_<hex identifier>
is created with a trust policy
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::...:saml-provider/AWSSSO_..._DO_NOT_DELETE"
},
"Action": [
"sts:AssumeRoleWithSAML",
"sts:TagSession"
],
"Condition": {
"StringEquals": {
"SAML:aud": "https://signin.aws.amazon.com/saml"
}
}
}
]
}
Here’s how I configure my AWS CLI:
~/.aws/config
[sso-session sso]
sso_region = us-east-1
sso_start_url = https://my-identifier.awsapps.com/start # Change me according to your own login page URL.
sso_registration_scopes = sso:account:access
[profile read-only]
sso_session = sso
sso_account_id = 123456789012 # Change me to the account this profile is for.
sso_role_name = ReadOnly # This corresponds to the permission set name.
region = us-west-2
output = json
I don’t have an ~/.aws/credentials
file.
To access the AWS web UI, I visit that sso_start_url
in a browser, log in and am then presented with a choice of AWS account and permission set.
Here’s an excerpt from a script which I use to assume a role before uploading to S3:
#! /usr/bin/env nix-shell
#! nix-shell -i bash -p awscli2 restic
set -e
export AWS_PROFILE=upload-backups
echo Opening AWS SSO to acquire temporary credentials for uploading to S3.
aws sso login
# This exports the credentials as environment variables. Restic doesn't support loading SSO credentials
# using the profile.
$(aws configure export-credentials --format env)
...
Running AWS_PROFILE=... aws sso login
pops up a browser to the IAMIC login page. Specifying the profile (corresponding to the name of an ~/.aws/config
entry like read-only
above) makes the choice of account and permission set, so you don’t need to choose them after login. The ephemeral credentials are then stored as JSON in ~/.aws/sso/cache/
, and the handy aws configure export-credentials --format env
command prints them as a series of export AWS_...=...
statements which we evaluate in order to set those variables for the commands that come next.
For most software built for interacting with AWS (using official AWS SDKs), the AWS_PROFILE
environment variable will be respected so it won’t be necessary to run the export-credentials
command. For example, to run Terraform, I just set AWS_PROFILE=...
, run aws sso login
, and then terraform plan
to my heart’s content.
It’s called a “permission set” rather than a “role”. ↩︎