Post

Managing AWS IAM Identity Center Multi-Account Permissions using Terraform and GitHub Actions

Introduction

Maintaining accounts with Terraform ensures a single source of truth about what changes we made for our principals and recommended policies based on organization guidelines. We can also structure and manage the changes for our IAM Identity Center Multi-account Permission declaratively with the help of Hashicorp Configuration Language (HCL). Either a long-time or newly hired operations engineer can change quickly.

Quick Demo

AWS Console Access Portal AWS Console Access Portal

Recorded with asciinema asciinema

Requirements

AWS Organization

Ensure we have at least two AWS Organization Accounts in a different environment.

AWS Organization AccountDescription
Operations ManagementOur main AWS Account where we manage our AWS Organization Accounts.
DevelopmentThe Account for Development, QA, and Testing.
ProductionThe Account for Production Workloads.

IAM Identity Center Users

Display NameUser NameGroup Name
John Doe[email protected]administrator
Sarah Jane[email protected]operations
Marcello Kelley[email protected]developer
Miliana Smith[email protected]developer
Antonia Morris[email protected]<NO GROUP>

A user with <NO GROUP> could be independently set or under other groups, such as qualityassurance, or a combination of any existing groups in our user’s table.

Download and Install Tooling Softwares

Instead of using Vanilla Terraform, we will use a Terraform Framework called Terraspace. Terraspace helps us organize the resources by stacks, keeping them organized and manageable at the same time.

  1. Terraform
  2. Terraspace
  3. Git

AWS IAM Account

Create an AWS IAM Account with Programmatic Access having the following managed permissions policies attached.

  1. AmazonS3FullAccess - Provides full access to all buckets.
  2. AmazonDynamoDBFullAccess - Provides full access to Amazon DynamoDB.
  3. IAMFullAccess - Provides full access to IAM.
  4. AWSSSOMasterAccountAdministrator - Provides access within AWS SSO to manage AWS Organizations master and member accounts and cloud application.

Create an AWS IAM for our Terraform

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
aws iam create-user --user-name provisioner
# {
#   "User": {
#     "Path": "/",
#     "UserName": "provisioner",
#     "UserId": "XXXXXXXXXXXXXXXXXXXX",
#     "Arn": "arn:aws:iam::< AWS ACCOUNT ID >:user/provisioner",
#     "CreateDate": "2023-02-23T11:25:55+00:00"
#   }
# }

aws iam create-access-key --user-name provisioner
# {
#   "AccessKey": {
#     "UserName": "provisioner",
#     "AccessKeyId": "< AWS_ACCESS_KEY_ID >",
#     "Status": "Active",
#     "SecretAccessKey": "< AWS_SECRET_ACCESS_KEY >",
#     "CreateDate": "2023-02-23T11:31:51+00:00"
#   }
# }

aws iam attach-user-policy --user-name provisioner \
  --policy-arn arn:aws:iam::aws:policy/AmazonS3FullAccess

aws iam attach-user-policy --user-name provisioner \
  --policy-arn arn:aws:iam::aws:policy/AmazonDynamoDBFullAccess

aws iam attach-user-policy --user-name provisioner \
  --policy-arn arn:aws:iam::aws:policy/AWSSSODirectoryAdministrator

aws iam attach-user-policy --user-name provisioner \
  --policy-arn arn:aws:iam::aws:policy/IAMFullAccess

aws iam list-attached-user-policies --user-name provisioner
# {
#   "AttachedPolicies": [
#     {
#         "PolicyName": "IAMFullAccess",
#         "PolicyArn": "arn:aws:iam::aws:policy/IAMFullAccess"
#     },
#     {
#         "PolicyName": "AWSSSOMasterAccountAdministrator",
#         "PolicyArn": "arn:aws:iam::aws:policy/AWSSSOMasterAccountAdministrator"
#     },
#     {
#         "PolicyName": "AmazonDynamoDBFullAccess",
#         "PolicyArn": "arn:aws:iam::aws:policy/AmazonDynamoDBFullAccess"
#     },
#     {
#         "PolicyName": "AmazonS3FullAccess",
#         "PolicyArn": "arn:aws:iam::aws:policy/AmazonS3FullAccess"
#     }
#   ]
# }

Load the AWS IAM Credentials to our System Profile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
aws configure --profile provisioner
# AWS Access Key ID [None]: < AWS_ACCESS_KEY_ID >
# AWS Secret Access Key [None]: < AWS_SECRET_ACCESS_KEY >
# Default region name [None]: us-east-1
# Default output format [None]: json

export AWS_PROFILE=provisioner

