Abstract
The post introduced a solution for automating AWS Identity Center groups to AWS accounts and permission set assignments for groups created within AWS Identity Center from an external Identity Provider.
This post assumes a familiarity with the AWS IAM service and concepts such as roles and policies.
Introduction to Central Identity and Access Control on AWS
AWS multi-account environments are commonly setup with central identity and access control instead of doing this on a per individual account basis. AWS offers the IAM Identity Center (formerly Single Sign On) service for centralised access control, permissions and access to AWS accounts.
Identity Center supports bringing an organisation's existing users and groups into the AWS environment. IAM's documentation refers to this as using an external identity provider (IdP) to federate users and groups with Identity Center.
Within Identity Center permissions are managed via permission sets, which in turn are a collection of IAM policies. Once a permission set has been assigned for a user or group to an account, Identity Center will automatically create IAM roles in the account. The role's policies are configured from the permission set. In addition the role has a trust policy configured that allows the role to be only assumed when the user has been authenticated from the federated identity provider.
Managing AWS access control and permissions should always follow the least privilege permissions concept for any task for the environment's users. AWS environments typically are setup for role based access control where roles have sufficient permissions setup to accomplish the required tasks. Roles in the AWS Identity Center are commonly mapped to federated groups from the IdP. Depending on the requirements for granularity this potentially results in a 1:1 relationship between an IdP's groups and the combination of AWS account and permission set.
Let's look at a practical example. The AWS account 'app1-prod' should be accessed by the operators of the account using three different permissions depending on the tasks at hand.
For day to day regular operations most tasks should be read only and fall within the category of visibility, monitoring and observation. One can use AWS' provided AWS managed policy ReadOnlyAccess
and the permissions defined by the policies would get the job done.
For emergency changes to the environment and management of the IAM service within the account the policies from AWS's managed AdministratorAccess
policy can be used.
For any other tasks requiring interactive changes to the environment permissions defined the policies from AWS's managed PowerUserAccess
policy can be used.
In the example scenario the IdP would have to hold three groups, giving the group's members access to the AWS account app1-prod with the three permission sets ReadOnlyAccess
, AdministratorAccess
and PowerUserAccess
.
Creating the groups in the IdP and assignment of the permission sets to the AWS accounts for the group must be accomplished first before users from the groups can log into the AWS account.
Many organizations have automated AWS account creation since new applications and workloads need to be accommodated frequently. It is not uncommon for a workload to be deployed across several AWS accounts, each requiring IAM Identity Center setup before being handed over to the users of the accounts.
AWS does not provide an out of the box solution for automating the creation groups and permission sets to AWS account assignments and thus frequently this step is performed manually introducing delays and additional work.
Solution
This post describes a solution using AWS serverless resources for AWS Identity Center federated group to account and permission set assignment automation. This solution assumes that the naming of the group follows a regular pattern that contains the target AWS account and permission set as part of the name of the group.
As an example we will use the group name aws_accountname_a
. The solution implementation is built with the following assumptions that the group name encodes:
- A constant prefix for groups for AWS account access (the string
aws_
) - The account name where the group should be assigned to (the string
accountname
- any non white space characters between the_
characters of the whole group name) - A short name of the permission set (the string
_p
to refer to forAWSPowerUserAccess
permission set) that the group should be assigned with on the account
Prerequisites
- The user has an AWS account that has IAM Identity Center enabled.
- The user has IAM Identity Center configured to use an external identity provider for user and group federation.
- The external identity provider is configured, and set up for automatic provisioning, see the references section of this post.
- The user has adequate permissions to manage resources within the account (using CloudFormation or using other methods).
High Level Architecture
The following diagram shows a high level architecture of the solution.
Workflow of the Solution
The basic work flow of the solution is highlighted in the following steps:
- The external identity provider provisions a group within the AWS IAM Identity Center using the SCIM protocol between the identity provider and AWS IAM Identity Center, triggering an AWS CloudTrail event.
- A AWS EventBridge rule monitors for the event raised upon provisioning and triggers an AWS Lambda function, passing the event details to the function.
- The function decodes the group name from the event detail as it contains the desired accout name, the desired permission set name to be assigned to the group and performs the assignment within IAM Identity Center API.
- The process is logged in an AWS CloudWatch log group for auditing and debugging purposes.
- In case of failure an email will be dispatched.
Implementation
The solution is implemented in AWS CloudFormation but should be fairly portable to be implemented in other Infrastructure as Code frameworks such as Terraform, etc.
The main components are the EventBridge rule to listen for the desired event, the Lambda function and it's IAM permissions and the SNS component for sending email on failure.
CloudFormation Template
The solution ships as a single CloudFormation template that should be fairly easy to deploy to the AWS account that has Identity Center enabled.
Description: AWS SSO Automation Components
Parameters:
ManagedResourcePrefix:
Type: String
InstanceArn:
Type: String
SMTPNotifyAddress:
Type: String
TopicName:
Type: String
Default: "AssigmentTopic"
Resources:
NewSSOGroupEventRule:
Type: AWS::Events::Rule
Properties:
Description: Trigger for when a new SSO Group is propagated from external IdP via SCIM
EventPattern:
source:
- aws.sso-directory
detail-type:
- AWS API Call via CloudTrail
detail:
eventSource:
- sso-directory.amazonaws.com
eventName:
- CreateGroup
Targets:
- Arn: !GetAtt SsoAssignGroupsFunction.Arn
Id: sso-assign-group-function
ExecutionRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Principal:
Service: lambda.amazonaws.com
Action:
- sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
Policies:
- PolicyName: SSOandOrgPermissions
PolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action:
- sso-directory:Describe*
- sso-directory:Get*
- sso-directory:List*
- sso-directory:Describe*
- sso-directory:Search*
- sso:Describe*
- sso:Get*
- sso:List*
- sso:CreateAccountAssignment
- sso:ProvisionPermissionSet
- identitystore:List*
- identitystore:Describe*
- organizations:ListAccounts
- sns:Publish
Resource: '*'
AssignmentTopic:
Type: AWS::SNS::Topic
Properties:
Subscription:
- Endpoint: !Ref SMTPNotifyAddress
Protocol: "email"
TopicName: !Ref TopicName
SsoAssignGroupsFunction:
Type: AWS::Lambda::Function
Properties:
Code:
ZipFile: |
import re
import os
import logging
import json
from time import sleep
import boto3
import traceback
logger = logging.getLogger()
logger.setLevel(logging.DEBUG)
# This function assumes that groups are named like aws_<account-name>_<a|r|p>
# see the documentation of this function for details.
CONST_PREFIX = "aws"
g_account_pattern = rf"^{CONST_PREFIX}_(\S*)_(\S*)"
PSET_NAME_MAPPING_DICT = {
"r": "AWSReadOnlyAccess",
"a": "AWSAdministratorAccess",
"p": "AWSPowerUserAccess",
}
sso_admin_client = boto3.client("sso-admin")
org_client = boto3.client("organizations")
sns_resource = boto3.resource("sns")
def list_permission_sets(sso_instance_arn) -> dict:
"""Returns a dictionary of permissionssets for a given SSO Instance."""
perm_set_dict = {}
response = sso_admin_client.list_permission_sets(InstanceArn=sso_instance_arn)
results = response["PermissionSets"]
while "NextToken" in response:
response = sso_admin_client.list_permission_sets(
InstanceArn=sso_instance_arn, NextToken=response["NextToken"]
)
results.extend(response["PermissionSets"])
for permission_set in results:
perm_description = sso_admin_client.describe_permission_set(
InstanceArn=sso_instance_arn, PermissionSetArn=permission_set
)
perm_set_dict[perm_description["PermissionSet"]["Name"]] = permission_set
return perm_set_dict
def list_aws_accounts() -> list:
"""Returns a list of account dictionaries containing name id of each account"""
account_list = []
paginator = org_client.get_paginator("list_accounts")
page_iterator = paginator.paginate()
for page in page_iterator:
for acct in page["Accounts"]:
# only add active accounts
if acct["Status"] == "ACTIVE":
data = {"name": acct["Name"], "id": acct["Id"]}
account_list.append(data)
#logger.debug("List of accounts: %s", account_list)
return account_list
def lambda_handler(event, context):
logger.debug("Invoked with event: %s", event)
try:
group_display_name = event["detail"]["responseElements"]["group"]["displayName"]
if group_display_name == "":
logger.debug(
"Recieved SCIM CreateGroup event for roup name '%s'", group_display_name
)
raise Exception("Event did not contain the group display name property")
result = re.search(g_account_pattern, group_display_name)
print(result)
if not result:
logger.error(
"Security group: '%s' does not matching naming convention for account assignment. REGEX retourned matches: %s",
group_display_name, result,
)
raise Exception(
"Security group does not match convention for account assignment automation"
)
account_name = result.group(1)
short_name_pset = result.group(2) # "a" "p" or "r"
if short_name_pset not in PSET_NAME_MAPPING_DICT:
logger.error(
"Short name '%s' for permission set is not known", short_name_pset
)
raise Exception("Short name for permission set is not known")
permission_set_name = PSET_NAME_MAPPING_DICT[short_name_pset]
logger.debug(
"Searching for account '%s' and permission set '%s'",
account_name,
permission_set_name,
)
accounts = list_aws_accounts()
instance_arn = os.getenv("INSTANCE_ARN")
logger.info("IAM Identity Center Instance ARN is configured '%s'", instance_arn)
if instance_arn is None:
raise Exception("No IAM Idenity Center Instance ARN is configured.")
logger.debug("Found IAM Idenity Center instance arn '%s'", instance_arn)
permission_sets = list_permission_sets(instance_arn)
logger.debug("Permission sets found '%s'", permission_sets)
account_id, permission_set_arn, account_name = None, None, None
for account in accounts:
logger.info("Id of desired account is '%s' ", account["id"])
account_id = account.get("id")
account_name = account.get("name")
if account_id is None:
logger.error("Can't find desired account '%s'", account_name)
raise Exception("Clould not find account")
for name, arn in permission_sets.items():
if name == permission_set_name:
logger.info("ARN of desired permission set is '%s'", arn)
permission_set_arn = arn
if permission_set_arn is None:
logger.error("Can't find desired permission set %s", permission_set_arn)
raise Exception("Can't find desired permission set")
principal_id = event["detail"]["responseElements"]["group"]["groupId"]
logger.info("PrincipalId of the group is: %s", principal_id)
if principal_id is None:
logger.error("Could not retrieve the princiapal id of group %s", principal_id)
raise Exception("Could not retrieve the princial id of the group")
request = {
"InstanceArn": instance_arn,
"TargetId": account_id,
"TargetType": "AWS_ACCOUNT",
"PermissionSetArn": permission_set_arn,
"PrincipalType": "GROUP",
"PrincipalId": principal_id, #AWS IAM Identity Center group identifier
}
cracct_response = sso_admin_client.create_account_assignment(**request)
cracct_request_id = cracct_response["AccountAssignmentCreationStatus"][
"RequestId"
]
for tries in range(5):
# The docs explain the following valid status states "IN_PROGRESS"|"FAILED"|"SUCCEEDED"
ps_prov_set_status = (
sso_admin_client.describe_account_assignment_creation_status(
InstanceArn=instance_arn,
AccountAssignmentCreationRequestId=cracct_request_id,
)
)
logger.info("Assignment attempt %s", tries)
status = ps_prov_set_status["AccountAssignmentCreationStatus"]["Status"]
if status == "IN_PROGRESS":
logger.info("Assignment is in progress")
logger.info("Sleeping for 5 seconds")
sleep(5.0)
continue
if status == "FAILED":
logger.error("Account assigned has failed")
raise Exception("Account assignment has failed")
return
logger.info(
"SUCCESS: Security Group: %s assigned to Account: %s with permission set: %s",
group_display_name,
account_name,
permission_set_name,
)
except Exception as err:
message = {"Exception Details": str(err),
"event": event}
return message
sns_topic = os.getenv("SNS_TOPIC")
topic = sns_resource.Topic(sns_topic)
topic.publish(
Message=json.dumps(message),
Subject="Account Association Operation Failed: SSO Automation",
)
logger.info("Error notification sent via SNS")
Handler: 'assign_group_to_account.lambda_handler'
Role: !GetAtt ExecutionRole.Arn
Runtime: 'python3.9'
MemorySize: 128
Timeout: 900
Environment:
Variables:
SNS_TOPIC: !Ref AssignmentTopic
INSTANCE_ARN: !Ref InstanceArn
EventsFunctionPermission:
Type: AWS::Lambda::Permission
Properties:
Action: lambda:InvokeFunction
FunctionName: !GetAtt SsoAssignGroupsFunction.Arn
Principal: events.amazonaws.com
SourceArn: !GetAtt NewSSOGroupEventRule.Arn
References
See AWS Identity Center Automated Provisioning Docs for details on how to set a external Identity Provider configuration for provisioning.
Top comments (0)