├── .github └── workflows │ └── validate.yml ├── .gitignore ├── .markdownlintrc ├── .pre-commit-config.yaml ├── .terraform-docs.yml ├── LICENSE ├── README.md ├── examples └── simple │ ├── main.tf │ └── versions.tf ├── main.tf ├── renovate.json ├── variables.tf └── versions.tf /.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@main 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .terraform 3 | terraform.tfstate 4 | terraform.tfstate.backup 5 | terraform.tfstate.*.backup 6 | .envrc.local 7 | -------------------------------------------------------------------------------- /.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 | } 8 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v4.4.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.16 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.33.0 29 | hooks: 30 | - id: markdownlint 31 | 32 | - repo: https://github.com/detailyang/pre-commit-shell 33 | rev: 1.0.5 34 | hooks: 35 | - id: shell-lint 36 | 37 | - repo: https://github.com/antonbabenko/pre-commit-terraform 38 | rev: v1.77.1 39 | hooks: 40 | - id: terraform_fmt 41 | -------------------------------------------------------------------------------- /.terraform-docs.yml: -------------------------------------------------------------------------------- 1 | settings: 2 | html: false 3 | anchor: false 4 | formatter: "markdown table" 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2019, 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 | 2 | 3 | Configures IAM policy to enforce MFA when accessing the AWS API. 4 | 5 | This configured policy also requires users to assume a role for most API calls. 6 | 7 | Creates the following resources: 8 | 9 | * IAM policy requiring a valid MFA security token for all API calls except those needed for managing a user's own IAM user. 10 | * IAM group policy attachment for defining which IAM groups to enforce MFA on. 11 | * IAM user policy attachment for defining which IAM users to enforce MFA on. 12 | 13 | ## Usage 14 | 15 | ```hcl 16 | module "aws_mfa" { 17 | source = "trussworks/mfa/aws" 18 | 19 | iam_groups = ["engineers"] 20 | iam_users = ["jill"] 21 | } 22 | ``` 23 | 24 | ## Requirements 25 | 26 | | Name | Version | 27 | |------|---------| 28 | | terraform | >= 1.0 | 29 | | aws | >= 3 | 30 | 31 | ## Providers 32 | 33 | | Name | Version | 34 | |------|---------| 35 | | aws | >= 3 | 36 | 37 | ## Modules 38 | 39 | No modules. 40 | 41 | ## Resources 42 | 43 | | Name | Type | 44 | |------|------| 45 | | [aws_iam_group_policy_attachment.main](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_group_policy_attachment) | resource | 46 | | [aws_iam_policy.main](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource | 47 | | [aws_iam_user_policy_attachment.main](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_user_policy_attachment) | resource | 48 | | [aws_iam_policy_document.main](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | 49 | | [aws_partition.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/partition) | data source | 50 | 51 | ## Inputs 52 | 53 | | Name | Description | Type | Default | Required | 54 | |------|-------------|------|---------|:--------:| 55 | | iam\_groups | List of IAM groups to enforce MFA when accessing the AWS API. | `list(string)` | `[]` | no | 56 | | iam\_users | List of IAM users to enforce MFA when accessing the AWS API. | `list(string)` | `[]` | no | 57 | 58 | ## Outputs 59 | 60 | No outputs. 61 | 62 | 63 | ## Developer Setup 64 | 65 | Install dependencies (macOS) 66 | 67 | ```shell 68 | brew install pre-commit go terraform terraform-docs 69 | ``` 70 | -------------------------------------------------------------------------------- /examples/simple/main.tf: -------------------------------------------------------------------------------- 1 | # 2 | # Enforce MFA 3 | # 4 | 5 | module "aws_mfa" { 6 | source = "../../" 7 | } 8 | -------------------------------------------------------------------------------- /examples/simple/versions.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = "~> 0.13.0" 3 | 4 | required_providers { 5 | aws = { 6 | source = "hashicorp/aws" 7 | version = "~> 3" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /main.tf: -------------------------------------------------------------------------------- 1 | /** 2 | * Configures IAM policy to enforce MFA when accessing the AWS API. 3 | * 4 | * This configured policy also requires users to assume a role for most API calls. 5 | * 6 | * Creates the following resources: 7 | * 8 | * * IAM policy requiring a valid MFA security token for all API calls except those needed for managing a user's own IAM user. 9 | * * IAM group policy attachment for defining which IAM groups to enforce MFA on. 10 | * * IAM user policy attachment for defining which IAM users to enforce MFA on. 11 | * 12 | * ## Usage 13 | * 14 | * ```hcl 15 | * module "aws_mfa" { 16 | * source = "trussworks/mfa/aws" 17 | * 18 | * iam_groups = ["engineers"] 19 | * iam_users = ["jill"] 20 | * } 21 | * ``` 22 | */ 23 | 24 | data "aws_partition" "current" { 25 | } 26 | 27 | data "aws_iam_policy_document" "main" { 28 | statement { 29 | sid = "AllowAllUsersToListAccounts" 30 | effect = "Allow" 31 | 32 | actions = [ 33 | "iam:ListAccountAliases", 34 | "iam:ListUsers", 35 | "iam:ListVirtualMFADevices", 36 | "iam:GetAccountPasswordPolicy", 37 | "iam:GetAccountSummary", 38 | ] 39 | 40 | resources = [ 41 | "*", 42 | ] 43 | } 44 | 45 | statement { 46 | sid = "AllowIndividualUserToSeeAndManageOnlyTheirOwnAccountInformation" 47 | effect = "Allow" 48 | 49 | actions = [ 50 | "iam:CreateAccessKey", 51 | "iam:DeleteAccessKey", 52 | "iam:DeleteLoginProfile", 53 | "iam:GetLoginProfile", 54 | "iam:ListAccessKeys", 55 | "iam:UpdateAccessKey", 56 | "iam:ListSigningCertificates", 57 | "iam:DeleteSigningCertificate", 58 | "iam:UpdateSigningCertificate", 59 | "iam:UploadSigningCertificate", 60 | ] 61 | 62 | resources = [ 63 | "arn:${data.aws_partition.current.partition}:iam::*:user/&{aws:username}", 64 | ] 65 | } 66 | 67 | statement { 68 | sid = "AllowIndividualUserToListOnlyTheirOwnMFA" 69 | effect = "Allow" 70 | 71 | actions = [ 72 | "iam:ListMFADevices", 73 | ] 74 | 75 | resources = [ 76 | "arn:${data.aws_partition.current.partition}:iam::*:mfa/*", 77 | "arn:${data.aws_partition.current.partition}:iam::*:user/&{aws:username}", 78 | ] 79 | } 80 | 81 | statement { 82 | sid = "AllowIndividualUserToManageTheirOwnMFA" 83 | effect = "Allow" 84 | 85 | actions = [ 86 | "iam:CreateVirtualMFADevice", 87 | "iam:DeleteVirtualMFADevice", 88 | "iam:EnableMFADevice", 89 | "iam:ResyncMFADevice", 90 | ] 91 | 92 | resources = [ 93 | "arn:${data.aws_partition.current.partition}:iam::*:mfa/*", 94 | "arn:${data.aws_partition.current.partition}:iam::*:user/&{aws:username}", 95 | ] 96 | } 97 | 98 | statement { 99 | sid = "AllowIndividualUserToDeactivateOnlyTheirOwnMFAOnlyWhenUsingMFA" 100 | effect = "Allow" 101 | 102 | actions = [ 103 | "iam:DeactivateMFADevice", 104 | ] 105 | 106 | resources = [ 107 | "arn:${data.aws_partition.current.partition}:iam::*:user/&{aws:username}", 108 | ] 109 | 110 | condition { 111 | test = "Bool" 112 | variable = "aws:MultiFactorAuthPresent" 113 | values = ["true"] 114 | } 115 | } 116 | 117 | statement { 118 | # Since this statement uses not_actions, it effectively blocks some statements 119 | # that do not support MFA, such as sts:GetFederationToken. 120 | # Therefore, this policy effectively forbids directly calling AWS APIs 121 | # without assuming a role first. 122 | sid = "BlockMostAccessUnlessSignedInWithMFA" 123 | 124 | effect = "Deny" 125 | 126 | # not_actions is a list of actions that this statement does not apply to. 127 | # Used to apply a policy statement to all actions except those listed. 128 | not_actions = [ 129 | "iam:ChangePassword", 130 | "iam:CreateLoginProfile", 131 | "iam:CreateVirtualMFADevice", 132 | "iam:DeleteVirtualMFADevice", 133 | "iam:ListVirtualMFADevices", 134 | "iam:EnableMFADevice", 135 | "iam:ResyncMFADevice", 136 | "iam:ListAccountAliases", 137 | "iam:ListUsers", 138 | "iam:ListSSHPublicKeys", 139 | "iam:ListAccessKeys", 140 | "iam:ListServiceSpecificCredentials", 141 | "iam:ListMFADevices", 142 | "iam:GetAccountSummary", 143 | "sts:GetSessionToken", 144 | ] 145 | 146 | resources = [ 147 | "*", 148 | ] 149 | 150 | condition { 151 | test = "BoolIfExists" 152 | variable = "aws:MultiFactorAuthPresent" 153 | values = ["false"] 154 | } 155 | } 156 | } 157 | 158 | resource "aws_iam_policy" "main" { 159 | #Use alphanumeric and '+=,.@-_' characters. Maximum 128 characters. 160 | name = "enforce-mfa" 161 | path = "/" 162 | description = "Requires valid MFA security token for all API calls except those needed for managing a user's own IAM user." 163 | policy = data.aws_iam_policy_document.main.json 164 | } 165 | 166 | resource "aws_iam_group_policy_attachment" "main" { 167 | count = length(var.iam_groups) 168 | group = element(var.iam_groups, count.index) 169 | policy_arn = aws_iam_policy.main.arn 170 | } 171 | 172 | resource "aws_iam_user_policy_attachment" "main" { 173 | count = length(var.iam_users) 174 | user = element(var.iam_users, count.index) 175 | policy_arn = aws_iam_policy.main.arn 176 | } 177 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:recommended" 4 | ], 5 | "labels": [ 6 | "dependencies" 7 | ], 8 | "packageRules": [ 9 | { 10 | "description": "Tag the waddlers Github Team for major updates", 11 | "matchUpdateTypes": [ 12 | "major" 13 | ], 14 | "reviewers": [ 15 | "team:waddlers" 16 | ] 17 | }, 18 | { 19 | "automerge": true, 20 | "description": "Group minor and patch updates into a single PR", 21 | "groupName": "dependencies", 22 | "matchUpdateTypes": [ 23 | "minor", 24 | "patch", 25 | "pin", 26 | "digest" 27 | ] 28 | } 29 | ], 30 | "schedule": [ 31 | "every weekend" 32 | ], 33 | "separateMinorPatch": true, 34 | "timezone": "America/Los_Angeles" 35 | } 36 | -------------------------------------------------------------------------------- /variables.tf: -------------------------------------------------------------------------------- 1 | variable "iam_groups" { 2 | description = "List of IAM groups to enforce MFA when accessing the AWS API." 3 | type = list(string) 4 | default = [] 5 | } 6 | 7 | variable "iam_users" { 8 | description = "List of IAM users to enforce MFA when accessing the AWS API." 9 | type = list(string) 10 | default = [] 11 | } 12 | -------------------------------------------------------------------------------- /versions.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.0" 3 | 4 | required_providers { 5 | aws = { 6 | source = "hashicorp/aws" 7 | version = ">= 3" 8 | } 9 | } 10 | } 11 | --------------------------------------------------------------------------------