Setting up AWS SSO with Google Workspace

Setting up AWS SSO with Google Workspace

Alan Raison
3rd May 2022

Home Insights Setting up AWS SSO with Google Workspace

I recently configured Single Sign On (SSO) from our Google accounts to AWS. AWS SSO is the recommended way to configure SSO across multiple AWS accounts, yet Google is not a supported identity provider. However, this simply meant that there was more work to do than, for example, with Azure Active Directory or Okta. The documentation was also pretty sparse, so I have tried to pull together my experiences so that it is clearer for others.

The Design

The aim is to create groups in Google Workspace and assign permissions to these groups for AWS; allowing their members access to certain AWS accounts. NewRedo is a small enough organisation to make this design very simple; we will have two groups, AWS Admins and AWS Users, who will be granted Administrator and Power User privileges in all AWS accounts respectively.

We have many AWS Accounts in our AWS Organisation (as per best practice) and this method means that we don’t need to maintain separate users and passwords in each account; we can log in once with our Google account and select the “context” that we want to work in (i.e. the appropriate account).

AWS SSO enables this configuration through the use of Permission Sets, which links an IAM Role to an account in your Organisation and will create the role in the target account for you. AWS SSO associates users and groups with these Permission Set/Account bindings. This means that AWS SSO needs to understand the structure of the groups and the users in the directory in order to correctly match up a login request to the permissions that have been granted. AWS SSO will automatically sync this information if your users are in Microsoft Azure Active Directory or in Okta, but for Google Workspace we must synchronise our directory structure ourselves, using a protocol called “the System for Cross-domain Identity Management” or SCIM. Since this is a standard, there are tools available to help us with this; we have used Terraform.

The Process

Terraform is a tool for keeping the state of a system up-to-date with a declared configuration. It inspects the system to check whether any part of the state has changed and applies actions to make the system match the configuration. This makes it possible to run repeatedly with no side-effects. It can provision resources in a very large number of systems, including all the major cloud providers and can be extended into other systems using “Providers”, of which many are listed in the Terraform Provider Registry at https://registry.terraform.io/.

Our use case will require Terraform to access Google Workspace, AWS and the (separate) AWS SCIM API. There already exist Terraform providers for all of these systems on the Terraform Provider Registry:

  • googleworkspace, by Hashicorp,
  • aws, by Hashicorp, and
  • aws-sso-scim, provided by BurdaForward

It should be noted that at no point are passwords read or transmitted across the internet, nor are these secrets stored in the terraform state file. The only sensitive information in the terraform state is names and email addresses of users, groups and the membership of those groups.

Google Workspace and Google Cloud

The first step is to read the user and group structure from Google Workspace.

We must create an identity for terraform to use to interrogate our groups’ membership. Create a new project in the Google Cloud console in the organisation linked to the Google Workspace account. Then create a service account under the “IAM and admin” section. No role grants or user access grants are necessary for this user.

Creating a Service User in GCP console

Once the account has been created, create and download an access key, in JSON format, so that Terraform can authenticate as this user. Take a note of the service account’s email address. Creating a Service account key

Next, in the Google Workspace console, https://admin.google.com, find the Admin roles configuration, under the Account menu. Click on the “User Management” system role and select the “Assign admin” action. Click “Assign service accounts” and enter the email address of the service account. Repeat this process for the “Groups Admin” role.

SAML and SCIM

In order to set up a trust between Google Identity and AWS SSO, we create a SAML application link. Navigate to “Apps”, “Web and mobile apps” in the Google Workspace admin console and click “Settings”, “SAML certificates”. This page shows you the settings that can be used in an external application to set up a trust to the Google Identity Provider. At the bottom of the page is a button to download this “metadata” into a single file. Do this and save the file locally.

AWS SSO Console

