Terraform module for implementing temporary elevated access via AWS IAM Identity Center (Successor to AWS Single Sign-On) and Slack
- Terraform module for implementing temporary elevated access via AWS IAM Identity Center (Successor to AWS Single Sign-On) and Slack
- Introduction
- Functionality
- Important Considerations and Assumptions
- Deployment and Usage
- Terraform docs
- Development
Currently, AWS IAM Identity Center does not support the temporary assignment of permission sets to users. As a result, teams using AWS IAM Identity Center are forced to either create highly restricted permission sets or rely on AWS IAM role chaining. Both approaches have significant drawbacks and result in an overly complex security model. The desired solution is one where AWS operators are granted access only when necessary and for the exact duration needed, with a default state of no access or read-only access.
The terraform-aws-sso-elevator module addresses this issue by allowing the implementation of temporary elevated access to AWS accounts while avoiding permanently assigned permission sets, thereby achieving the principle of least privilege access.
For more information on temporary elevated access for AWS and the AWS-provided solution, visit Managing temporary elevated access to your AWS environment.
The key difference between the terraform-aws-sso-elevator module and the option described in the blog post above is that the module enables requesting access elevation via a Slack form. We hope that this implementation may inspire AWS to incorporate native support for temporary access elevation in AWS IAM Identity Center.
AWS announced that Customers of AWS IAM Identity Center (successor to AWS Single Sign-On) can use CyberArk Secure Cloud Access, Ermetic, and Okta Access Requests for temporary elevated access. So if you are already using one of those vendors we recommend checking their offering first.
sequenceDiagram
Requester->>Slack: submits form in Slack - CMD+K, search access or /access command
Slack->>AWS Lambda - Access Requester: sends request to access-requester
AWS Lambda - Access Requester->>Slack: sends a message to Slack channel with approve/deny buttons and tags approvers
Approver->>Slack: pressed approve button in Slack message
Slack->>AWS Lambda - Access Requester: Send approved request to access-requester
AWS Lambda - Access Requester->>AWS IAM Identity Center(SSO): creates user-level permission set assignment based on approved request
AWS Lambda - Access Requester->>AWS EventBridge: creates revocation schedule
AWS Lambda - Access Requester->>AWS S3: logs audit record
AWS EventBridge->>AWS Lambda - Access Revoker: sends revocation event when times come
AWS Lambda - Access Revoker->>AWS IAM Identity Center(SSO): revokes user-level permission set assignment
AWS Lambda - Access Revoker->>AWS S3: logs audit record
AWS Lambda - Access Revoker->>Slack: send notification about revocation
The module deploys two AWS Lambda functions: access-requester and access-revoker. The access-requester handles requests from Slack, creating user-level permission set assignments and an Amazon EventBridge trigger that activates the access-revoker Lambda when it is time to revoke access. The access-revoker revokes user access when triggered by EventBridge and also runs daily to revoke any user-level permission set assignments without an associated EventBridge trigger. Group-level permission sets are not affected.
For auditing purposes, information about all access grants and revocations is stored in S3. See documentation here to find out how to configure AWS Athena to query audit logs.
Additionally, the Access-Revoker continuously reconciles the revocation schedule with all user-level permission set assignments and issues warnings if it detects assignments without a revocation schedule (presumably created by someone manually). By default, the Access-Revoker will automatically revoke all unknown user-level permission set assignments daily. However, you can configure it to operate more or less frequently.
Starting from version 2.0, Terraform AWS SSO Elevator introduces support for group access. SSO elevator now can add users to a groups, to do so, you will need to use /group-access command, which, instead of showing the form for account assignments, will present a Slack form where the user can select a group they want access to, specify a reason, and define the duration for which access is required.
The basic logic for access, configuration, and Slack integration remains the same as before. To enable the new Group Assignments Mode, you need to provide the module with a new group_config Terraform variable:
group_config = [
{
"Resource" : ["99999999-8888-7777-6666-555555555555"], #ManagementAccountAdmins
"Approvers" : [
"email@gmail.com"
]
"ApprovalIsNotRequired": true
},
{
"Resource" : ["11111111-2222-3333-4444-555555555555"], #prod read only
"Approvers" : [
"email@gmail.com"
]
"AllowSelfApproval" : true,
},
{
"Resource" : ["44445555-3333-2222-1111-555557777777"], #ProdAdminAccess
"Approvers" : [
"email@gmail.com"
]
},
]
There are two key differences compared to the standard Elevator configuration:
- ResourceType is not required for group access configurations.
- In the Resource field, you must provide group IDs instead of account IDs.
The Elevator will only work with groups specified in the configuration.
If you were using Terraform AWS SSO Elevator before version 2.0.0, you need to update your Slack app manifest by adding a new shortcut to enable this functionality: { "name": "group-access", "type": "global", "callback_id": "request_for_group_membership", "description": "Request access to SSO Group" } To disable this functionality, simply remove the shortcut from the manifest.
SSO elevator assumes that your Slack user email will match SSO user id otherwise it won't be able to match Slack user sending request to an AWS SSO user.
When onboarding your organization, be aware that the access-revoker will revoke all user-level Permission Set assignments in the AWS accounts you specified in the module configuration. If you specify Accounts: '*' in any of rules, it will remove user-level assignments from all accounts. Therefore, if you want to maintain some permanent SSO assignments (e.g., read-only in production and admin in development or test accounts), you should use group-level assignments. It is advisable to ensure your AWS admin has the necessary access level to your AWS SSO management account through group-level assignments so that you can experiment with the module's configuration.
Lambdas are built using Python 3.10 and rely on Poetry for package management and dependency resolution. To run Terraform, both Python 3.10 and Poetry need to be installed on your system. If these tools are not available, you can opt to package the Lambdas using Docker by providing the appropriate flag to the module. We do recommend using Docker build where possible to avoid misconfigurations or missing packages.
The deployment process is divided into two main parts: deploying the Terraform module, which sets up the necessary infrastructure and resources for the Lambdas to function, and creating a Slack App, which will be the interface through which users can interact with the Lambdas. Detailed instructions on how to perform both of these steps, along with the Slack App manifest, can be found below.
In 1.4.0 release, the ability to build zip files locally without Docker has been removed due to issues with Python environments and version mismatches. Now, GitHub CI will pre-build the requester and revoker lambda Docker images and push them to FivexL's private ECR. Users can use these pre-built Docker images to build lambdas.
ECR is private for the following reasons:
- AWS Lambda can't use any other source of images except ECR.
- AWS Lambda can't use public ECR.
- AWS Lambda doesn't support pulling container images from Amazon ECR using a pull-through cache rule (so we can't create a private repo from the user's side to pull images from the GHCR, for example).
Images and repositories are replicated in every region that AWS SSO supports except these:
- ap_east_1
- eu_south_1
- ap_southeast_3
- af_south_1
- me_south_1
- il_central_1
- me_central_1
- eu_south_2
- ap_south_2
- eu_central_2
- ap_southeast_4
- ca_west_1
- us_gov_east_1
- us_gov_west_1
Those regions are not enabled by default. If you need to use a region that is not supported by the module, please let us know by creating an issue, and we will add support for it.
Conclusion: Now there are only two ways to build an SSO elevator:
Using pre-created images pulled from ECR (Default) Using Docker build to build images locally (provide the variable use_pre_created_image = false) There is also an option to host ECR yourself by providing the following variables:
ecr_repo_name = "example_repo_name"
ecr_owner_account_id = "<example_account_id>"
To address the lambda-1 SecurityHub control alert triggered by the default creation of a FunctionURLAllowPublicAccess resource-based policy for lambda, in 1.4.0 release module will eventually migrate to the usage of API Gateway by default. You still can use lambda URL to seamlessly migrate to the API Gateway url, but it is deprecated and will be removed in future releases. You can use the following variables to control the behavior:
create_api_gateway = true # This will create an API Gateway for the requester lambda
create_lambda_url = false # This will delete lambda url
To fix the Security Hub issue when migrating to API Gateway, manually delete the FunctionURLAllowPublicAccess policy statement in the AWS Console. After updating the module, you can find the API URL in the output of the module. Please don't forget to update the Slack App manifest with the new URL.
The configuration is a list of dictionaries, where each dictionary represents a single configuration rule.
Each configuration rule specifies which resource(s) the rule applies to, which permission set(s) are being requested, who the approvers are, and any additional options for approving the request.
The fields in the configuration dictionary are:
- ResourceType: This field specifies the type of resource being requested, such as "Account." As of now, the only supported value is "Account."
- Resource: This field defines the specific resource(s) being requested. It accepts either a single string or a list of strings. Setting this field to "*" allows the rule to match all resources associated with the specified
ResourceType
. - PermissionSet: Here, you indicate the permission set(s) being requested. This can be either a single string or a list of strings. If set to "*", the rule matches all permission sets available for the defined
Resource
andResourceType
. - Approvers: This field lists the potential approvers for the request. It accepts either a single string or a list of strings representing different approvers.
- AllowSelfApproval: This field can be a boolean, indicating whether the requester, if present in the
Approvers
list, is permitted to approve their own request. It defaults toNone
. - ApprovalIsNotRequired: This field can also be a boolean, signifying whether the approval can be granted automatically, bypassing the approvers entirely. The default value is
None
.
In the system, an explicit denial in any statement overrides any approvals. For instance, if one statement designates an individual as an approver for all accounts, but another statement specifies that the same individual is not allowed to self-approve or to bypass the approval process for a particular account and permission set (by setting "allow_self_approval" and "approval_is_not_required" to False
), then that individual will not be able to approve requests for that specific account, thereby enforcing a stricter control.
Requests will be approved automatically if either of the following conditions are met:
- AllowSelfApproval is set to true and the requester is in the Approvers list.
- ApprovalIsNotRequired is set to true.
The approval decision and final list of reviewers will be calculated dynamically based on the aggregate of all rules. If you have a rule that specifies that someone is an approver for all accounts, then that person will be automatically added to all requests, even if there are more detailed rules for specific accounts or permission sets.
If there is only one approver and AllowSelfApproval is not set to true, nobody will be able to approve the request.
data "aws_ssoadmin_instances" "this" {}
# You will have to create /sso-elevator/slack-signing-secret AWS SSM Parameter
# and store Slack app signing secret there, if you have not created app yet then
# you can leave a dummy value there and update it after Slack app is ready
data "aws_ssm_parameter" "sso_elevator_slack_signing_secret" {
name = "/sso-elevator/slack-signing-secret"
}
# You will have to create /sso-elevator/slack-bot-token AWS SSM Parameter
# and store Slack bot token there, if you have not created app yet then
# you can leave a dummy value there and update it after Slack app is ready
data "aws_ssm_parameter" "sso_elevator_slack_bot_token" {
name = "/sso-elevator/slack-bot-token"
}
module "aws_sso_elevator" {
source = "github.com/fivexl/terraform-aws-sso-elevator.git"
aws_sns_topic_subscription_email = "email@gmail.com"
slack_signing_secret = data.aws_ssm_parameter.sso_elevator_slack_signing_secret.value
slack_bot_token = data.aws_ssm_parameter.sso_elevator_slack_bot_token.value
slack_channel_id = "***********"
schedule_expression = "cron(0 23 * * ? *)" # revoke access schedule expression
schedule_expression_for_check_on_inconsistency = "rate(1 hour)"
build_in_docker = true
revoker_post_update_to_slack = true
# The initial wait time before the first re-notification to the approver is sent.
approver_renotification_initial_wait_time = 15
# The multiplier applied to the wait time for each subsequent notification sent to the approver.
# Default is 2, which means the wait time will double for each attempt.
approver_renotification_backoff_multiplier = 2
sso_instance_arn = one(data.aws_ssoadmin_instances.this.arns)
# If you wish to use your own S3 bucket for audit_entry logs,
# specify its name here:
s3_name_of_the_existing_bucket = "your-s3-bucket-name"
# If you do not provide a value for s3_name_of_the_existing_bucket,
# the module will create a new bucket with the default name 'sso-elevator-audit-entry':
s3_bucket_name_for_audit_entry = "fivexl-sso-elevator"
# The default partition prefix is "logs/":
s3_bucket_partition_prefix = "some_prefix/"
# MFA delete setting for the S3 bucket:
s3_mfa_delete = false
# Object lock setting for the S3 bucket:
s3_object_lock = true
# The default object lock configuration is as follows:
# {
# rule = {
# default_retention = {
# mode = "GOVERNANCE"
# years = 2
# }
# }
#}
# You can specify a different configuration here:
s3_object_lock_configuration = {
rule = {
default_retention = {
mode = "GOVERNANCE"
years = 1
}
}
}
# Here, you can specify the target_bucket and prefix for access logs of the sso_elevator bucket.
# If s3_logging is not specified, logs will not be written:
s3_logging = {
target_bucket = "some_access_logging_bucket"
target_prefix = "some_prefix_for_access_logs"
}
config = [
# This could be a config for dev/stage account where developers can self-serve
# permissions
# Allows Bob and Alice to approve requests for all
# PermissionSets in accounts dev_account_id and stage_account_id as
# well as approve its own requests
# You have to specify at AllowSelfApproval: true or specify two approvers
# so you do not lock out approver
{
"ResourceType" : "Account",
"Resource" : ["dev_account_id", "stage_account_id"],
"PermissionSet" : "*",
"Approvers" : ["bob@corp.com", "alice@corp.com"],
"AllowSelfApproval" : true,
},
# This could be an option for a financial person
# allows self approval for Billing PermissionSet
# for account_id for user finances@corp.com
{
"ResourceType" : "Account",
"Resource" : "account_id",
"PermissionSet" : "Billing",
"Approvers" : "finances@corp.com",
"AllowSelfApproval" : true,
},
# Your typical CTO - can approve all accounts and all permissions
# as well as his/hers own requests to avoid lock out
# Careful withi Resource * since it will cause revocation of all
# non-module-created user-level permission set assignments in all
# accounts, add this one later when you are done with single account
# testing
{
"ResourceType" : "Account",
"Resource" : "*",
"PermissionSet" : "*",
"Approvers" : "cto@corp.com",
"AllowSelfApproval" : true,
},
# Read only config for production accounts so developers
# can check prod when needed
{
"ResourceType" : "Account",
"Resource" : ["prod_account_id", "prod_account_id2"],
"PermissionSet" : "ReadOnly",
"AllowSelfApproval" : true,
},
# Prod access
{
"ResourceType" : "Account",
"Resource" : ["prod_account_id", "prod_account_id2"],
"PermissionSet" : "AdministratorAccess",
"Approvers" : ["manager@corp.com", "ciso@corp.com"],
"ApprovalIsNotRequired" : false,
"AllowSelfApproval" : false,
},
# example of list being used for permissions sets
{
"ResourceType" : "Account",
"Resource" : "account_id",
"PermissionSet" : ["ReadOnlyPlus", "AdministratorAccess"],
"Approvers" : ["ciso@corp.com"],
"AllowSelfApproval" : true,
},
]
group_config = [
{
"Resource" : ["99999999-8888-7777-6666-555555555555"], #ManagementAccountAdmins
"Approvers" : [
"email@gmail.com"
]
"ApprovalIsNotRequired": true
},
{
"Resource" : ["11111111-2222-3333-4444-555555555555"], #prod read only
"Approvers" : [
"email@gmail.com"
]
"AllowSelfApproval" : true,
},
{
"Resource" : ["44445555-3333-2222-1111-555557777777"], #ProdAdminAccess
"Approvers" : [
"email@gmail.com"
]
},
]
}
output "aws_sso_elevator_lambda_function_url" {
value = module.aws_sso_elevator.lambda_function_url
}
- Go to https://api.slack.com/
- Click
create an app
- Click
From an app manifest
- Select workspace, click
next
- Choose
yaml
for app manifest format - Update lambda url (from output
aws_sso_elevator_lambda_function_url
) torequest_url
field and paste the following into the text box:
display_information:
name: AWS SSO Access Elevator
description: Slack bot to temporary assign AWS SSO Permission set to a user
features:
bot_user:
display_name: AWS SSO Access Elevator
always_online: false
shortcuts:
- name: access
type: global
callback_id: request_for_access
description: Request access to Permission Set in AWS Account
- name: group-access # Delete this shortcut if you want to prohibit access to the Group Assignments Mode
type: global
callback_id: request_for_group_membership
description: Request access to SSO Group
oauth_config:
scopes:
bot:
# 'commands': This permission adds shortcuts and/or slash commands that people can use.
- commands
# 'chat:write': This permission is required for the app to post messages to Slack.
- chat:write
# 'users:read' and 'users:read.email': These permissions are required for the app to find the user's email address, which is necessary for creating AWS account assignments and including user mentions in requests.
- users:read.email
- users:read
# 'channels:history': This permission is needed for the app to find old messages in order to handle "discard button" events.
- channels:history
settings:
interactivity:
is_enabled: true
request_url: <LAMBDA URL GOES HERE - CHECK LAMBDA CONFIGURATION IN AWS CONSOLE OR GET IT FORM TERRAFORM OUTPUT>
org_deploy_enabled: false
socket_mode_enabled: false
token_rotation_enabled: false
- Check permissions and click
create
- Click
install to workspace
- Copy
Signing Secret
# forslack_signing_secret
module input - Copy
Bot User OAuth Token
# forslack_bot_token
module input
Name | Version |
---|---|
terraform | ~> 1.0 |
aws | >= 4.64 |
external | >= 1.0 |
local | >= 1.0 |
null | >= 2.0 |
random | >= 3.0 |
Name | Version |
---|---|
aws | 5.65.0 |
random | 3.6.2 |
Name | Source | Version |
---|---|---|
access_requester_slack_handler | terraform-aws-modules/lambda/aws | 4.16.0 |
access_revoker | terraform-aws-modules/lambda/aws | 4.16.0 |
audit_bucket | fivexl/account-baseline/aws//modules/s3_baseline | 1.3.2 |
http_api | terraform-aws-modules/apigateway-v2/aws | 5.0.0 |
sso_elevator_dependencies | terraform-aws-modules/lambda/aws | 4.16.0 |
Name | Type |
---|---|
aws_cloudwatch_event_rule.sso_elevator_check_on_inconsistency | resource |
aws_cloudwatch_event_rule.sso_elevator_scheduled_revocation | resource |
aws_cloudwatch_event_target.check_inconsistency | resource |
aws_cloudwatch_event_target.sso_elevator_scheduled_revocation | resource |
aws_iam_role.eventbridge_role | resource |
aws_iam_role_policy.eventbridge_policy | resource |
aws_lambda_permission.eventbridge | resource |
aws_lambda_permission.url | resource |
aws_scheduler_schedule_group.one_time_schedule_group | resource |
aws_sns_topic.dlq | resource |
aws_sns_topic_subscription.dlq | resource |
random_string.random | resource |
aws_caller_identity.current | data source |
aws_iam_policy_document.revoker | data source |
aws_iam_policy_document.slack_handler | data source |
aws_region.current | data source |
aws_ssoadmin_instances.all | data source |
Name | Description | Type | Default | Required |
---|---|---|---|---|
approver_renotification_backoff_multiplier | The multiplier applied to the wait time for each subsequent notification sent to the approver. Default is 2, which means the wait time will double for each attempt. | number |
2 |
no |
approver_renotification_initial_wait_time | The initial wait time before the first re-notification to the approver is sent. This is measured in minutes. If set to 0, no re-notifications will be sent. | number |
15 |
no |
aws_sns_topic_subscription_email | value for the email address to subscribe to the SNS topic | string |
"" |
no |
config | value for the SSO Elevator config | any |
[] |
no |
create_api_gateway | If true, module will create & configure API Gateway for the Lambda function | bool |
true |
no |
create_lambda_url | If true, the Lambda function will continue to use the Lambda URL, which will be deprecated in the future If false, Lambda url will be deleted. |
bool |
true |
no |
ecr_owner_account_id | In what account is the ECR repository located. | string |
"222341826240" |
no |
ecr_repo_name | The name of the ECR repository. | string |
"aws-sso-elevator" |
no |
event_brige_check_on_inconsistency_rule_name | value for the event bridge check on inconsistency rule name | string |
"sso-elevator-check-on-inconsistency" |
no |
event_brige_scheduled_revocation_rule_name | value for the event bridge scheduled revocation rule name | string |
"sso-elevator-scheduled-revocation" |
no |
group_config | value for the SSO Elevator group config | any |
[] |
no |
log_level | value for the log level | string |
"INFO" |
no |
logs_retention_in_days | The number of days you want to retain log events in the log group for both Lambda functions and API Gateway. | number |
365 |
no |
max_permissions_duration_time | Maximum duration of the permissions granted by the Elevator in hours. | number |
24 |
no |
request_expiration_hours | After how many hours should the request expire? If set to 0, the request will never expire. | number |
8 |
no |
requester_lambda_name | value for the requester lambda name | string |
"access-requester" |
no |
revoker_lambda_name | value for the revoker lambda name | string |
"access-revoker" |
no |
revoker_post_update_to_slack | Should revoker send a confirmation of the revocation to Slack? | bool |
true |
no |
s3_bucket_name_for_audit_entry | Unique name of the S3 bucket | string |
"sso-elevator-audit-entry" |
no |
s3_bucket_partition_prefix | The prefix for the S3 audit bucket object partitions. Don't use slashes (/) in the prefix, as it will be added automatically, e.g. "logs" will be transformed to "logs/". If you want to use the root of the bucket, leave this empty. |
string |
"logs" |
no |
s3_logging | Map containing access bucket logging configuration. | map(string) |
{} |
no |
s3_mfa_delete | Whether to enable MFA delete for the S3 bucket | bool |
false |
no |
s3_name_of_the_existing_bucket | Specify the name of an existing S3 bucket to use. If not provided, a new bucket will be created. | string |
"" |
no |
s3_object_lock | Enable object lock | bool |
false |
no |
s3_object_lock_configuration | Object lock configuration | any |
{ |
no |
schedule_expression | recovation schedule expression (will revoke all user-level assignments unknown to the Elevator) | string |
"cron(0 23 * * ? *)" |
no |
schedule_expression_for_check_on_inconsistency | how often revoker should check for inconsistency (warn if found unknown user-level assignments) | string |
"rate(2 hours)" |
no |
schedule_group_name | value for the schedule group name | string |
"sso-elevator-scheduled-revocation" |
no |
schedule_role_name | value for the schedule role name | string |
"sso-elevator-event-bridge-role" |
no |
slack_bot_token | value for the Slack bot token | string |
n/a | yes |
slack_channel_id | value for the Slack channel ID | string |
n/a | yes |
slack_signing_secret | value for the Slack signing secret | string |
n/a | yes |
sso_instance_arn | value for the SSO instance ARN | string |
"" |
no |
tags | A map of tags to assign to resources. | map(string) |
{} |
no |
use_pre_created_image | If true, the image will be pulled from the ECR repository. If false, the image will be built using Docker from the source code. | bool |
true |
no |
Name | Description |
---|---|
lambda_function_url | value for the access_requester lambda function URL |
requester_api_endpoint_url | The full URL to invoke the API. Pass this URL into the Slack App manifest as the Request URL. |
sso_elevator_bucket_id | The name of the SSO elevator bucket. |