├── .cfnlintrc ├── .github └── workflows │ ├── ci.yml │ └── secure_workflows.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── docs ├── aws_iam_identity_center_enable.png ├── github_actions_enable_actions.png ├── github_actions_run_workflow.png └── github_actions_variables.png ├── enable_control_tower.yml ├── github_ci_template.yml ├── requirements-dev.txt ├── src ├── activation_lambda │ ├── index.py │ └── requirements.txt ├── stackset_parameters │ └── template.yml └── stackset_roles │ └── template.yml └── template.yml /.cfnlintrc: -------------------------------------------------------------------------------- 1 | templates: 2 | - enable_control_tower.yml 3 | - template.yml 4 | - src/stackset_parameters/template.yml 5 | - src/stackset_roles/template.yml 6 | include_checks: 7 | - I 8 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | workflow_dispatch: 11 | 12 | concurrency: 13 | group: ${{ github.workflow }}-${{ github.ref_name }} 14 | cancel-in-progress: true 15 | 16 | env: 17 | AWS_REGION: us-east-1 18 | SAM_CLI_TELEMETRY: '0' 19 | STACK_NAME: DO-NOT-DELETE-organization 20 | 21 | permissions: 22 | id-token: write 23 | contents: read 24 | 25 | jobs: 26 | deploy: 27 | if: github.repository_owner != 'aws-samples' 28 | runs-on: ubuntu-latest 29 | steps: 30 | - name: Checkout code 31 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 32 | 33 | - name: Setup Python 34 | uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0 35 | with: 36 | python-version: '3.13' 37 | architecture: x64 38 | cache: pip 39 | 40 | - name: Setup AWS SAM 41 | uses: aws-actions/setup-sam@12a6719db503425e98edcc798b6779590a450e8f # v2 42 | with: 43 | use-installer: true 44 | 45 | - name: Get AWS credentials 46 | uses: aws-actions/configure-aws-credentials@ececac1a45f3b08a01d2dd070d28d111c5fe6722 # v4.1.0 47 | with: 48 | role-to-assume: ${{ vars.ASSUME_ROLE_ARN }} 49 | role-session-name: ci 50 | aws-region: ${{ env.AWS_REGION }} 51 | mask-aws-account-id: true 52 | 53 | #- name: Validate template 54 | # run: sam validate --lint 55 | 56 | - name: Cache SAM build 57 | id: cache-sam-build 58 | uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 59 | env: 60 | cache-name: cache-sam-build 61 | with: 62 | path: .aws-sam 63 | key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/requirements.txt') }} 64 | restore-keys: | 65 | ${{ runner.os }}-build-${{ env.cache-name }}- 66 | ${{ runner.os }}-build- 67 | ${{ runner.os }}- 68 | 69 | - name: Run sam build 70 | run: sam build --cached --parallel 71 | 72 | - name: Run sam deploy 73 | run: | 74 | sam deploy \ 75 | --capabilities CAPABILITY_IAM CAPABILITY_NAMED_IAM CAPABILITY_AUTO_EXPAND \ 76 | --no-confirm-changeset \ 77 | --disable-rollback \ 78 | --no-fail-on-empty-changeset \ 79 | --no-progressbar \ 80 | --parameter-overrides "pGithubOrganization=${{ github.repository_owner }}" \ 81 | --role-arn ${{ vars.CF_ROLE_ARN }} \ 82 | --stack-name ${{ env.STACK_NAME }} \ 83 | --s3-bucket ${{ vars.ARTIFACT_BUCKET }} \ 84 | --s3-prefix ${{ github.repository }} \ 85 | --tags \ 86 | github:org=${{ github.repository_owner }} \ 87 | github:repo=${{ github.repository }} \ 88 | git:sha=${{ github.sha }} -------------------------------------------------------------------------------- /.github/workflows/secure_workflows.yml: -------------------------------------------------------------------------------- 1 | name: Lockdown untrusted workflows 2 | 3 | # PROCESS 4 | # 5 | # 1. Scans for any external GitHub Action being used without version pinning (@ vs @v3) 6 | # 2. Scans for insecure practices for inline bash scripts (shellcheck) 7 | # 3. Fail CI and prevent PRs to be merged if any malpractice is found 8 | 9 | # USAGE 10 | # 11 | # Always triggered on new PR, PR changes and PR merge. 12 | 13 | 14 | on: 15 | push: 16 | paths: 17 | - ".github/workflows/**" 18 | pull_request: 19 | paths: 20 | - ".github/workflows/**" 21 | 22 | permissions: 23 | contents: read 24 | 25 | jobs: 26 | enforce_pinned_workflows: 27 | if: github.repository_owner == 'aws-samples' 28 | name: Harden Security 29 | runs-on: ubuntu-latest 30 | permissions: 31 | contents: read # checkout code and subsequently GitHub action workflows 32 | steps: 33 | - name: Checkout code 34 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 35 | - name: Ensure 3rd party workflows have SHA pinned 36 | uses: zgosalvez/github-actions-ensure-sha-pinned-actions@25ed13d0628a1601b4b44048e63cc4328ed03633 # v3.0.22 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .venv 3 | .vscode 4 | .aws-sam 5 | *.toml 6 | -------------------------------------------------------------------------------- /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 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *main* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 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. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | 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. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | 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. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT No Attribution 2 | 3 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 12 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 13 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 14 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 15 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 16 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | 18 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: setup build deploy clean format outdated bootstrap 2 | 3 | setup: 4 | python3 -m venv .venv 5 | .venv/bin/python3 -m pip install -U pip setuptools wheel 6 | .venv/bin/python3 -m pip install -r requirements-dev.txt 7 | .venv/bin/python3 -m pip install -r src/activation_lambda/requirements.txt 8 | 9 | build: 10 | sam build --parallel --cached 11 | 12 | deploy: 13 | sam deploy 14 | 15 | clean: 16 | sam delete 17 | 18 | format: 19 | .venv/bin/python3 -m black . 20 | 21 | outdated: 22 | .venv/bin/python3 -m pip list -o 23 | 24 | bootstrap: 25 | aws --region us-east-1 cloudformation deploy --template-file github_ci_template.yml --stack-name orgs-prescriptive-guidance-cicd --capabilities CAPABILITY_IAM CAPABILITY_NAMED_IAM -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Guidance for Organization on AWS 2 | 3 | ### Table of contents 4 | 5 | - [Guidance for Organization on AWS](#guidance-for-organization-on-aws) 6 | - [Table of contents](#table-of-contents) 7 | - [Introduction](#introduction) 8 | - [Prerequisites](#prerequisites) 9 | - [Tools and services](#tools-and-services) 10 | - [Usage](#usage) 11 | - [Parameters](#parameters) 12 | - [Installation](#installation) 13 | - [Enabling Control Tower](#enabling-control-tower) 14 | - [Use Cases](#use-cases) 15 | - [Emergency Access](#emergency-access) 16 | - [To Access an EC2 Instance](#to-access-an-ec2-instance) 17 | - [Clean up](#clean-up) 18 | - [Reference](#reference) 19 | - [Contributing](#contributing) 20 | - [License](#license) 21 | 22 | ## Introduction 23 | 24 | This repository contains a collection of [AWS CloudFormation](https://aws.amazon.com/cloudformation/) templates to create up an [AWS Organizations](https://aws.amazon.com/organizations/) structure. 25 | 26 | ## Prerequisites 27 | 28 | - [AWS Command Line Interface (AWS CLI)](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) version 2, installed 29 | 30 | ## Tools and services 31 | 32 | - [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. 33 | - [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. 34 | - [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. 35 | - [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. 36 | 37 | ## Usage 38 | 39 | #### Parameters 40 | 41 | | Parameter | Type | Default | Description | 42 | | ------------------------ | :----: | :----------------------: | -------------------- | 43 | | pInstanceArn | String | _None_ | Optional - AWS IAM Identity Center instance ARN | 44 | | pDeveloperPrefix | String | app | Prefix used by developers when creating IAM roles and CloudFormation stacks | 45 | | pCloudFormationRoleName | String | CloudFormationRole | Name of the IAM role used by AWS CloudFormation | 46 | | pServiceCatalogRoleName | String | ServiceCatalogRole | Name of the IAM role used by AWS Service Catalog | 47 | | pRegions | CommaDelimitedList | us-east-1 | Comma-delimited list of AWS Regions | 48 | | pSandboxOuName | String | Sandbox | Name of the organizational unit for sandbox AWS accounts | 49 | | pSecurityOuName | String | Security_Prod | Name of the organizational unit for security-related AWS accounts | 50 | | pWorkloadsOuName | String | Workloads | Name of the organizational unit for workload AWS accounts | 51 | | pInfrastructureOuName | String | Infrastructure | Name of the organization unit for infrastructure AWS accounts | 52 | | pGithubOrganization | String | _None_ | GitHub Organization | 53 | | pCreateNewAwsOrg | String | Yes | Whether to create a new AWS Organization or not | 54 | | pOrganizationId | String | _None_ | Existing AWS Organization ID | 55 | | pOrganizationRootId | String | _None_ | Existing AWS Organization Root ID | 56 | 57 | #### Installation 58 | 59 | To deploy the sample template, first install the [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html). [Fork](https://github.com/aws-samples/orgs-prescriptive-guidance/fork) this respository to your own GitHub owner account. Then execute these commands to check out the sample from GitHub and deploy a CloudFormation template that creates an IAM role that will be used by GitHub Actions to deploy the sample. 60 | 61 | ```bash 62 | git clone https://github.com//orgs-prescriptive-guidance 63 | cd orgs-prescriptive-guidance 64 | aws --region us-east-1 cloudformation deploy \ 65 | --template-file github_ci_template.yml \ 66 | --stack-name orgs-prescriptive-guidance-cicd \ 67 | --capabilities CAPABILITY_IAM CAPABILITY_NAMED_IAM \ 68 | --parameter-overrides pGithubOrganization= pGitHubRepository=orgs-prescriptive-guidance 69 | 70 | aws --region us-east-1 cloudformation describe-stacks --stack-name orgs-prescriptive-guidance-cicd --query "Stacks[0].Outputs" 71 | ``` 72 | 73 | Then, follow this [guide](https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/store-information-in-variables#creating-configuration-variables-for-a-repository) to create GitHub Action variables in the repository: 74 | 75 | * `ARTIFACT_BUCKET` = value of `oArtifactBucket` from above 76 | * `ASSUME_ROLE_ARN` = value of `oGitHubRoleArn` from above 77 | * `CF_ROLE_ARN` = value of `oCloudFormationRoleArn` from above 78 | 79 | The variables should look like the image below: 80 | 81 | ![GitHub Action Variables](./docs/github_actions_variables.png) 82 | 83 | Next we will run the deployment from Github Actions. If it's the first time you're accesing the Actions tab from a fork, you might need to enable the Actions feature for your fork. 84 | 85 | ![GitHub Actions Enable Actions](./docs/github_actions_enable_actions.png) 86 | 87 | Now, you can trigger the GitHub Actions workflow by clicking the `Run workflow` button in the GitHub Actions UI. 88 | 89 | ![GitHub Actions Run Workflow](./docs/github_actions_run_workflow.png) 90 | 91 | After the GitHub Actions deployment is successful, navigate to [IAM Identity Center](https://console.aws.amazon.com/singlesignon/home) in the AWS Console and `Enable` IAM Identity Center. 92 | 93 | ![IAM Identity Center Enable](./docs/aws_iam_identity_center_enable.png) 94 | 95 | On the next screen, click `Go to settings`. Copy the value of the `Instance ARN` (it will look like `arn:aws:sso:::instance/ssoins-XXXXXX`) to your clipboard. 96 | 97 | Next we want to update the CloudFormation stack with the IAM Identity Center Instance ARN to provision a set of [Permission Sets](https://docs.aws.amazon.com/singlesignon/latest/userguide/permissionsetsconcept.html). 98 | 99 | ```bash 100 | aws --region us-east-1 cloudformation update-stack \ 101 | --stack-name DO-NOT-DELETE-organization \ 102 | --use-previous-template \ 103 | --parameters "ParameterKey=pInstanceArn,ParameterValue=arn:aws:sso:::instance/ssoins-XXXX" \ 104 | --capabilities CAPABILITY_NAMED_IAM 105 | ``` 106 | 107 | #### Enabling Control Tower 108 | To enable Control Tower you can deploy the cloudformation template _enable_control_tower.yml_. 109 | Please note that the deployment of the sample template _template.yml_ is not a requirement for deploying _enable_control_tower.yml_, only a recommendation. 110 | _enable_control_tower.yml_ will by default create a new Organization. 111 | 112 | ```bash 113 | git clone https://github.com//orgs-prescriptive-guidance 114 | cd orgs-prescriptive-guidance 115 | aws --region us-east-1 cloudformation deploy \ 116 | --template-file enable_control_tower.yml \ 117 | --stack-name control-tower \ 118 | --capabilities CAPABILITY_IAM CAPABILITY_NAMED_IAM \ 119 | --parameter-overrides pLogArchiveAccountEmailAddress= pSecurityAccountEmailAddress= 120 | ``` 121 | 122 | ## Use Cases 123 | 124 | #### Emergency Access 125 | 126 | In the event that there are any issues with AWS IAM Identity Center, IAM users `EmergencyAccess_RO` and `EmergencyAccess_Ops` have been deployed in the management account. These users can assume IAM roles `EmergencyAccess_RO` and `EmergencyAccess_Ops` in every account. These users thus have privileged access to all accounts which necessitates that they be used sparingly in a secure manner. 127 | 128 | There are no credentials associated with these users. To set credentials, and enable multi-factor authentication for these users, follow these [instructions](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_mfa_enable.html) to configure MFA devices for each EmergencyAccess user. 129 | 130 | #### To Access an EC2 Instance 131 | 132 | After installing the AWS CLI, install the [AWS Systems Manager Session Manager plugin](https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager-working-with-install-plugin.html 133 | ). 134 | 135 | ```bash 136 | aws sso login --profile 137 | aws --profile ssm start-session --target --document-name SSM-SessionManagerRunShell 138 | ``` 139 | 140 | ## Clean up 141 | 142 | Deleting the CloudFormation Stack will remove the CloudFormation StackSets, IAM Identity Center Permission Sets, and the AWS Organization. 143 | 144 | ``` 145 | sam delete 146 | ``` 147 | 148 | ## Reference 149 | 150 | This solution is inspired by these references: 151 | 152 | - [AWS Security Reference Architecture](https://docs.aws.amazon.com/prescriptive-guidance/latest/security-reference-architecture/architecture.html) 153 | 154 | ## Contributing 155 | 156 | See [CONTRIBUTING](CONTRIBUTING.md#security-issue-notifications) for more information. 157 | 158 | ## License 159 | 160 | This library is licensed under the MIT-0 License. See the LICENSE file. 161 | 162 | -------------------------------------------------------------------------------- /docs/aws_iam_identity_center_enable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/orgs-prescriptive-guidance/c38408650a752023f2c046d104201e8dbaa8a147/docs/aws_iam_identity_center_enable.png -------------------------------------------------------------------------------- /docs/github_actions_enable_actions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/orgs-prescriptive-guidance/c38408650a752023f2c046d104201e8dbaa8a147/docs/github_actions_enable_actions.png -------------------------------------------------------------------------------- /docs/github_actions_run_workflow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/orgs-prescriptive-guidance/c38408650a752023f2c046d104201e8dbaa8a147/docs/github_actions_run_workflow.png -------------------------------------------------------------------------------- /docs/github_actions_variables.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/orgs-prescriptive-guidance/c38408650a752023f2c046d104201e8dbaa8a147/docs/github_actions_variables.png -------------------------------------------------------------------------------- /enable_control_tower.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 | Description: Cloud Foundations on AWS Control Tower deployment 7 | 8 | Metadata: 9 | "AWS::CloudFormation::Interface": 10 | ParameterGroups: 11 | - Label: 12 | default: "AWS Organization" 13 | Parameters: 14 | - pCreateNewAwsOrg 15 | - pSecurityOuName 16 | - pSandboxOuName 17 | - Label: 18 | default: "Security Account" 19 | Parameters: 20 | - pDeployNewSecurityAccount 21 | - pImportedSecurityAccountId 22 | - pSecurityAccountAlias 23 | - pSecurityAccountEmailAddress 24 | - Label: 25 | default: "Log Archive Account" 26 | Parameters: 27 | - pDeployNewLogArchiveAccount 28 | - pImportedLogArchiveAccountId 29 | - pLogArchiveAccountAlias 30 | - pLogArchiveAccountEmailAddress 31 | - Label: 32 | default: "Control Tower" 33 | Parameters: 34 | - pVersion 35 | - pGovernedRegions 36 | - pLoggingBucketRetentionPeriod 37 | - pAccessLoggingBucketRetentionPeriod 38 | ParameterLabels: 39 | pCreateNewAwsOrg: 40 | default: "Create new AWS Organization" 41 | pSecurityOuName: 42 | default: "Security OU Name" 43 | pSandboxOuName: 44 | default: "Additional Control Tower OU" 45 | pImportedSecurityAccountId: 46 | default: "Import Security Account ID" 47 | pSecurityAccountAlias: 48 | default: "Security account alias" 49 | pSecurityAccountEmailAddress: 50 | default: "Security account email address" 51 | pImportedLogArchiveAccountId: 52 | default: "Import Log Archive account ID" 53 | pLogArchiveAccountAlias: 54 | default: "Log Archive account alias" 55 | pLogArchiveAccountEmailAddress: 56 | default: "Log Archive account email address" 57 | pVersion: 58 | default: "Landing Zone version" 59 | pGovernedRegions: 60 | default: "Governed Regions" 61 | pLoggingBucketRetentionPeriod: 62 | default: "Logging Bucket Retention Period" 63 | pAccessLoggingBucketRetentionPeriod: 64 | default: "Access Logging Bucket Retention Period" 65 | pDeployNewSecurityAccount: 66 | default: "Deploy new Security account" 67 | pDeployNewLogArchiveAccount: 68 | default: "Deploy new Log Archive account" 69 | 70 | Parameters: 71 | pCreateNewAwsOrg: 72 | Type: String 73 | Description: Specify whether to create the AWS Organization or if one already exists. Select no if you already have an AWS Organization. 74 | Default: "Yes" 75 | AllowedValues: 76 | - "Yes" 77 | - "No" 78 | pDeployNewLogArchiveAccount: 79 | Type: String 80 | Description: Deploy a NEW Log Archive account or select No and enter an existing AWS account ID to use a pre-existing account 81 | Default: "Yes" 82 | AllowedValues: 83 | - "Yes" 84 | - "No" 85 | pLogArchiveAccountAlias: 86 | Type: String 87 | Description: The AWS account alias for the Log Archive account. If importing a pre-existing Log Archive account, leave blank. 88 | Default: "Log Archive" 89 | pLogArchiveAccountEmailAddress: 90 | Type: String 91 | Description: The Log Archive email address for any newly created Log Archive account. Leave this blank if importing a pre-existing Log Archive account. 92 | pImportedLogArchiveAccountId: 93 | Type: String 94 | Description: If you selected No for creating a new Log Archive account, enter the existing account ID that will serve as your Log Archive account ID. 95 | pDeployNewSecurityAccount: 96 | Type: String 97 | Description: Deploy a NEW Security account or select No and enter an existing AWS account ID to use a pre-existing account 98 | Default: "Yes" 99 | AllowedValues: 100 | - "Yes" 101 | - "No" 102 | pSecurityAccountAlias: 103 | Type: String 104 | Description: The AWS account alias for the Security account. If importing a pre-existing Security account, leave blank. 105 | Default: "Audit" 106 | pSecurityAccountEmailAddress: 107 | Type: String 108 | Description: The Security email address for any newly created Security account. Leave this blank if importing a pre-existing Security account. 109 | pImportedSecurityAccountId: 110 | Type: String 111 | Description: If you selected No for creating a new Security account, enter the existing account ID that will serve as your Security account ID. 112 | pVersion: 113 | Type: String 114 | Description: The version number of Landing Zone 115 | Default: "3.3" 116 | pGovernedRegions: 117 | Type: CommaDelimitedList 118 | Description: List of governed regions 119 | Default: "us-east-1, us-west-2" 120 | pSecurityOuName: 121 | Type: String 122 | Description: The security Organizational Unit name 123 | Default: "Security" 124 | pSandboxOuName: 125 | Type: String 126 | Description: Name of additional OU to be created and registered in Control Tower 127 | Default: "Sandbox" 128 | pLoggingBucketRetentionPeriod: 129 | Type: Number 130 | Description: Retention period for centralized logging bucket 131 | Default: 365 132 | pAccessLoggingBucketRetentionPeriod: 133 | Type: Number 134 | Description: Retention period for access logging bucket 135 | Default: 90 136 | 137 | Conditions: 138 | cCreateNewAwsOrg: !Equals ["Yes", !Ref pCreateNewAwsOrg] 139 | cDeployNewLogArchiveAccount: !Equals ["Yes", !Ref pDeployNewLogArchiveAccount] 140 | cDeployNewSecurityAccount: !Equals ["Yes", !Ref pDeployNewSecurityAccount] 141 | 142 | Resources: 143 | rOrganization: 144 | Type: "AWS::Organizations::Organization" 145 | Condition: cCreateNewAwsOrg 146 | Properties: 147 | FeatureSet: ALL 148 | 149 | rOrgWaiter: 150 | Type: "AWS::CloudFormation::WaitConditionHandle" 151 | Metadata: 152 | WaitOn: !If [cCreateNewAwsOrg, !Ref rOrganization, !Ref AWS::NoValue ] 153 | 154 | rLoggingAccount: 155 | Type: "AWS::Organizations::Account" 156 | DependsOn: 157 | - rOrgWaiter 158 | Condition: cDeployNewLogArchiveAccount 159 | UpdateReplacePolicy: Retain 160 | DeletionPolicy: Retain 161 | Properties: 162 | AccountName: !Ref pLogArchiveAccountAlias 163 | Email: !Ref pLogArchiveAccountEmailAddress 164 | 165 | rSecurityAccount: 166 | Type: "AWS::Organizations::Account" 167 | DependsOn: 168 | - rOrgWaiter 169 | Condition: cDeployNewSecurityAccount 170 | UpdateReplacePolicy: Retain 171 | DeletionPolicy: Retain 172 | Properties: 173 | AccountName: !Ref pSecurityAccountAlias 174 | Email: !Ref pSecurityAccountEmailAddress 175 | 176 | rAWSControlTowerAdmin: 177 | Type: "AWS::IAM::Role" 178 | Metadata: 179 | cfn_nag: 180 | rules_to_suppress: 181 | - id: W28 182 | reason: "Ignoring explicit role name" 183 | Properties: 184 | AssumeRolePolicyDocument: 185 | Version: "2012-10-17" 186 | Statement: 187 | - Effect: Allow 188 | Principal: 189 | Service: "controltower.amazonaws.com" 190 | Action: "sts:AssumeRole" 191 | Path: "/service-role/" 192 | ManagedPolicyArns: 193 | - !Sub "arn:${AWS::Partition}:iam::aws:policy/service-role/AWSControlTowerServiceRolePolicy" 194 | RoleName: AWSControlTowerAdmin 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: rAWSControlTowerAdmin 202 | 203 | rAWSControlTowerAdminPolicy: 204 | Type: "AWS::IAM::Policy" 205 | Metadata: 206 | cfn_nag: 207 | rules_to_suppress: 208 | - id: W12 209 | reason: "Ignoring star in policy" 210 | Properties: 211 | PolicyName: AWSControlTowerAdminPolicy 212 | PolicyDocument: 213 | Version: "2012-10-17" 214 | Statement: 215 | - Effect: Allow 216 | Action: "ec2:DescribeAvailabilityZones" 217 | Resource: "*" 218 | Roles: 219 | - !Ref rAWSControlTowerAdmin 220 | 221 | rAWSControlTowerCloudTrailRole: 222 | Type: "AWS::IAM::Role" 223 | Metadata: 224 | cfn_nag: 225 | rules_to_suppress: 226 | - id: W28 227 | reason: "Ignoring explicit role name" 228 | Properties: 229 | AssumeRolePolicyDocument: 230 | Version: "2012-10-17" 231 | Statement: 232 | - Effect: Allow 233 | Principal: 234 | Service: "cloudtrail.amazonaws.com" 235 | Action: "sts:AssumeRole" 236 | Path: "/service-role/" 237 | RoleName: AWSControlTowerCloudTrailRole 238 | Tags: 239 | - Key: "aws-cloudformation:stack-name" 240 | Value: !Ref "AWS::StackName" 241 | - Key: "aws-cloudformation:stack-id" 242 | Value: !Ref "AWS::StackId" 243 | - Key: "aws-cloudformation:logical-id" 244 | Value: rAWSControlTowerCloudTrailRole 245 | 246 | rAWSControlTowerCloudTrailRolePolicy: 247 | Type: "AWS::IAM::Policy" 248 | Properties: 249 | PolicyName: AWSControlTowerCloudTrailRolePolicy 250 | PolicyDocument: 251 | Version: "2012-10-17" 252 | Statement: 253 | - Effect: Allow 254 | Action: 255 | - "logs:CreateLogStream" 256 | - "logs:PutLogEvents" 257 | Resource: !Sub "arn:${AWS::Partition}:logs:*:*:log-group:aws-controltower/CloudTrailLogs:*" 258 | Roles: 259 | - !Ref rAWSControlTowerCloudTrailRole 260 | 261 | rAWSControlTowerConfigAggregatorRoleForOrganizations: 262 | Type: "AWS::IAM::Role" 263 | Metadata: 264 | cfn_nag: 265 | rules_to_suppress: 266 | - id: W28 267 | reason: "Ignoring explicit role name" 268 | Properties: 269 | AssumeRolePolicyDocument: 270 | Version: "2012-10-17" 271 | Statement: 272 | - Effect: Allow 273 | Principal: 274 | Service: "config.amazonaws.com" 275 | Action: "sts:AssumeRole" 276 | ManagedPolicyArns: 277 | - !Sub "arn:${AWS::Partition}:iam::aws:policy/service-role/AWSConfigRoleForOrganizations" 278 | Path: "/service-role/" 279 | RoleName: AWSControlTowerConfigAggregatorRoleForOrganizations 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: rAWSControlTowerConfigAggregatorRoleForOrganizations 287 | 288 | rAWSControlTowerStackSetRole: 289 | Type: "AWS::IAM::Role" 290 | Metadata: 291 | cfn_nag: 292 | rules_to_suppress: 293 | - id: W28 294 | reason: "Ignoring explicit role name" 295 | Properties: 296 | AssumeRolePolicyDocument: 297 | Version: "2012-10-17" 298 | Statement: 299 | - Effect: Allow 300 | Principal: 301 | Service: "cloudformation.amazonaws.com" 302 | Action: "sts:AssumeRole" 303 | Path: "/service-role/" 304 | RoleName: AWSControlTowerStackSetRole 305 | Tags: 306 | - Key: "aws-cloudformation:stack-name" 307 | Value: !Ref "AWS::StackName" 308 | - Key: "aws-cloudformation:stack-id" 309 | Value: !Ref "AWS::StackId" 310 | - Key: "aws-cloudformation:logical-id" 311 | Value: rAWSControlTowerStackSetRole 312 | 313 | rAWSControlTowerStackSetRolePolicy: 314 | Type: "AWS::IAM::Policy" 315 | Properties: 316 | PolicyName: AWSControlTowerStackSetRolePolicy 317 | PolicyDocument: 318 | Version: "2012-10-17" 319 | Statement: 320 | - Effect: Allow 321 | Action: "sts:AssumeRole" 322 | Resource: !Sub "arn:${AWS::Partition}:iam::*:role/AWSControlTowerExecution" 323 | Roles: 324 | - !Ref rAWSControlTowerStackSetRole 325 | 326 | rSecurityAccountWaiter: 327 | Type: "AWS::CloudFormation::WaitConditionHandle" 328 | Metadata: 329 | WaitOn: !If 330 | - cDeployNewSecurityAccount 331 | - !Ref rSecurityAccount 332 | - !Ref "AWS::NoValue" 333 | 334 | rLogArchiveAccountWaiter: 335 | Type: "AWS::CloudFormation::WaitConditionHandle" 336 | Metadata: 337 | WaitOn: !If 338 | - cDeployNewLogArchiveAccount 339 | - !Ref rLoggingAccount 340 | - !Ref "AWS::NoValue" 341 | 342 | rControlTowerLandingZone: 343 | Type: "AWS::ControlTower::LandingZone" 344 | DependsOn: 345 | - rSecurityAccountWaiter 346 | - rLogArchiveAccountWaiter 347 | - rAWSControlTowerAdmin 348 | Properties: 349 | Version: !Ref pVersion 350 | Manifest: 351 | governedRegions: !Ref pGovernedRegions 352 | organizationStructure: 353 | security: 354 | name: !Ref pSecurityOuName 355 | sandbox: 356 | name: !Ref pSandboxOuName 357 | centralizedLogging: 358 | accountId: !If 359 | - cDeployNewLogArchiveAccount 360 | - !Ref rLoggingAccount 361 | - !Ref pImportedLogArchiveAccountId 362 | configurations: 363 | loggingBucket: 364 | retentionDays: !Ref pLoggingBucketRetentionPeriod 365 | accessLoggingBucket: 366 | retentionDays: !Ref pAccessLoggingBucketRetentionPeriod 367 | enabled: true 368 | securityRoles: # specify your Audit/Security Tooling account 369 | accountId: !If 370 | - cDeployNewSecurityAccount 371 | - !Ref rSecurityAccount 372 | - !Ref pImportedSecurityAccountId 373 | accessManagement: # enable identity center or not 374 | enabled: true 375 | 376 | Outputs: 377 | LoggingAccountId: 378 | Description: Logging Account ID 379 | Condition: cDeployNewLogArchiveAccount 380 | Value: !Ref rLoggingAccount 381 | SecurityAccountId: 382 | Description: Security Account ID 383 | Condition: cDeployNewSecurityAccount 384 | Value: !Ref rSecurityAccount 385 | LandingZoneId: 386 | Description: Control Tower Landing Zone ID 387 | Value: !GetAtt rControlTowerLandingZone.LandingZoneIdentifier -------------------------------------------------------------------------------- /github_ci_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 | Description: Guidance for Organizations on AWS - GitHub Actions CI/CD 7 | 8 | Parameters: 9 | pStackName: 10 | Type: String 11 | Description: Name of the stack to use for this deployment 12 | Default: DO-NOT-DELETE-organization 13 | pGithubOrganization: 14 | Type: String 15 | Description: GitHub Organization or User 16 | pGitHubRepository: 17 | Type: String 18 | Description: GitHub Repository 19 | Default: orgs-prescriptive-guidance 20 | 21 | Resources: 22 | rCloudFormationRole: 23 | Type: "AWS::IAM::Role" 24 | Properties: 25 | AssumeRolePolicyDocument: 26 | Version: "2012-10-17" 27 | Statement: 28 | Effect: Allow 29 | Principal: 30 | Service: !Sub "cloudformation.${AWS::URLSuffix}" 31 | Action: "sts:AssumeRole" 32 | Description: !Sub "DO NOT DELETE - Used by CloudFormation. Created by CloudFormation ${AWS::StackId}" 33 | ManagedPolicyArns: 34 | - !Sub "arn:${AWS::Partition}:iam::aws:policy/AWSOrganizationsFullAccess" 35 | - !Sub "arn:${AWS::Partition}:iam::aws:policy/AWSCloudFormationFullAccess" 36 | - !Sub "arn:${AWS::Partition}:iam::aws:policy/IAMReadOnlyAccess" 37 | - !Sub "arn:${AWS::Partition}:iam::aws:policy/AWSSSOReadOnly" 38 | Policies: 39 | - PolicyName: CloudFormationPolicy 40 | PolicyDocument: 41 | Version: "2012-10-17" 42 | Statement: 43 | - Effect: Allow 44 | Action: 45 | - "iam:AddUserToGroup" 46 | - "iam:CreateGroup" 47 | - "iam:CreateServiceLinkedRole" 48 | - "iam:CreateRole" 49 | - "iam:CreateUser" 50 | - "iam:DeleteGroup" 51 | - "iam:DeleteRole" 52 | - "iam:DeleteRolePolicy" 53 | - "iam:DeleteUser" 54 | - "iam:DeleteUserPolicy" 55 | - "iam:PutRolePolicy" 56 | - "iam:PutUserPolicy" 57 | - "iam:RemoveUserFromGroup" 58 | - "iam:TagRole" 59 | - "iam:TagUser" 60 | - "iam:UntagRole" 61 | - "iam:UntagUser" 62 | - "kms:CreateGrant" 63 | - "kms:Decrypt" 64 | - "kms:DescribeKey" 65 | - "kms:Encrypt" 66 | - "lambda:ListTags" 67 | - "lambda:TagResource" 68 | - "lambda:CreateFunction" 69 | - "lambda:DeleteFunction" 70 | - "lambda:GetFunction" 71 | - "lambda:GetFunctionCodeSigningConfig" 72 | - "lambda:GetFunctionRecursionConfig" 73 | - "lambda:GetRuntimeManagementConfig" 74 | - "lambda:InvokeFunction" 75 | - "lambda:UpdateFunctionCode" 76 | - "lambda:UpdateFunctionConfiguration" 77 | - "logs:DescribeLogGroups" 78 | - "logs:ListTagsForResource" 79 | - "logs:TagResource" 80 | - "logs:CreateLogGroup" 81 | - "logs:DeleteLogGroup" 82 | - "logs:PutRetentionPolicy" 83 | - "sso:CreatePermissionSet" 84 | - "sso:AttachCustomerManagedPolicyReferenceToPermissionSet" 85 | - "sso:AttachManagedPolicyToPermissionSet" 86 | - "sso:DeleteInlinePolicyFromPermissionSet" 87 | - "sso:DeletePermissionSet" 88 | - "sso:DeletePermissionsBoundaryFromPermissionSet" 89 | - "sso:DeletePermissionsPolicy" 90 | - "sso:ProvisionPermissionSet" 91 | - "sso:PutInlinePolicyToPermissionSet" 92 | - "sso:PutPermissionsBoundaryToPermissionSet" 93 | - "sso:PutPermissionsPolicy" 94 | - "sso:StartSSO" 95 | - "sso:TagResource" 96 | - "sso:UntagResource" 97 | - "sso:UpdatePermissionSet" 98 | Resource: "*" 99 | - Effect: Allow 100 | Action: "s3:GetObject" 101 | Resource: !Sub "${rArtifactBucket.Arn}/*" 102 | - Effect: Allow 103 | Action: "iam:PassRole" 104 | Resource: "*" 105 | Condition: 106 | StringEquals: 107 | "iam:PassedToService": "lambda.amazonaws.com" 108 | Tags: 109 | - Key: "aws-cloudformation:stack-name" 110 | Value: !Ref "AWS::StackName" 111 | - Key: "aws-cloudformation:stack-id" 112 | Value: !Ref "AWS::StackId" 113 | - Key: "aws-cloudformation:logical-id" 114 | Value: rCloudFormationRole 115 | 116 | rGitHubRole: 117 | Type: "AWS::IAM::Role" 118 | Properties: 119 | AssumeRolePolicyDocument: 120 | Version: "2012-10-17" 121 | Statement: 122 | Effect: Allow 123 | Principal: 124 | Federated: !Ref rGitHubProvider 125 | Action: "sts:AssumeRoleWithWebIdentity" 126 | Condition: 127 | StringEquals: 128 | "token.actions.githubusercontent.com:aud": "sts.amazonaws.com" 129 | StringLike: 130 | "token.actions.githubusercontent.com:sub": !Sub "repo:${pGithubOrganization}/${pGitHubRepository}:*" 131 | Description: !Sub "DO NOT DELETE - Used by GitHub Actions. Created by CloudFormation ${AWS::StackId}" 132 | ManagedPolicyArns: 133 | - !Sub "arn:${AWS::Partition}:iam::aws:policy/AWSCloudFormationReadOnlyAccess" 134 | Policies: 135 | - PolicyName: GitHubPolicy 136 | PolicyDocument: 137 | Version: "2012-10-17" 138 | Statement: 139 | - Effect: Allow 140 | Action: "iam:PassRole" 141 | Resource: !GetAtt rCloudFormationRole.Arn 142 | Condition: 143 | StringEquals: 144 | "aws:ResourceAccount": "${aws:PrincipalAccount}" 145 | "iam:PassedToService": !Sub "cloudformation.${AWS::URLSuffix}" 146 | - Effect: Allow 147 | Action: 148 | - "cloudformation:ContinueUpdateRollback" 149 | - "cloudformation:CreateChangeSet" 150 | - "cloudformation:CreateStack" 151 | - "cloudformation:DeleteStack" 152 | - "cloudformation:RollbackStack" 153 | - "cloudformation:UpdateStack" 154 | Resource: !Sub "arn:${AWS::Partition}:cloudformation:*:${AWS::AccountId}:stack/${pStackName}*" 155 | Condition: 156 | ArnLike: 157 | "cloudformation:RoleArn": !GetAtt rCloudFormationRole.Arn 158 | "Null": 159 | "cloudformation:ImportResourceTypes": true 160 | - Effect: Allow 161 | Action: 162 | - "cloudformation:CancelUpdateStack" 163 | - "cloudformation:DeleteChangeSet" 164 | - "cloudformation:DetectStackDrift" 165 | - "cloudformation:DetectStackResourceDrift" 166 | - "cloudformation:ExecuteChangeSet" 167 | - "cloudformation:TagResource" 168 | - "cloudformation:UntagResource" 169 | - "cloudformation:UpdateTerminationProtection" 170 | Resource: !Sub "arn:${AWS::Partition}:cloudformation:*:${AWS::AccountId}:stack/${pStackName}*" 171 | - Effect: Allow 172 | Action: 173 | - "cloudformation:ValidateTemplate" 174 | - "cloudformation:EstimateTemplateCost" 175 | Resource: "*" 176 | - Effect: Allow 177 | Action: 178 | - "s3:GetObject" 179 | - "s3:GetObjectVersion" 180 | - "s3:PutObject" 181 | Resource: !Sub "${rArtifactBucket.Arn}/*" 182 | - Effect: Allow 183 | Action: 184 | - "s3:ListBucket" 185 | - "s3:GetBucketPolicy" 186 | - "s3:GetBucketVersioning" 187 | - "s3:GetBucketLocation" 188 | Resource: !GetAtt rArtifactBucket.Arn 189 | 190 | rGitHubProvider: 191 | Type: "AWS::IAM::OIDCProvider" 192 | Properties: 193 | ClientIdList: 194 | - "sts.amazonaws.com" 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: rGitHubProvider 202 | Url: https://token.actions.githubusercontent.com 203 | 204 | rArtifactBucket: 205 | Type: "AWS::S3::Bucket" 206 | UpdateReplacePolicy: Delete 207 | DeletionPolicy: RetainExceptOnCreate 208 | Properties: 209 | BucketEncryption: 210 | ServerSideEncryptionConfiguration: 211 | - ServerSideEncryptionByDefault: 212 | SSEAlgorithm: AES256 213 | LifecycleConfiguration: 214 | Rules: 215 | - ExpirationInDays: 30 216 | Id: ExpireAfter30Days 217 | Status: Enabled 218 | - AbortIncompleteMultipartUpload: 219 | DaysAfterInitiation: 1 220 | Id: ExpireNonCurrentAndIncompleteMultipartUpload 221 | NoncurrentVersionExpiration: 222 | NoncurrentDays: 1 223 | Status: Enabled 224 | OwnershipControls: 225 | Rules: 226 | - ObjectOwnership: BucketOwnerEnforced 227 | PublicAccessBlockConfiguration: 228 | BlockPublicAcls: true 229 | BlockPublicPolicy: true 230 | IgnorePublicAcls: true 231 | RestrictPublicBuckets: true 232 | VersioningConfiguration: 233 | Status: Enabled 234 | 235 | rArtifactBucketPolicy: 236 | Type: "AWS::S3::BucketPolicy" 237 | Properties: 238 | Bucket: !Ref rArtifactBucket 239 | PolicyDocument: 240 | Statement: 241 | - Effect: Deny 242 | Principal: "*" 243 | Action: "s3:*" 244 | Resource: 245 | - !Sub "${rArtifactBucket.Arn}/*" 246 | - !GetAtt rArtifactBucket.Arn 247 | Condition: 248 | Bool: 249 | "aws:SecureTransport": false 250 | 251 | Outputs: 252 | oCloudFormationRoleArn: 253 | Description: CloudFormation IAM Role ARN 254 | Value: !GetAtt rCloudFormationRole.Arn 255 | oGitHubRoleArn: 256 | Description: GitHub Actions IAM Role ARN 257 | Value: !GetAtt rGitHubRole.Arn 258 | oArtifactBucket: 259 | Description: Artifact Bucket Name 260 | Value: !Ref rArtifactBucket 261 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | black==24.10.0 2 | aws-lambda-powertools[all,aws-sdk]==3.3.0 3 | boto3-stubs[iam,organizations,cloudformation]==1.35.77 -------------------------------------------------------------------------------- /src/activation_lambda/index.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 | import time 24 | from typing import TYPE_CHECKING 25 | 26 | from aws_lambda_powertools import Logger 27 | from aws_lambda_powertools.utilities.typing import LambdaContext 28 | import boto3 29 | from botocore.config import Config 30 | from crhelper import CfnResource 31 | 32 | if TYPE_CHECKING: 33 | from mypy_boto3_cloudformation import CloudFormationClient 34 | from mypy_boto3_iam import IAMClient 35 | from mypy_boto3_organizations import OrganizationsClient 36 | 37 | logger = Logger(utc=True, use_rfc3339=True) 38 | helper = CfnResource( 39 | json_logging=True, 40 | log_level="INFO", 41 | boto_level="CRITICAL", 42 | sleep_on_delete=120, 43 | ssl_verify=None, 44 | ) 45 | 46 | config = Config( 47 | retries={ 48 | "max_attempts": 10, 49 | "mode": "standard", 50 | }, 51 | tcp_keepalive=True, 52 | ) 53 | 54 | cloudformation: "CloudFormationClient" = boto3.client("cloudformation", config=config) 55 | iam: "IAMClient" = boto3.client("iam", config=config) 56 | organizations: "OrganizationsClient" = boto3.client( 57 | "organizations", 58 | region_name="us-east-1", 59 | endpoint_url="https://organizations.us-east-1.amazonaws.com", 60 | config=config, 61 | ) 62 | 63 | try: 64 | root_id = os.getenv("ROOT_ID") 65 | 66 | response = cloudformation.describe_organizations_access(CallAs="SELF") 67 | status: str = response.get("Status") 68 | logger.info("Organizations Access Status: " + status) 69 | except Exception as e: 70 | helper.init_failure(e) 71 | 72 | policy_types = [ 73 | "SERVICE_CONTROL_POLICY", 74 | "RESOURCE_CONTROL_POLICY", 75 | "DECLARATIVE_POLICY_EC2", 76 | "AISERVICES_OPT_OUT_POLICY", 77 | "BACKUP_POLICY", 78 | "CHATBOT_POLICY", 79 | "TAG_POLICY", 80 | ] 81 | 82 | 83 | @helper.create 84 | def create(event: dict, context: LambdaContext): 85 | if status == "ENABLED": 86 | logger.warning("Organizations access is already enabled") 87 | else: 88 | logger.debug("Activating organizations access...") 89 | cloudformation.activate_organizations_access() 90 | logger.info("Successfully activated organizations access") 91 | 92 | for policy_type in policy_types: 93 | logger.debug(f"Enabling {policy_type} policy type...") 94 | while True: 95 | try: 96 | organizations.enable_policy_type(RootId=root_id, PolicyType=policy_type) 97 | except organizations.exceptions.PolicyTypeAlreadyEnabledException: 98 | break 99 | except organizations.exceptions.ConcurrentModificationException: 100 | time.sleep(0.1) 101 | else: 102 | break 103 | logger.info(f"Enabled {policy_type} policy type") 104 | 105 | logger.debug("Enabling AWS service access for IAM...") 106 | organizations.enable_aws_service_access(ServicePrincipal="iam.amazonaws.com") 107 | logger.info("Enabled AWS service access for IAM") 108 | 109 | logger.debug("Enabling organizations root credentials management...") 110 | iam.enable_organizations_root_credentials_management() 111 | logger.info("Enabled organizations root credentials management") 112 | 113 | logger.debug("Enabling organizations root sessions...") 114 | iam.enable_organizations_root_sessions() 115 | logger.info("Enabled organizations root sessions") 116 | 117 | 118 | @helper.delete 119 | def delete(event: dict, context: LambdaContext): 120 | if status == "DISABLED": 121 | logger.warning("Organizations access is already disabled") 122 | else: 123 | logger.debug("Deactivating organizations access...") 124 | try: 125 | cloudformation.deactivate_organizations_access() 126 | except cloudformation.exceptions.InvalidOperationException: 127 | pass 128 | logger.info("Successfully deactivated organizations access") 129 | 130 | for policy_type in policy_types: 131 | logger.debug(f"Disabling {policy_type} policy type...") 132 | while True: 133 | try: 134 | organizations.disable_policy_type( 135 | RootId=root_id, PolicyType=policy_type 136 | ) 137 | except organizations.exceptions.PolicyTypeNotEnabledException: 138 | break 139 | except organizations.exceptions.ConcurrentModificationException: 140 | time.sleep(0.1) 141 | else: 142 | break 143 | logger.info(f"Disabled {policy_type} policy type") 144 | 145 | logger.debug("Disabling organizations root sessions...") 146 | iam.disable_organizations_root_sessions() 147 | logger.info("Disabled organizations root sessions") 148 | 149 | logger.debug("Disabling organizations root credentials management...") 150 | iam.disable_organizations_root_credentials_management() 151 | logger.info("Disabled organizations root credentials management") 152 | 153 | logger.debug("Disabling AWS service access for IAM...") 154 | organizations.disable_aws_service_access(ServicePrincipal="iam.amazonaws.com") 155 | logger.info("Disabled AWS service access for IAM") 156 | 157 | 158 | @logger.inject_lambda_context(log_event=True) 159 | def handler(event: dict, context: LambdaContext) -> dict: 160 | helper(event, context) 161 | -------------------------------------------------------------------------------- /src/activation_lambda/requirements.txt: -------------------------------------------------------------------------------- 1 | aws-lambda-powertools[aws-sdk]==3.3.0 2 | crhelper==2.0.12 -------------------------------------------------------------------------------- /src/stackset_parameters/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 | Description: "DO NOT DELETE - Organizational SSM Parameters" 7 | 8 | Parameters: 9 | pManagementAccountId: 10 | Type: String 11 | Description: AWS Account ID for Management Account 12 | ConstraintDescription: "must only contain numbers" 13 | AllowedPattern: "^[0-9]{12}$" 14 | pOrganizationId: 15 | Type: String 16 | Description: Organization ID 17 | ConstraintDescription: "must only contain lowercase letters and numbers" 18 | AllowedPattern: "^o\\-[a-z0-9]+" 19 | pCloudFormationRoleName: 20 | Type: String 21 | Description: CloudFormation IAM role name 22 | Default: CloudFormationRole 23 | ConstraintDescription: "must only contain uppercase and lowercase letters and numbers" 24 | AllowedPattern: "[a-zA-Z0-9]+" 25 | pServiceCatalogRoleName: 26 | Type: String 27 | Description: ServiceCatalog IAM role name 28 | Default: ServiceCatalogRole 29 | ConstraintDescription: "must only contain uppercase and lowercase letters and numbers" 30 | AllowedPattern: "[a-zA-Z0-9]+" 31 | 32 | Resources: 33 | ManagementIdParameter: 34 | Type: "AWS::SSM::Parameter" 35 | Properties: 36 | DataType: text 37 | Description: Management AWS Account ID 38 | Name: /org/core/accounts/management 39 | Tier: Standard 40 | Type: String 41 | Value: !Ref pManagementAccountId 42 | 43 | OrganizationIdParameter: 44 | Type: "AWS::SSM::Parameter" 45 | Properties: 46 | DataType: text 47 | Description: Organization ID 48 | Name: /org/core/organization-id 49 | Tier: Standard 50 | Type: String 51 | Value: !Ref pOrganizationId 52 | 53 | CloudFormationRoleNameParameter: 54 | Type: "AWS::SSM::Parameter" 55 | Properties: 56 | DataType: text 57 | Description: CloudFormation IAM role name 58 | Name: /platform/iam/CloudFormationRole 59 | Tier: Standard 60 | Type: String 61 | Value: !Ref pCloudFormationRoleName 62 | 63 | CloudFormationRoleArnParameter: 64 | Type: "AWS::SSM::Parameter" 65 | Properties: 66 | DataType: text 67 | Description: CloudFormation IAM role ARN 68 | Name: /platform/iam/CloudFormationRoleArn 69 | Tier: Standard 70 | Type: String 71 | Value: !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/${pCloudFormationRoleName}" 72 | 73 | ServiceCatalogRoleNameParameter: 74 | Type: "AWS::SSM::Parameter" 75 | Properties: 76 | DataType: text 77 | Description: ServiceCatalog IAM role name 78 | Name: /platform/iam/ServiceCatalogRole 79 | Tier: Standard 80 | Type: String 81 | Value: !Ref pServiceCatalogRoleName 82 | 83 | ServiceCatalogRoleArnParameter: 84 | Type: "AWS::SSM::Parameter" 85 | Properties: 86 | DataType: text 87 | Description: ServiceCatalog IAM role ARN 88 | Name: /platform/iam/ServiceCatalogRoleArn 89 | Tier: Standard 90 | Type: String 91 | Value: !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/${pServiceCatalogRoleName}" 92 | -------------------------------------------------------------------------------- /src/stackset_roles/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 | Description: "DO NOT DELETE - Organizational IAM Roles" 7 | 8 | Parameters: 9 | pManagementAccountId: 10 | Type: String 11 | Description: AWS Account ID for Management Account 12 | ConstraintDescription: "must only contain numbers" 13 | AllowedPattern: "^[0-9]{12}$" 14 | pDeveloperBoundaryName: 15 | Type: String 16 | Description: Name of the developers permission boundary 17 | Default: DeveloperBoundary 18 | ConstraintDescription: "must only contain uppercase and lowercase letters and numbers" 19 | AllowedPattern: "[a-zA-Z0-9]+" 20 | MinLength: 1 21 | pDeveloperPrefix: 22 | Type: String 23 | Description: Required prefix for self-service IAM roles and CloudFormation stacks 24 | Default: app 25 | ConstraintDescription: "must only contain lowercase letters and numbers" 26 | AllowedPattern: "[a-z0-9]+" 27 | MinLength: 1 28 | pCloudFormationRoleName: 29 | Type: String 30 | Description: CloudFormation IAM role name 31 | Default: CloudFormationRole 32 | ConstraintDescription: "must only contain uppercase and lowercase letters and numbers" 33 | AllowedPattern: "[a-zA-Z0-9]+" 34 | MinLength: 1 35 | MaxLength: 64 36 | pServiceCatalogRoleName: 37 | Type: String 38 | Description: Service Catalog IAM role name 39 | Default: ServiceCatalogRole 40 | ConstraintDescription: "must only contain uppercase and lowercase letters and numbers" 41 | AllowedPattern: "[a-zA-Z0-9]+" 42 | MinLength: 1 43 | MaxLength: 64 44 | pSupportRoleName: 45 | Type: String 46 | Description: AWS Support IAM role name 47 | Default: SupportRole 48 | ConstraintDescription: "must only contain uppercase and lowercase letters and numbers" 49 | AllowedPattern: "[a-zA-Z0-9]+" 50 | MinLength: 1 51 | MaxLength: 64 52 | 53 | Resources: 54 | rEmergencyAccessOpsRole: 55 | Type: "AWS::IAM::Role" 56 | Metadata: 57 | cfn_nag: 58 | rules_to_suppress: 59 | - id: W28 60 | reason: "Ignoring explicit role name" 61 | Properties: 62 | AssumeRolePolicyDocument: 63 | Version: "2012-10-17" 64 | Statement: 65 | Effect: Allow 66 | Principal: 67 | AWS: !Sub "arn:${AWS::Partition}:iam::${pManagementAccountId}:user/EmergencyAccess_Ops" 68 | Action: "sts:AssumeRole" 69 | Description: !Sub "DO NOT DELETE - Used for emergency access. Created by CloudFormation ${AWS::StackId}" 70 | ManagedPolicyArns: 71 | - !Sub "arn:${AWS::Partition}:iam::aws:policy/job-function/SystemAdministrator" 72 | RoleName: EmergencyAccess_Ops 73 | Tags: 74 | - Key: "aws-cloudformation:stack-name" 75 | Value: !Ref "AWS::StackName" 76 | - Key: "aws-cloudformation:stack-id" 77 | Value: !Ref "AWS::StackId" 78 | - Key: "aws-cloudformation:logical-id" 79 | Value: rEmergencyAccessOpsRole 80 | 81 | rEmergencyAccessReadOnlyRole: 82 | Type: "AWS::IAM::Role" 83 | Metadata: 84 | cfn_nag: 85 | rules_to_suppress: 86 | - id: W28 87 | reason: "Ignoring explicit role name" 88 | Properties: 89 | AssumeRolePolicyDocument: 90 | Version: "2012-10-17" 91 | Statement: 92 | Effect: Allow 93 | Principal: 94 | AWS: !Sub "arn:${AWS::Partition}:iam::${pManagementAccountId}:user/EmergencyAccess_RO" 95 | Action: "sts:AssumeRole" 96 | Description: !Sub "DO NOT DELETE - Used for emergency access (read-only). Created by CloudFormation ${AWS::StackId}" 97 | ManagedPolicyArns: 98 | - !Sub "arn:${AWS::Partition}:iam::aws:policy/ReadOnlyAccess" 99 | RoleName: EmergencyAccess_RO 100 | Tags: 101 | - Key: "aws-cloudformation:stack-name" 102 | Value: !Ref "AWS::StackName" 103 | - Key: "aws-cloudformation:stack-id" 104 | Value: !Ref "AWS::StackId" 105 | - Key: "aws-cloudformation:logical-id" 106 | Value: rEmergencyAccessReadOnlyRole 107 | 108 | rServiceCatalogRole: 109 | Type: "AWS::IAM::Role" 110 | Metadata: 111 | cfn_nag: 112 | rules_to_suppress: 113 | - id: W28 114 | reason: "Ignoring explicit role name" 115 | Properties: 116 | AssumeRolePolicyDocument: 117 | Version: "2012-10-17" 118 | Statement: 119 | Effect: Allow 120 | Principal: 121 | Service: !Sub "servicecatalog.${AWS::URLSuffix}" 122 | Action: "sts:AssumeRole" 123 | Description: !Sub "DO NOT DELETE - Used by Service Catalog. Created by CloudFormation ${AWS::StackId}" 124 | ManagedPolicyArns: 125 | - !Sub "arn:${AWS::Partition}:iam::aws:policy/AdministratorAccess" 126 | RoleName: !Ref pServiceCatalogRoleName 127 | Tags: 128 | - Key: "aws-cloudformation:stack-name" 129 | Value: !Ref "AWS::StackName" 130 | - Key: "aws-cloudformation:stack-id" 131 | Value: !Ref "AWS::StackId" 132 | - Key: "aws-cloudformation:logical-id" 133 | Value: rServiceCatalogRole 134 | 135 | rCloudFormationRole: 136 | Type: "AWS::IAM::Role" 137 | Metadata: 138 | cfn_nag: 139 | rules_to_suppress: 140 | - id: W28 141 | reason: "Ignoring explicit role name" 142 | Properties: 143 | AssumeRolePolicyDocument: 144 | Version: "2012-10-17" 145 | Statement: 146 | Effect: Allow 147 | Principal: 148 | Service: !Sub "cloudformation.${AWS::URLSuffix}" 149 | Action: "sts:AssumeRole" 150 | Condition: 151 | StringEquals: 152 | "aws:SourceAccount": !Ref "AWS::AccountId" 153 | ArnLike: 154 | "aws:SourceArn": !Sub "aws:${AWS::Partition}:cloudformation:${AWS::Region}:${AWS::AccountId}:stack/${pDeveloperPrefix}*" 155 | Description: !Sub "DO NOT DELETE - Used by CloudFormation. Created by CloudFormation ${AWS::StackId}" 156 | ManagedPolicyArns: 157 | - !Sub "arn:${AWS::Partition}:iam::aws:policy/AdministratorAccess" # limited by permissions boundary 158 | PermissionsBoundary: !Ref rDeveloperBoundary 159 | RoleName: !Ref pCloudFormationRoleName 160 | Tags: 161 | - Key: "aws-cloudformation:stack-name" 162 | Value: !Ref "AWS::StackName" 163 | - Key: "aws-cloudformation:stack-id" 164 | Value: !Ref "AWS::StackId" 165 | - Key: "aws-cloudformation:logical-id" 166 | Value: rCloudFormationRole 167 | 168 | rApiGatewayLoggingRole: 169 | Type: "AWS::IAM::Role" 170 | Properties: 171 | AssumeRolePolicyDocument: 172 | Version: "2012-10-17" 173 | Statement: 174 | Effect: Allow 175 | Principal: 176 | Service: !Sub "apigateway.${AWS::URLSuffix}" 177 | Action: "sts:AssumeRole" 178 | Description: !Sub "DO NOT DELETE - Used by API Gateway. Created by CloudFormation ${AWS::StackId}" 179 | ManagedPolicyArns: 180 | - !Sub "arn:${AWS::Partition}:iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs" 181 | Tags: 182 | - Key: "aws-cloudformation:stack-name" 183 | Value: !Ref "AWS::StackName" 184 | - Key: "aws-cloudformation:stack-id" 185 | Value: !Ref "AWS::StackId" 186 | - Key: "aws-cloudformation:logical-id" 187 | Value: rApiGatewayLoggingRole 188 | 189 | rAppSyncLoggingRole: 190 | Type: "AWS::IAM::Role" 191 | Properties: 192 | AssumeRolePolicyDocument: 193 | Version: "2012-10-17" 194 | Statement: 195 | Effect: Allow 196 | Principal: 197 | Service: !Sub "appsync.${AWS::URLSuffix}" 198 | Action: "sts:AssumeRole" 199 | Description: !Sub "DO NOT DELETE - Used by AppSync. Created by CloudFormation ${AWS::StackId}" 200 | ManagedPolicyArns: 201 | - !Sub "arn:${AWS::Partition}:iam::aws:policy/service-role/AWSAppSyncPushToCloudWatchLogs" 202 | Tags: 203 | - Key: "aws-cloudformation:stack-name" 204 | Value: !Ref "AWS::StackName" 205 | - Key: "aws-cloudformation:stack-id" 206 | Value: !Ref "AWS::StackId" 207 | - Key: "aws-cloudformation:logical-id" 208 | Value: rAppSyncLoggingRole 209 | 210 | rCloudFormationStackSetExecutionRole: 211 | Type: "AWS::IAM::Role" 212 | Metadata: 213 | cfn_nag: 214 | rules_to_suppress: 215 | - id: W28 216 | reason: "Ignoring explicit role name" 217 | Properties: 218 | AssumeRolePolicyDocument: 219 | Version: "2012-10-17" 220 | Statement: 221 | - Effect: Allow 222 | Principal: 223 | AWS: !Sub "arn:${AWS::Partition}:iam::${pManagementAccountId}:root" 224 | Action: "sts:AssumeRole" 225 | Description: !Sub "DO NOT DELETE - Used by CloudFormation StackSets. Created by CloudFormation ${AWS::StackId}" 226 | ManagedPolicyArns: 227 | - !Sub "arn:${AWS::Partition}:iam::aws:policy/AdministratorAccess" 228 | RoleName: AWSCloudFormationStackSetExecutionRole 229 | Tags: 230 | - Key: "aws-cloudformation:stack-name" 231 | Value: !Ref "AWS::StackName" 232 | - Key: "aws-cloudformation:stack-id" 233 | Value: !Ref "AWS::StackId" 234 | - Key: "aws-cloudformation:logical-id" 235 | Value: rCloudFormationStackSetExecutionRole 236 | 237 | rDeveloperBoundary: 238 | Type: "AWS::IAM::ManagedPolicy" 239 | Metadata: 240 | cfn_nag: 241 | rules_to_suppress: 242 | - id: F5 243 | reason: All actions allowed in boundary policy 244 | - id: W13 245 | reason: All resources allowed in boundary policy 246 | - id: W28 247 | reason: Boundary needs defined name to self reference 248 | Properties: 249 | Description: Permission boundary for developers 250 | ManagedPolicyName: !Ref pDeveloperBoundaryName 251 | PolicyDocument: 252 | Version: "2012-10-17" 253 | Statement: 254 | - Sid: AllowModifyIamRolesWithBoundary 255 | Effect: Allow 256 | Action: 257 | - "iam:AttachRolePolicy" 258 | - "iam:CreateRole" 259 | - "iam:DeleteRolePolicy" 260 | - "iam:DetachRolePolicy" 261 | - "iam:PutRolePermissionsBoundary" 262 | - "iam:PutRolePolicy" 263 | Resource: !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/${pDeveloperPrefix}/*" 264 | Condition: 265 | ArnEquals: 266 | "iam:PermissionsBoundary": !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:policy/${pDeveloperBoundaryName}" # references self 267 | - Sid: AllowModifyIamRoles 268 | Effect: Allow 269 | Action: 270 | - "iam:DeleteRole" 271 | - "iam:GetRole" 272 | - "iam:PassRole" 273 | - "iam:TagRole" 274 | - "iam:UntagRole" 275 | - "iam:UpdateAssumeRolePolicy" 276 | - "iam:UpdateRole" 277 | - "iam:UpdateRoleDescription" 278 | Resource: !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/${pDeveloperPrefix}/*" 279 | - Sid: OverlyPermissiveAllowedServices 280 | Effect: Allow 281 | Action: 282 | - "amplify:*" 283 | - "apigateway:*" 284 | - "appsync:*" 285 | - "events:*" 286 | - "dynamodb:*" 287 | - "lambda:*" 288 | - "logs:*" 289 | - "s3:*" 290 | Resource: "*" 291 | 292 | # Required for SecurityHub [IAM.18] 293 | # https://docs.aws.amazon.com/securityhub/latest/userguide/iam-controls.html#iam-18 294 | rSupportRole: 295 | Type: "AWS::IAM::Role" 296 | Metadata: 297 | cfn_nag: 298 | rules_to_suppress: 299 | - id: W28 300 | reason: "Ignoring explicit role name" 301 | Properties: 302 | AssumeRolePolicyDocument: 303 | Version: "2012-10-17" 304 | Statement: 305 | Effect: Allow 306 | Principal: 307 | AWS: !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:root" 308 | Action: "sts:AssumeRole" 309 | Description: !Sub "DO NOT DELETE - Used for access to AWS Support. Created by CloudFormation ${AWS::StackId}" 310 | ManagedPolicyArns: 311 | - !Sub "arn:${AWS::Partition}:iam::aws:policy/AWSSupportAccess" 312 | RoleName: !Ref pSupportRoleName 313 | Tags: 314 | - Key: "aws-cloudformation:stack-name" 315 | Value: !Ref "AWS::StackName" 316 | - Key: "aws-cloudformation:stack-id" 317 | Value: !Ref "AWS::StackId" 318 | - Key: "aws-cloudformation:logical-id" 319 | Value: rSupportRole 320 | 321 | Outputs: 322 | oCloudFormationRoleArn: 323 | Description: CloudFormation IAM role ARN 324 | Value: !GetAtt rCloudFormationRole.Arn 325 | oCloudFormationRoleName: 326 | Description: CloudFormation IAM role name 327 | Value: !Ref rCloudFormationRole 328 | oServiceCatalogRoleArn: 329 | Description: Service Catalog IAM role ARN 330 | Value: !GetAtt rServiceCatalogRole.Arn 331 | oServiceCatalogRoleName: 332 | Description: Service Catalog IAM role name 333 | Value: !Ref rServiceCatalogRole 334 | oSupportRoleArn: 335 | Description: Support IAM role ARN 336 | Value: !GetAtt rSupportRole.Arn 337 | oSupportRoleName: 338 | Description: Support IAM role name 339 | Value: !Ref rSupportRole 340 | oDeveloperBoundaryPolicyArn: 341 | Description: Developer boundary IAM policy ARN 342 | Value: !Ref rDeveloperBoundary -------------------------------------------------------------------------------- /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: "DO NOT DELETE - AWS Organization" 8 | 9 | Parameters: 10 | pInstanceArn: 11 | Type: String 12 | Description: AWS Identity Center Center Instance ARN 13 | Default: "" 14 | pDeveloperPrefix: 15 | Type: String 16 | Description: Required prefix for self-service IAM roles and CloudFormation stacks 17 | Default: app 18 | ConstraintDescription: "must only contain lowercase letters and numbers" 19 | AllowedPattern: "[a-z0-9]+" 20 | MinLength: 1 21 | pCloudFormationRoleName: 22 | Type: String 23 | Description: CloudFormation IAM role name 24 | Default: CloudFormationRole 25 | ConstraintDescription: "must only contain uppercase and lowercase letters and numbers" 26 | AllowedPattern: "[a-zA-Z0-9]+" 27 | MinLength: 1 28 | pServiceCatalogRoleName: 29 | Type: String 30 | Description: Service Catalog IAM role name 31 | Default: ServiceCatalogRole 32 | ConstraintDescription: "must only contain uppercase and lowercase letters and numbers" 33 | AllowedPattern: "[a-zA-Z0-9]+" 34 | MinLength: 1 35 | pRegions: 36 | Type: CommaDelimitedList 37 | Description: Comma-delimited list of regions to deploy the stacksets 38 | Default: us-east-1 39 | pSandboxOuName: 40 | Type: String 41 | Description: Organizational Unit (OU) for sandbox accounts 42 | Default: Sandbox 43 | pSecurityOuName: 44 | Type: String 45 | Description: Organizational Unit (OU) for security accounts 46 | Default: Security_Prod 47 | pWorkloadsOuName: 48 | Type: String 49 | Description: Organizational Unit (OU) for workloads accounts 50 | Default: Workloads 51 | pInfrastructureOuName: 52 | Type: String 53 | Description: Organizational Unit (OU) for infrastructure accounts 54 | Default: Infrastructure 55 | pGithubOrganization: 56 | Type: String 57 | Description: GitHub Organization or User 58 | pCreateNewAwsOrg: 59 | Type: String 60 | Description: Specify whether to create the AWS Organization or if one already exists. Select no if you already have an AWS Organization. 61 | Default: "Yes" 62 | AllowedValues: 63 | - "Yes" 64 | - "No" 65 | pOrganizationId: 66 | Type: String 67 | Description: Existing AWS Organizations ID 68 | Default: "" 69 | pOrganizationRootId: 70 | Type: String 71 | Description: Existing AWS Organizations Root ID 72 | Default: "" 73 | 74 | Conditions: 75 | cCreateNewAwsOrg: !Equals ["Yes", !Ref pCreateNewAwsOrg] 76 | cHasInstanceArn: !Not [!Equals [!Ref pInstanceArn, ""]] 77 | cHasOrganizationId: !And 78 | - !Not [!Condition cCreateNewAwsOrg] 79 | - !Not [!Equals [!Ref pOrganizationId, ""]] 80 | cHasOrganizationRootId: !And 81 | - !Not [!Condition cCreateNewAwsOrg] 82 | - !Not [!Equals [!Ref pOrganizationRootId, ""]] 83 | 84 | Rules: 85 | tCreateOrganization: 86 | RuleCondition: !Equals ["No", !Ref pCreateNewAwsOrg] 87 | Assertions: 88 | - Assert: !And 89 | - !Not [!Equals [!Ref pOrganizationId, ""]] 90 | - !Not [!Equals [!Ref pOrganizationRootId, ""]] 91 | AssertDescription: "Must provide existing AWS Organizations ID and AWS Organizations Root ID" 92 | 93 | Resources: 94 | rOrganization: 95 | Type: "AWS::Organizations::Organization" 96 | Condition: cCreateNewAwsOrg 97 | DeletionPolicy: Delete 98 | UpdateReplacePolicy: Delete 99 | Properties: 100 | FeatureSet: ALL 101 | 102 | rOrgWaiter: 103 | Type: "AWS::CloudFormation::WaitConditionHandle" 104 | Metadata: 105 | WaitOn: !If 106 | - cCreateNewAwsOrg 107 | - !Ref rOrganization 108 | - !Ref AWS::NoValue 109 | 110 | rOrganizationPolicy: 111 | Type: "AWS::Organizations::ResourcePolicy" 112 | DependsOn: rOrgWaiter 113 | Properties: 114 | Content: 115 | Version: "2012-10-17" 116 | Statement: 117 | - Sid: DelegatingDescribeListActions 118 | Effect: Allow 119 | Principal: "*" 120 | Action: 121 | - "organizations:List*" 122 | - "organizations:Describe*" 123 | Resource: "*" 124 | Condition: 125 | StringEquals: 126 | "aws:PrincipalOrgID": !If 127 | - cHasOrganizationId 128 | - !Ref pOrganizationId 129 | - !GetAtt rOrganization.Id 130 | 131 | rRootServicePolicy: 132 | Type: "AWS::Organizations::Policy" 133 | DependsOn: rActivateCustomResource 134 | Properties: 135 | Content: 136 | Version: "2012-10-17" 137 | Statement: 138 | - Sid: DenyRootUserActions 139 | Effect: Deny 140 | Action: "*" 141 | Resource: "*" 142 | Condition: 143 | ArnLike: 144 | "aws:PrincipalArn": !Sub "arn:${AWS::Partition}:iam::*:root" 145 | "Null": 146 | "aws:AssumedRoot": "true" 147 | - Sid: DenyLeaveOrganization 148 | Effect: Deny 149 | Action: "organizations:LeaveOrganization" 150 | Resource: "*" 151 | - Sid: DenyAccountClosure 152 | Effect: Deny 153 | Action: 154 | - "organizations:CloseAccount" 155 | - "account:CloseAccount" 156 | Resource: "*" 157 | Description: Deny Root User Actions, Leaving Organization, and Account Closure 158 | Name: RootPolicy 159 | Tags: 160 | - Key: "aws-cloudformation:stack-name" 161 | Value: !Ref "AWS::StackName" 162 | - Key: "aws-cloudformation:stack-id" 163 | Value: !Ref "AWS::StackId" 164 | - Key: "aws-cloudformation:logical-id" 165 | Value: rRootServicePolicy 166 | TargetIds: 167 | - !If 168 | - cHasOrganizationRootId 169 | - !Ref pOrganizationRootId 170 | - !GetAtt rOrganization.RootId 171 | Type: SERVICE_CONTROL_POLICY 172 | 173 | rRootResourcePolicy: 174 | Type: "AWS::Organizations::Policy" 175 | DependsOn: rActivateCustomResource 176 | Properties: 177 | Content: 178 | Version: "2012-10-17" 179 | Statement: 180 | - Sid: EnforceOrgIdentities 181 | Effect: Deny 182 | Principal: "*" 183 | Action: 184 | - "s3:*" 185 | - "sqs:*" 186 | - "kms:*" 187 | - "secretsmanager:*" 188 | - "sts:AssumeRole" 189 | - "sts:DecodeAuthorizationMessage" 190 | - "sts:GetAccessKeyInfo" 191 | - "sts:GetFederationToken" 192 | - "sts:GetServiceBearerToken" 193 | - "sts:GetSessionToken" 194 | - "sts:SetContext" 195 | Resource: "*" 196 | Condition: 197 | StringNotEqualsIfExists: 198 | "aws:PrincipalOrgID": !If 199 | - cHasOrganizationId 200 | - !Ref pOrganizationId 201 | - !GetAtt rOrganization.Id 202 | "aws:ResourceTag/dp:exclude:identity": "true" 203 | BoolIfExists: 204 | "aws:PrincipalIsAWSService": "false" 205 | - Sid: EnforceConfusedDeputyProtection 206 | Effect: Deny 207 | Principal: "*" 208 | Action: 209 | - "s3:*" 210 | - "sqs:*" 211 | - "kms:*" 212 | - "secretsmanager:*" 213 | - "sts:*" 214 | Resource: "*" 215 | Condition: 216 | StringNotEqualsIfExists: 217 | "aws:SourceOrgID": !If 218 | - cHasOrganizationId 219 | - !Ref pOrganizationId 220 | - !GetAtt rOrganization.Id 221 | "aws:ResourceTag/dp:exclude:identity": "true" 222 | "Null": 223 | "aws:SourceAccount": "false" 224 | "Bool": 225 | "aws:PrincipalIsAWSService": "true" 226 | - Sid: EnforceSecureTransport 227 | Effect: Deny 228 | Principal: "*" 229 | Action: 230 | - "s3:*" 231 | - "sqs:*" 232 | - "kms:*" 233 | - "secretsmanager:*" 234 | - "sts:*" 235 | Resource: "*" 236 | Condition: 237 | BoolIfExists: 238 | "aws:SecureTransport": "false" 239 | - Sid: ProtectDataPerimeterSessionTags 240 | Effect: Deny 241 | Principal: "*" 242 | Action: "sts:TagSession" 243 | Resource: "*" 244 | Condition: 245 | "Null": 246 | "SAML:aud": "true" 247 | StringNotEqualsIfExists: 248 | "aws:PrincipalTag/team": admin 249 | "aws:PrincipalOrgID": !If 250 | - cHasOrganizationId 251 | - !Ref pOrganizationId 252 | - !GetAtt rOrganization.Id 253 | "ForAnyValue:StringLike": 254 | "aws:TagKeys": 255 | - "dp:*" 256 | - team 257 | Description: Enforce resource perimeter 258 | Name: RootPolicy 259 | Tags: 260 | - Key: "aws-cloudformation:stack-name" 261 | Value: !Ref "AWS::StackName" 262 | - Key: "aws-cloudformation:stack-id" 263 | Value: !Ref "AWS::StackId" 264 | - Key: "aws-cloudformation:logical-id" 265 | Value: rRootResourcePolicy 266 | TargetIds: 267 | - !If 268 | - cHasOrganizationRootId 269 | - !Ref pOrganizationRootId 270 | - !GetAtt rOrganization.RootId 271 | Type: RESOURCE_CONTROL_POLICY 272 | 273 | rTrustedOIDCTenantsPolicy: 274 | Type: "AWS::Organizations::Policy" 275 | DependsOn: rActivateCustomResource 276 | Properties: 277 | Content: 278 | Version: "2012-10-17" 279 | Statement: 280 | - Sid: EnforceTrustedOIDCTenants 281 | Effect: Deny 282 | Principal: "*" 283 | Action: "sts:AssumeRoleWithWebIdentity" 284 | Resource: "*" 285 | Condition: 286 | StringNotLikeIfExists: 287 | "token.actions.githubusercontent.com:sub": !Sub "repo:${pGithubOrganization}/*" 288 | "aws:ResourceTag/dp:exclude:identity": "true" 289 | "Null": 290 | "token.actions.githubusercontent.com:sub": "false" 291 | Description: Limit access to trusted OIDC identity providers 292 | Name: TrustedOIDCProvidersPolicy 293 | Tags: 294 | - Key: "aws-cloudformation:stack-name" 295 | Value: !Ref "AWS::StackName" 296 | - Key: "aws-cloudformation:stack-id" 297 | Value: !Ref "AWS::StackId" 298 | - Key: "aws-cloudformation:logical-id" 299 | Value: rTrustedOIDCTenantsPolicy 300 | TargetIds: 301 | - !If 302 | - cHasOrganizationRootId 303 | - !Ref pOrganizationRootId 304 | - !GetAtt rOrganization.RootId 305 | Type: RESOURCE_CONTROL_POLICY 306 | 307 | rRootDeclarativePolicyEC2: 308 | Type: "AWS::Organizations::Policy" 309 | DependsOn: rActivateCustomResource 310 | Properties: 311 | Content: |- 312 | { 313 | "ec2_attributes": { 314 | "allowed_images_settings": { 315 | "state": { 316 | "@@assign": "enabled" 317 | }, 318 | "image_criteria": { 319 | "criteria_1": { 320 | "allowed_image_providers": { 321 | "@@assign": [ 322 | "amazon", 323 | "aws_marketplace", 324 | "aws_backup_vault" 325 | ] 326 | } 327 | } 328 | } 329 | }, 330 | "image_block_public_access": { 331 | "state": { 332 | "@@assign": "block_new_sharing" 333 | } 334 | }, 335 | "instance_metadata_defaults": { 336 | "http_tokens": { 337 | "@@assign": "required" 338 | }, 339 | "http_put_response_hop_limit": { 340 | "@@assign": 2 341 | }, 342 | "http_endpoint": { 343 | "@@assign": "enabled" 344 | }, 345 | "instance_metadata_tags": { 346 | "@@assign": "enabled" 347 | } 348 | }, 349 | "serial_console_access": { 350 | "status": { 351 | "@@assign": "disabled" 352 | } 353 | }, 354 | "snapshot_block_public_access": { 355 | "state": { 356 | "@@assign": "block_all_sharing" 357 | } 358 | }, 359 | "vpc_block_public_access": { 360 | "internet_gateway_block": { 361 | "mode": { 362 | "@@assign": "off" 363 | }, 364 | "exclusions_allowed": { 365 | "@@assign": "disabled" 366 | } 367 | } 368 | } 369 | } 370 | } 371 | Description: Enforce strong security practices 372 | Name: RootPolicy 373 | Tags: 374 | - Key: "aws-cloudformation:stack-name" 375 | Value: !Ref "AWS::StackName" 376 | - Key: "aws-cloudformation:stack-id" 377 | Value: !Ref "AWS::StackId" 378 | - Key: "aws-cloudformation:logical-id" 379 | Value: rRootDeclarativePolicyEC2 380 | TargetIds: 381 | - !If 382 | - cHasOrganizationRootId 383 | - !Ref pOrganizationRootId 384 | - !GetAtt rOrganization.RootId 385 | Type: DECLARATIVE_POLICY_EC2 386 | 387 | rRootAIOptOutPolicy: 388 | Type: "AWS::Organizations::Policy" 389 | DependsOn: rActivateCustomResource 390 | Properties: 391 | Content: |- 392 | { 393 | "services": { 394 | "@@operators_allowed_for_child_policies": ["@@none"], 395 | "default": { 396 | "@@operators_allowed_for_child_policies": ["@@none"], 397 | "opt_out_policy": { 398 | "@@operators_allowed_for_child_policies": ["@@none"], 399 | "@@assign": "optOut" 400 | } 401 | } 402 | } 403 | } 404 | Description: Opt-out of AI service data collection 405 | Name: RootPolicy 406 | Tags: 407 | - Key: "aws-cloudformation:stack-name" 408 | Value: !Ref "AWS::StackName" 409 | - Key: "aws-cloudformation:stack-id" 410 | Value: !Ref "AWS::StackId" 411 | - Key: "aws-cloudformation:logical-id" 412 | Value: rRootAIOptOutPolicy 413 | #TargetIds: 414 | # - !If 415 | # - cHasOrganizationRootId 416 | # - !Ref pOrganizationRootId 417 | # - !GetAtt rOrganization.RootId 418 | Type: AISERVICES_OPT_OUT_POLICY 419 | 420 | rExceptionsOu: 421 | Type: "AWS::Organizations::OrganizationalUnit" 422 | DependsOn: rOrgWaiter 423 | Properties: 424 | Name: Exceptions 425 | ParentId: !If 426 | - cHasOrganizationRootId 427 | - !Ref pOrganizationRootId 428 | - !GetAtt rOrganization.RootId 429 | 430 | rInfrastructureOu: 431 | Type: "AWS::Organizations::OrganizationalUnit" 432 | DependsOn: rOrgWaiter 433 | Properties: 434 | Name: !Ref pInfrastructureOuName 435 | ParentId: !If 436 | - cHasOrganizationRootId 437 | - !Ref pOrganizationRootId 438 | - !GetAtt rOrganization.RootId 439 | 440 | rSecurityOu: 441 | Type: "AWS::Organizations::OrganizationalUnit" 442 | DependsOn: rOrgWaiter 443 | Properties: 444 | Name: !Ref pSecurityOuName 445 | ParentId: !If 446 | - cHasOrganizationRootId 447 | - !Ref pOrganizationRootId 448 | - !GetAtt rOrganization.RootId 449 | 450 | rSandboxOu: 451 | Type: "AWS::Organizations::OrganizationalUnit" 452 | DependsOn: rOrgWaiter 453 | Properties: 454 | Name: !Ref pSandboxOuName 455 | ParentId: !If 456 | - cHasOrganizationRootId 457 | - !Ref pOrganizationRootId 458 | - !GetAtt rOrganization.RootId 459 | 460 | rWorkloadsOu: 461 | Type: "AWS::Organizations::OrganizationalUnit" 462 | DependsOn: rOrgWaiter 463 | Properties: 464 | Name: !Ref pWorkloadsOuName 465 | ParentId: !If 466 | - cHasOrganizationRootId 467 | - !Ref pOrganizationRootId 468 | - !GetAtt rOrganization.RootId 469 | 470 | rNonProdOu: 471 | Type: "AWS::Organizations::OrganizationalUnit" 472 | DependsOn: rOrgWaiter 473 | Properties: 474 | Name: NonProd 475 | ParentId: !Ref rWorkloadsOu 476 | 477 | rProdOu: 478 | Type: "AWS::Organizations::OrganizationalUnit" 479 | DependsOn: rOrgWaiter 480 | Properties: 481 | Name: Prod 482 | ParentId: !Ref rWorkloadsOu 483 | 484 | rEmergencyAccessGroup: 485 | Type: "AWS::IAM::Group" 486 | Metadata: 487 | cfn_nag: 488 | rules_to_suppress: 489 | - id: W28 490 | reason: "Ignoring explicit group name" 491 | Properties: 492 | GroupName: EmergencyAccess 493 | 494 | rEmergencyAccessOpsUser: 495 | Type: "AWS::IAM::User" 496 | Properties: 497 | Groups: 498 | - !Ref rEmergencyAccessGroup 499 | Tags: 500 | - Key: "aws-cloudformation:stack-name" 501 | Value: !Ref "AWS::StackName" 502 | - Key: "aws-cloudformation:stack-id" 503 | Value: !Ref "AWS::StackId" 504 | - Key: "aws-cloudformation:logical-id" 505 | Value: rEmergencyAccessOpsUser 506 | UserName: EmergencyAccess_Ops 507 | 508 | rEmergencyAccessOpsPolicy: 509 | Type: "AWS::IAM::UserPolicy" 510 | Properties: 511 | PolicyName: EmergencyAccessOpsPolicy 512 | PolicyDocument: 513 | Version: "2012-10-17" 514 | Statement: 515 | - Effect: Allow 516 | Action: "sts:AssumeRole" 517 | Resource: !Sub "arn:${AWS::Partition}:iam::*:role/EmergencyAccess_Ops" 518 | UserName: !Ref rEmergencyAccessOpsUser 519 | 520 | rEmergencyAccessReadOnlyUser: 521 | Type: "AWS::IAM::User" 522 | Metadata: 523 | cfn_nag: 524 | rules_to_suppress: 525 | - id: F2000 526 | reason: "Ignoring no groups" 527 | Properties: 528 | Groups: 529 | - !Ref rEmergencyAccessGroup 530 | Tags: 531 | - Key: "aws-cloudformation:stack-name" 532 | Value: !Ref "AWS::StackName" 533 | - Key: "aws-cloudformation:stack-id" 534 | Value: !Ref "AWS::StackId" 535 | - Key: "aws-cloudformation:logical-id" 536 | Value: rEmergencyAccessReadOnlyUser 537 | UserName: EmergencyAccess_RO 538 | 539 | rEmergencyAccessReadOnlyPolicy: 540 | Type: "AWS::IAM::UserPolicy" 541 | Properties: 542 | PolicyName: EmergencyAccessReadOnlyPolicy 543 | PolicyDocument: 544 | Version: "2012-10-17" 545 | Statement: 546 | - Effect: Allow 547 | Action: "sts:AssumeRole" 548 | Resource: !Sub "arn:${AWS::Partition}:iam::*:role/EmergencyAccess_RO" 549 | UserName: !Ref rEmergencyAccessReadOnlyUser 550 | 551 | rFunctionLogGroup: 552 | Type: "AWS::Logs::LogGroup" 553 | UpdateReplacePolicy: Delete 554 | DeletionPolicy: Delete 555 | Metadata: 556 | cfn_nag: 557 | rules_to_suppress: 558 | - id: W84 559 | reason: "Ignoring KMS key" 560 | Properties: 561 | LogGroupName: !Sub "/aws/lambda/${rFunction}" 562 | RetentionInDays: 3 563 | 564 | rFunctionRole: 565 | Type: "AWS::IAM::Role" 566 | Metadata: 567 | cfn_nag: 568 | rules_to_suppress: 569 | - id: W11 570 | reason: "Ignoring wildcard policy" 571 | Properties: 572 | AssumeRolePolicyDocument: 573 | Version: "2012-10-17" 574 | Statement: 575 | Effect: Allow 576 | Principal: 577 | Service: !Sub "lambda.${AWS::URLSuffix}" 578 | Action: "sts:AssumeRole" 579 | Condition: 580 | StringEquals: 581 | "aws:SourceAccount": !Ref "AWS::AccountId" 582 | Description: !Sub "DO NOT DELETE - Used by Lambda. Created by CloudFormation ${AWS::StackId}" 583 | Policies: 584 | - PolicyName: OrganizationAccessPolicy 585 | PolicyDocument: 586 | Version: "2012-10-17" 587 | Statement: 588 | - Effect: Allow 589 | Action: 590 | - "cloudformation:ActivateOrganizationsAccess" 591 | - "cloudformation:DeactivateOrganizationsAccess" 592 | - "cloudformation:DescribeOrganizationsAccess" 593 | - "iam:DisableOrganizationsRootCredentialsManagement" 594 | - "iam:DisableOrganizationsRootSessions" 595 | - "iam:EnableOrganizationsRootCredentialsManagement" 596 | - "iam:EnableOrganizationsRootSessions" 597 | - "organizations:ListRoots" 598 | - "organizations:EnablePolicyType" 599 | - "organizations:DisablePolicyType" 600 | Resource: "*" 601 | - Effect: Allow 602 | Action: 603 | - "iam:GetRole" 604 | - "iam:DeleteServiceLinkedRole" 605 | - "iam:GetServiceLinkedRoleDeletionStatus" 606 | Resource: !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/aws-service-role/stacksets.cloudformation.amazonaws.com/AWSServiceRoleForCloudFormationStackSetsOrgAdmin*" 607 | - Effect: Allow 608 | Action: "iam:GetRole" 609 | Resource: !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/*AWSServiceRoleForCloudFormationStackSetsOrgAdmin*" 610 | - Effect: Allow 611 | Action: 612 | - "organizations:EnableAWSServiceAccess" 613 | - "organizations:DisableAWSServiceAccess" 614 | Resource: "*" 615 | Condition: 616 | StringEquals: 617 | "organizations:ServicePrincipal": 618 | - "member.org.stacksets.cloudformation.amazonaws.com" 619 | - "iam.amazonaws.com" 620 | - Effect: Allow 621 | Action: "iam:CreateServiceLinkedRole" 622 | Resource: !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/aws-service-role/stacksets.cloudformation.amazonaws.com/AWSServiceRoleForCloudFormationStackSetsOrgAdmin*" 623 | Tags: 624 | - Key: "aws-cloudformation:stack-name" 625 | Value: !Ref "AWS::StackName" 626 | - Key: "aws-cloudformation:stack-id" 627 | Value: !Ref "AWS::StackId" 628 | - Key: "aws-cloudformation:logical-id" 629 | Value: rFunctionRole 630 | 631 | rCloudWatchLogsPolicy: 632 | Type: "AWS::IAM::Policy" 633 | Properties: 634 | PolicyName: CloudWatchLogs 635 | PolicyDocument: 636 | Version: "2012-10-17" 637 | Statement: 638 | - Effect: Allow 639 | Action: 640 | - "logs:CreateLogStream" 641 | - "logs:PutLogEvents" 642 | Resource: !GetAtt rFunctionLogGroup.Arn 643 | Roles: 644 | - !Ref rFunctionRole 645 | 646 | rFunction: 647 | Type: "AWS::Serverless::Function" 648 | Metadata: 649 | cfn_nag: 650 | rules_to_suppress: 651 | - id: W58 652 | reason: "Ignoring CloudWatch" 653 | - id: W89 654 | reason: "Ignoring VPC" 655 | - id: W92 656 | reason: "Ignoring Reserved Concurrency" 657 | Properties: 658 | Architectures: 659 | - x86_64 # can't use ARM here due to Github Actions availability 660 | CodeUri: ./src/activation_lambda 661 | Description: "DO NOT DELETE - Organization Activation Function" 662 | Environment: 663 | Variables: 664 | ROOT_ID: !If 665 | - cHasOrganizationRootId 666 | - !Ref pOrganizationRootId 667 | - !GetAtt rOrganization.RootId 668 | AWS_STS_REGIONAL_ENDPOINTS: regional 669 | Handler: index.handler 670 | MemorySize: 1024 # megabytes 671 | PropagateTags: true 672 | Role: !GetAtt rFunctionRole.Arn 673 | Runtime: python3.13 674 | Timeout: 20 # seconds 675 | 676 | rActivateCustomResource: 677 | Type: "AWS::CloudFormation::CustomResource" 678 | DependsOn: 679 | - rOrgWaiter 680 | - rCloudWatchLogsPolicy 681 | Properties: 682 | ServiceTimeout: 15 # seconds 683 | ServiceToken: !GetAtt rFunction.Arn 684 | 685 | rCloudFormationStackSetAdminRole: 686 | Type: "AWS::IAM::Role" 687 | Metadata: 688 | cfn_nag: 689 | rules_to_suppress: 690 | - id: W28 691 | reason: "Ignoring explicit role name" 692 | Properties: 693 | AssumeRolePolicyDocument: 694 | Version: "2012-10-17" 695 | Statement: 696 | - Effect: Allow 697 | Principal: 698 | Service: "cloudformation.amazonaws.com" 699 | Action: "sts:AssumeRole" 700 | Condition: 701 | StringEquals: 702 | "aws:SourceAccount": !Ref "AWS::AccountId" 703 | ArnLike: 704 | "aws:SourceArn": !Sub "arn:${AWS::Partition}:cloudformation:*:${AWS::AccountId}:stackset/*" 705 | Description: !Sub "DO NOT DELETE - Used by CloudFormation StackSets. Created by CloudFormation ${AWS::StackId}" 706 | Policies: 707 | - PolicyName: StackSetAdministration 708 | PolicyDocument: 709 | Version: "2012-10-17" 710 | Statement: 711 | - Effect: Allow 712 | Action: "sts:AssumeRole" 713 | Resource: !Sub "arn:${AWS::Partition}:iam::*:role/AWSCloudFormationStackSetExecutionRole" 714 | RoleName: AWSCloudFormationStackSetAdministrationRole 715 | Tags: 716 | - Key: "aws-cloudformation:stack-name" 717 | Value: !Ref "AWS::StackName" 718 | - Key: "aws-cloudformation:stack-id" 719 | Value: !Ref "AWS::StackId" 720 | - Key: "aws-cloudformation:logical-id" 721 | Value: rCloudFormationStackSetAdminRole 722 | 723 | rRoleStackSet: 724 | Type: "AWS::CloudFormation::StackSet" 725 | DependsOn: rActivateCustomResource 726 | Properties: 727 | AutoDeployment: 728 | Enabled: true 729 | RetainStacksOnAccountRemoval: false 730 | Capabilities: 731 | - CAPABILITY_IAM 732 | - CAPABILITY_NAMED_IAM 733 | Description: StackSet to deploy IAM roles to member accounts 734 | # ExecutionRoleName not supported on SERVICE_MANAGED 735 | OperationPreferences: 736 | FailureToleranceCount: 1 737 | MaxConcurrentPercentage: 100 738 | RegionConcurrencyType: PARALLEL 739 | Parameters: 740 | - ParameterKey: pManagementAccountId 741 | ParameterValue: !Ref "AWS::AccountId" 742 | - ParameterKey: pDeveloperPrefix 743 | ParameterValue: !Ref pDeveloperPrefix 744 | - ParameterKey: pCloudFormationRoleName 745 | ParameterValue: !Ref pCloudFormationRoleName 746 | - ParameterKey: pServiceCatalogRoleName 747 | ParameterValue: !Ref pServiceCatalogRoleName 748 | PermissionModel: SERVICE_MANAGED 749 | StackInstancesGroup: 750 | - DeploymentTargets: 751 | OrganizationalUnitIds: 752 | - !If 753 | - cHasOrganizationRootId 754 | - !Ref pOrganizationRootId 755 | - !GetAtt rOrganization.RootId 756 | Regions: 757 | - !Ref "AWS::Region" # single region only for the IAM roles 758 | StackSetName: AWSPlatform-BASELINE-ROLES 759 | TemplateURL: ./src/stackset_roles/template.yml 760 | 761 | rParameterStackSet: 762 | Type: "AWS::CloudFormation::StackSet" 763 | DependsOn: rActivateCustomResource 764 | Properties: 765 | AutoDeployment: 766 | Enabled: true 767 | RetainStacksOnAccountRemoval: false 768 | Description: StackSet to deploy SSM parameters to member accounts 769 | # ExecutionRoleName not supported on SERVICE_MANAGED 770 | OperationPreferences: 771 | FailureToleranceCount: 1 772 | MaxConcurrentPercentage: 100 773 | RegionConcurrencyType: PARALLEL 774 | Parameters: 775 | - ParameterKey: pManagementAccountId 776 | ParameterValue: !Ref "AWS::AccountId" 777 | - ParameterKey: pOrganizationId 778 | ParameterValue: !If 779 | - cHasOrganizationId 780 | - !Ref pOrganizationId 781 | - !GetAtt rOrganization.Id 782 | - ParameterKey: pCloudFormationRoleName 783 | ParameterValue: !Ref pCloudFormationRoleName 784 | - ParameterKey: pServiceCatalogRoleName 785 | ParameterValue: !Ref pServiceCatalogRoleName 786 | PermissionModel: SERVICE_MANAGED 787 | StackInstancesGroup: 788 | - DeploymentTargets: 789 | OrganizationalUnitIds: 790 | - !If 791 | - cHasOrganizationRootId 792 | - !Ref pOrganizationRootId 793 | - !GetAtt rOrganization.RootId 794 | Regions: !Ref pRegions 795 | StackSetName: AWSPlatform-BASELINE-PARAMETERS 796 | TemplateURL: ./src/stackset_parameters/template.yml 797 | 798 | rAccountManagerPermissionSet: 799 | Type: "AWS::SSO::PermissionSet" 800 | Condition: cHasInstanceArn 801 | Properties: 802 | Description: Access to Billing and Cost Explorer 803 | InstanceArn: !Ref pInstanceArn 804 | ManagedPolicies: 805 | - !Sub "arn:${AWS::Partition}:iam::aws:policy/job-function/Billing" 806 | - !Sub "arn:${AWS::Partition}:iam::aws:policy/CostOptimizationHubReadOnlyAccess" 807 | Name: AccountManager 808 | RelayStateType: https://console.aws.amazon.com/costmanagement/home 809 | SessionDuration: PT8H 810 | Tags: 811 | - Key: "aws-cloudformation:stack-name" 812 | Value: !Ref "AWS::StackName" 813 | - Key: "aws-cloudformation:stack-id" 814 | Value: !Ref "AWS::StackId" 815 | - Key: "aws-cloudformation:logical-id" 816 | Value: rAccountManagerPermissionSet 817 | 818 | rAdministratorPermissionSet: 819 | Type: "AWS::SSO::PermissionSet" 820 | Condition: cHasInstanceArn 821 | Properties: 822 | Description: RESTRICTED - Highly priviledged administrator access 823 | InstanceArn: !Ref pInstanceArn 824 | ManagedPolicies: 825 | - !Sub "arn:${AWS::Partition}:iam::aws:policy/AdministratorAccess" 826 | Name: RESTRICTED-AdministratorAccess 827 | SessionDuration: PT1H 828 | Tags: 829 | - Key: "aws-cloudformation:stack-name" 830 | Value: !Ref "AWS::StackName" 831 | - Key: "aws-cloudformation:stack-id" 832 | Value: !Ref "AWS::StackId" 833 | - Key: "aws-cloudformation:logical-id" 834 | Value: rAdministratorPermissionSet 835 | 836 | rShellAccessPermissionSet: 837 | Type: "AWS::SSO::PermissionSet" 838 | Condition: cHasInstanceArn 839 | Properties: 840 | Description: Command-line shell access to EC2 instances 841 | InlinePolicy: !Sub |- 842 | { 843 | "Version": "2012-10-17", 844 | "Statement": [ 845 | { 846 | "Effect": "Allow", 847 | "Action": [ 848 | "ssm:SendCommand", 849 | "ssm:StartSession" 850 | ], 851 | "Resource": [ 852 | "arn:${AWS::Partition}:ec2:*:*:instance/*", 853 | "arn:${AWS::Partition}:ssm:*:*:document/SSM-SessionManagerRunShell", 854 | "arn:${AWS::Partition}:ssm:*:*:document/AWS-StartPortForwardingSession", 855 | "arn:${AWS::Partition}:ssm:*:*:document/AWS-StartPortForwardingSessionToRemoteHost" 856 | ], 857 | "Condition": { 858 | "StringEquals": { 859 | "aws:ResourceAccount": "${!aws:PrincipalAccount}" 860 | } 861 | } 862 | }, 863 | { 864 | "Effect": "Allow", 865 | "Action": [ 866 | "ssm:ResumeSession", 867 | "ssm:TerminateSession" 868 | ], 869 | "Resource": "arn:${AWS::Partition}:ssm:*:*:session/*", 870 | "Condition": { 871 | "StringLike": { 872 | "ssm:resourceTag/aws:ssmmessages:session-id": "${!aws:userid}" 873 | } 874 | } 875 | } 876 | ] 877 | } 878 | InstanceArn: !Ref pInstanceArn 879 | Name: ShellAccess 880 | SessionDuration: PT8H 881 | Tags: 882 | - Key: "aws-cloudformation:stack-name" 883 | Value: !Ref "AWS::StackName" 884 | - Key: "aws-cloudformation:stack-id" 885 | Value: !Ref "AWS::StackId" 886 | - Key: "aws-cloudformation:logical-id" 887 | Value: rShellAccessPermissionSet 888 | 889 | rDeveloperPermissionSet: 890 | Type: "AWS::SSO::PermissionSet" 891 | Condition: cHasInstanceArn 892 | Properties: 893 | Description: Access to provision resources through CloudFormation 894 | InlinePolicy: !Sub |- 895 | { 896 | "Version": "2012-10-17", 897 | "Statement": [ 898 | { 899 | "Effect": "Allow", 900 | "Action": "iam:PassRole", 901 | "Resource": "arn:${AWS::Partition}:iam::*:role/${pCloudFormationRoleName}", 902 | "Condition": { 903 | "StringEquals": { 904 | "aws:ResourceAccount": "${!aws:PrincipalAccount}", 905 | "iam:PassedToService": "cloudformation.${AWS::URLSuffix}" 906 | } 907 | } 908 | }, 909 | { 910 | "Effect": "Allow", 911 | "Action": [ 912 | "cloudformation:ContinueUpdateRollback", 913 | "cloudformation:CreateChangeSet", 914 | "cloudformation:CreateStack", 915 | "cloudformation:DeleteStack", 916 | "cloudformation:RollbackStack", 917 | "cloudformation:UpdateStack" 918 | ], 919 | "Resource": "arn:${AWS::Partition}:cloudformation:*:*:stack/${pDeveloperPrefix}*", 920 | "Condition": { 921 | "StringEquals": { 922 | "aws:ResourceAccount": "${!aws:PrincipalAccount}" 923 | }, 924 | "ArnLike": { 925 | "cloudformation:RoleArn": "arn:${AWS::Partition}:iam::*:role/${pCloudFormationRoleName}" 926 | }, 927 | "Null": { 928 | "cloudformation:ImportResourceTypes": true 929 | } 930 | } 931 | }, 932 | { 933 | "Effect": "Allow", 934 | "Action": [ 935 | "cloudformation:CancelUpdateStack", 936 | "cloudformation:DeleteChangeSet", 937 | "cloudformation:DetectStackDrift", 938 | "cloudformation:DetectStackResourceDrift", 939 | "cloudformation:ExecuteChangeSet", 940 | "cloudformation:TagResource", 941 | "cloudformation:UntagResource", 942 | "cloudformation:UpdateTerminationProtection" 943 | ], 944 | "Resource": "arn:${AWS::Partition}:cloudformation:*:*:stack/${pDeveloperPrefix}*", 945 | "Condition": { 946 | "StringEquals": { 947 | "aws:ResourceAccount": "${!aws:PrincipalAccount}" 948 | } 949 | } 950 | }, 951 | { 952 | "Effect": "Allow", 953 | "Action": [ 954 | "cloudformation:CreateUploadBucket", 955 | "cloudformation:ValidateTemplate", 956 | "cloudformation:EstimateTemplateCost" 957 | ], 958 | "Resource": "*", 959 | "Condition": { 960 | "StringEquals": { 961 | "aws:ResourceAccount": "${!aws:PrincipalAccount}" 962 | } 963 | } 964 | }, 965 | { 966 | "Effect": "Allow", 967 | "Action": "s3:CreateBucket", 968 | "Resource": "arn:${AWS::Partition}:s3:::cf-templates-*", 969 | "Condition": { 970 | "ForAnyValue:StringEquals": { 971 | "aws:CalledVia": "cloudformation.${AWS::URLSuffix}" 972 | }, 973 | "StringEquals": { 974 | "aws:ResourceAccount": "${!aws:PrincipalAccount}" 975 | } 976 | } 977 | }, 978 | { 979 | "Effect": "Allow", 980 | "Action": "s3:PutObject", 981 | "Resource": "arn:${AWS::Partition}:s3:::cf-templates-*/*", 982 | "Condition": { 983 | "StringEquals": { 984 | "s3:ResourceAccount": "${!aws:PrincipalAccount}" 985 | } 986 | } 987 | }, 988 | { 989 | "Effect": "Allow", 990 | "Action": [ 991 | "ssm:SendCommand", 992 | "ssm:StartSession" 993 | ], 994 | "Resource": [ 995 | "arn:${AWS::Partition}:ec2:*:*:instance/*", 996 | "arn:${AWS::Partition}:ssm:*:*:document/SSM-SessionManagerRunShell", 997 | "arn:${AWS::Partition}:ssm:*:*:document/AWS-StartPortForwardingSession", 998 | "arn:${AWS::Partition}:ssm:*:*:document/AWS-StartPortForwardingSessionToRemoteHost" 999 | ], 1000 | "Condition": { 1001 | "StringEquals": { 1002 | "aws:ResourceAccount": "${!aws:PrincipalAccount}" 1003 | } 1004 | } 1005 | }, 1006 | { 1007 | "Effect": "Allow", 1008 | "Action": [ 1009 | "ssm:ResumeSession", 1010 | "ssm:TerminateSession" 1011 | ], 1012 | "Resource": "arn:${AWS::Partition}:ssm:*:*:session/*", 1013 | "Condition": { 1014 | "StringLike": { 1015 | "ssm:resourceTag/aws:ssmmessages:session-id": "${!aws:userid}" 1016 | } 1017 | } 1018 | } 1019 | ] 1020 | } 1021 | InstanceArn: !Ref pInstanceArn 1022 | ManagedPolicies: 1023 | - !Sub "arn:${AWS::Partition}:iam::aws:policy/AWSServiceCatalogEndUserFullAccess" 1024 | - !Sub "arn:${AWS::Partition}:iam::aws:policy/AWSBillingReadOnlyAccess" 1025 | - !Sub "arn:${AWS::Partition}:iam::aws:policy/AWSSupportAccess" 1026 | - !Sub "arn:${AWS::Partition}:iam::aws:policy/job-function/ViewOnlyAccess" 1027 | Name: Developer 1028 | SessionDuration: PT8H 1029 | Tags: 1030 | - Key: "aws-cloudformation:stack-name" 1031 | Value: !Ref "AWS::StackName" 1032 | - Key: "aws-cloudformation:stack-id" 1033 | Value: !Ref "AWS::StackId" 1034 | - Key: "aws-cloudformation:logical-id" 1035 | Value: rDeveloperPermissionSet 1036 | 1037 | rSupportPermissionSet: 1038 | Type: "AWS::SSO::PermissionSet" 1039 | Condition: cHasInstanceArn 1040 | Properties: 1041 | Description: Access to production accounts for troubleshooting and support 1042 | InlinePolicy: !Sub |- 1043 | { 1044 | "Version": "2012-10-17", 1045 | "Statement": [ 1046 | { 1047 | "Effect": "Allow", 1048 | "Action": "iam:PassRole", 1049 | "Resource": "arn:${AWS::Partition}:iam::*:role/${pCloudFormationRoleName}", 1050 | "Condition": { 1051 | "StringEquals": { 1052 | "aws:ResourceAccount": "${!aws:PrincipalAccount}", 1053 | "iam:PassedToService": "cloudformation.${AWS::URLSuffix}" 1054 | } 1055 | } 1056 | }, 1057 | { 1058 | "Effect": "Allow", 1059 | "Action": "cloudformation:ContinueUpdateRollback", 1060 | "Resource": "arn:${AWS::Partition}:cloudformation:*:*:stack/${pDeveloperPrefix}*", 1061 | "Condition": { 1062 | "StringEquals": { 1063 | "aws:ResourceAccount": "${!aws:PrincipalAccount}" 1064 | }, 1065 | "ArnLike": { 1066 | "cloudformation:RoleArn": "arn:${AWS::Partition}:iam::*:role/${pCloudFormationRoleName}" 1067 | }, 1068 | "Null": { 1069 | "cloudformation:ImportResourceTypes": true 1070 | } 1071 | } 1072 | }, 1073 | { 1074 | "Effect": "Allow", 1075 | "Action": "cloudformation:CancelUpdateStack", 1076 | "Resource": "arn:${AWS::Partition}:cloudformation:*:*:stack/${pDeveloperPrefix}*", 1077 | "Condition": { 1078 | "StringEquals": { 1079 | "aws:ResourceAccount": "${!aws:PrincipalAccount}" 1080 | } 1081 | } 1082 | }, 1083 | { 1084 | "Effect": "Allow", 1085 | "Action": [ 1086 | "ssm:SendCommand", 1087 | "ssm:StartSession" 1088 | ], 1089 | "Resource": [ 1090 | "arn:${AWS::Partition}:ec2:*:*:instance/*", 1091 | "arn:${AWS::Partition}:ssm:*:*:document/SSM-SessionManagerRunShell", 1092 | "arn:${AWS::Partition}:ssm:*:*:document/AWS-StartPortForwardingSession", 1093 | "arn:${AWS::Partition}:ssm:*:*:document/AWS-StartPortForwardingSessionToRemoteHost" 1094 | ], 1095 | "Condition": { 1096 | "StringEquals": { 1097 | "aws:ResourceAccount": "${!aws:PrincipalAccount}" 1098 | } 1099 | } 1100 | }, 1101 | { 1102 | "Effect": "Allow", 1103 | "Action": [ 1104 | "ssm:ResumeSession", 1105 | "ssm:TerminateSession" 1106 | ], 1107 | "Resource": "arn:${AWS::Partition}:ssm:*:*:session/*", 1108 | "Condition": { 1109 | "StringLike": { 1110 | "ssm:resourceTag/aws:ssmmessages:session-id": "${!aws:userid}" 1111 | } 1112 | } 1113 | } 1114 | ] 1115 | } 1116 | InstanceArn: !Ref pInstanceArn 1117 | ManagedPolicies: 1118 | - !Sub "arn:${AWS::Partition}:iam::aws:policy/AWSBillingReadOnlyAccess" 1119 | - !Sub "arn:${AWS::Partition}:iam::aws:policy/AWSSupportAccess" 1120 | - !Sub "arn:${AWS::Partition}:iam::aws:policy/job-function/ViewOnlyAccess" 1121 | Name: Support 1122 | RelayStateType: https://support.console.aws.amazon.com/support/home 1123 | SessionDuration: PT8H 1124 | Tags: 1125 | - Key: "aws-cloudformation:stack-name" 1126 | Value: !Ref "AWS::StackName" 1127 | - Key: "aws-cloudformation:stack-id" 1128 | Value: !Ref "AWS::StackId" 1129 | - Key: "aws-cloudformation:logical-id" 1130 | Value: rSupportPermissionSet 1131 | 1132 | Outputs: 1133 | oOrganizationId: 1134 | Description: Organization ID 1135 | Value: !If 1136 | - cHasOrganizationId 1137 | - !Ref pOrganizationId 1138 | - !GetAtt rOrganization.Id 1139 | oInstanceArn: 1140 | Description: IAM Identity Center Instance Arn 1141 | Value: !Ref pInstanceArn 1142 | Condition: cHasInstanceArn --------------------------------------------------------------------------------