Next, log into your AWS Organization’s management account, and go to the AWS SSO console (https://console.aws.amazon.com/singlesignon). Enable SSO if it has not already been enabled and choose “Settings”. From the “Actions” menu, change the identity source to be an external identity provider and upload the metadata file saved from the Google Workspace console earlier. This sets up the trust between Google and AWS, but AWS also needs to understand what users will login and what groups they belong to; we need to sync our directory structure using the SCIM protocol.

To use SCIM, return to the Actions menu on the Settings page and select “Manage provisioning”. This page provides a SCIM endpoint URL and allows the creation of up to 2 Access tokens. Create a token and store its value safely.

Now we can use a new Terraform project to read the membership of all groups, or a subset of them, from Google Workspace and create the same in AWS SSO.

The providers we need are as follows:

terraform {
 required_providers {
   googleworkspace = {
     source  = "hashicorp/googleworkspace"
     version = "0.6.0"
   }

   aws-sso-scim = {
     source  = "BurdaForward/aws-sso-scim"
     version = "0.5.0"
   }
 }
 // backend configuration
}

provider "aws-sso-scim" {
 endpoint = "<endpoint provided by AWS SSO console>"
}

provider "googleworkspace" {
 customer_id = "<google workspace customer id>"
}

I have configured a variable that is the list of groups we wish to synchronise with AWS:

variable "groups_to_sync" {
 type = map(string)
 default = {
   "xxxx@example.com" = "<value used later>"
   "yyyy@example.com" = "<value used later>"
 }
}

We can then synchronise all users with the following data and resource objects:

data "googleworkspace_users" "all_users" {}

resource "aws-sso-scim_user" "user" {
 for_each = {
   for user in data.googleworkspace_users.all_users.users : user.primary_email => {
     display_name = one(user.name).full_name
     family_name  = one(user.name).family_name
     given_name   = one(user.name).given_name
     suspended    = user.suspended
   }
 }
 display_name = each.value.display_name
 family_name  = each.value.family_name
 given_name   = each.value.given_name
 user_name    = each.key
 active       = !each.value.suspended
}

Here, the data source (data.googleworkspace_users.all_users) is reading an unfiltered list of all the users in the Google Workspace. The aws-sso-scim_user.user resource is repeated for each of the users found in the data source and creates a user with the details from the Google Workspace user object.

Similarly for groups, but this time only for the requested groups:

data "googleworkspace_group" "group" {
 for_each = var.groups_to_sync
 email    = each.key
}

resource "aws-sso-scim_group" "group" {
 for_each     = var.groups_to_sync
 display_name = data.googleworkspace_group.group[each.key].name
}

The data source here is being repeated for each entry in our groups_to_sync list; each will be called data.googleworkspace_group.group[<email address>] where <email address> is substituted for an email address from our groups_to_sync list. We see this used in the aws-sso-scim_group resource to create a matching display_name from the Google Workspace group.

Finally, to create the membership structure. If we just had one group, we might specify this for all its members like so:

data "googleworkspace_group_members" "admins" {
  group_id = "admins@example.com"
}

resource "aws-sso-scim_group_member "admins" {
  for_each = data.googleworkspace_group_members.admin.members
  group_id = "admins@example.com"
  user_id  = each.value.id
}

But because data.googleworkspace_group_members.admin.members is a list of objects, we must convert it into a map so that Terraform knows what the key of the for_each loop should be:

resource "aws-sso-scim_group_member "admins" {
  for_each = { member in data.googleworkspace_group_members.admin.members: member.email => member }
  group_id = "admins@example.com"
  user_id  = each.value.id
}

This results in aws-sso-scim_group_member.admins being indexed by the user’s email address.

In order to iterate over all groups, the for_each expression becomes even more complex:

data "googleworkspace_group_members" "group_members" {
 for_each = var.groups_to_sync
 group_id = data.googleworkspace_group.group[each.key].id
}

