I Am Secure, Therefore IAM

Sharethrough serves millions of ad requests daily, all on top of Amazon Web Services (AWS). At one time, sharing AWS credentials via a shared password vault worked OK. But as we grew, we needed a better, more secure way to manage AWS logins.

We use Terraform to maintain infrastructure and prevent unintentional version drift. Using it to maintain our AWS credentials seemed a logical choice. AWS Identity and Access Management (IAM) gives us the ability to create individual login (IAM user), assosicate users to a group (IAM group) and assign access control policy (IAM policy). Together, they offer a great solution for user, group and access management.

IAM in Action

In the example below, we are creating an iam_user mason who is a member of the stx iam_group. The stx iam_group is attached to the stx iam_policy with the access policy defined in stx.json. These files are checked into github to prevent drift, the same way we use Terraform to maintain other parts of our infrastructure.

Our Terraform directory structure looks like below. (client, sam, sfp, and stx are teams within Engineering)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# Directory Structure
├── policies
│   ├── client.json
│   ├── sam.json
│   ├── sfp.json
│   └── stx.json
└── production
    ├── aws.tf
    ├── iam_group_membership.tf
    ├── iam_groups
    │   └── iam_groups.tf
    ├── iam_policies
    │   ├── client.tf
    │   ├── sam.tf
    │   ├── sfp.tf
    │   └── stx.tf
    ├── iam_policy_attachment.tf
    ├── iam_users
    │   ├── client.tf
    │   ├── sam.tf
    │   ├── sfp.tf
    │   └── stx.tf
    ├── modules.tf
    ├── production.tfstate
    ├── production.tfstate.backup
    ├── temp.tf
    └── variables.tf

Tree Structure and Explanation

The iam_users directory contains files that create individual users:

1
2
3
4
5
6
7
8
9
# iam_users/stx.tf
resource "aws_iam_user" "mason" {
    name = "mason"
    path = "/"
}

output "mason" {
    value = "${aws_iam_user.mason.name}"
}

The iam_group directory contains a file that creates engineering groups:

1
2
3
4
5
6
7
8
9
# iam_groups/iam_groups.tf
resource "aws_iam_group" "stx" {
    name = "stx"
    path = "/"
}

output "stx" {
    value = "${aws_iam_group.stx.name}"
}

The iam_policies directory files indicate the group policy to use. Actual policies are defined in the policies directory. In this case, the stx engineering group is using the stx.json policy file.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# iam_policies/stx.tf
resource "aws_iam_policy" "stx" {
    name = "stx"
    path = "/"
    description = "Team stx policy"
    policy = "${file(\"terraform/policies/stx.json\")}"
}

output "stx" {
  value = "${aws_iam_policy.stx.name}"
}

output "stx_arn" {
  value = "${aws_iam_policy.stx.arn}"
}

Policies are defined as json blob inside the policies directory. This policy allows full access to EC2 and S3.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# policy file: stx.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Action": [
        "ec2:*",
        "s3:*",
      ],
      "Effect": "Allow",
      "Resource": "*"
    }
  ]
}

The group membership file assigns an iam_user membership to an iam_group:

1
2
3
4
5
6
7
8
# iam_group_membership.tf
resource "aws_iam_group_membership" "stx" {
    name = "stx"
    users = [
        "${module.iam_users.mason}"
    ]
    group = "${module.iam_groups.stx}"
}

The policy attachment file attaches an iam_policy to an iam_group:

1
2
3
4
5
6
# iam_policy_attachment.tf
resource "aws_iam_policy_attachment" "stx" {
    name = "stx"
    groups = ["${module.iam_groups.stx}"]
    policy_arn = "${module.iam_policies.stx_arn}"
}

At last, the module file maps where the resources are located to better organize the IAM identities :

1
2
3
4
5
6
7
8
9
10
11
12
# module.tf
module "iam_users" {
    source = "./iam_users/"
}

module "iam_groups" {
    source = "./iam_groups/"
}

module "iam_policies" {
    source = "./iam_policies/"
}

AWS Multi-Factor Authentication (MFA)

We use two-factor authentication (2FA) as additonal security for AWS logins. The Amazon SDK provides information to enable 2FA for users, but associating an MFA device to a user requires entering two authentication codes from the device. Creating such devices is not within the scope of the SDK.

To associate a MFA device with a user, we wrote a tool that uses oath-tool (to create a virtual MFA for generating the authentication codes) and google charts api to create a qr_code.

Code snippet of the 2FA associator

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
  # save the qr_code into a markdown file to display on browser
  def write_qr_code_file username
      data = @mfa[username]
      File.open("qr_codes/#{username}.md", "w") do |file|
          s = "<a href=\"#{data[:qr_code]}\" rel=\"#{username}\">![#{username}](#{data[:qr_code]})</a>"
          file.write(s)
          file.write("\n")
          file.close
      end
  end

  # generate a vMFA and capture the next 10 codes for user association
    def create_auth_codes base_32_string_seed, serial_number, username
        @mfa[username] = { base_32_string_seed: base_32_string_seed,
                           serial_number: serial_number,
                           qr_code: "https://chart.googleapis.com/chart?chs=200x200&chld=M%7C0&cht=qr&chl=otpauth://totp/#{username}@sharethrough?secret=#{base_32_string_seed}"
                         }
        codes, stderr, status = Open3.capture3("oathtool --base32 #{base_32_string_seed} -w 10 --totp")
        codes.split
    end

Conclusion

Balancing security with usability is tough. Terraform and IAM identities helped us make our systems more secure without hindering engineers. Engineers now have their own AWS access and secret keys saved in a credential file, and teams can focus on features, knowing that we have our bases covered.