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 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.
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:
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.
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.
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.
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.
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.
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.
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.
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
.
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.
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.
a community we’ve proudly supported for over a decade
Discover MoreThis event will explore and promote strategies for increasing accessibility to technology careers for individuals from disadvantaged communities.
Discover More“This week has been an eye-opening experience. I’ve learned so much from Alan and the team, not just about engineering but also about how our work can make a real difference. It’s been incredibly inspiring,”
Discover More