├── .cfnlintrc ├── .editorconfig ├── .gitattributes ├── .github ├── PULL_REQUEST_TEMPLATE.md └── dependabot.yml ├── .gitignore ├── .pre-commit-config.yaml ├── CODEOWNERS ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── dependencies └── requirements.txt ├── doc ├── architecture.drawio └── architecture.png ├── events └── CreateManagedAccount.json ├── mypy.ini ├── pyproject.toml ├── requirements-dev.txt ├── src ├── regional │ ├── account_setup │ │ ├── lambda_handler.py │ │ ├── resources │ │ │ ├── __init__.py │ │ │ ├── ec2.py │ │ │ ├── ecs.py │ │ │ └── sts.py │ │ └── schemas.py │ └── requirements.txt ├── service_catalog_portfolio │ ├── account_setup │ │ ├── __init__.py │ │ ├── lambda_handler.py │ │ ├── resources │ │ │ ├── __init__.py │ │ │ ├── iam.py │ │ │ ├── servicecatalog.py │ │ │ └── sts.py │ │ └── schemas.py │ └── requirements.txt └── sso_assignment │ ├── account_setup │ ├── __init__.py │ ├── constants.py │ ├── lambda_handler.py │ ├── resources │ │ ├── __init__.py │ │ ├── identity_store.py │ │ ├── organizations.py │ │ └── sso.py │ └── utils.py │ └── requirements.txt └── template.yml /.cfnlintrc: -------------------------------------------------------------------------------- 1 | templates: 2 | - template.yml 3 | include_checks: 4 | - I 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | end_of_line = lf 10 | max_line_length = 120 11 | 12 | [*.json] 13 | indent_size = 2 14 | quote_type = double 15 | 16 | [*.yml] 17 | quote_type = double 18 | 19 | [*.py] 20 | quote_type = double 21 | indent_size = 4 22 | 23 | [Makefile] 24 | indent_style = tab -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | *.py text eol=lf 3 | *.yml text eol=lf 4 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | _Issue #, if available:_ 2 | 3 | _Description of changes:_ 4 | 5 | By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice. 6 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "pip" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | open-pull-requests-limit: 1 8 | commit-message: 9 | prefix: chore 10 | include: scope 11 | groups: 12 | pip-dependencies: 13 | applies-to: version-updates 14 | update-types: [minor, patch] 15 | patterns: 16 | - "*" 17 | - package-ecosystem: "github-actions" 18 | directory: "/" 19 | schedule: 20 | interval: "weekly" 21 | open-pull-requests-limit: 1 22 | commit-message: 23 | prefix: chore 24 | include: scope 25 | groups: 26 | github-action-dependencies: 27 | applies-to: version-updates 28 | update-types: [minor, patch] 29 | patterns: 30 | - "*" 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .venv 2 | .vscode 3 | .aws-sam 4 | *.zip 5 | *.bkp 6 | .DS_Store 7 | samconfig.toml 8 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/aws-cloudformation/cfn-lint 3 | rev: v0.83.6 4 | hooks: 5 | - id: cfn-lint-rc 6 | - repo: https://github.com/psf/black 7 | rev: 23.12.0 8 | hooks: 9 | - id: black 10 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @aws-samples/aws-startup-sa-fintech 2 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | 3 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 4 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 5 | opensource-codeofconduct@amazon.com with any additional questions or comments. 6 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | ## Reporting Bugs/Feature Requests 10 | 11 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 12 | 13 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already 14 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 15 | 16 | - A reproducible test case or series of steps 17 | - The version of our code being used 18 | - Any modifications you've made relevant to the bug 19 | - Anything unusual about your environment or deployment 20 | 21 | ## Contributing via Pull Requests 22 | 23 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 24 | 25 | 1. You are working against the latest source on the _main_ branch. 26 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 27 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 28 | 29 | To send us a pull request, please: 30 | 31 | 1. Fork the repository. 32 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 33 | 3. Ensure local tests pass. 34 | 4. Commit to your fork using clear commit messages. 35 | 5. Send us a pull request, answering any default questions in the pull request interface. 36 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 37 | 38 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 39 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 40 | 41 | ## Finding contributions to work on 42 | 43 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start. 44 | 45 | ## Code of Conduct 46 | 47 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 48 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 49 | opensource-codeofconduct@amazon.com with any additional questions or comments. 50 | 51 | ## Security issue notifications 52 | 53 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 54 | 55 | ## Licensing 56 | 57 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 58 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 10 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 11 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 12 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 13 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 14 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | 16 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: setup build deploy format create-signing-profile clean 2 | 3 | setup: 4 | python3 -m venv .venv 5 | .venv/bin/python3 -m pip install -U pip 6 | .venv/bin/python3 -m pip install -r requirements-dev.txt 7 | .venv/bin/python3 -m pip install -r dependencies/requirements.txt 8 | .venv/bin/pre-commit install 9 | 10 | create-signing-profile: 11 | aws signer put-signing-profile --platform-id "AWSLambda-SHA384-ECDSA" --profile-name AccountSetupProfile 12 | 13 | build: 14 | sam build -u 15 | 16 | deploy: 17 | sam deploy \ 18 | --signing-profiles \ 19 | SSOAssignmentFunction=AccountSetupProfile \ 20 | ServiceCatalogPortfolioFunction=AccountSetupProfile \ 21 | RegionalFunction=AccountSetupProfile \ 22 | DependencyLayer=AccountSetupProfile \ 23 | --tags "GITHUB_ORG=aws-samples GITHUB_REPO=aws-control-tower-account-setup-using-step-functions" 24 | 25 | clean: 26 | sam delete 27 | 28 | format: 29 | .venv/bin/black . 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Automate activities in Control Tower provisioned AWS accounts 2 | 3 | ### Table of contents 4 | 5 | 1. [Introduction](#introduction) 6 | 2. [Architecture](#architecture) 7 | 3. [Prerequisites](#prerequisites) 8 | 4. [Tools and services](#tools-and-services) 9 | 5. [Usage](#usage) 10 | 6. [Clean up](#clean-up) 11 | 7. [Reference](#reference) 12 | 8. [Contributing](#contributing) 13 | 9. [License](#license) 14 | 15 | ## Introduction 16 | 17 | This project will configure the following settings on a new AWS account provisioned by [AWS Control Tower](https://aws.amazon.com/controltower/): 18 | 19 | 1. Deletes the [default VPC](https://docs.aws.amazon.com/vpc/latest/userguide/default-vpc.html) in every region 20 | 2. Adds a CloudWatch Logs resource policy that allows Route53 to [log DNS requests](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/query-logs.html) to CloudWatch in the us-east-1 (Northern Virginia) region 21 | 3. Enables the account-wide public [S3 block setting](https://docs.aws.amazon.com/AmazonS3/latest/userguide/configuring-block-public-access-account.html) 22 | 4. Modifies account-level ECS [settings](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs-account-settings.html) 23 | 5. Associates [specific principals](https://docs.aws.amazon.com/servicecatalog/latest/adminguide/catalogs_portfolios_users.html) to shared AWS Service Catalog portfolios 24 | 6. Grants specific AWS SSO [groups](https://docs.aws.amazon.com/singlesignon/latest/userguide/users-groups-provisioning.html) access to the new account 25 | 7. Blocks [public SSM document sharing](https://docs.aws.amazon.com/systems-manager/latest/userguide/ssm-share-block.html) 26 | 8. Enables [EBS encryption by default](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/EBSEncryption.html#encryption-by-default) 27 | 9. Applies an [IAM password policy](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_passwords_account-policy.html) that complies with the [CIS AWS Foundations Benchmark](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-cis-controls.html) 28 | 29 | ## Architecture 30 | 31 | ![architecture](doc/architecture.png) 32 | 33 | 1. When [AWS Control Tower](https://aws.amazon.com/controltower/) provisions a new account, a [CreateManagedAccount](https://docs.aws.amazon.com/controltower/latest/userguide/lifecycle-events.html#create-managed-account) event is sent to the [Amazon EventBridge](https://aws.amazon.com/eventbridge/) default event bus. 34 | 2. An Amazon EventBridge rule matches the `CreateManagedAccount` event and triggers an [AWS Step Functions](https://aws.amazon.com/step-functions/) state machine that executes [AWS Lambda](https://aws.amazon.com/lambda/) functions. 35 | 3. Step Functions assumes the `AWSControlTowerExecution` IAM role in the new account and uses the [AWS SDK service integration](https://docs.aws.amazon.com/step-functions/latest/dg/supported-services-awssdk.html) to set the password policy using `iam:UpdateAccountPasswordPolicy`, adds the account-level [S3 public block setting](https://docs.aws.amazon.com/AmazonS3/latest/userguide/configuring-block-public-access-account.html), creates a CloudWatch Logs resource policy in the us-east-1 region that allows Route 53 to write DNS [query logs](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/query-logs.html#query-logs-configuring) to CloudWatch 36 | 4. Step Functions then uses the [AWS SDK service integration](https://docs.aws.amazon.com/step-functions/latest/dg/supported-services-awssdk.html) to call `ec2:DescribeRegions` to get a list of regions 37 | 5. The "Regional Lambda" function assumes the `AWSControlTowerExecution` IAM role in the new account and enables various ECS [settings](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs-account-settings.html), deletes the [default VPC](https://docs.aws.amazon.com/vpc/latest/userguide/default-vpc.html), enables [EBS encryption by default](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/EBSEncryption.html#encryption-by-default), and blocks [public SSM document sharing](https://docs.aws.amazon.com/systems-manager/latest/userguide/ssm-share-block.html) from all regions 38 | 6. The "Portfolio Share Lambda" function assumes the `AWSControlTowerExecution` IAM role in the new account and accepts shared Service Catalog portfolios in the new account and grants specific principals access to those portfolios. 39 | 7. The "SSO Group Assignment Lambda" function assigns any AWS SSO groups following the convention `AWS-O-` access to the new account with the `` permission set. The groups are defined in the `OrganizationGroups` CloudFormation stack parameter. 40 | 41 | ## Prerequisites 42 | 43 | - [Python 3](https://www.python.org/downloads/), installed 44 | - [AWS Command Line Interface (AWS CLI)](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) version 2, installed 45 | - [AWS Serverless Application Model (SAM)](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-getting-started.html), installed 46 | - [Docker Desktop](https://www.docker.com/products/docker-desktop), installed 47 | 48 | ## Tools and services 49 | 50 | - [AWS SAM](https://aws.amazon.com/serverless/sam/) - The AWS Serverless Application Model (SAM) is an open-source framework for building serverless applications. It provides shorthand syntax to express functions, APIs, databases, and event source mappings. 51 | - [AWS Lambda](https://aws.amazon.com/lambda/) - AWS Lambda is a serverless compute service that lets you run code without provisioning or managing servers, creating workload-aware cluster scaling logic, maintaining event integrations, or managing runtimes. 52 | - [AWS Control Tower](https://aws.amazon.com/controltower/) - AWS Control Tower provides the easiest way to set up and govern a secure, multi-account AWS environment, called a landing zone. 53 | - [AWS Organizations](https://aws.amazon.com/organizations/) - AWS Organizations helps you centrally manage and govern your environment as you grow and scale your AWS resources. 54 | - [Amazon EventBridge](https://aws.amazon.com/eventbridge/) - Amazon EventBridge is a serverless event bus service that you can use to connect your applications with data from a variety of sources. 55 | - [AWS Service Catalog](https://aws.amazon.com/servicecatalog/) - AWS Service Catalog allows organizations to create and manage catalogs of IT services that are approved for use on AWS. 56 | - [AWS Single Sign-On](https://aws.amazon.com/single-sign-on/) - AWS Single Sign-On (AWS SSO) is where you create, or connect, your workforce identities in AWS once and manage access centrally across your AWS organization. 57 | - [AWS Systems Manager](https://aws.amazon.com/systems-manager/) - Systems Manager provides a unified user interface so you can track and resolve operational issues across your AWS applications and resources from a central place. 58 | 59 | ## Usage 60 | 61 | #### Parameters 62 | 63 | | Parameter | Type | Default | Description | 64 | | ------------------------ | :----: | :--------------------------------------------------: | -------------------------------------------------------------- | 65 | | OrganizationGroups | String | us-east-1 | List of AWS SSO groups that should have access to all accounts | 66 | | ExecutionRoleName | String | AWSControlTowerExecution | Execution IAM role name | 67 | | PortfolioIds | String | _None_ | Service Catalog Portfolio IDs | 68 | | PermissionSets | String | _None_ | AWS SSO Permission Set names | 69 | | SigningProfileVersionArn | String | _None_ | Code Signing Profile Version ARN | 70 | | GitHubOrg | String | aws-samples | Source code organization | 71 | | GitHubRepo | String | aws-control-tower-account-setup-using-step-functions | Source code repository | 72 | 73 | #### Installation 74 | 75 | The CloudFormation stack must be deployed in the same AWS account and region where the AWS Control Tower landing zone has been created. This is usually the AWS Organizations [Management](https://docs.aws.amazon.com/organizations/latest/userguide/orgs_getting-started_concepts.html#account) account. 76 | 77 | ``` 78 | git clone https://github.com/aws-samples/aws-control-tower-account-setup-using-step-functions 79 | cd aws-control-tower-account-setup-using-step-functions 80 | aws signer put-signing-profile --platform-id "AWSLambda-SHA384-ECDSA" --profile-name AccountSetupProfile 81 | sam build 82 | sam deploy \ 83 | --guided \ 84 | --signing-profiles \ 85 | SSOAssignmentFunction=AccountSetupProfile \ 86 | ServiceCatalogPortfolioFunction=AccountSetupProfile \ 87 | RegionalFunction=AccountSetupProfile \ 88 | AccountFunction=AccountSetupProfile \ 89 | DependencyLayer=AccountSetupProfile \ 90 | --tags "GITHUB_ORG=aws-samples GITHUB_REPO=aws-control-tower-account-setup-using-step-functions" 91 | ``` 92 | 93 | ## Clean up 94 | 95 | Deleting the CloudFormation Stack will remove the Lambda functions, state machine and EventBridge rule and new accounts will no longer be updated after they are created. 96 | 97 | ``` 98 | sam delete 99 | ``` 100 | 101 | ## Reference 102 | 103 | This solution is inspired by these references: 104 | 105 | - [Why not Terraform?](https://www.linkedin.com/pulse/why-terraform-justin-plock/) 106 | - [AWS Solutions Library - Customizations for AWS Control Tower](https://aws.amazon.com/solutions/implementations/customizations-for-aws-control-tower/) 107 | - [AWS Deployment Framework](https://github.com/awslabs/aws-deployment-framework) 108 | - [How to automate the creation of multiple accounts in AWS Control Tower](https://aws.amazon.com/blogs/mt/how-to-automate-the-creation-of-multiple-accounts-in-aws-control-tower/) 109 | - [Enabling AWS IAM Access Analyzer on AWS Control Tower accounts](https://aws.amazon.com/blogs/mt/enabling-aws-identity-and-access-analyzer-on-aws-control-tower-accounts/) 110 | - [Automating AWS Security Hub Alerts with AWS Control Tower lifecycle events](https://aws.amazon.com/blogs/mt/automating-aws-security-hub-alerts-with-aws-control-tower-lifecycle-events/) 111 | - [Using lifecycle events to track AWS Control Tower actions and trigger automated workflows](https://aws.amazon.com/blogs/mt/using-lifecycle-events-to-track-aws-control-tower-actions-and-trigger-automated-workflows/) 112 | 113 | ## Contributing 114 | 115 | See [CONTRIBUTING](CONTRIBUTING.md#security-issue-notifications) for more information. 116 | 117 | ## License 118 | 119 | This library is licensed under the MIT-0 License. See the [LICENSE](LICENSE) file. 120 | -------------------------------------------------------------------------------- /dependencies/requirements.txt: -------------------------------------------------------------------------------- 1 | aws-lambda-powertools[tracer,validation,aws-sdk]==2.43.1 2 | -------------------------------------------------------------------------------- /doc/architecture.drawio: -------------------------------------------------------------------------------- 1 | 7V1bV+I6FP41PspqmqSXRwRxnKPnqIzjjC+sUgJ0LIRpg6C//iS9AG0C4liKDHVcI9lN03Tv/X07l91yAhuj+UXgTIbXtEf8E13rzU9g80TXbWTw/4XgJRZgy4wFg8DrxSKwFLS9V5IItUQ69XokzFRklPrMm2SFLh2PicsyMicI6CxbrU/97FUnzoBIgrbr+LL0weuxYSy1sLaUfyHeYJheGWjJkZGTVk4E4dDp0dmKCJ6fwEZAKYs/jeYN4gvdpXqJz2utObroWEDGbJsTHk8frfHl5EZ7tNraw/nX27ZBTxNjPDv+NLnhOzLw6JjLLhkJHEaDpPfsJVVJ3/P9BvX5Edgc0zGXnoUsoE8kFZ7oENeNhmXwIz0nHBLRA8ALzyRgHtdt3fcGYy5jdMKlfTpm7aR5LSmrmkq6ytsg87U6AAvNco8kdERY8MKrJCfA1G6JN0Kg1XAsmS2tC1KTDVcta9tpVSfxqcGi/aXa+YdE82orPNz8/uf35PdPx8BNSG6/PRmvV6dAssK1M+aeNRK3pWt116VT8SlniAn1xizqCz7jv7zTDe0E8yMNUarpOCfIl82sAMgl0UZWkC+bWQHINw9y1wf5Dq4IpFKmeS13fW2lg/wXntEp870x98KUCYQzDQKn55GlQyUOO2QjP3HK2dBjpD1xXKHVGSex1CVjLgK6wkU5lifi82g+EKxXc2Yhqg0COp1El7zkbKQ82uEfO65Pp72O4zMVcPi/lvCcMwXK1PBxkpJP+lGL/E688eAqKjWhAlCLSyywqS3QJUFJAbi16FpwX4IuQ5OghTQFtEyofRxY096P5gWbWr3L6+vb71+7aPAfVQCr/tDmgjYjE/6nNR27jLNdKDpJg6e+zwm6QtlfgbKQm7jTTw3cWZhXgblGk/sq2inmFpcoGnMQZjEHoAw6gBWgM1ABoFNGM0sCnYQp0uODrKRIAzakAzp2/POl9IzbcdxbKGpZ54oKA0Qu9Ysw9pL4jzNlNOtwa3Ub0mngkg39T0aszAkGhG2ol4ydxL1stFRAfId5z9kBZeFaN9ZQHQcq93d+uhEFnC4fzxkD8ekbnRF5cKdkOyXjqVhPyXwy+2WqRXykuEJeqJKZshDI1VIKk4UqmYqv82cDxdkgd3bElpU6C1WnMvisG97kgxI/1oIIWfrKsaYXkChCRIwfCMbMRAF+zlkDQGyo4kY/+smTehoxrpwu8W9o6CXNdyljdPRmSHF5rzguM1z2VqB0wkmsjr43F/1QR86AxNQXx80zXlRFUDfmiw6L6KGYcWEuRkENKmZdyJKDVCornC3tA49Rhcee5NQbwVfrRxdQzxkkDpLJWTmbLLrx52aSlyfqI+dVLE/ko9n5M9fhWeAJhVQxrSLhwyZhIry5G3tzIRSso09HwUAesUrI/TDDkrnHfqx8/ima4ncel5rzpOWo8LJSuCGBx29T+MDmmdoqUyuXIJA8m3ilz6/Bd3TfRNZP47Hjfm+91E9hOYyOc44AIKrZGtYNCLBp2cjMrXGuIXg5UthW1sGAUbMMpBs2QABDgLLNxmrbWdwA75yDJpP9vbjbVq6lXLwHW7pWSYOFvGtBYNVg5FPQsFC+vW0dC+l2plVdQ/l1+OKcSak+dDhjRSUDmVu6iV7SmNJE60JRWewgLwv/S2ZccE1GXbEmUu24/F1rwX/tjgvWs5Sr2HEBqazwHRc1tuQp2+cY1e0+zBrl8OefB8Q3Izb3U47/ZcjeKmIXxcqpKx/CmG0/UwRrPw6GkP3H7lXD0ECaBrFm8llGtlkN1ywLYts0gWWhNFGoeG+zrvH9lV0fda87d+zXfSO8Hj4pdoYXa0rpklIqEGSecUPj95SmB07DyK/qvALAk/nyYNoKSJvh3YxbyrbOxStXzPu773uTkKyJmStOLC0+5NZPWi2N/8iRabF6UkAw0rVt1hhUuTV6AdFIaWW9LCvrR2Nl+PmsDMuyMjweK+cS5YCNSrSyMvqtzeUhwbPHVSltCTQc5vh0IBmi2g6otrg/rzqr3ZWd766EMWN03IQgipmKm0aGMbGlyH7c0f7KpuU8mS/b/1WU+MkxjJHJJ13vwnDDBBC0jgfD3njgk07Ie9OJ6hUxgckthxjmniEsD2zT9Aft+02jQvHnRrGNcLP1zkiM61A7w0eD4ueJu5voi3Rjv9BVpOOuyVxqiM2JB4e5Q17/ig7CCtfVhOWA1FlNWHafkysoYiYooqPvhi+BpXoQslTGXJ/rqbVhxYmfG8SGVofQfB+IddME4HhAHMLdQFc38b6hK2fbLaB73mhX2P3c2G2Z1rmG3ofdpoYbwDwa7BI33NEqYbqbti/ovvNBmKKSLtaq8s1kCcXTmZu2i3aezZjPe4ZWTdMRNICNLBtjK5fWsHXuBAa5CS2oWSaCCGNkm9DaLpu2qBTY9G0su99iRUe7xaqbZW6kq81cWlYMPlozYxvt2cilJcUYR2tk1c7Broz8FXz7BWed+YPV6t66F4Pbh/EXZeJTTsGHn4WrvPOS4n6pj6VsyhqVXt3liDOvnFG350g2r2Ze1czrsGZefuzIxbC0bebG1OVtEm2ay6xA+IYbvU99j8r7RO0hv/cK2hW0K2groJ1OiPeQfbXpocsVaEd5V9pF9MiaBO56KDJZkvc9VgivEF4h/A2El5mcdf/cQs//otZj/8fd3dD+cn/q9dbno7+EjIzEKyXjl7gGEpDDJyKyPWJ9V6iukjs+rzqr5I7d7wvHfNEZJWyxk30miEGJm8RKvtziNRoHtiSleG53U6Qoe0nKRDt76c6mlTcpIF7Wr6sQeEicXT198BZnez3eAY+9dJxxr+O4LglT/o6mcDuhcNsqkcGVfKd6znZDrsBOX4SUI9PC9hSU9fQP8vWHtK6g1TCcjsRS4B31xR9PJFqte2XRUt/rXr2zupfWDak/ZaQeuCmUhHRZQguIp9+GskxI/dg3c+Smd8BSPXCcrupk3L2APTSl4vHf5u76lu6O9unu8s7lBRGDnHhzS3444kD9WzdU2R6l+vcW7988LP9WjMfX75zuy7/ladAx0DnEqiTlgtydF5dfoxXPZpbfRQbP/wc= -------------------------------------------------------------------------------- /doc/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-control-tower-account-setup-using-step-functions/133fec417604d8e6ea85ecf7624c516324e10ec1/doc/architecture.png -------------------------------------------------------------------------------- /events/CreateManagedAccount.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0", 3 | "id": "999cccaa-eaaa-0000-1111-123456789012", 4 | "detail-type": "AWS Service Event via CloudTrail", 5 | "source": "aws.controltower", 6 | "account": "XXXXXXXXXXXX", 7 | "time": "2018-08-30T21:42:18Z", 8 | "region": "us-east-1", 9 | "resources": [], 10 | "detail": { 11 | "eventVersion": "1.05", 12 | "userIdentity": { 13 | "accountId": "XXXXXXXXXXXX", 14 | "invokedBy": "AWS Internal" 15 | }, 16 | "eventTime": "2018-08-30T21:42:18Z", 17 | "eventSource": "controltower.amazonaws.com", 18 | "eventName": "CreateManagedAccount", 19 | "awsRegion": "us-east-1", 20 | "sourceIPAddress": "AWS Internal", 21 | "userAgent": "AWS Internal", 22 | "eventID": "0000000-0000-0000-1111-123456789012", 23 | "readOnly": false, 24 | "eventType": "AwsServiceEvent", 25 | "serviceEventDetails": { 26 | "createManagedAccountStatus": { 27 | "organizationalUnit": { 28 | "organizationalUnitName": "Custom", 29 | "organizationalUnitId": "ou-XXXX-l3zc8b3h" 30 | }, 31 | "account": { 32 | "accountName": "LifeCycle1", 33 | "accountId": "XXXXXXXXXXXX" 34 | }, 35 | "state": "SUCCEEDED", 36 | "message": "AWS Control Tower successfully created a managed account.", 37 | "requestedTimestamp": "2019-11-15T11:45:18+0000", 38 | "completedTimestamp": "2019-11-16T12:09:32+0000" 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | warn_return_any=False 3 | warn_unused_configs=True 4 | no_implicit_optional=True 5 | warn_redundant_casts=True 6 | warn_unused_ignores=True 7 | show_column_numbers = True 8 | show_error_codes = True 9 | show_error_context = True 10 | disable_error_code = annotation-unchecked 11 | 12 | [mypy-boto3] 13 | ignore_missing_imports = True 14 | 15 | [mypy-botocore] 16 | ignore_missing_imports = True 17 | 18 | [mypy-botocore.response] 19 | ignore_missing_imports = True 20 | 21 | [mypy-botocore.config] 22 | ignore_missing_imports = True 23 | 24 | [mypy-botocore.compat] 25 | ignore_missing_imports = True 26 | 27 | [mypy-botocore.exceptions] 28 | ignore_missing_imports = True 29 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.black] 2 | line-length = 120 3 | target-version = ['py312'] 4 | include = '\.pyi?$' 5 | extend-exclude = ''' 6 | ( 7 | /( 8 | \.venv 9 | | \.aws-sam 10 | )/ 11 | ) 12 | ''' 13 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | black==24.10.0 2 | wheel==0.45.1 3 | pre-commit==3.8.0 4 | boto3-stubs[ec2,ecs,iam,identitystore,organizations,sts,servicecatalog,sso-admin]==1.36.16 5 | -------------------------------------------------------------------------------- /src/regional/account_setup/lambda_handler.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 6 | * SPDX-License-Identifier: MIT-0 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 9 | * software and associated documentation files (the "Software"), to deal in the Software 10 | * without restriction, including without limitation the rights to use, copy, modify, 11 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 12 | * permit persons to whom the Software is furnished to do so. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 15 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 16 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 17 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 18 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 19 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | """ 21 | 22 | from typing import Dict, Any 23 | 24 | from aws_lambda_powertools import Logger, Tracer 25 | from aws_lambda_powertools.utilities.typing import LambdaContext 26 | from aws_lambda_powertools.utilities.validation import validator 27 | import boto3 28 | 29 | from account_setup.resources import EC2, ECS, STS 30 | from account_setup.schemas import INPUT 31 | 32 | tracer = Tracer() 33 | logger = Logger() 34 | 35 | 36 | @validator(inbound_schema=INPUT) 37 | @tracer.capture_lambda_handler 38 | @logger.inject_lambda_context(log_event=True) 39 | def handler(event: Dict[str, Any], context: LambdaContext) -> None: 40 | account_id = event["AccountId"] 41 | region_name = event["Region"] 42 | execution_role_arn = event["ExecutionRoleArn"] 43 | 44 | logger.append_keys(account_id=account_id, region=region_name) 45 | tracer.put_annotation("AccountId", account_id) 46 | tracer.put_annotation("Region", region_name) 47 | 48 | session = boto3.Session() 49 | 50 | assumed_session = STS(session).assume_role(execution_role_arn) 51 | 52 | ec2 = EC2(assumed_session, region_name) 53 | default_vpc_id = ec2.get_default_vpc_id() 54 | if default_vpc_id: 55 | logger.info(f"Deleting default VPC {default_vpc_id} from {region_name} in {account_id}") 56 | ec2.delete_vpc(default_vpc_id) 57 | else: 58 | logger.debug(f"No default VPC found in {region_name} in {account_id}") 59 | 60 | # TODO 11/15: move this into the state machine once the aws-sdk integration has been updated 61 | logger.info(f"Enabling snapshot block public access in {region_name} in {account_id}") 62 | ec2.enable_snapshot_block_public_access() 63 | 64 | logger.info(f"Enabling AMI block public access in {region_name} in {account_id}") 65 | ec2.enable_ami_block_public_access() 66 | 67 | logger.info(f"Setting default ECS settings in {region_name} in {account_id}") 68 | ecs = ECS(assumed_session, region_name) 69 | ecs.put_account_setting_default() 70 | -------------------------------------------------------------------------------- /src/regional/account_setup/resources/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 6 | * SPDX-License-Identifier: MIT-0 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 9 | * software and associated documentation files (the "Software"), to deal in the Software 10 | * without restriction, including without limitation the rights to use, copy, modify, 11 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 12 | * permit persons to whom the Software is furnished to do so. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 15 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 16 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 17 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 18 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 19 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | """ 21 | 22 | from .ec2 import EC2 23 | from .ecs import ECS 24 | from .sts import STS 25 | 26 | __all__ = ["EC2", "ECS", "STS"] 27 | -------------------------------------------------------------------------------- /src/regional/account_setup/resources/ec2.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 6 | * SPDX-License-Identifier: MIT-0 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 9 | * software and associated documentation files (the "Software"), to deal in the Software 10 | * without restriction, including without limitation the rights to use, copy, modify, 11 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 12 | * permit persons to whom the Software is furnished to do so. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 15 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 16 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 17 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 18 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 19 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | """ 21 | 22 | from typing import Optional, TYPE_CHECKING 23 | 24 | from aws_lambda_powertools import Logger 25 | import boto3 26 | import botocore 27 | 28 | if TYPE_CHECKING: 29 | from mypy_boto3_ec2 import EC2Client, EC2ServiceResource 30 | 31 | logger = Logger(child=True) 32 | 33 | __all__ = ["EC2"] 34 | 35 | 36 | class EC2: 37 | def __init__(self, session: boto3.Session, region: str) -> None: 38 | self.client: EC2Client = session.client("ec2", region_name=region) 39 | self.session = session 40 | self.region_name = region 41 | 42 | def get_default_vpc_id(self) -> Optional[str]: 43 | params = { 44 | "Filters": [ 45 | { 46 | "Name": "isDefault", 47 | "Values": [ 48 | "true", 49 | ], 50 | } 51 | ] 52 | } 53 | 54 | response = self.client.describe_vpcs(**params) 55 | for vpc in response.get("Vpcs", []): 56 | if vpc.get("IsDefault", False): 57 | return vpc["VpcId"] 58 | 59 | logger.debug(f"No default VPC found in {self.region_name}", region=self.region_name) 60 | return None 61 | 62 | def delete_vpc(self, vpc_id: str) -> None: 63 | ec2: EC2ServiceResource = self.session.resource("ec2", region_name=self.region_name) 64 | vpc = ec2.Vpc(vpc_id) 65 | 66 | # detach and delete all gateways associated with the vpc 67 | for gw in vpc.internet_gateways.all(): 68 | vpc.detach_internet_gateway(InternetGatewayId=gw.id) 69 | gw.delete() 70 | 71 | # Route table associations 72 | for rt in vpc.route_tables.all(): 73 | for rta in rt.associations: 74 | if not rta.main: 75 | rta.delete() 76 | 77 | # Security Group 78 | for sg in vpc.security_groups.all(): 79 | if sg.group_name != "default": 80 | sg.delete() 81 | 82 | # Network interfaces 83 | for subnet in vpc.subnets.all(): 84 | for interface in subnet.network_interfaces.all(): 85 | interface.delete() 86 | subnet.delete() 87 | 88 | # Network ACLs 89 | for nacl in vpc.network_acls.all(): 90 | if not nacl.is_default: 91 | nacl.delete() 92 | 93 | # DHCP Options 94 | if vpc.dhcp_options and vpc.dhcp_options_id != "default": 95 | dhcp_options_id = vpc.dhcp_options_id 96 | 97 | vpc.associate_dhcp_options(DhcpOptionsId="default") # associate no DHCP options 98 | 99 | dhcp_options = ec2.DhcpOptions(dhcp_options_id) 100 | dhcp_options.delete() 101 | 102 | # Delete VPC 103 | self.client.delete_vpc(VpcId=vpc_id) 104 | logger.info( 105 | f"VPC {vpc_id} and associated resources has been deleted in {self.region_name}.", region=self.region_name 106 | ) 107 | 108 | def enable_snapshot_block_public_access(self) -> None: 109 | try: 110 | self.client.enable_snapshot_block_public_access(State="block-all-sharing") 111 | except botocore.exceptions.ClientError: 112 | logger.exception(f"Unable to enable snapshot block public access in {self.region}") 113 | 114 | def enable_ami_block_public_access(self) -> None: 115 | try: 116 | self.client.enable_image_block_public_access(ImageBlockPublicAccessState="block-new-sharing") 117 | except botocore.exceptions.ClientError: 118 | logger.exception(f"Unable to enable AMI block public access in {self.region_name}") 119 | -------------------------------------------------------------------------------- /src/regional/account_setup/resources/ecs.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 6 | * SPDX-License-Identifier: MIT-0 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 9 | * software and associated documentation files (the "Software"), to deal in the Software 10 | * without restriction, including without limitation the rights to use, copy, modify, 11 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 12 | * permit persons to whom the Software is furnished to do so. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 15 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 16 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 17 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 18 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 19 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | """ 21 | 22 | from typing import TYPE_CHECKING 23 | 24 | from aws_lambda_powertools import Logger 25 | import boto3 26 | import botocore 27 | 28 | if TYPE_CHECKING: 29 | from mypy_boto3_ecs import ECSClient 30 | 31 | logger = Logger(child=True) 32 | 33 | __all__ = ["ECS"] 34 | 35 | 36 | class ECS: 37 | def __init__(self, session: boto3.Session, region: str) -> None: 38 | self.client: ECSClient = session.client("ecs", region_name=region) 39 | self.region = region 40 | 41 | def put_account_setting_default(self) -> None: 42 | names = [ 43 | "serviceLongArnFormat", 44 | "taskLongArnFormat", 45 | "containerInstanceLongArnFormat", 46 | "awsvpcTrunking", 47 | "containerInsights", 48 | "dualStackIPv6", 49 | ] 50 | for name in names: 51 | try: 52 | self.client.put_account_setting_default(name=name, value="enabled") 53 | except botocore.exceptions.ClientError: 54 | logger.exception(f"Unable to enable ECS setting {name} in {self.region}") 55 | 56 | # documentation on https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs-account-settings.html#tag-resources-setting is incorrect 57 | try: 58 | self.client.put_account_setting_default(name="tagResourceAuthorization", value="on") 59 | except botocore.exceptions.ClientError: 60 | logger.exception(f"Unable to enable ECS setting tagResourceAuthorization in {self.region}") 61 | -------------------------------------------------------------------------------- /src/regional/account_setup/resources/sts.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 6 | * SPDX-License-Identifier: MIT-0 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 9 | * software and associated documentation files (the "Software"), to deal in the Software 10 | * without restriction, including without limitation the rights to use, copy, modify, 11 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 12 | * permit persons to whom the Software is furnished to do so. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 15 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 16 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 17 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 18 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 19 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | """ 21 | 22 | from typing import TYPE_CHECKING 23 | 24 | from aws_lambda_powertools import Logger 25 | import boto3 26 | 27 | if TYPE_CHECKING: 28 | from mypy_boto3_sts import STSClient 29 | 30 | logger = Logger(child=True) 31 | 32 | __all__ = ["STS"] 33 | 34 | 35 | class STS: 36 | def __init__(self, session: boto3.Session) -> None: 37 | self.client: STSClient = session.client("sts") 38 | 39 | def assume_role(self, role_arn: str, role_session_name: str = "AccountSetup") -> boto3.Session: 40 | """ 41 | Assume the AWSControlTowerExecution role in an account 42 | """ 43 | 44 | logger.info(f"Assuming role {role_arn}") 45 | response = self.client.assume_role( 46 | RoleArn=role_arn, 47 | RoleSessionName=role_session_name, 48 | DurationSeconds=900, # shortest duration 15 minutes 49 | ) 50 | 51 | credentials = response["Credentials"] 52 | 53 | return boto3.Session( 54 | aws_access_key_id=credentials["AccessKeyId"], 55 | aws_secret_access_key=credentials["SecretAccessKey"], 56 | aws_session_token=credentials["SessionToken"], 57 | ) 58 | -------------------------------------------------------------------------------- /src/regional/account_setup/schemas.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 6 | * SPDX-License-Identifier: MIT-0 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 9 | * software and associated documentation files (the "Software"), to deal in the Software 10 | * without restriction, including without limitation the rights to use, copy, modify, 11 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 12 | * permit persons to whom the Software is furnished to do so. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 15 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 16 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 17 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 18 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 19 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | """ 21 | 22 | INPUT = { 23 | "$schema": "http://json-schema.org/draft-07/schema", 24 | "type": "object", 25 | "properties": { 26 | "AccountId": { 27 | "type": "string", 28 | }, 29 | "Region": { 30 | "type": "string", 31 | }, 32 | "ExecutionRoleArn": { 33 | "type": "string", 34 | }, 35 | }, 36 | "required": ["AccountId", "Region", "ExecutionRoleArn"], 37 | } 38 | -------------------------------------------------------------------------------- /src/regional/requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-control-tower-account-setup-using-step-functions/133fec417604d8e6ea85ecf7624c516324e10ec1/src/regional/requirements.txt -------------------------------------------------------------------------------- /src/service_catalog_portfolio/account_setup/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 6 | * SPDX-License-Identifier: MIT-0 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 9 | * software and associated documentation files (the "Software"), to deal in the Software 10 | * without restriction, including without limitation the rights to use, copy, modify, 11 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 12 | * permit persons to whom the Software is furnished to do so. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 15 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 16 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 17 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 18 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 19 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | """ 21 | -------------------------------------------------------------------------------- /src/service_catalog_portfolio/account_setup/lambda_handler.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 6 | * SPDX-License-Identifier: MIT-0 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 9 | * software and associated documentation files (the "Software"), to deal in the Software 10 | * without restriction, including without limitation the rights to use, copy, modify, 11 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 12 | * permit persons to whom the Software is furnished to do so. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 15 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 16 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 17 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 18 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 19 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | """ 21 | 22 | import os 23 | from typing import Dict, Any, List 24 | 25 | from aws_lambda_powertools import Logger, Tracer 26 | from aws_lambda_powertools.utilities.typing import LambdaContext 27 | from aws_lambda_powertools.utilities.validation import validator 28 | 29 | from account_setup.resources import IAM, ServiceCatalog, STS 30 | from account_setup.schemas import INPUT 31 | 32 | tracer = Tracer() 33 | logger = Logger() 34 | 35 | 36 | def get_env_list(key: str) -> List[str]: 37 | """ 38 | Return an optional environment variable as a list 39 | """ 40 | value = os.getenv(key, "").split(",") 41 | return list(filter(None, value)) 42 | 43 | 44 | PORTFOLIO_IDS = get_env_list("PORTFOLIO_IDS") 45 | PERMISSION_SET_NAMES = get_env_list("PERMISSION_SET_NAMES") 46 | 47 | 48 | @validator(inbound_schema=INPUT) 49 | @tracer.capture_lambda_handler 50 | @logger.inject_lambda_context(log_event=True) 51 | def handler(event: Dict[str, Any], context: LambdaContext) -> None: 52 | session = STS().assume_role(event["ExecutionRoleArn"], "service_catalog_portfolio") 53 | 54 | iam = IAM(session) 55 | 56 | role_arns = set() 57 | 58 | for permission_set_name in PERMISSION_SET_NAMES: 59 | role_arn = iam.get_role_arn(permission_set_name) 60 | if role_arn: 61 | role_arns.add(role_arn) 62 | 63 | servicecatalog = ServiceCatalog(session) 64 | 65 | for portfolio_id in PORTFOLIO_IDS: 66 | servicecatalog.accept_portfolio_share(portfolio_id) 67 | 68 | existing_principals = servicecatalog.list_principals_for_portfolio(portfolio_id) 69 | 70 | to_add = role_arns - existing_principals 71 | to_remove = existing_principals - role_arns 72 | 73 | for role_arn in to_add: 74 | servicecatalog.associate_principal_with_portfolio(portfolio_id=portfolio_id, principal_arn=role_arn) 75 | 76 | for role_arn in to_remove: 77 | servicecatalog.disassociate_principal_from_portfolio(portfolio_id=portfolio_id, principal_arn=role_arn) 78 | -------------------------------------------------------------------------------- /src/service_catalog_portfolio/account_setup/resources/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 6 | * SPDX-License-Identifier: MIT-0 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 9 | * software and associated documentation files (the "Software"), to deal in the Software 10 | * without restriction, including without limitation the rights to use, copy, modify, 11 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 12 | * permit persons to whom the Software is furnished to do so. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 15 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 16 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 17 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 18 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 19 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | """ 21 | 22 | from .iam import IAM 23 | from .servicecatalog import ServiceCatalog 24 | from .sts import STS 25 | 26 | __all__ = ["IAM", "ServiceCatalog", "STS"] 27 | -------------------------------------------------------------------------------- /src/service_catalog_portfolio/account_setup/resources/iam.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 6 | * SPDX-License-Identifier: MIT-0 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 9 | * software and associated documentation files (the "Software"), to deal in the Software 10 | * without restriction, including without limitation the rights to use, copy, modify, 11 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 12 | * permit persons to whom the Software is furnished to do so. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 15 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 16 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 17 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 18 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 19 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | """ 21 | 22 | from functools import lru_cache 23 | from typing import Optional, Dict, TYPE_CHECKING 24 | 25 | import boto3 26 | 27 | if TYPE_CHECKING: 28 | from mypy_boto3_iam import IAMClient 29 | 30 | __all__ = ["IAM"] 31 | 32 | AWS_SSO_ROLE_PREFIX = "AWSReservedSSO_" 33 | 34 | 35 | class IAM: 36 | def __init__(self, session: Optional[boto3.Session] = None) -> None: 37 | if not session: 38 | session = boto3._get_default_session() 39 | self.client: IAMClient = session.client("iam") 40 | self._roles: Dict[str, str] = {} 41 | 42 | def get_sso_roles(self) -> Dict[str, str]: 43 | """ 44 | Get the list of AWS SSO permission set role ARNs organized by the permission set name 45 | """ 46 | if self._roles: 47 | return self._roles 48 | 49 | roles = {} 50 | paginator = self.client.get_paginator("list_roles") 51 | page_iterator = paginator.paginate(PaginationConfig={"PageSize": 1000}) 52 | for page in page_iterator: 53 | for role in page.get("Roles", []): 54 | if role.get("RoleName", "").startswith(AWS_SSO_ROLE_PREFIX): 55 | # AWSReservedSSO_AWSAdministratorAccess_a1ff75f56dfb0e2f -> AWSAdministratorAccess 56 | permission_set_name = role["RoleName"].rsplit("_", 1)[0].replace(AWS_SSO_ROLE_PREFIX, "") 57 | roles[permission_set_name] = role["Arn"] 58 | 59 | self._roles = roles 60 | 61 | return roles 62 | 63 | @lru_cache 64 | def get_role_arn(self, permission_set_name: str) -> Optional[str]: 65 | roles = self.get_sso_roles() 66 | return roles.get(permission_set_name) 67 | -------------------------------------------------------------------------------- /src/service_catalog_portfolio/account_setup/resources/servicecatalog.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 6 | * SPDX-License-Identifier: MIT-0 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 9 | * software and associated documentation files (the "Software"), to deal in the Software 10 | * without restriction, including without limitation the rights to use, copy, modify, 11 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 12 | * permit persons to whom the Software is furnished to do so. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 15 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 16 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 17 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 18 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 19 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | """ 21 | 22 | from typing import Set, TYPE_CHECKING, Optional 23 | 24 | from aws_lambda_powertools import Logger 25 | import boto3 26 | import botocore 27 | 28 | if TYPE_CHECKING: 29 | from mypy_boto3_servicecatalog import ServiceCatalogClient, ListPrincipalsForPortfolioPaginator 30 | 31 | logger = Logger(child=True) 32 | 33 | __all__ = ["ServiceCatalog"] 34 | 35 | 36 | class ServiceCatalog: 37 | def __init__(self, session: Optional[boto3.Session] = None) -> None: 38 | if not session: 39 | session = boto3._get_default_session() 40 | self.client: ServiceCatalogClient = session.client("servicecatalog") 41 | 42 | def accept_portfolio_share(self, portfolio_id: str) -> None: 43 | try: 44 | self.client.accept_portfolio_share(PortfolioId=portfolio_id, PortfolioShareType="AWS_ORGANIZATIONS") 45 | except botocore.exceptions.ClientError: 46 | logger.exception("Unable to accept portfolio share") 47 | raise 48 | 49 | def associate_principal_with_portfolio(self, portfolio_id: str, principal_arn: str) -> None: 50 | try: 51 | self.client.associate_principal_with_portfolio( 52 | PortfolioId=portfolio_id, 53 | PrincipalARN=principal_arn, 54 | PrincipalType="IAM", 55 | ) 56 | except botocore.exceptions.ClientError: 57 | logger.exception("Unable to associate princpal with portfolio") 58 | raise 59 | 60 | def disassociate_principal_from_portfolio(self, portfolio_id: str, principal_arn: str) -> None: 61 | try: 62 | self.client.disassociate_principal_from_portfolio(PortfolioId=portfolio_id, PrincipalARN=principal_arn) 63 | except botocore.exceptions.ClientError as error: 64 | if error.response["Error"]["Code"] != "ResourceNotFoundException": 65 | logger.exception("Unable to disassociate princpal from portfolio") 66 | raise 67 | 68 | def list_principals_for_portfolio(self, portfolio_id: str) -> Set[str]: 69 | principals = set() 70 | 71 | paginator: ListPrincipalsForPortfolioPaginator = self.client.get_paginator("list_principals_for_portfolio") 72 | page_iterator = paginator.paginate(PortfolioId=portfolio_id) 73 | for page in page_iterator: 74 | for principal in page.get("Principals", []): 75 | principals.add(principal["PrincipalARN"]) 76 | 77 | return principals 78 | -------------------------------------------------------------------------------- /src/service_catalog_portfolio/account_setup/resources/sts.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 6 | * SPDX-License-Identifier: MIT-0 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 9 | * software and associated documentation files (the "Software"), to deal in the Software 10 | * without restriction, including without limitation the rights to use, copy, modify, 11 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 12 | * permit persons to whom the Software is furnished to do so. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 15 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 16 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 17 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 18 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 19 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | """ 21 | 22 | from typing import TYPE_CHECKING, Optional 23 | 24 | from aws_lambda_powertools import Logger 25 | import boto3 26 | 27 | if TYPE_CHECKING: 28 | from mypy_boto3_sts import STSClient 29 | 30 | logger = Logger(child=True) 31 | 32 | __all__ = ["STS"] 33 | 34 | 35 | class STS: 36 | def __init__(self, session: Optional[boto3.Session] = None) -> None: 37 | if not session: 38 | session = boto3._get_default_session() 39 | self.client: STSClient = session.client("sts") 40 | 41 | def assume_role(self, role_arn: str, role_session_name: str) -> boto3.Session: 42 | """ 43 | Assume a role and return a new boto3 session 44 | """ 45 | logger.info(f"Assuming role {role_arn}") 46 | response = self.client.assume_role( 47 | RoleArn=role_arn, 48 | RoleSessionName=role_session_name, 49 | DurationSeconds=900, # shortest duration 15 minutes 50 | ) 51 | 52 | credentials = response["Credentials"] 53 | 54 | return boto3.Session( 55 | aws_access_key_id=credentials["AccessKeyId"], 56 | aws_secret_access_key=credentials["SecretAccessKey"], 57 | aws_session_token=credentials["SessionToken"], 58 | ) 59 | -------------------------------------------------------------------------------- /src/service_catalog_portfolio/account_setup/schemas.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 6 | * SPDX-License-Identifier: MIT-0 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 9 | * software and associated documentation files (the "Software"), to deal in the Software 10 | * without restriction, including without limitation the rights to use, copy, modify, 11 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 12 | * permit persons to whom the Software is furnished to do so. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 15 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 16 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 17 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 18 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 19 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | """ 21 | 22 | INPUT = { 23 | "$schema": "http://json-schema.org/draft-07/schema", 24 | "type": "object", 25 | "properties": { 26 | "ExecutionRoleArn": { 27 | "type": "string", 28 | }, 29 | }, 30 | "required": ["ExecutionRoleArn"], 31 | } 32 | -------------------------------------------------------------------------------- /src/service_catalog_portfolio/requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-control-tower-account-setup-using-step-functions/133fec417604d8e6ea85ecf7624c516324e10ec1/src/service_catalog_portfolio/requirements.txt -------------------------------------------------------------------------------- /src/sso_assignment/account_setup/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 6 | * SPDX-License-Identifier: MIT-0 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 9 | * software and associated documentation files (the "Software"), to deal in the Software 10 | * without restriction, including without limitation the rights to use, copy, modify, 11 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 12 | * permit persons to whom the Software is furnished to do so. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 15 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 16 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 17 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 18 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 19 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | """ 21 | -------------------------------------------------------------------------------- /src/sso_assignment/account_setup/constants.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 6 | * SPDX-License-Identifier: MIT-0 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 9 | * software and associated documentation files (the "Software"), to deal in the Software 10 | * without restriction, including without limitation the rights to use, copy, modify, 11 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 12 | * permit persons to whom the Software is furnished to do so. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 15 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 16 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 17 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 18 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 19 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | """ 21 | 22 | GROUP_ACCOUNT_PREFIX = "AWS-A-" 23 | 24 | GROUP_ORG_PREFIX = "AWS-O-" 25 | -------------------------------------------------------------------------------- /src/sso_assignment/account_setup/lambda_handler.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 6 | * SPDX-License-Identifier: MIT-0 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 9 | * software and associated documentation files (the "Software"), to deal in the Software 10 | * without restriction, including without limitation the rights to use, copy, modify, 11 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 12 | * permit persons to whom the Software is furnished to do so. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 15 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 16 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 17 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 18 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 19 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | """ 21 | 22 | from typing import Dict, Any, Optional 23 | 24 | from aws_lambda_powertools import Logger, Tracer 25 | from aws_lambda_powertools.utilities.typing import LambdaContext 26 | import boto3 27 | 28 | from .resources import Organizations, IdentityStore, SSO 29 | from .utils import parse_group 30 | from .constants import GROUP_ORG_PREFIX 31 | 32 | tracer = Tracer() 33 | logger = Logger() 34 | 35 | 36 | @tracer.capture_method(capture_response=False) 37 | def create_group_event(event: Dict[str, Any]) -> None: 38 | """ 39 | Assign the new group to an account and permission set 40 | """ 41 | group: Dict[str, str] = event.get("responseElements", {}).get("group", {}) 42 | if not group: 43 | logger.warn("No group found in event") 44 | return 45 | 46 | group_id = group["groupId"] 47 | group_name = group["groupName"] 48 | 49 | try: 50 | account_name, permission_set_name = parse_group(group_name) 51 | except Exception as error: 52 | logger.warn(error) 53 | return 54 | 55 | if not account_name: 56 | logger.warn(f"Unrecognized account group name: {group_name}") 57 | return 58 | 59 | session = boto3.Session() 60 | organizations = Organizations(session) 61 | account_id = organizations.get_account_id(account_name) 62 | if not account_id: 63 | logger.warn(f"No account named '{account_name}'") 64 | return 65 | 66 | sso = SSO(session) 67 | 68 | permission_set_arn = None 69 | instances = sso.list_instances() 70 | 71 | for instance in instances: 72 | instance_arn = instance["InstanceArn"] 73 | 74 | permission_set_arn = sso.get_permission_set_arn(instance_arn=instance_arn, name=permission_set_name) 75 | if permission_set_arn: 76 | logger.info(f"Assigning {group_name} permission set {permission_set_name} in {account_id}") 77 | sso.create_account_assignment( 78 | account_id=account_id, 79 | instance_arn=instance_arn, 80 | permission_set_arn=permission_set_arn, 81 | principal_id=group_id, 82 | ) 83 | break 84 | 85 | if not permission_set_arn: 86 | logger.warn(f"Permission Set '{permission_set_name}' not found") 87 | 88 | 89 | @tracer.capture_lambda_handler(capture_response=False) 90 | @logger.inject_lambda_context(log_event=True) 91 | def handler(event: Dict[str, Any], context: LambdaContext) -> None: 92 | # Handle single-account groups 93 | if event.get("eventName") == "CreateGroup": 94 | return create_group_event(event) 95 | 96 | # Below handles organizational groups 97 | 98 | account_id: Optional[str] = event.get("AccountId") 99 | if not account_id: 100 | raise Exception("Account ID not found in event") 101 | 102 | logger.info(f"Assigning organizational groups to account {account_id}") 103 | 104 | session = boto3.Session() 105 | sso = SSO(session) 106 | 107 | instances = sso.list_instances() 108 | for instance in instances: 109 | instance_arn = instance["InstanceArn"] 110 | identity_store_id = instance["IdentityStoreId"] 111 | 112 | identity_store = IdentityStore(session, identity_store_id) 113 | organizational_groups = identity_store.get_groups_by_prefix(GROUP_ORG_PREFIX) 114 | 115 | logger.info(f"Found organizational groups: {organizational_groups}") 116 | 117 | for group_id, group_name in organizational_groups.items(): 118 | _, permission_set_name = parse_group(group_name) 119 | 120 | permission_set_arn = sso.get_permission_set_arn(instance_arn=instance_arn, name=permission_set_name) 121 | if not permission_set_arn: 122 | logger.error(f"Permission Set '{permission_set_name}' not found, skipping") 123 | continue 124 | 125 | logger.info(f"Assigning {group_name} permission set {permission_set_name} in {account_id}") 126 | sso.create_account_assignment( 127 | account_id=account_id, 128 | instance_arn=instance_arn, 129 | permission_set_arn=permission_set_arn, 130 | principal_id=group_id, 131 | ) 132 | -------------------------------------------------------------------------------- /src/sso_assignment/account_setup/resources/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 6 | * SPDX-License-Identifier: MIT-0 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 9 | * software and associated documentation files (the "Software"), to deal in the Software 10 | * without restriction, including without limitation the rights to use, copy, modify, 11 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 12 | * permit persons to whom the Software is furnished to do so. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 15 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 16 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 17 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 18 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 19 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | """ 21 | 22 | from .identity_store import IdentityStore 23 | from .organizations import Organizations 24 | from .sso import SSO 25 | 26 | __all__ = ["IdentityStore", "Organizations", "SSO"] 27 | -------------------------------------------------------------------------------- /src/sso_assignment/account_setup/resources/identity_store.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 6 | * SPDX-License-Identifier: MIT-0 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 9 | * software and associated documentation files (the "Software"), to deal in the Software 10 | * without restriction, including without limitation the rights to use, copy, modify, 11 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 12 | * permit persons to whom the Software is furnished to do so. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 15 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 16 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 17 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 18 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 19 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | """ 21 | 22 | from typing import Dict, TYPE_CHECKING 23 | 24 | import boto3 25 | 26 | if TYPE_CHECKING: 27 | from mypy_boto3_identitystore import IdentityStoreClient, ListGroupsPaginator 28 | 29 | __all__ = ["IdentityStore"] 30 | 31 | 32 | class IdentityStore: 33 | def __init__(self, session: boto3.Session, identity_store_id: str) -> None: 34 | self.client: IdentityStoreClient = session.client("identitystore") 35 | self._identity_store_id = identity_store_id 36 | 37 | def get_groups_by_prefix(self, prefix: str) -> Dict[str, str]: 38 | """ 39 | Return all of the groups that match a given prefix 40 | """ 41 | paginator: ListGroupsPaginator = self.client.get_paginator("list_groups") 42 | page_iterator = paginator.paginate( 43 | IdentityStoreId=self._identity_store_id, 44 | PaginationConfig={ 45 | "PageSize": 100, 46 | }, 47 | ) 48 | 49 | groups: Dict[str, str] = {} 50 | for page in page_iterator: 51 | for group in page.get("Groups", []): 52 | if group["DisplayName"].startswith(prefix): 53 | groups[group["GroupId"]] = group["DisplayName"] 54 | 55 | return groups 56 | -------------------------------------------------------------------------------- /src/sso_assignment/account_setup/resources/organizations.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 6 | * SPDX-License-Identifier: MIT-0 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 9 | * software and associated documentation files (the "Software"), to deal in the Software 10 | * without restriction, including without limitation the rights to use, copy, modify, 11 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 12 | * permit persons to whom the Software is furnished to do so. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 15 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 16 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 17 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 18 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 19 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | """ 21 | 22 | from functools import lru_cache 23 | from typing import Optional, TYPE_CHECKING 24 | 25 | import boto3 26 | 27 | if TYPE_CHECKING: 28 | from mypy_boto3_organizations import OrganizationsClient, ListAccountsPaginator 29 | 30 | __all__ = ["Organizations"] 31 | 32 | 33 | class Organizations: 34 | def __init__(self, session: boto3.Session) -> None: 35 | # @see https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/organizations.html 36 | self.client: OrganizationsClient = session.client( 37 | "organizations", 38 | region_name="us-east-1", 39 | endpoint_url="https://organizations.us-east-1.amazonaws.com", 40 | ) 41 | 42 | @lru_cache 43 | def get_account_id(self, name: str) -> Optional[str]: 44 | """ 45 | Return the account ID 46 | """ 47 | paginator: ListAccountsPaginator = self.client.get_paginator("list_accounts") 48 | page_iterator = paginator.paginate(PaginationConfig={"PageSize": 100}) 49 | for page in page_iterator: 50 | for account in page.get("Accounts", []): 51 | if account["Name"] == name and account["Status"] == "ACTIVE": 52 | return account["Id"] 53 | return None 54 | -------------------------------------------------------------------------------- /src/sso_assignment/account_setup/resources/sso.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 6 | * SPDX-License-Identifier: MIT-0 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 9 | * software and associated documentation files (the "Software"), to deal in the Software 10 | * without restriction, including without limitation the rights to use, copy, modify, 11 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 12 | * permit persons to whom the Software is furnished to do so. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 15 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 16 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 17 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 18 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 19 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | """ 21 | 22 | from functools import lru_cache 23 | from typing import Optional, Dict, Any, TYPE_CHECKING 24 | 25 | from aws_lambda_powertools import Logger 26 | import boto3 27 | import botocore 28 | 29 | if TYPE_CHECKING: 30 | from mypy_boto3_sso_admin import SSOAdminClient, ListInstancesPaginator, ListPermissionSetsPaginator 31 | 32 | __all__ = ["SSO"] 33 | 34 | logger = Logger(child=True) 35 | 36 | 37 | class SSO: 38 | def __init__(self, session: boto3.Session) -> None: 39 | self.client: SSOAdminClient = session.client("sso-admin") 40 | self._permission_sets: Dict[str, str] = {} 41 | self._instances = [] 42 | 43 | def list_instances(self) -> Dict[str, str]: 44 | if self._instances: 45 | return self._instances 46 | 47 | instances = [] 48 | paginator: ListInstancesPaginator = self.client.get_paginator("list_instances") 49 | page_iterator = paginator.paginate() 50 | for page in page_iterator: 51 | instances.extend(page.get("Instances", [])) 52 | 53 | self._instances = instances 54 | return instances 55 | 56 | def list_permission_sets(self, instance_arn: str) -> Dict[str, str]: 57 | if instance_arn in self._permission_sets: 58 | return self._permission_sets[instance_arn] 59 | 60 | permission_sets: Dict[str, str] = {} 61 | 62 | paginator: ListPermissionSetsPaginator = self.client.get_paginator("list_permission_sets") 63 | page_iterator = paginator.paginate(InstanceArn=instance_arn) 64 | for page in page_iterator: 65 | for permission_set_arn in page.get("PermissionSets", []): 66 | response = self.client.describe_permission_set( 67 | InstanceArn=instance_arn, PermissionSetArn=permission_set_arn 68 | ) 69 | 70 | name = response["PermissionSet"]["Name"] 71 | permission_sets[name] = permission_set_arn 72 | 73 | self._permission_sets[instance_arn] = permission_sets 74 | 75 | return permission_sets 76 | 77 | @lru_cache 78 | def get_permission_set_arn(self, instance_arn: str, name: str) -> Optional[str]: 79 | permission_sets = self.list_permission_sets(instance_arn) 80 | return permission_sets.get(name) 81 | 82 | def create_account_assignment( 83 | self, 84 | account_id: str, 85 | instance_arn: str, 86 | permission_set_arn: str, 87 | principal_id: str, 88 | ) -> Dict[str, Any]: 89 | try: 90 | response = self.client.create_account_assignment( 91 | InstanceArn=instance_arn, 92 | TargetId=account_id, 93 | TargetType="AWS_ACCOUNT", 94 | PermissionSetArn=permission_set_arn, 95 | PrincipalType="GROUP", 96 | PrincipalId=principal_id, 97 | ) 98 | return response["AccountAssignmentCreationStatus"] 99 | except botocore.exceptions.ClientError as error: 100 | if error.response["Error"]["Code"] != "ConflictException": 101 | logger.exception(f"Unable to add {permission_set_arn} to {principal_id} in {account_id}") 102 | raise error 103 | 104 | def delete_account_assignment( 105 | self, 106 | account_id: str, 107 | instance_arn: str, 108 | permission_set_arn: str, 109 | principal_id: str, 110 | ) -> Dict[str, Any]: 111 | try: 112 | response = self.client.delete_account_assignment( 113 | InstanceArn=instance_arn, 114 | TargetId=account_id, 115 | TargetType="AWS_ACCOUNT", 116 | PermissionSetArn=permission_set_arn, 117 | PrincipalType="GROUP", 118 | PrincipalId=principal_id, 119 | ) 120 | return response["AccountAssignmentDeletionStatus"] 121 | except botocore.exceptions.ClientError as error: 122 | if error.response["Error"]["Code"] != "ResourceNotFoundException": 123 | logger.exception(f"Unable to delete {permission_set_arn} from {principal_id} in {account_id}") 124 | raise error 125 | -------------------------------------------------------------------------------- /src/sso_assignment/account_setup/utils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 6 | * SPDX-License-Identifier: MIT-0 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 9 | * software and associated documentation files (the "Software"), to deal in the Software 10 | * without restriction, including without limitation the rights to use, copy, modify, 11 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 12 | * permit persons to whom the Software is furnished to do so. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 15 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 16 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 17 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 18 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 19 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | """ 21 | 22 | import os 23 | from typing import Optional, Tuple, List 24 | 25 | from .constants import GROUP_ACCOUNT_PREFIX, GROUP_ORG_PREFIX 26 | 27 | __all__ = ["get_env_list", "parse_group"] 28 | 29 | 30 | def get_env_list(key: str) -> List[str]: 31 | """ 32 | Return an optional environment variable as a list 33 | """ 34 | value = os.getenv(key, "").split(",") 35 | return list(filter(None, value)) 36 | 37 | 38 | def parse_group(group_name: str) -> Tuple[Optional[str], str]: 39 | """ 40 | Parse a group name into the account name and permission set name 41 | """ 42 | account_name = None 43 | permission_set_name = None 44 | 45 | # ex. AWS-A-AccountA-DeveloperAccess 46 | if group_name.startswith(GROUP_ACCOUNT_PREFIX): 47 | group_parts = group_name.replace(GROUP_ACCOUNT_PREFIX, "").rsplit("-", 1) 48 | account_name = group_parts[2] # AccountA 49 | permission_set_name = group_parts[3] # DeveloperAccess 50 | 51 | # ex. AWS-O-AWSReadOnlyAccess 52 | elif group_name.startswith(GROUP_ORG_PREFIX): 53 | group_parts = group_name.split("-", 2) 54 | permission_set_name = group_parts[2] # AWSReadOnlyAccess 55 | 56 | else: 57 | raise Exception(f"Unrecognized group name: {group_name}") 58 | 59 | return (account_name, permission_set_name) 60 | -------------------------------------------------------------------------------- /src/sso_assignment/requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-control-tower-account-setup-using-step-functions/133fec417604d8e6ea85ecf7624c516324e10ec1/src/sso_assignment/requirements.txt -------------------------------------------------------------------------------- /template.yml: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | 4 | --- 5 | AWSTemplateFormatVersion: "2010-09-09" 6 | Transform: "AWS::Serverless-2016-10-31" 7 | Description: New account provisioning automation 8 | 9 | Parameters: 10 | ExecutionRoleName: 11 | Type: String 12 | Description: Execution IAM role name 13 | Default: AWSControlTowerExecution 14 | PortfolioIds: 15 | Type: CommaDelimitedList 16 | Description: Service Catalog Portfolio IDs 17 | Default: "" 18 | PermissionSets: 19 | Type: CommaDelimitedList 20 | Description: AWS SSO Permission Set names 21 | Default: "" 22 | SigningProfileVersionArn: 23 | Type: String 24 | Description: Code Signing Profile Version ARN 25 | GitHubOrg: 26 | Type: String 27 | Description: Source code organization 28 | Default: aws-samples 29 | GitHubRepo: 30 | Type: String 31 | Description: Source code repository 32 | Default: aws-control-tower-account-setup-using-step-functions 33 | 34 | Globals: 35 | Function: 36 | Architectures: 37 | - arm64 38 | Environment: 39 | Variables: 40 | POWERTOOLS_METRICS_NAMESPACE: AccountSetup 41 | LOG_LEVEL: INFO 42 | Handler: lambda_handler.handler 43 | Layers: 44 | - !Ref DependencyLayer 45 | MemorySize: 128 # megabytes 46 | Runtime: python3.12 47 | Tags: 48 | GITHUB_ORG: !Ref GitHubOrg 49 | GITHUB_REPO: !Ref GitHubRepo 50 | Timeout: 20 # seconds 51 | Tracing: Active 52 | 53 | Resources: 54 | CodeSigningConfig: 55 | Type: "AWS::Lambda::CodeSigningConfig" 56 | Properties: 57 | Description: AccountSetup Code Signing 58 | AllowedPublishers: 59 | SigningProfileVersionArns: 60 | - !Ref SigningProfileVersionArn 61 | CodeSigningPolicies: 62 | UntrustedArtifactOnDeployment: Enforce 63 | 64 | DependencyLayer: 65 | Type: "AWS::Serverless::LayerVersion" 66 | Metadata: 67 | BuildMethod: python3.12 68 | BuildArchitecture: arm64 69 | Properties: 70 | LicenseInfo: MIT-0 71 | CompatibleArchitectures: 72 | - arm64 73 | CompatibleRuntimes: 74 | - python3.12 75 | ContentUri: dependencies 76 | Description: DO NOT DELETE - AccountSetup - Latest versions of common Python packages 77 | RetentionPolicy: Delete 78 | 79 | RegionalFunctionLogGroup: 80 | Type: "AWS::Logs::LogGroup" 81 | UpdateReplacePolicy: Delete 82 | DeletionPolicy: Delete 83 | Metadata: 84 | cfn_nag: 85 | rules_to_suppress: 86 | - id: W84 87 | reason: "Ignoring KMS key" 88 | Properties: 89 | LogGroupName: !Sub "/aws/lambda/${RegionalFunction}" 90 | RetentionInDays: 3 91 | 92 | RegionalFunctionRole: 93 | Type: "AWS::IAM::Role" 94 | Properties: 95 | AssumeRolePolicyDocument: 96 | Version: "2012-10-17" 97 | Statement: 98 | Effect: Allow 99 | Principal: 100 | Service: !Sub "lambda.${AWS::URLSuffix}" 101 | Action: "sts:AssumeRole" 102 | Description: !Sub "DO NOT DELETE - Used by Lambda. Created by CloudFormation ${AWS::StackId}" 103 | Tags: 104 | - Key: "aws-cloudformation:stack-name" 105 | Value: !Ref "AWS::StackName" 106 | - Key: "aws-cloudformation:stack-id" 107 | Value: !Ref "AWS::StackId" 108 | - Key: "aws-cloudformation:logical-id" 109 | Value: RegionalFunctionRole 110 | - Key: GITHUB_ORG 111 | Value: !Ref GitHubOrg 112 | - Key: GITHUG_REPO 113 | Value: !Ref GitHubRepo 114 | 115 | RegionalFunctionPolicy: 116 | Type: "AWS::IAM::Policy" 117 | Properties: 118 | PolicyName: CloudWatchLogs 119 | PolicyDocument: 120 | Version: "2012-10-17" 121 | Statement: 122 | - Effect: Allow 123 | Action: 124 | - "logs:CreateLogStream" 125 | - "logs:PutLogEvents" 126 | Resource: !GetAtt RegionalFunctionLogGroup.Arn 127 | Roles: 128 | - !Ref RegionalFunctionRole 129 | 130 | RegionalFunction: 131 | Type: "AWS::Serverless::Function" 132 | Metadata: 133 | cfn_nag: 134 | rules_to_suppress: 135 | - id: W58 136 | reason: "Ignoring CloudWatch Logs" 137 | - id: W89 138 | reason: "Ignoring VPC" 139 | Properties: 140 | CodeSigningConfigArn: !Ref CodeSigningConfig 141 | CodeUri: src/regional 142 | Description: DO NOT DELETE - AccountSetup - Regional Configuration 143 | Environment: 144 | Variables: 145 | POWERTOOLS_SERVICE_NAME: regional 146 | Handler: account_setup.lambda_handler.handler 147 | ReservedConcurrentExecutions: 30 148 | Role: !GetAtt RegionalFunctionRole.Arn 149 | 150 | SSOAssignmentFunctionLogGroup: 151 | Type: "AWS::Logs::LogGroup" 152 | UpdateReplacePolicy: Delete 153 | DeletionPolicy: Delete 154 | Metadata: 155 | cfn_nag: 156 | rules_to_suppress: 157 | - id: W84 158 | reason: "Ignoring KMS key" 159 | Properties: 160 | LogGroupName: !Sub "/aws/lambda/${SSOAssignmentFunction}" 161 | RetentionInDays: 3 162 | 163 | SSOAssignmentFunctionRole: 164 | Type: "AWS::IAM::Role" 165 | Metadata: 166 | cfn_nag: 167 | rules_to_suppress: 168 | - id: W11 169 | reason: "Ignoring wildcard resource" 170 | Properties: 171 | AssumeRolePolicyDocument: 172 | Version: "2012-10-17" 173 | Statement: 174 | Effect: Allow 175 | Principal: 176 | Service: !Sub "lambda.${AWS::URLSuffix}" 177 | Action: "sts:AssumeRole" 178 | Description: !Sub "DO NOT DELETE - Used by Lambda. Created by CloudFormation ${AWS::StackId}" 179 | Policies: 180 | - PolicyName: SSOAssignmentFunctionPolicy 181 | PolicyDocument: 182 | Version: "2012-10-17" 183 | Statement: 184 | - Effect: Allow 185 | Action: 186 | - "organizations:ListAccounts" 187 | - "identitystore:GetGroupId" 188 | - "identitystore:ListGroups" 189 | - "sso:CreateAccountAssignment" 190 | - "sso:DescribePermissionSet" 191 | - "sso:DeleteAccountAssignment" 192 | - "sso:ListInstances" 193 | - "sso:ListPermissionSets" 194 | Resource: "*" 195 | Tags: 196 | - Key: "aws-cloudformation:stack-name" 197 | Value: !Ref "AWS::StackName" 198 | - Key: "aws-cloudformation:stack-id" 199 | Value: !Ref "AWS::StackId" 200 | - Key: "aws-cloudformation:logical-id" 201 | Value: SSOAssignmentFunctionRole 202 | - Key: GITHUB_ORG 203 | Value: !Ref GitHubOrg 204 | - Key: GITHUG_REPO 205 | Value: !Ref GitHubRepo 206 | 207 | SSOAssignmentFunctionPolicy: 208 | Type: "AWS::IAM::Policy" 209 | Properties: 210 | PolicyName: CloudWatchLogs 211 | PolicyDocument: 212 | Version: "2012-10-17" 213 | Statement: 214 | - Effect: Allow 215 | Action: 216 | - "logs:CreateLogStream" 217 | - "logs:PutLogEvents" 218 | Resource: !GetAtt SSOAssignmentFunctionLogGroup.Arn 219 | Roles: 220 | - !Ref SSOAssignmentFunctionRole 221 | 222 | SSOAssignmentFunction: 223 | Type: "AWS::Serverless::Function" 224 | Metadata: 225 | cfn_nag: 226 | rules_to_suppress: 227 | - id: W58 228 | reason: "Ignoring CloudWatch Logs" 229 | - id: W89 230 | reason: "Ignoring VPC" 231 | Properties: 232 | CodeSigningConfigArn: !Ref CodeSigningConfig 233 | CodeUri: src/sso_assignment 234 | Description: DO NOT DELETE - AccountSetup - SSO Assignment 235 | Environment: 236 | Variables: 237 | POWERTOOLS_SERVICE_NAME: sso_assignment 238 | Events: 239 | CreateGroupEvent: 240 | Type: EventBridgeRule 241 | Properties: 242 | InputPath: "$.detail" 243 | Pattern: 244 | "detail-type": 245 | - "AWS API Call via CloudTrail" 246 | detail: 247 | eventSource: 248 | - "sso-directory.amazonaws.com" 249 | eventName: 250 | - CreateGroup 251 | Handler: account_setup.lambda_handler.handler 252 | ReservedConcurrentExecutions: 1 253 | Role: !GetAtt SSOAssignmentFunctionRole.Arn 254 | Timeout: 300 # 5 minutes 255 | 256 | ServiceCatalogPortfolioFunctionLogGroup: 257 | Type: "AWS::Logs::LogGroup" 258 | UpdateReplacePolicy: Delete 259 | DeletionPolicy: Delete 260 | Metadata: 261 | cfn_nag: 262 | rules_to_suppress: 263 | - id: W84 264 | reason: "Ignoring KMS key" 265 | Properties: 266 | LogGroupName: !Sub "/aws/lambda/${ServiceCatalogPortfolioFunction}" 267 | RetentionInDays: 3 268 | 269 | ServiceCatalogPortfolioFunctionRole: 270 | Type: "AWS::IAM::Role" 271 | Properties: 272 | AssumeRolePolicyDocument: 273 | Version: "2012-10-17" 274 | Statement: 275 | Effect: Allow 276 | Principal: 277 | Service: !Sub "lambda.${AWS::URLSuffix}" 278 | Action: "sts:AssumeRole" 279 | Description: !Sub "DO NOT DELETE - Used by Lambda. Created by CloudFormation ${AWS::StackId}" 280 | Tags: 281 | - Key: "aws-cloudformation:stack-name" 282 | Value: !Ref "AWS::StackName" 283 | - Key: "aws-cloudformation:stack-id" 284 | Value: !Ref "AWS::StackId" 285 | - Key: "aws-cloudformation:logical-id" 286 | Value: ServiceCatalogPortfolioFunctionRole 287 | - Key: GITHUB_ORG 288 | Value: !Ref GitHubOrg 289 | - Key: GITHUG_REPO 290 | Value: !Ref GitHubRepo 291 | 292 | ServiceCatalogPortfolioFunctionPolicy: 293 | Type: "AWS::IAM::Policy" 294 | Properties: 295 | PolicyName: CloudWatchLogs 296 | PolicyDocument: 297 | Version: "2012-10-17" 298 | Statement: 299 | - Effect: Allow 300 | Action: 301 | - "logs:CreateLogStream" 302 | - "logs:PutLogEvents" 303 | Resource: !GetAtt ServiceCatalogPortfolioFunctionLogGroup.Arn 304 | Roles: 305 | - !Ref ServiceCatalogPortfolioFunctionRole 306 | 307 | ServiceCatalogPortfolioFunction: 308 | Type: "AWS::Serverless::Function" 309 | Metadata: 310 | cfn_nag: 311 | rules_to_suppress: 312 | - id: W58 313 | reason: "Ignoring CloudWatch Logs" 314 | - id: W89 315 | reason: "Ignoring VPC" 316 | Properties: 317 | CodeSigningConfigArn: !Ref CodeSigningConfig 318 | CodeUri: src/service_catalog_portfolio 319 | Description: DO NOT DELETE - AccountSetup - Service Catalog Portfolio 320 | Environment: 321 | Variables: 322 | POWERTOOLS_SERVICE_NAME: service_catalog_portfolio 323 | PORTFOLIO_IDS: !Join [",", !Ref PortfolioIds] 324 | PERMISSION_SET_NAMES: !Join [",", !Ref PermissionSets] 325 | Handler: account_setup.lambda_handler.handler 326 | ReservedConcurrentExecutions: 1 327 | Role: !GetAtt ServiceCatalogPortfolioFunctionRole.Arn 328 | Timeout: 300 # 5 minutes 329 | 330 | ControlTowerAssumePolicy: 331 | Type: "AWS::IAM::Policy" 332 | Properties: 333 | PolicyName: ControlTowerAssumePolicy 334 | PolicyDocument: 335 | Version: "2012-10-17" 336 | Statement: 337 | - Effect: Allow 338 | Action: "sts:AssumeRole" 339 | Resource: !Sub "arn:${AWS::Partition}:iam::*:role/AWSControlTowerExecution" 340 | Roles: 341 | - !Ref ServiceCatalogPortfolioFunctionRole 342 | - !Ref RegionalFunctionRole 343 | - !Ref StateMachineRole 344 | 345 | StateMachine: 346 | Type: "AWS::Serverless::StateMachine" 347 | Properties: 348 | Definition: 349 | StartAt: BuildParameters 350 | States: 351 | BuildParameters: 352 | Type: Pass 353 | InputPath: "$.account" 354 | Parameters: 355 | "AccountId.$": "$.accountId" 356 | "ExecutionRoleArn.$": "States.Format('arn:aws:iam::{}:role/${ExecutionRoleName}', $.accountId)" 357 | Next: UpdatePasswordPolicy 358 | UpdatePasswordPolicy: 359 | Type: Task 360 | Resource: "arn:aws:states:::aws-sdk:iam:updateAccountPasswordPolicy" 361 | Credentials: 362 | "RoleArn.$": "$.ExecutionRoleArn" 363 | Parameters: 364 | MinimumPasswordLength: 14 # https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-cis-controls.html#securityhub-cis-controls-1.9 365 | RequireSymbols: true # https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-cis-controls.html#securityhub-cis-controls-1.7 366 | RequireNumbers: true # https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-cis-controls.html#securityhub-cis-controls-1.8 367 | RequireUppercaseCharacters: true # https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-cis-controls.html#securityhub-cis-controls-1.5 368 | RequireLowercaseCharacters: true # https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-cis-controls.html#securityhub-cis-controls-1.6 369 | AllowUsersToChangePassword: true 370 | MaxPasswordAge: 90 # https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-cis-controls.html#securityhub-cis-controls-1.11 371 | PasswordReusePrevention: 24 # https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-cis-controls.html#securityhub-cis-controls-1.10 372 | HardExpiry: false 373 | ResultPath: null # discard result and keep original input 374 | Next: PublicAccessBlock 375 | PublicAccessBlock: 376 | Type: Task 377 | Resource: "arn:aws:states:::aws-sdk:s3control:putPublicAccessBlock" 378 | Credentials: 379 | "RoleArn.$": "$.ExecutionRoleArn" 380 | Parameters: 381 | PublicAccessBlockConfiguration: 382 | BlockPublicAcls: true 383 | IgnorePublicAcls: true 384 | BlockPublicPolicy: true 385 | RestrictPublicBuckets: true 386 | "AccountId.$": "$.AccountId" 387 | ResultPath: null # discard result and keep original input 388 | Next: Route53PolicyDocument 389 | Route53PolicyDocument: 390 | Type: Pass 391 | Result: 392 | PolicyDocument: |- 393 | \{ 394 | "Version": "2012-10-17", 395 | "Statement": [ 396 | \{ 397 | "Sid": "Route53LogsToCloudWatchLogs", 398 | "Effect": "Allow", 399 | "Principal": \{ 400 | "Service": "route53.amazonaws.com" 401 | \}, 402 | "Action": [ 403 | "logs:CreateLogStream", 404 | "logs:PutLogEvents" 405 | ], 406 | "Resource": "arn:aws:logs:us-east-1:{}:log-group:/aws/route53/*", 407 | "Condition": \{ 408 | "StringEquals": \{ 409 | "aws:SourceAccount": "{}" 410 | \} 411 | \} 412 | \} 413 | ] 414 | \} 415 | ResultPath: "$.Policy" 416 | Next: Route53LoggingPolicy 417 | Route53LoggingPolicy: 418 | Type: Task 419 | Resource: "arn:aws:states:::aws-sdk:cloudwatchlogs:putResourcePolicy" 420 | Credentials: 421 | "RoleArn.$": "$.ExecutionRoleArn" 422 | Parameters: 423 | PolicyName: AWSServiceRoleForRoute53 424 | "PolicyDocument.$": States.Format($.Policy.PolicyDocument, $.AccountId, $.AccountId) 425 | ResultPath: null # discard result and keep original input 426 | Next: DescribeRegions 427 | DescribeRegions: 428 | Type: Task 429 | Resource: "arn:aws:states:::aws-sdk:ec2:describeRegions" 430 | Parameters: 431 | Filters: 432 | - Name: opt-in-status 433 | Values: 434 | - opt-in-not-required 435 | AllRegions: false 436 | ResultSelector: 437 | "RegionNames.$": "$.Regions[*].RegionName" 438 | ResultPath: "$.Regions" 439 | Next: AllRegions 440 | AllRegions: 441 | Type: Map 442 | ItemsPath: "$.Regions.RegionNames" 443 | MaxConcurrency: 0 444 | ItemSelector: 445 | "AccountId.$": "$.AccountId" 446 | "Region.$": "$$.Map.Item.Value" 447 | "ExecutionRoleArn.$": "$.ExecutionRoleArn" 448 | ItemProcessor: 449 | StartAt: EbsEncryptionByDefault 450 | States: 451 | EbsEncryptionByDefault: 452 | Type: Task 453 | Resource: "arn:aws:states:::aws-sdk:ec2:enableEbsEncryptionByDefault" 454 | Credentials: 455 | "RoleArn.$": "$.ExecutionRoleArn" 456 | Parameters: {} 457 | ResultPath: null # discard result and keep original input 458 | Next: DisableSsmPublicSharing 459 | DisableSsmPublicSharing: 460 | Type: Task 461 | Resource: "arn:aws:states:::aws-sdk:ssm:updateServiceSetting" 462 | Credentials: 463 | "RoleArn.$": "$.ExecutionRoleArn" 464 | Parameters: 465 | "SettingId.$": "States.Format('arn:aws:ssm:{}:{}:servicesetting/ssm/documents/console/public-sharing-permission', $.Region, $.AccountId)" 466 | SettingValue: Disable 467 | Catch: 468 | - ErrorEquals: 469 | - States.ALL 470 | ResultPath: null # discard result and keep original input 471 | Next: IgnoreError 472 | ResultPath: null # discard result and keep original input 473 | Next: Regional 474 | IgnoreError: 475 | Type: Pass 476 | Next: Regional 477 | Regional: 478 | Type: Task 479 | Resource: !GetAtt RegionalFunction.Arn 480 | Retry: 481 | - ErrorEquals: 482 | - Lambda.TooManyRequestsException 483 | - Lambda.ServiceException 484 | - Lambda.AWSLambdaException 485 | - Lambda.SdkClientException 486 | IntervalSeconds: 2 487 | MaxAttempts: 6 488 | BackoffRate: 2 489 | TimeoutSeconds: 20 490 | End: true 491 | ResultPath: null # discard result and keep original input 492 | Next: SSOAssignment 493 | SSOAssignment: 494 | Type: Task 495 | Resource: !GetAtt SSOAssignmentFunction.Arn 496 | Retry: 497 | - ErrorEquals: 498 | - Lambda.TooManyRequestsException 499 | - Lambda.ServiceException 500 | - Lambda.AWSLambdaException 501 | - Lambda.SdkClientException 502 | IntervalSeconds: 2 503 | MaxAttempts: 6 504 | BackoffRate: 2 505 | TimeoutSeconds: 300 506 | ResultPath: null # discard result and keep original input 507 | Next: ServiceCatalogPortfolio 508 | ServiceCatalogPortfolio: 509 | Type: Task 510 | Resource: !GetAtt ServiceCatalogPortfolioFunction.Arn 511 | Retry: 512 | - ErrorEquals: 513 | - Lambda.TooManyRequestsException 514 | - Lambda.ServiceException 515 | - Lambda.AWSLambdaException 516 | - Lambda.SdkClientException 517 | IntervalSeconds: 2 518 | MaxAttempts: 6 519 | BackoffRate: 2 520 | TimeoutSeconds: 300 521 | End: true 522 | DefinitionSubstitutions: 523 | ExecutionRoleName: !Ref ExecutionRoleName 524 | Events: 525 | CreateAccountEvent: 526 | Type: EventBridgeRule 527 | Properties: 528 | InputPath: "$.detail.serviceEventDetails.createManagedAccountStatus" 529 | Pattern: 530 | source: 531 | - "aws.controltower" 532 | "detail-type": 533 | - "AWS Service Event via CloudTrail" 534 | detail: 535 | eventName: 536 | - CreateManagedAccount 537 | serviceEventDetails: 538 | createManagedAccountStatus: 539 | state: 540 | - SUCCEEDED 541 | Policies: 542 | - Version: "2012-10-17" 543 | Statement: 544 | - Effect: Allow 545 | Action: "ec2:DescribeRegions" 546 | Resource: "*" 547 | - Effect: Allow 548 | Action: "lambda:InvokeFunction" 549 | Resource: 550 | - !GetAtt SSOAssignmentFunction.Arn 551 | - !GetAtt ServiceCatalogPortfolioFunction.Arn 552 | - !GetAtt RegionalFunction.Arn 553 | Tags: 554 | GITHUB_ORG: !Ref GitHubOrg 555 | GITHUB_REPO: !Ref GitHubRepo 556 | Tracing: 557 | Enabled: true 558 | Type: STANDARD 559 | --------------------------------------------------------------------------------