AWS Setup: Secure Identity Foundation with Terraform


AWS Setup: Secure Identity Foundation with Terraform

When it comes to access management in AWS, often I see a basic setup, with Users in IAM, as described here. Clearly, most people focus on building actual running applications, at first. After the first running POCs, the next migrations are on the road map; your architecture evolves, but the initial IAM setup stays. So it’s better to have a super secure set-up right from the beginning.

Hopefully, you have followed the AWS best practice guide to secure the root user (i.e. the credentials that consist of an e-mail address and a password), switched to one IAM user per individual and informed everyone they currently have admin rights within AWS. Most of our customers even set up IAM groups to bundle all the Admin users (in most cases, all AWS IAM users, except technical users) and tell people to require a multi factor authentication (MFA) set-up.

login-workflow not available

Unfortunately, this only protects the AWS console access (i.e. access through the web UI). For signing API calls (e.g. through the aws-cli) one uses an access key and a secret access key. Writing out these keys inside a test script and pushing it to git with some commit is common at the beginning of one’s cloud journey. Then someone gets unauthorized access to git and uses your credit card to set up a Minecraft server (or even worse). You are not the first, and will not be the last, to unintentionally share access keys.

credentials not available

So, to protect your access keys, one should switch to a role based model: using IAM roles instead of IAM users. The rough idea is to use Users solely as an Identity Provider (or your own AD) and allow users just to assume roles. You can add conditions for assuming roles (i.e. getting temporary credentials from STS). Let’s take a befitting example for a use-case that requires MFA.

To enforce MFA for all API calls, revoke the admin rights of IAM user:

1) Set up different IAM groups with restricted permissions. For example, create a default IAM group, that permits everyone attached, to change their own password and enable MFA for their own IAM user. Nothing else! 2) Create two IAM roles, one with read only permissions and one with full admin permissions, which are allowed to be assumed by users in their own AWS account. 3) Add a condition for assuming the roles, only if MFA is present. 4) Create two IAM groups with a simple policy attached, that only allows sts:assumeRole on the respective IAM role (plus a condition to double enforce MFA usage). 5) Attach the designated Users to the respective groups and revoke admin rights from these users (and your own).

From now on, the only chance of seeing and changing something is via MFA.

Terraform: Exploring the code

To get this running, basic knowlegde of Terraform is required, as well as an installed AWS CLI. Download the full Terraform example with running code here.

Let me guide you through the code, a bit. At first, we define some variables for naming and tagging. Change the name that is used for prefixing AWS resource names if more than one state should be rolled out in a single AWS account.

# Variables

variable "name" {
  description = "Name for prefixes (including tailing dash)"
  type        = string
  default     = "initial"

variable "tags-to-value" {
  type = map
  default = {
    contact     = ""
    ttl         = "infinity"
    deployed-by = "terraform"
  description = "Map of mandatory tags"

As described above, there will be three IAM groups with corresponding IAM policies and policy attachments with a mix of AWS and self managed IAM policies.


resource "aws_iam_group" "default-user-group" {
  name = format("%sdefault-users",

resource "aws_iam_group" "readonly-user-group" {
  name = format("%sreadonly-users",

## Read Only group

data "aws_iam_policy" "readonly-access-managed-policy" {
  arn = "arn:aws:iam::aws:policy/ReadOnlyAccess"

resource "aws_iam_group_policy_attachment" "readonly-attach" {
  group      =
  policy_arn = data.aws_iam_policy.readonly-access-managed-policy.arn
  depends_on = [

## Admin group

resource "aws_iam_group" "mfa-only-admin-group" {
  name = format("%smfa-only-admins",

Last, but not least, the Admin role, that requires MFA to be assumed, will be created as follows:

resource "aws_iam_role" "admin-role" {
  name_prefix          = substr(format("%smfa-only-admins-role",, 0, 31)
  assume_role_policy   = data.aws_iam_policy_document.admin-assume-role-document.json
  max_session_duration = 43200 # 43200 seconds is the supported maximum, i.e. 12 hours

resource "aws_iam_policy" "assume-role-mfa-policy" {
  depends_on  = [aws_iam_group.mfa-only-admin-group]
  name_prefix = substr(format("%sassume-admin-role-with-mfa-policy2",, 0, 31)
  path        = "/"
  description = format("Assume admin role with MFA only policy for %s",

  policy = <<EOP
    "Version": "2012-10-17",
    "Statement": [
            "Effect": "Allow",
            "Action": "sts:AssumeRole",
            "Resource": "${aws_iam_role.admin-role.arn}",
            "Condition": {
                "BoolIfExists": {
                    "aws:MultiFactorAuthPresent": "true"


Learn how to configure named profiles for the AWS CLI here.

Use awsume or aws-mfa for integration with 3rd party CLI/SDK tools.

Title Photo by Markus Winkler on Unsplash