aws sts get-caller-identity
# {
#   "UserId": "XXXXXXXXXXXXXXXXXXXX",
#   "Account": "< AWS ACCOUNT ID >",
#   "Arn": "arn:aws:iam::< AWS ACCOUNT ID >:user/provisioner"
# }

Our AWS IAM User for Terraform provisioner is now ready to use.

Multi-Account Permissions Project

Prepare the Terraform Project

Initialized a Terraform Project using Terraspace Framework

Please create a new project where our Infrastructure is a Code definition for Terraform.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
terraspace new project aws-iam-ic-multi-account-permissions-example --plugin aws
cd aws-iam-ic-multi-account-permissions-example

# Makes sure we have the pregenerated files listed.
tree -L 2
# .
# ├── Gemfile
# ├── Gemfile.lock
# ├── README.md
# ├── Terrafile
# ├── app
# │   ├── modules
# │   └── stacks
# └── config
#     ├── app.rb
#     └── terraform

Prepare the Modules

To reduce the IaC bloating and maintains the simplicity, we will be using a reusable module from CloudPosse.

Install the SSO Module

1
2
echo -e 'mod "sso", source: "cloudposse/sso/aws", version: "0.7.1"' >> Terrafile
terraspace bundle

Terraspace Stack for the Permission Sets

1
terraspace new stack permission-sets

Terraspace Stacks for our AWS Accounts

1
2
3
terraspace new stack operations
terraspace new stack development
terraspace new stack production

Download the Cloudposse Modules Context File

1
2
3
ls ./app/stacks/ | xargs -I {} \
curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf \
-o ./app/stacks/{}/context.tf

Create a Terraform variable files to identify our sources on AWS Console

Cloudposse context files use these variables and form proper resource naming conventions.

1
2
3
4
5
6
7
ls ./app/stacks/ | xargs -I {} \
sh -c 'mkdir -p ./app/stacks/{}/tfvars && tee ./app/stacks/{}/tfvars/base.tfvars <<EOF
enabled   = true
namespace = "aws-sso"
name      = "aws-sso"
stage     = "<%= expansion(':ENV') %>"
EOF'

Create a permission-sets Terraform outputs

These Terraform outputs will be variables for our AWS accounts and principal assignment.

1
2
3
4
5
6
tee ./app/stacks/permission-sets/outputs.tf <<EOF
output "permission_sets" {
  description = "Show All Permission Sets"
  value       = module.permission_sets.permission_sets
}
EOF

Create a Terraform variable that binding permission-sets

AWS accounts and principal assignments require permission set via outputs.

1
2
3
4
ls --ignore "permission-sets" ./app/stacks/ | xargs -I {} \
sh -c 'tee -a ./app/stacks/{}/tfvars/base.tfvars <<EOF
permission_sets = <%= output("permission-sets.permission_sets") %>
EOF'

Create an organization Terraform outputs

Applying any Terraform action shows the output details, so we can review or validate that everything applies correctly.

1
2
3
4
5
6
7
ls --ignore "permission-sets" ./app/stacks/ | xargs -I {} \
sh -c 'tee -a ./app/stacks/{}/outputs.tf <<EOF
output "assignments" {
  description = "Show All Account Assignments for a Permission Set"
  value       = module.sso_account_assignments.assignments
}
EOF'

Create a an variables for our Permission Sets and AWS Accounts

1
2
3
4
5
6
7
ls --ignore "permission-sets" ./app/stacks/ | xargs -I {} \
sh -c 'tee ./app/stacks/{}/variables.tf <<EOF
variable "permission_sets" {
}
variable "aws_account_{}" {
}
EOF'

Principals and Accounts Management

Create a Permission Sets Resource

First, create an example of an inline policy for one of our Permission Sets

1
2
3
4
5
6
7
8
9
10
11
12
13
tee ./app/stacks/permission-sets/inline_policy_S3Access.tf <<EOF
data "aws_iam_policy_document" "inline_policy_S3Acccess" {
  statement {
    sid = "1"

    actions = ["*"]

    resources = [
      "arn:aws:s3:::*",
    ]
  }
}
EOF

Permission Sets Resource Definitions

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
tee ./app/stacks/permission-sets/main.tf <<EOF
module "permission_sets" {
  source  = "../../modules/sso/modules/permission-sets"
  context = module.this.context