resource "aws-sso-scim_group_member" "group_member" {
 for_each = {
   for group_member in flatten([
     for group in data.googleworkspace_group.group : [
       for member in data.googleworkspace_group_members.group_members[group.email].members : {
         user_id     = member.email
         group_email = group.email
       }
     ]
   ]) : "${group_member.group_email}/${group_member.user_id}" => group_member
 }
 group_id = aws-sso-scim_group.group[each.value.group_email].id
 user_id  = aws-sso-scim_user.user[each.value.user_id].id
}

So now we have aws-sso-scim_group_member for each member in each group, and the for_each key is <group-email>/<user-email>.

To apply this, the GOOGLEWORKSPACE_CREDENTIALS environment variable must be set to the path or the contents of the service account’s credential file and the AWS_SSO_SCIM_TOKEN environment variable must have the value of the token from the AWS SSO console.

In order for these users and groups to be granted access to the organization’s AWS accounts, users or groups must be assigned Permission sets for each account. A Permission set is an IAM role that is automatically created in the target account. It can have both managed and custom policies attached to it. In this instance we use the AWS manage policies for job functions: AdministratorAccess and PowerUser. Each permission set also needs a name, so we update our terraform variables to add these to the mapping:

variable "groups_to_sync" {
 type = map(string)
 default = {
   "xxxx@example.com" = "arn:aws:iam::aws:policy/AdministratorAccess"
   "yyyy@example.com" = "arn:aws:iam::aws:policy/PowerUserAccess"
 }
}

variable "permission_set_names" {
 type = map(string)
 default = {
   "xxxx@example.com" = "SSOAdmins"
   "yyyy@example.com" = "SSOPowerUsers"
 }
}

The keys of each map must match.

We use the AWS terraform provider to create these Permission Sets, first adding the AWS provider to the terraform configuration:

terraform {
 required_providers {
   // ...
   aws = {
     source  = "hashicorp/aws"
     version \= "4.5.0"
   }
 }
 // ...
}
// ...

provider "aws" {}

Next we create each of the permission sets (using for_each over the two map variables) and attach the correct policy to each:

data "aws_ssoadmin_instances" "sso" {}

locals {
 sso_instance_arn = one(data.aws_ssoadmin_instances.sso.arns)
}

resource "aws_ssoadmin_permission_set" "permission_set" {
 for_each     = var.permission_set_names
 name         = each.value
 instance_arn = local.sso_instance_arn
}
resource "aws_ssoadmin_managed_policy_attachment" "attachment" {
 for_each           = var.groups_to_sync
 instance_arn       = local.sso_instance_arn
 permission_set_arn = aws_ssoadmin_permission_set.permission_set[each.key].arn
 managed_policy_arn = each.value
}

We use a local, sso_instance_arn for convenience.

This will create four objects:

  • aws_ssoadmin_permission_set.permission_set["xxxx@example.com"]
  • aws_ssoadmin_permission_set.permission_set["yyyy@example.com"]
  • aws_ssoadmin_managed_policy_attachment.attachment["xxxx@example.com"]
  • aws_ssoadmin_managed_policy_attachment.attachment["yyyy@example.com"]

The permission_set objects have the names from the permission_set_names map and the managed_policy_attachment objects have the policy given by the groups_to_sync map. Again, the keys for each map must match exactly.

With our simple organisational structure, we wish to assign the same permission sets to the two groups, for each account. We need to create an aws_ssoadmin_account_assignment for each account and group pair, and assign to this the correct permission set. Creating each by hand might look like so:

resource "aws_ssoadmin_account_assignment" "account_1_admins" {
 instance_arn       = local.sso_instance_arn
 permission_set_arn = aws_ssoadmin_permission_set.permission_set["xxxx@example.com"].arn
 target_id          = "111111111111"
 target_type        = "AWS_ACCOUNT"
 principal_id       = aws-sso-scim_group.group["xxxx@example.com"].id
 principal_type     = "GROUP"
}

The aws-sso-scim_group resource’s id attribute is the same value that the aws_ssoadmin_account_assignment resource’s principal_id attribute requires, so we can reference it directly.

