Using IAM Identity Center in a Personal AWS Account

2024-03-14 tech aws terraform

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:

  • I have no IAM users and no long-lived secrets in ~/.aws.
  • To log in to the web console, I login with MFA to an AWS-hosted login page (https://<subdomain>.awsapps.com) and then choose a “role” 1.
  • To get temporary credentials for terminal access, I use AWS CLI v2 to run aws sso login, which opens a browser to initiate the same login flow mentioned above.
  • It’s almost all codified with Terraform.
  • It doesn’t cost any money.

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.

Terraforming it

I’ll start by listing what about this setup can’t be Terraformed (as of March 2024):

  • You need to first click the “Enable” button in the IAM Identity Center web console. Refer to the documentation for more information on this (e.g. choosing region). Using an AWS organization is recommended, but not required. This step must be done by the root user in the account (or, if using an organization, in the organization’s management account).
  • We can’t configure MFA requirements, though in my case MFA was required by default.
  • When we Terraform IAMIC identity provider users, we don’t set their passwords or MFA settings.

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.

Behind the scenes

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"
        }
      }
    }
  ]
}

My local setup

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.

Web console access

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.

Programmatic access

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.


  1. It’s called a “permission set” rather than a “role”. ↩︎

comments powered by Disqus