  permission_sets = [

    {
      name             = "AdministratorAccess",
      description      = "Provides full access to AWS services and resources.",
      relay_state      = "",
      session_duration = "PT12H",
      tags             = {},
      inline_policy    = "",
      policy_attachments = [
        "arn:aws:iam::aws:policy/AdministratorAccess",
      ]
      customer_managed_policy_attachments = []
    },

    {
      name             = "PowerUserAccess",
      description      = "Provides full access to AWS services and resources, but does not allow management of Users and groups.",
      relay_state      = "",
      session_duration = "PT12H",
      tags             = {},
      inline_policy    = "",
      policy_attachments = [
        "arn:aws:iam::aws:policy/PowerUserAccess",
      ]
      customer_managed_policy_attachments = []
    },

    {
      name             = "ReadOnlyAccess"
      description      = "Provides read-only access to AWS services and resources."
      relay_state      = ""
      session_duration = "PT12H"
      tags             = {}
      inline_policy    = data.aws_iam_policy_document.inline_policy_S3Acccess.json
      policy_attachments = [
        "arn:aws:iam::aws:policy/ReadOnlyAccess",
      ]
      customer_managed_policy_attachments = []
    },

  ]
}
EOF

At this point, we completed the Permission Sets stack creation and can initially apply it if we want.

1
2
terraspace plan permission-sets
terraspace up permission-sets

Create an Accounts Assignments Resources

Operations Account Resource Definitions

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
28
29
30
31
32
tee ./app/stacks/operations/main.tf <<EOF
module "sso_account_assignments" {
  source  = "../../modules/sso/modules/account-assignments"
  context = module.this.context

  account_assignments = [

    ############
    ## GROUPS ##
    ############
    {
      principal_name      = "administrator"
      principal_type      = "GROUP"
      permission_set_name = "AdministratorAccess"
      permission_set_arn  = var.permission_sets["AdministratorAccess"].arn
      account             = var.aws_account_operations
    },

    ###########
    ## USERS ##
    ###########
    {
      principal_name      = "[email protected]"
      principal_type      = "USER"
      permission_set_name = "AdministratorAccess"
      permission_set_arn  = var.permission_sets["AdministratorAccess"].arn
      account             = var.aws_account_operations
    },

  ]
}
EOF

Plan and apply for Operations Account.

1
2
terraspace plan operations
terraspace up operations

Development Account Resource Definitions

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
tee ./app/stacks/development/main.tf <<EOF
module "sso_account_assignments" {
  source  = "../../modules/sso/modules/account-assignments"
  context = module.this.context

  account_assignments = [

    ############
    ## GROUPS ##
    ############
    {
      principal_name      = "administrator"
      principal_type      = "GROUP"
      permission_set_name = "AdministratorAccess"
      permission_set_arn  = var.permission_sets["AdministratorAccess"].arn
      account             = var.aws_account_development
    },

    {
      principal_name      = "developer"
      principal_type      = "GROUP"
      permission_set_name = "PowerUserAccess"
      permission_set_arn  = var.permission_sets["PowerUserAccess"].arn
      account             = var.aws_account_development
    },

    ###########
    ## USERS ##
    ###########
    {
      principal_name      = "[email protected]"
      principal_type      = "USER"
      permission_set_name = "PowerUserAccess"
      permission_set_arn  = var.permission_sets["PowerUserAccess"].arn
      account             = var.aws_account_development
    },
    {
      principal_name      = "[email protected]"
      principal_type      = "USER"
      permission_set_name = "ReadOnlyAccess"
      permission_set_arn  = var.permission_sets["ReadOnlyAccess"].arn
      account             = var.aws_account_development
    },

  ]
}
EOF

Plan and apply for Development Account.

1
2
terraspace plan development
terraspace up development

Production Account Resource Definition

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
28
29
30
31
32
33
34
35
36
37
38
39
40
tee ./app/stacks/production/main.tf <<EOF
module "sso_account_assignments" {
  source  = "../../modules/sso/modules/account-assignments"
  context = module.this.context

  account_assignments = [

    ############
    ## GROUPS ##
    ############
    {
      principal_name      = "administrator"
      principal_type      = "GROUP"
      permission_set_name = "AdministratorAccess"
      permission_set_arn  = var.permission_sets["AdministratorAccess"].arn
      account             = var.aws_account_production
    },

    {
      principal_name      = "developer"
      principal_type      = "GROUP"
      permission_set_name = "ReadOnlyAccess"
      permission_set_arn  = var.permission_sets["ReadOnlyAccess"].arn
      account             = var.aws_account_production
    },

    ###########
    ## USERS ##
    ###########
    {
      principal_name      = "[email protected]"
      principal_type      = "USER"
      permission_set_name = "AdministratorAccess"
      permission_set_arn  = var.permission_sets["AdministratorAccess"].arn
      account             = var.aws_account_production
    },

  ]
}
EOF

Plan and apply for Production Account.

1
2
terraspace plan production
terraspace up production