Since the aws_ssoadmin_permission_set.permission_set resources have the same index key as aws-sso-scim_group.group, we can extract this as a parameter. The aws_organizations_organisation data source also lets us iterate over all the accounts in our organization. To combine these, we need a complex for_each statement:

 for_each = {
   for pair in setproduct(data.aws_organizations_organization.org.accounts, toset(keys(var.permission_set_names))) : "${pair[0].id}/${pair[1]}" => {
     account            = pair[0].id
     principal          = aws-sso-scim_group.group[pair[1]].id
     permission_set_arn = aws_ssoadmin_permission_set.permission_set[pair[1]].arn
   }
 }

This will create an array of objects which pairs up each account with each permission set and the group that it is assigned to. This is sufficient for our needs, but if you have a more complex account or permissions structure, you may have to duplicate these blocks and/or make the controlling variables more detailed.

The final part of the terraform code looks like this:

data "aws_organizations_organization" "org" {}

resource "aws_ssoadmin_account_assignment" "global_assignments" {
 for_each = {
   for pair in setproduct(data.aws_organizations_organization.org.accounts, toset(keys(var.permission_set_names))) : "${pair[0].id}/${pair[1]}" => {
     account            = pair[0].id
     principal          = aws-sso-scim_group.group[pair[1]].id
     permission_set_arn = aws_ssoadmin_permission_set.permission_set[pair[1]].arn
   }
 }
 instance_arn       = local.sso_instance_arn
 permission_set_arn = each.value.permission_set_arn
 target_id          = each.value.account
 target_type        = "AWS_ACCOUNT"
 principal_id       = each.value.principal
 principal_type     = "GROUP"
}

Now we have a mechanism to synchronise users, groups, permissions and organisational accounts, each time we run terraform apply, the associations will be checked and updated.

You can now log in to AWS with your Google account by navigating to the User portal URL, displayed in the AWS SSO console. This is called the “Service Provider” (SP) SAML flow (where login is initiated by AWS). It is also desirable to enable the “Identity Provider” (IdP) flow, where users can select to open AWS from a Google applications list.

Identity Provider Flow

Back in the Google Workspace (https://admin.google.com) we want to add a SAML app. Navigate to “Apps”, “Web and mobile apps” and click on the “Add app”, “Add custom SAML app” menu item.

Add custom SAML app

Give the App a name and icon and skip through the Identity Metadata page, as we have already provided this to AWS. In the Service provider screen, you must enter the “ACS URL” and “Entity ID”. These values can be found in the AWS SSO Settings page under “AWS SSO ACS URL” and “AWS SSO issuer URL” respectively. Leave the start URL empty, but change the name ID format to EMAIL.

ACS Settings - Google Workspace

Now you will be able to see an icon in your Google Applications menu for AWS, which you can use to log directly into the AWS SSO portal.

ACS Values - AWS SSO

Summary

In this blog we saw in detail how to configure Terraform to synchronise Google Workspace users with AWS SSO using SCIM. We also looked at the configuration required in AWS and in Google Cloud and Google Workspace to enable SSO. In the next instalment of this blog series, I will detail how I package and run this as a Lambda function.

Share Article

Insights.

Taking a look at Dagger
Taking a look at Dagger

Solomon Hykes is probably most famous for being the founder and former CTO of Docker. Docker revolutionised the way we package, run and distribute server applications, so when Hykes starts a new venture, it's worth checking out.

Discover More
Running Terraform in a Lambda Function
Running Terraform in a Lambda Function

I recently set up a Terraform project which I wanted to run on a regular schedule. There are a number of ways to achieve this, but I decided to package the project as a Lambda function and schedule it with… 

Discover More
The Crowd Wisdom Project launches, through NewRedo
The Crowd Wisdom Project launches, through NewRedo

This morning we launched the Crowd Wisdom Project for our client Andrew Grey. It’s innovative and it’s intention is ground breaking. We genuinely can’t wait to see the data it collects and the impact it has. Rather than us try… 

Discover More