Terraforming AWS IAM users

2020-08-03 tech programming terraform

I found myself Terraforming some AWS IAM users today. It can be hard to remember the ins-and-outs of managing the PGP-encrypted credentials, so I’m writing this down for future reference.

The model here is to create IAM users (probably one per person on your team), and to set them up with long-lived IAM credentials (access key/secret) and login access to the web console. We’ll have Terraform generate these secrets for us and give us PGP-encrypted output that we can distribute to the user.

Regarding the security of this setup:

  • AWS has an option to force the user to set his or her own password upon first login to the web console, and we’ll use that. The login profile resource will not interfere with the user changing his or her password (i.e. it won’t show up dirty on the next plan), so the use of Terraform here doesn’t get in the way.

  • In practice, it’s best to rotate these long-lived IAM credentials periodically (Here’s AWS’s doc about best practices). That’s outside the scope of this article, but one approach might be to taint the aws_iam_access_key resources on a schedule, and then re-run (it might be nice to depend on a null_resource referring to the date, e.g. via substring(timestamp(), 0, 7) to get the month, but this is not currently possible).

PGP usage with GPG

Each user will have to supply a PGP key. This can be done using GNU Privacy Guard (GPG). If you have it installed, great. If not, you can run in it Docker like this:

$ alias gpg='docker run -it --rm -u $(id -u):$(id -g) -e HOME -v "$HOME":"$HOME" -v "$(pwd)":"$(pwd)" -w "$(pwd)" dockerizedtools/gpg:2.2.20'

Generate a key by running this and following the prompts:

$ gpg --gen-key

Now we need the base64-encoded public key. We can get the full PEM by running gpg --export -a "Key name". We can clean it up into the format Terraform wants (single line, no header/footer, remove second base64-encoded string) by doing the following:

$ gpg --export -a "Key name" | tail -n +2 | head -n -2 | tr -d '\n' > gpg-public-key

Now we’ll use the contents of that file as input to Terraform. It should look something like this public key I generated for demonstration:

mQENBF8oU3UBCAC461EBgqxKDfvjAjuVa2WzYmHHPyBavkjAO3VEjFD8Ms9PH6dJD84untykshKf+lPSpPh+zhpnxRiuNTmREaaygTfJVcGnWHhcBKQiQgJcUHKOBEaTpDQE7ZIWuDtAMfSnnkWqm2TZ86hPdYf/XdMCj7ArGlbFZ1DT84jc1ZhZqlsV+7TmSjRJnkyl3gDy+oLXoE1F5VDGVi4JnZZZkL2D4OGzDi4a3pNM0CsWkaB3igmhv6zdy+MJh01E7F7KRCBHr0DIUKUVkvMP9l1IzDzN99hnXXsdbF2eu0y2LNkRBWZUTbkEEHovSfoefHoBfVy/YEfwjbQqUFjZkq+JfIHjABEBAAG0EGFiY2RlZiA8YUBiLmNvbT6JAVQEEwEIAD4WIQQIEWtzVJxsXpFoqSiUtapgQequbwUCXyhTdQIbAwUJA8JnAAULCQgHAgYVCgkICwIEFgIDAQIeAQIXgAAKCRCUtapgQequb4sfB/9GoSEuVNLDYY8rw8NwJbqAOJAGJeyUX0vJujAS9re9W09xAet7WFZUmsBRbeeqFH66npcVVVVqzgGGK8SmTJrooAvAzIX/lrpGIcxVasIpJscFu4ob3bKCGUcSQgdLlQr9sT3pkHOy/f5A5ZJFebofWa2C08kup7Cuvz8oVrWCJVDNpnxJ6BrKMm2QHZKRQ0zUiGenYJVQeXXwO989KRIxokIjmePwWPhQeAGnEw+FLTYkGf5+0WzkVV/rcbjCctGwMNy6IFDmKcGhOvGi5KC2OtGgcYw1fpZQNjNzSbRul5S3vG9hC2AX74kDjFW27Ul92Yq0kv8fVwVdec8Hiiq3uQENBF8oU3UBCAC8xKRaFg4CHJJmS2yqUlARDjTL+xXA9gWyFWegG8SSvn4KXvtleZ3517C/mfRYGJvyNvM4WQifsDuKbIkF06GlYPd2lefDAKG8of3c1TwK7pi+Z4FBIIrKV6VUJGTeEoUpvz4Z/rPP7RAdFHs79vGM4H0E/HdC5pK0hmXADrd7IzUVKnflQ+rQ9xEN5Ql0qeirpphYtf3auHmPn4YWnuUWlIHC1HWHoCMhGPfHKf5qyXGGDwDhCeo7kdtMTxdNFPTs8sybgkOkpm0M1AKTieyyDVnHubBHKqrAA7iPSjuPICc/gngyoJ9j1TwbVaF9Lmr+/T5z8TiLd1zcPgc5gYyPABEBAAGJATwEGAEIACYWIQQIEWtzVJxsXpFoqSiUtapgQequbwUCXyhTdQIbDAUJA8JnAAAKCRCUtapgQequbx17B/40BQymbfyJ4Yj07JUA1B4PvpyryWbgDnI8vvH9Ix2xQj/Ug7Mjq3+h6kUlY1T2B/7KnlRL2J3qH9UDOLMCUxh9+wSVMBh9QbimWN/2DTwLLz11160ggnYBTV/8/+PRYiWv6tXuITxYh+Zlkt+Z4AQ027w5S3emhPRWmPEe3NhpDzENcedKK0MXR6cxrAjiXqTD5awycQ50NcdbFsF0jbuvpSAw24MfQaHNdxw/piaykWp3sIJrznbvoDNKk18NXjE3cqORd7S0y5RB0hRQVW+9yil8LZK9HPJjlYqc2q0ebtsdeJ8MDxeUBvUtr0tYk7SeTdv8fTKlk9L6tPlX4pDP

The length can vary.

Terraform definitions

Let’s start by defining our input:

variable "users" {
  description = "Map of IAM username to user details"
  type        = map(any)
}

which we’ll supply with a value like this:

users = {
  alice = {
    pgp_key = "<Alice's single-line base64-encoded public key (see above)>",
  },
  bob = {
    pgp_key = "<Bob's single-line base64-encoded public key (see above)>",
  },
}

If, in the future, we want to manage these users more extensively (e.g. adding a policy to them to allow them to assume certain roles), this data structure can be extended to support that.

The resources look like:

resource "aws_iam_user" "user" {
  for_each = var.users

  name = each.key
}

resource "aws_iam_access_key" "user" {
  for_each = toset([for user in aws_iam_user.user : user.name])

  user    = each.key
  pgp_key = var.users[each.key].pgp_key
}

resource "aws_iam_user_login_profile" "user" {
  for_each = toset([for user in aws_iam_user.user : user.name])

  user                    = each.key
  pgp_key                 = var.users[each.key].pgp_key
  password_reset_required = true
}

and we’ll output the encrypted values, which can be disseminated to the users:

output "users" {
  value = {
    for user in aws_iam_user.user :
    user.name => {
      access_key         = aws_iam_access_key.user[user.name].id,
      encrypted_secret   = aws_iam_access_key.user[user.name].encrypted_secret,
      encrypted_password = aws_iam_user_login_profile.user[user.name].encrypted_password,
    }
  }
}
comments powered by Disqus