├── .github └── workflows │ ├── inactive_issue.yml │ └── validate.yml ├── .gitignore ├── .markdownlintrc ├── .pre-commit-config.yaml ├── .terraform-docs.yml ├── LICENSE ├── README.md ├── main.tf ├── outputs.tf ├── renovate.json ├── variables.tf └── versions.tf /.github/workflows/inactive_issue.yml: -------------------------------------------------------------------------------- 1 | name: Close inactive issues 2 | on: 3 | schedule: 4 | - cron: "30 1 * * *" 5 | 6 | jobs: 7 | close-issues: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9 11 | with: 12 | days-before-issue-stale: 30 13 | days-before-issue-close: 14 14 | stale-issue-label: "stale" 15 | stale-issue-message: "This issue is stale because it has been open for 30 days with no activity." 16 | close-issue-message: "This issue was closed because it has been inactive for 14 days since being marked as stale." 17 | days-before-pr-stale: -1 18 | days-before-pr-close: -1 19 | repo-token: ${{ secrets.GITHUB_TOKEN }} 20 | -------------------------------------------------------------------------------- /.github/workflows/validate.yml: -------------------------------------------------------------------------------- 1 | name: validate-tf 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | validate-tf: 13 | uses: trussworks/shared-actions/.github/workflows/validate-tf.yml@aae5a2c52d67c27a1ab11c348c4460ae09f21375 # v0.0.6 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .terraform 3 | terraform.tfstate 4 | terraform.tfstate.backup 5 | terraform.tfstate.*.backup 6 | -------------------------------------------------------------------------------- /.markdownlintrc: -------------------------------------------------------------------------------- 1 | { 2 | "default": true, 3 | "first-header-h1": false, 4 | "first-line-h1": false, 5 | "line_length": false, 6 | "no-multiple-blanks": false, 7 | "no-inline-html": false, 8 | "no-alt-text": false 9 | } 10 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v5.0.0 4 | hooks: 5 | - id: check-json 6 | - id: check-merge-conflict 7 | - id: check-yaml 8 | - id: detect-private-key 9 | - id: pretty-format-json 10 | args: 11 | - --autofix 12 | - id: trailing-whitespace 13 | - id: check-symlinks 14 | - id: end-of-file-fixer 15 | - id: mixed-line-ending 16 | 17 | - repo: https://github.com/executablebooks/mdformat 18 | rev: 0.7.21 19 | hooks: 20 | - id: mdformat 21 | additional_dependencies: 22 | - mdformat-gfm 23 | - mdformat-toc 24 | # mdformat fights with terraform_docs 25 | exclude: README.m(ark)?d(own)? 26 | 27 | - repo: https://github.com/igorshubovych/markdownlint-cli 28 | rev: v0.43.0 29 | hooks: 30 | - id: markdownlint 31 | 32 | - repo: https://github.com/terraform-docs/terraform-docs 33 | rev: "v0.19.0" 34 | hooks: 35 | - id: terraform-docs-go 36 | 37 | - repo: https://github.com/antonbabenko/pre-commit-terraform 38 | rev: v1.96.3 39 | hooks: 40 | - id: terraform_fmt 41 | -------------------------------------------------------------------------------- /.terraform-docs.yml: -------------------------------------------------------------------------------- 1 | settings: 2 | html: false 3 | anchor: false 4 | escape: false 5 | lockfile: false 6 | hide-empty: true 7 | formatter: "markdown table" 8 | 9 | sections: 10 | show: 11 | - requirements 12 | - providers 13 | - modules 14 | - data-sources 15 | - resources 16 | - inputs 17 | - outputs 18 | 19 | output: 20 | file: README.md 21 | mode: inject 22 | template: |- 23 | 24 | {{ .Content }} 25 | 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2020, TrussWorks, Inc. 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## AWS OU SCP Terraform Module 2 | 3 | Supports two main use cases: 4 | 5 | * Combines multiple Service Control Policy (SCP) statements - based on the module [`terraform-aws-org-scp`](https://github.com/trussworks/terraform-aws-org-scp) (_deprecated_). Combining multiple policy statements into a single policy allows more than 5 policies to be be applied to a single Organizational Unit (OU). 6 | * Alternatively, creates a "Deny All Access" Service Control Policy. 7 | 8 | Alternatively, enables creation of a "Deny All Access" Service Control Policy. 9 | 10 | Policy options (listed by `sid`) are: 11 | 12 | * Deny leaving AWS Organizations (DenyLeavingOrgs) 13 | * Deny creating IAM users or access keys (DenyCreatingIAMUsers) 14 | * Deny deleting KMS Keys (DenyDeletingKMSKeys) 15 | * Deny deleting Route53 Hosted Zones (DenyDeletingRoute53Zones) 16 | * Deny deleting VPC Flow logs, Cloudwatch log groups, and Cloudwatch log streams (DenyDeletingCloudwatchLogs) 17 | * Deny root account (DenyRootAccount) 18 | * Protect S3 Buckets (ProtectS3Buckets) 19 | * Deny S3 Buckets Public Access (DenyS3BucketsPublicAccess) 20 | * Protect IAM Roles (ProtectIAMRoles) 21 | * Restrict EC2 Instance Types (LimitEC2InstanceTypes) 22 | * Restrict Regional Operations (LimitRegions) 23 | * Require S3 encryption (DenyIncorrectEncryptionHeader + DenyUnEncryptedObjectUploads) 24 | 25 | ### Usage for combined policy statements 26 | 27 | To include a policy in your combined policy block, set it to `true`. Otherwise omit the policy variable. 28 | 29 | ```hcl 30 | module "github_terraform_aws_ou_scp" { 31 | source = "trussworks/ou-scp/aws" 32 | target = aws_organizations_organizational_unit.my_ou 33 | 34 | # don't allow all accounts to be able to leave the org 35 | deny_leaving_orgs = true 36 | # applies to accounts that are not managing IAM users 37 | deny_creating_iam_users = true 38 | # don't allow deleting KMS keys 39 | deny_deleting_kms_keys = true 40 | # don't allow deleting Route53 zones 41 | deny_deleting_route53_zones = true 42 | # don't allow deleting CloudWatch logs 43 | deny_deleting_cloudwatch_logs = true 44 | # don't allow access to the root user 45 | deny_root_account = true 46 | 47 | protect_s3_buckets = true 48 | # protect terraform statefile bucket 49 | protect_s3_bucket_resources = [ 50 | "arn:aws:s3:::prod-terraform-state-us-west-2", 51 | "arn:aws:s3:::prod-terraform-state-us-west-2/*" 52 | ] 53 | 54 | # don't allow public access to bucket 55 | deny_s3_buckets_public_access = true 56 | deny_s3_bucket_public_access_resources = [ 57 | "arn:aws:s3:::log-delivery-august-2020" 58 | ] 59 | 60 | protect_iam_roles = true 61 | # - protect OrganizationAccountAccessRole 62 | protect_iam_role_resources = [ 63 | "arn:aws:iam::*:role/OrganizationAccountAccessRole" 64 | ] 65 | 66 | # restrict EC2 instance types 67 | limit_ec2_instance_types = true 68 | allowed_ec2_instance_types = ["t2.medium"] 69 | 70 | # restrict region-specific operations to us-west-2 71 | limit_regions = true 72 | # - restrict region-specific operations to us-west-2 73 | allowed_regions = ["us-west-2"] 74 | 75 | # require s3 objects be encrypted 76 | require_s3_encryption = true 77 | 78 | # SCP policy tags 79 | tags = { 80 | managed_by = "terraform" 81 | } 82 | } 83 | ``` 84 | 85 | ### Usage for a policy which denies all access 86 | 87 | ```hcl 88 | module "github_terraform_aws_ou_scp" { 89 | source = "trussworks/ou-scp/aws" 90 | target = aws_organizations_organizational_unit.my_ou 91 | 92 | # don't allow any access at all 93 | deny_all=true 94 | } 95 | ``` 96 | 97 | 98 | ## Requirements 99 | 100 | | Name | Version | 101 | |------|---------| 102 | | terraform | >= 1.0 | 103 | | aws | >= 3.0 | 104 | 105 | ## Providers 106 | 107 | | Name | Version | 108 | |------|---------| 109 | | aws | >= 3.0 | 110 | 111 | ## Resources 112 | 113 | | Name | Type | 114 | |------|------| 115 | | [aws_organizations_policy.generated](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/organizations_policy) | resource | 116 | | [aws_organizations_policy_attachment.generated](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/organizations_policy_attachment) | resource | 117 | | [aws_iam_policy_document.combined_policy_block](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | 118 | | [aws_iam_policy_document.deny_all_access](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | 119 | 120 | ## Inputs 121 | 122 | | Name | Description | Type | Default | Required | 123 | |------|-------------|------|---------|:--------:| 124 | | allowed_ec2_instance_types | EC2 instances types allowed for use | `list(string)` | ```[ "" ]``` | no | 125 | | allowed_regions | AWS Regions allowed for use (for use with the restrict regions SCP) | `list(string)` | ```[ "" ]``` | no | 126 | | deny_all | If false, create a combined policy. If true, deny all access | `bool` | `false` | no | 127 | | deny_creating_iam_users | DenyCreatingIAMUsers in the OU policy. | `bool` | `false` | no | 128 | | deny_deleting_cloudwatch_logs | DenyDeletingCloudwatchLogs in the OU policy. | `bool` | `false` | no | 129 | | deny_deleting_kms_keys | DenyDeletingKMSKeys in the OU policy. | `bool` | `false` | no | 130 | | deny_deleting_route53_zones | DenyDeletingRoute53Zones in the OU policy. | `bool` | `false` | no | 131 | | deny_leaving_orgs | DenyLeavingOrgs in the OU policy. | `bool` | `false` | no | 132 | | deny_root_account | DenyRootAccount in the OU policy. | `bool` | `false` | no | 133 | | deny_s3_bucket_public_access_resources | S3 bucket resource ARNs to block public access | `list(string)` | ```[ "" ]``` | no | 134 | | deny_s3_buckets_public_access | DenyS3BucketsPublicAccess in the OU policy. | `bool` | `false` | no | 135 | | limit_ec2_instance_types | LimitEC2InstanceTypes in the OU policy. | `bool` | `false` | no | 136 | | limit_regions | LimitRegions in the OU policy. | `bool` | `false` | no | 137 | | protect_iam_role_resources | IAM role resource ARNs to protect from modification and deletion | `list(string)` | ```[ "" ]``` | no | 138 | | protect_iam_roles | ProtectIAMRoles in the OU policy. | `bool` | `false` | no | 139 | | protect_s3_bucket_resources | S3 bucket resource ARNs to protect from bucket and object deletion | `list(string)` | ```[ "" ]``` | no | 140 | | protect_s3_buckets | ProtectS3Buckets in the OU policy. | `bool` | `false` | no | 141 | | require_s3_encryption | DenyIncorrectEncryptionHeader and DenyUnEncryptedObjectUploads in the OU policy | `bool` | `false` | no | 142 | | tags | Tags applied to the SCP policy | `map(string)` | `{}` | no | 143 | | target | OU resource to attach SCP | ```object({ name = string id = string })``` | n/a | yes | 144 | 145 | 146 | ## Developer Setup 147 | 148 | Install dependencies (macOS) 149 | 150 | ```shell 151 | brew install pre-commit go terraform terraform-docs 152 | pre-commit install --install-hooks 153 | ``` 154 | -------------------------------------------------------------------------------- /main.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | # Each policy variable is set to a bool, defaulting to false (not included). 3 | # Include a policy by setting a policy variable to `true`, passing the hard-coded "Deny" statment 4 | # into the combined policy block. 5 | 6 | # Exclude a policy from the combined policy by omitting the policy variable in the module call. 7 | # This results in the default setting of `false`, and 8 | # the dynamic `for_each` statement will return an array with an empty string, 9 | # and the statement will not be included. 10 | deny_leaving_orgs_statement = var.deny_leaving_orgs ? [""] : [] 11 | deny_creating_iam_users_statement = var.deny_creating_iam_users ? [""] : [] 12 | deny_deleting_kms_keys_statement = var.deny_deleting_kms_keys ? [""] : [] 13 | deny_deleting_route53_zones_statement = var.deny_deleting_route53_zones ? [""] : [] 14 | deny_deleting_cloudwatch_logs_statement = var.deny_deleting_cloudwatch_logs ? [""] : [] 15 | deny_root_account_statement = var.deny_root_account ? [""] : [] 16 | protect_s3_buckets_statement = var.protect_s3_buckets ? [""] : [] 17 | deny_s3_buckets_public_access_statement = var.deny_s3_buckets_public_access ? [""] : [] 18 | protect_iam_roles_statement = var.protect_iam_roles ? [""] : [] 19 | limit_ec2_instance_types = var.limit_ec2_instance_types ? [""] : [] 20 | limit_regions_statement = var.limit_regions ? [""] : [] 21 | deny_unencrypted_object_uploads_statement = var.require_s3_encryption ? [""] : [] 22 | deny_incorrect_encryption_header_statement = var.require_s3_encryption ? [""] : [] 23 | } 24 | 25 | # 26 | # Combine Policies 27 | # 28 | 29 | data "aws_iam_policy_document" "combined_policy_block" { 30 | 31 | # 32 | # Deny leaving AWS Organizations 33 | # 34 | 35 | dynamic "statement" { 36 | for_each = local.deny_leaving_orgs_statement 37 | content { 38 | sid = "DenyLeavingOrgs" 39 | effect = "Deny" 40 | actions = ["organizations:LeaveOrganization"] 41 | resources = ["*"] 42 | } 43 | } 44 | 45 | # 46 | # Deny creating IAM users or access keys 47 | # 48 | 49 | dynamic "statement" { 50 | for_each = local.deny_creating_iam_users_statement 51 | content { 52 | sid = "DenyCreatingIAMUsers" 53 | effect = "Deny" 54 | actions = [ 55 | "iam:CreateUser", 56 | "iam:CreateAccessKey" 57 | ] 58 | resources = ["*"] 59 | } 60 | } 61 | 62 | # 63 | # Deny deleting KMS Keys 64 | # 65 | 66 | dynamic "statement" { 67 | for_each = local.deny_deleting_kms_keys_statement 68 | content { 69 | sid = "DenyDeletingKMSKeys" 70 | effect = "Deny" 71 | actions = [ 72 | "kms:ScheduleKeyDeletion", 73 | "kms:Delete*" 74 | ] 75 | resources = ["*"] 76 | } 77 | } 78 | 79 | # 80 | # Deny deleting Route53 Hosted Zones 81 | # 82 | 83 | dynamic "statement" { 84 | for_each = local.deny_deleting_route53_zones_statement 85 | content { 86 | sid = "DenyDeletingRoute53Zones" 87 | effect = "Deny" 88 | actions = [ 89 | "route53:DeleteHostedZone" 90 | ] 91 | resources = ["*"] 92 | } 93 | } 94 | 95 | # 96 | # Deny deleting VPC Flow logs, cloudwatch log groups, and cloudwatch log streams 97 | # 98 | 99 | dynamic "statement" { 100 | for_each = local.deny_deleting_cloudwatch_logs_statement 101 | content { 102 | sid = "DenyDeletingCloudwatchLogs" 103 | effect = "Deny" 104 | actions = [ 105 | "ec2:DeleteFlowLogs", 106 | "logs:DeleteLogGroup", 107 | "logs:DeleteLogStream" 108 | ] 109 | resources = ["*"] 110 | } 111 | } 112 | 113 | # 114 | # Deny root account 115 | # 116 | 117 | dynamic "statement" { 118 | for_each = local.deny_root_account_statement 119 | content { 120 | sid = "DenyRootAccount" 121 | actions = ["*"] 122 | resources = ["*"] 123 | effect = "Deny" 124 | condition { 125 | test = "StringLike" 126 | variable = "aws:PrincipalArn" 127 | values = ["arn:aws:iam::*:root"] 128 | } 129 | } 130 | } 131 | 132 | # 133 | # Protect S3 Buckets 134 | # 135 | 136 | dynamic "statement" { 137 | for_each = local.protect_s3_buckets_statement 138 | content { 139 | sid = "ProtectS3Buckets" 140 | effect = "Deny" 141 | actions = [ 142 | "s3:DeleteBucket", 143 | "s3:DeleteObject", 144 | "s3:DeleteObjectVersion", 145 | ] 146 | resources = var.protect_s3_bucket_resources 147 | } 148 | } 149 | 150 | 151 | # 152 | # Deny S3 Buckets Public Access (will not override an account-level setting) 153 | # 154 | 155 | dynamic "statement" { 156 | for_each = local.deny_s3_buckets_public_access_statement 157 | content { 158 | sid = "DenyS3BucketsPublicAccess" 159 | effect = "Deny" 160 | actions = [ 161 | "s3:PutBucketPublicAccessBlock", 162 | "s3:DeletePublicAccessBlock" 163 | ] 164 | resources = var.deny_s3_bucket_public_access_resources 165 | } 166 | } 167 | 168 | # 169 | # Protect IAM Roles 170 | # 171 | 172 | dynamic "statement" { 173 | for_each = local.protect_iam_roles_statement 174 | content { 175 | sid = "ProtectIAMRoles" 176 | effect = "Deny" 177 | actions = [ 178 | "iam:AttachRolePolicy", 179 | "iam:DeleteRole", 180 | "iam:DeleteRolePermissionsBoundary", 181 | "iam:DeleteRolePolicy", 182 | "iam:DetachRolePolicy", 183 | "iam:PutRolePermissionsBoundary", 184 | "iam:PutRolePolicy", 185 | "iam:UpdateAssumeRolePolicy", 186 | "iam:UpdateRole", 187 | "iam:UpdateRoleDescription" 188 | ] 189 | resources = var.protect_iam_role_resources 190 | } 191 | } 192 | 193 | # 194 | # Restrict EC2 Instance Types 195 | # 196 | 197 | dynamic "statement" { 198 | for_each = local.limit_ec2_instance_types 199 | content { 200 | sid = "LimitEC2InstanceTypes" 201 | effect = "Deny" 202 | 203 | actions = [ 204 | "ec2:RunInstances", 205 | "ec2:StartInstances" 206 | ] 207 | 208 | resources = ["*"] 209 | 210 | condition { 211 | test = "StringNotEquals" 212 | variable = "ec2:InstanceType" 213 | values = var.allowed_ec2_instance_types 214 | } 215 | } 216 | } 217 | 218 | # 219 | # Restrict Regional Operations 220 | # 221 | 222 | dynamic "statement" { 223 | for_each = local.limit_regions_statement 224 | content { 225 | sid = "LimitRegions" 226 | effect = "Deny" 227 | 228 | # These actions do not operate in a specific region, or only run in 229 | # a single region, so we don't want to try restricting them by region. 230 | # List of actions can be found in the following example: 231 | # https://docs.aws.amazon.com/organizations/latest/userguide/orgs_manage_policies_scps_examples_general.html 232 | not_actions = [ 233 | "a4b:*", 234 | "acm:*", 235 | "aws-marketplace-management:*", 236 | "aws-marketplace:*", 237 | "aws-portal:*", 238 | "budgets:*", 239 | "ce:*", 240 | "chatbot:*", 241 | "chime:*", 242 | "cloudfront:*", 243 | "config:*", 244 | "cur:*", 245 | "directconnect:*", 246 | "ec2:DescribeRegions", 247 | "ec2:DescribeTransitGateways", 248 | "ec2:DescribeVpnGateways", 249 | "fms:*", 250 | "globalaccelerator:*", 251 | "health:*", 252 | "iam:*", 253 | "importexport:*", 254 | "kms:*", 255 | "mobileanalytics:*", 256 | "networkmanager:*", 257 | "organizations:*", 258 | "pricing:*", 259 | "route53:*", 260 | "route53domains:*", 261 | "route53-recovery-cluster:*", 262 | "route53-recovery-control-config:*", 263 | "route53-recovery-readiness:*", 264 | "s3:GetAccountPublic*", 265 | "s3:ListAllMyBuckets", 266 | "s3:ListMultiRegionAccessPoints", 267 | "s3:PutAccountPublic*", 268 | "shield:*", 269 | "sts:*", 270 | "support:*", 271 | "supportapp:*", 272 | "supportplans:*", 273 | "trustedadvisor:*", 274 | "waf-regional:*", 275 | "waf:*", 276 | "wafv2:*", 277 | "wellarchitected:*" 278 | ] 279 | 280 | resources = ["*"] 281 | 282 | condition { 283 | test = "StringNotEquals" 284 | variable = "aws:RequestedRegion" 285 | values = var.allowed_regions 286 | } 287 | } 288 | } 289 | 290 | # 291 | # Require S3 encryption 292 | # 293 | # https://docs.aws.amazon.com/AmazonS3/latest/dev/UsingServerSideEncryption.html 294 | 295 | dynamic "statement" { 296 | for_each = local.deny_incorrect_encryption_header_statement 297 | content { 298 | sid = "DenyIncorrectEncryptionHeader" 299 | effect = "Deny" 300 | actions = ["s3:PutObject"] 301 | resources = ["*"] 302 | condition { 303 | test = "StringNotEquals" 304 | variable = "s3:x-amz-server-side-encryption" 305 | values = ["AES256", "aws:kms"] 306 | } 307 | } 308 | } 309 | 310 | dynamic "statement" { 311 | for_each = local.deny_unencrypted_object_uploads_statement 312 | content { 313 | sid = "DenyUnEncryptedObjectUploads" 314 | effect = "Deny" 315 | actions = ["s3:PutObject"] 316 | resources = ["*"] 317 | condition { 318 | test = "Null" 319 | variable = "s3:x-amz-server-side-encryption" 320 | values = [true] 321 | } 322 | } 323 | } 324 | } 325 | 326 | 327 | # 328 | # Deny all access 329 | # 330 | 331 | data "aws_iam_policy_document" "deny_all_access" { 332 | 333 | statement { 334 | sid = "DenyAllAccess" 335 | effect = "Deny" 336 | actions = ["*"] 337 | resources = ["*"] 338 | } 339 | } 340 | 341 | resource "aws_organizations_policy" "generated" { 342 | name = "${var.target.name}-generated-ou-scp" 343 | description = "${var.target.name} SCP generated by ou-scp module" 344 | content = var.deny_all ? data.aws_iam_policy_document.deny_all_access.json : data.aws_iam_policy_document.combined_policy_block.json 345 | 346 | tags = var.tags 347 | } 348 | 349 | resource "aws_organizations_policy_attachment" "generated" { 350 | policy_id = aws_organizations_policy.generated.id 351 | target_id = var.target.id 352 | } 353 | -------------------------------------------------------------------------------- /outputs.tf: -------------------------------------------------------------------------------- 1 | # Outputs placeholder 2 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:recommended", 4 | "helpers:pinGitHubActionDigests" 5 | ], 6 | "labels": [ 7 | "dependencies" 8 | ], 9 | "packageRules": [ 10 | { 11 | "automerge": true, 12 | "description": "Automerge all updates except major versions", 13 | "matchUpdateTypes": [ 14 | "patch", 15 | "pin", 16 | "digest", 17 | "minor" 18 | ] 19 | }, 20 | { 21 | "description": "Tag the waddlers Github Team for major updates", 22 | "matchUpdateTypes": [ 23 | "major" 24 | ], 25 | "reviewers": [ 26 | "team:waddlers" 27 | ] 28 | }, 29 | { 30 | "automerge": true, 31 | "description": "Group minor and patch updates into a single PR", 32 | "groupName": "dependencies", 33 | "matchManagers": [ 34 | "terraform", 35 | "pre-commit", 36 | "github-actions" 37 | ], 38 | "matchUpdateTypes": [ 39 | "minor", 40 | "patch" 41 | ] 42 | } 43 | ], 44 | "schedule": [ 45 | "every weekend" 46 | ], 47 | "separateMinorPatch": true, 48 | "timezone": "America/Los_Angeles" 49 | } 50 | -------------------------------------------------------------------------------- /variables.tf: -------------------------------------------------------------------------------- 1 | variable "target" { 2 | description = "OU resource to attach SCP" 3 | type = object({ 4 | name = string 5 | id = string 6 | }) 7 | } 8 | 9 | variable "deny_all" { 10 | description = "If false, create a combined policy. If true, deny all access" 11 | default = false 12 | type = bool 13 | } 14 | 15 | # Policy Statement Switches 16 | 17 | variable "deny_leaving_orgs" { 18 | description = "DenyLeavingOrgs in the OU policy." 19 | default = false 20 | type = bool 21 | } 22 | 23 | variable "deny_creating_iam_users" { 24 | description = "DenyCreatingIAMUsers in the OU policy." 25 | default = false 26 | type = bool 27 | } 28 | 29 | variable "deny_deleting_kms_keys" { 30 | description = "DenyDeletingKMSKeys in the OU policy." 31 | default = false 32 | type = bool 33 | } 34 | 35 | variable "deny_deleting_route53_zones" { 36 | description = "DenyDeletingRoute53Zones in the OU policy." 37 | default = false 38 | type = bool 39 | } 40 | 41 | variable "deny_deleting_cloudwatch_logs" { 42 | description = "DenyDeletingCloudwatchLogs in the OU policy." 43 | default = false 44 | type = bool 45 | } 46 | 47 | variable "deny_root_account" { 48 | description = "DenyRootAccount in the OU policy." 49 | default = false 50 | type = bool 51 | } 52 | 53 | variable "protect_s3_buckets" { 54 | description = "ProtectS3Buckets in the OU policy." 55 | default = false 56 | type = bool 57 | } 58 | 59 | variable "deny_s3_buckets_public_access" { 60 | description = "DenyS3BucketsPublicAccess in the OU policy." 61 | default = false 62 | type = bool 63 | } 64 | 65 | variable "protect_iam_roles" { 66 | description = "ProtectIAMRoles in the OU policy." 67 | default = false 68 | type = bool 69 | } 70 | 71 | variable "limit_ec2_instance_types" { 72 | description = "LimitEC2InstanceTypes in the OU policy." 73 | default = false 74 | type = bool 75 | } 76 | 77 | variable "limit_regions" { 78 | description = "LimitRegions in the OU policy." 79 | default = false 80 | type = bool 81 | } 82 | 83 | variable "require_s3_encryption" { 84 | description = "DenyIncorrectEncryptionHeader and DenyUnEncryptedObjectUploads in the OU policy" 85 | default = false 86 | type = bool 87 | } 88 | 89 | # Policy-specific resources 90 | 91 | variable "protect_s3_bucket_resources" { 92 | description = "S3 bucket resource ARNs to protect from bucket and object deletion" 93 | type = list(string) 94 | default = [""] 95 | } 96 | 97 | variable "deny_s3_bucket_public_access_resources" { 98 | description = "S3 bucket resource ARNs to block public access" 99 | type = list(string) 100 | default = [""] 101 | } 102 | 103 | variable "protect_iam_role_resources" { 104 | description = "IAM role resource ARNs to protect from modification and deletion" 105 | type = list(string) 106 | default = [""] 107 | } 108 | 109 | variable "allowed_regions" { 110 | description = "AWS Regions allowed for use (for use with the restrict regions SCP)" 111 | type = list(string) 112 | default = [""] 113 | } 114 | 115 | variable "allowed_ec2_instance_types" { 116 | description = "EC2 instances types allowed for use" 117 | type = list(string) 118 | default = [""] 119 | } 120 | 121 | variable "tags" { 122 | description = "Tags applied to the SCP policy" 123 | type = map(string) 124 | default = {} 125 | } 126 | -------------------------------------------------------------------------------- /versions.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.0" 3 | 4 | required_providers { 5 | aws = ">= 3.0" 6 | } 7 | } 8 | --------------------------------------------------------------------------------