Stacks Final Directory Structure

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
28
29
30
31
tree -L 3 ./app/stacks
# ./app/stacks
# ├── development
# │   ├── context.tf
# │   ├── main.tf
# │   ├── outputs.tf
# │   ├── tfvars
# │   │   └── base.tfvars
# │   └── variables.tf
# ├── operations
# │   ├── context.tf
# │   ├── main.tf
# │   ├── outputs.tf
# │   ├── tfvars
# │   │   └── base.tfvars
# │   └── variables.tf
# ├── permission-sets
# │   ├── context.tf
# │   ├── inline_policy_S3Access.tf
# │   ├── main.tf
# │   ├── outputs.tf
# │   ├── tfvars
# │   │   └── base.tfvars
# │   └── variables.tf
# └── production
#     ├── context.tf
#     ├── main.tf
#     ├── outputs.tf
#     ├── tfvars
#     │   └── base.tfvars
#     └── variables.tf

Plan and Apply All Stacks

We can also plan and apply at once the entire stacks, as you have seen in the Multi-Account Terraform Quick Action Demo.

1
2
terraspace all plan
terraspace all up

Access Portal Example Scenarios

Based on our Terraform resource accounts assignments.

User John Doe under the administrator group.

The administrator group is assigned to all accounts

AWS Console Access Portal AWS Console Access Portal

User Sarah Jane under the operations group.

The operations group is not assigned, however we specifically assigned the user.

AWS Console Access Portal AWS Console Access Portal

User Marcello Kelley and Miliana Smith under the developer group.

The developer group members are assigned to specific accounts with different access levels.

AWS Console Access Portal AWS Console Access Portal

AWS Console Access Portal AWS Console Access Portal

User Antonia Morris is under no group.

The user is assigned to specific account with its own access level.

AWS Console Access Portal AWS Console Access Portal

Automate using Github Actions

GitHub Actions Workflow

Create a GitHub Actions Workflow File

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
mkdir -p ./.github/workflows && tee ./.github/workflows/apply-assignments.yml <<EOF
name: "Apply Assignments for Accounts and Permission Sets"
on:
  push:
    branches:
      - main
env:
  AWS_REGION: "us-east-1"
  AWS_ACCESS_KEY_ID: \${{ secrets.PROVISIONER_AWS_ACCESS_KEY_ID }}
  AWS_SECRET_ACCESS_KEY: \${{ secrets.PROVISIONER_AWS_SECRET_ACCESS_KEY }}
  AWS_ACCOUNT_ID: \${{ secrets.OPERATIONS_AWS_ACCOUNT_ID }}

  TS_ENV: "dev"
  TF_VAR_aws_account_operations: \${{ secrets.OPERATIONS_AWS_ACCOUNT_ID }}
  TF_VAR_aws_account_development: \${{ secrets.DEVELOPMENT_AWS_ACCOUNT_ID }}
  TF_VAR_aws_account_production: \${{ secrets.PRODUCTION_AWS_ACCOUNT_ID }}

jobs:
  apply-assignments:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout source
        uses: actions/checkout@v3
        with:
          fetch-depth: 0
          submodules: true

      - name: Install Terraform
        uses: hashicorp/setup-terraform@v2
        with:
          terraform_wrapper: false
          terraform_version: 1.3.9

      - name: Setup Ruby
        uses: ruby/setup-ruby@v1
        with:
          ruby-version: '3.1.2'

      - name: Install Terraspace
        run: |
          bundle install

      - name: Download Terraform Modules
        run: |
          terraspace bundle

      - name: Terraspace All Init
        run: |
          terraspace all init -y

      - name: Terraspace All Up
        run: |
          terraspace all up -y

      - name: Upload Logs to Artifacts
        uses: actions/upload-artifact@v3
        if: always()
        with:
          name: terraspace-logs
          path: ./log/*
EOF

Create a Secrets in our GitHub Repository

1
2
3
4
5
PROVISIONER_AWS_ACCESS_KEY_ID
PROVISIONER_AWS_SECRET_ACCESS_KEY
OPERATIONS_AWS_ACCOUNT_ID
DEVELOPMENT_AWS_ACCOUNT_ID
PRODUCTION_AWS_ACCOUNT_ID

GitHub Actions Workflow Page

GitHub Actions Worflow Page

At this point, we can also automate the process using GitHub Actions. Without the locally installed Operations Tools, we maximized the cloud resource.

Final GitHub Project Repository

You can download the final and working example project repository at this link https://github.com/nathanielvarona/aws-iam-ic-multi-account-permissions-example.

This post is licensed under CC BY 4.0 by the author.