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 recomend checking their offering first.
sequenceDiagram
Requester->>Slack: submits form in Slack - CMD+K, search access or /access commad
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 assigment 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.
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.
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,
},
]
}
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
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.1.0 |
external | 2.3.1 |
null | 3.2.1 |
random | 3.5.1 |
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 |
sso_elevator_bucket | terraform-aws-modules/s3-bucket/aws | 3.10.1 |
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_scheduler_schedule_group.one_time_schedule_group | resource |
aws_sns_topic.dlq | resource |
aws_sns_topic_subscription.dlq | resource |
null_resource.python_version_check | 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 |
external_external.check_python_version | 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 |
n/a | yes |
build_in_docker | Whether to build the lambda in a docker container or using local python (poetry) | bool |
true |
no |
config | value for the SSO Elevator config | any |
n/a | yes |
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 |
log_level | value for the log level | string |
"INFO" |
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 bucket partitions | 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 |
"event-bridge-role-for-sso-elevator" |
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 |
Name | Description |
---|---|
lambda_function_url | value for the access_requester lambda function URL |