Skip to content

fivexl/terraform-aws-sso-elevator

Repository files navigation

FivexL

Terraform module for implementing temporary elevated access via AWS IAM Identity Center (Successor to AWS Single Sign-On) and Slack

Introduction

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.

Watch demo Demo

Functionality

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.

Important Considerations and Assumptions

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.

Deployment and Usage

Note on dependencies

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.

Module configuration options and automatic approval

Configuration structure

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 and ResourceType.
  • 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 to None.
  • 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.

Explicit Deny

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.

Automatic Approval

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.

Aggregation of Rules

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.

Single Approver

If there is only one approver and AllowSelfApproval is not set to true, nobody will be able to approve the request.

Diagram of processing a request:

Diagram of processing a request

Terraform deployment example

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
}

Slack App creation

  1. Go to https://api.slack.com/
  2. Click create an app
  3. Click From an app manifest
  4. Select workspace, click next
  5. Choose yaml for app manifest format
  6. Update lambda url (from output aws_sso_elevator_lambda_function_url) to request_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
  1. Check permissions and click create
  2. Click install to workspace
  3. Copy Signing Secret # for slack_signing_secret module input
  4. Copy Bot User OAuth Token # for slack_bot_token module input

Terraform docs

Requirements

Name Version
terraform ~> 1.0
aws >= 4.64
external >= 1.0
local >= 1.0
null >= 2.0
random >= 3.0

Providers

Name Version
aws 5.1.0
external 2.3.1
null 3.2.1
random 3.5.1

Modules

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

Resources

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

Inputs

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
{
"rule": {
"default_retention": {
"mode": "GOVERNANCE",
"years": 2
}
}
}
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

Outputs

Name Description
lambda_function_url value for the access_requester lambda function URL

More info

Development

Post review

  • Post review url
  • ToC generated with this