Skip to content

Commit

Permalink
Merge pull request #23 from quantum-sec/feature/EN-490
Browse files Browse the repository at this point in the history
  • Loading branch information
arledesma committed Sep 2, 2021
2 parents 31adec6 + 3c140bc commit 35a6254
Show file tree
Hide file tree
Showing 4 changed files with 151 additions and 8 deletions.
11 changes: 10 additions & 1 deletion examples/aws-roles-default/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,11 @@ provider "aws" {
}

locals {
# the setunion for the _from_accounts locals is in place only as an example
# typically you will not want to grant role access from the same account but use a group instead
auto_deploy_from_accounts = setunion(var.auto_deploy_from_accounts, [data.aws_caller_identity.current.account_id])
developer_from_accounts = setunion(var.developer_from_accounts, [data.aws_caller_identity.current.account_id])
support_from_accounts = setunion(var.support_from_accounts, [data.aws_caller_identity.current.account_id])
}

module "roles" {
Expand All @@ -23,9 +26,10 @@ module "roles" {
# this is our current account
aws_account_id = data.aws_caller_identity.current.account_id

# we allow the auto-deploy-from-external-accounts role to be assumed from these accounts
# we allow the auto-deploy-from-external-accounts, developer-from-external-accounts, and support-from-external-accounts roles to be assumed from these accounts
auto_deploy_from_accounts = local.auto_deploy_from_accounts
developer_from_accounts = local.developer_from_accounts
support_from_accounts = local.support_from_accounts

# we only allow codebuild to assume this role from the accounts listed in `auto_deploy_from_accounts`
auto_deploy_service_principals = []
Expand All @@ -39,6 +43,9 @@ module "roles" {
# we prefix all of our role names with `example-`
role_name_static_prefix = var.role_name_static_prefix

# we prefix all of our policy names with `example_`
policy_name_static_prefix = var.policy_name_static_prefix

# we exclude budget_management_tooling_readonly policy from being deployed
excluded_policy_names = setunion([
# the aws_principals must be updated to not reference 123456789012.
Expand All @@ -51,6 +58,8 @@ module "roles" {

developer_include_managed_policies = var.developer_include_managed_policies

default_path = "/examples/aws-roles-default/"

tags = {
cost_center = "engineering"
}
Expand Down
13 changes: 13 additions & 0 deletions examples/aws-roles-default/vars.tf
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ variable "developer_from_accounts" {
default = []
}

variable "support_from_accounts" {
description = "A list of AWS principals (ARNs) permitted to assume the support-from-external-accounts role. If not set then the role will not be created."
type = set(string)
default = []
}

variable "developer_include_managed_policies" {
type = list(string)
default = [
Expand Down Expand Up @@ -56,6 +62,12 @@ variable "my_custom_policies" {
}
}

variable "policy_name_static_prefix" {
description = "A static string that will be prefixed to the name of the policy."
type = string
default = "example_"
}

variable "role_name_static_prefix" {
description = "A static string that will be prefixed to the name of the role. This is not the same as `name_prefix` in that it does not generate a unique name, but instead provides a static string prefix to the role name."
type = string
Expand All @@ -68,6 +80,7 @@ variable "included_default_policy_names" {
default = [
"auto_deploy_from_external_accounts",
"developer_from_external_accounts",
"support_from_external_accounts",
]
}

Expand Down
101 changes: 94 additions & 7 deletions modules/aws-iam-roles/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ terraform {
}

locals {
create_policies = {
for key, value in local.policies : key => value if value.policy != null
}
policies = {
# exclude any policies matching values in the `excluded_policy_names` list.
for key, value in local.merged_policies : key => value if !contains(var.excluded_policy_names, key)
Expand Down Expand Up @@ -41,16 +44,35 @@ locals {
role_requires_mfa = var.developer_policy == null ? true : lookup(var.developer_policy, "role_requires_mfa", true)
}
self_manage = {
/* The default use of var.aws_account_id in aws_principals looks weird but a role must have at least one principal. This role will typically not be used as the user exists in another account. */
name = var.self_manage_policy == null ? "self-manage" : lookup(var.self_manage_policy, "name", "self-manage")
description = var.self_manage_policy == null ? "The policy providing access for managing ones own iam user" : lookup(var.self_manage_policy, "description", "The policy providing access for managing ones own iam user")
path = var.self_manage_policy == null ? var.default_path : lookup(var.self_manage_policy, "path", var.default_path)
policy = var.self_manage_policy == null ? data.aws_iam_policy_document.iam_manage_self.json : lookup(var.self_manage_policy, "policy", data.aws_iam_policy_document.iam_manage_self.json)
service_principals = var.self_manage_policy == null ? [] : lookup(var.self_manage_policy, "service_principals", [])
aws_principals = var.self_manage_policy == null ? [] : lookup(var.self_manage_policy, "aws_principals" /*aws_principals should not typically be set for self_manage as you can not assume role to yourself in a different account*/, [])
aws_principals = var.self_manage_policy == null ? (length(var.self_manage_from_accounts) > 0 ? var.self_manage_from_accounts : [var.aws_account_id]) : lookup(var.support_from_external_accounts_policy, "aws_principals", [var.aws_account_id])
federated_principals = var.self_manage_policy == null ? [] : lookup(var.self_manage_policy, "federated_principals", [])
iam_policy_arns = var.self_manage_policy == null ? [] : lookup(var.self_manage_policy, "iam_policy_arns", [])
role_requires_mfa = var.self_manage_policy == null ? true : lookup(var.self_manage_policy, "role_requires_mfa", true)
}
support_from_external_accounts = {
/*
* The default use of var.aws_account_id in aws_principals looks weird but a role must have at least one principal.
* Policy: CIS AWS Web Services Foundations Benchmark
* Control Name: Ensure a support role has been created to manage incidents with AWS Support
* Criticality: HIGH
* Without this role there may be a violation of the CIS Benchmark
*/
name = var.support_from_external_accounts_policy == null ? "support" : lookup(var.support_from_external_accounts_policy, "name", "support")
description = var.support_from_external_accounts_policy == null ? "The policy providing access for managing ones own iam user and use AWS Support" : lookup(var.support_from_external_accounts_policy, "description", "The policy providing access for managing ones own iam user and use AWS Support")
path = var.support_from_external_accounts_policy == null ? var.default_path : lookup(var.support_from_external_accounts_policy, "path", var.default_path)
policy = var.support_from_external_accounts_policy == null ? data.aws_iam_policy_document.require_mfa.json : lookup(var.support_from_external_accounts_policy, "policy", data.aws_iam_policy_document.require_mfa.json)
service_principals = var.support_from_external_accounts_policy == null ? [] : lookup(var.support_from_external_accounts_policy, "service_principals", [])
aws_principals = var.support_from_external_accounts_policy == null ? (length(var.support_from_accounts) > 0 ? var.support_from_accounts : [var.aws_account_id]) : lookup(var.support_from_external_accounts_policy, "aws_principals", [var.aws_account_id])
federated_principals = var.support_from_external_accounts_policy == null ? [] : lookup(var.support_from_external_accounts_policy, "federated_principals", [])
iam_policy_arns = var.support_from_external_accounts_policy == null ? ["arn:aws:iam::aws:policy/AWSSupportAccess"] : lookup(var.support_from_external_accounts_policy, "iam_policy_arns", ["arn:aws:iam::aws:policy/AWSSupportAccess"])
role_requires_mfa = var.support_from_external_accounts_policy == null ? true : lookup(var.support_from_external_accounts_policy, "role_requires_mfa", true)
}
deny_all = {
name = "deny-all"
description = "The policy providing access for denying all access"
Expand All @@ -74,11 +96,11 @@ locals {
# ----------------------------------------------------------------------------------------------------------------------

module "policy" {
for_each = local.policies
for_each = local.create_policies

source = "../aws-iam-policy"

name = each.key
name = format("%s%s", var.policy_name_static_prefix, each.key)
description = lookup(each.value, "description", "The policy providing access for ${each.value["name"]}")
path = lookup(each.value, "path", var.default_path)
policy = lookup(each.value, "policy", data.aws_iam_policy_document.deny_all.json)
Expand Down Expand Up @@ -106,9 +128,8 @@ module "role" {
# oidc federation
federated_principals = lookup(each.value, "federated_principals", [])

# flatten(setproduct()) gets around the following which occurs when using setunion() or concat()
# The "for_each" value depends on resource attributes that cannot be determined until apply, so Terraform cannot predict how many instances will be created.
iam_policy_arns = compact(flatten(setproduct([module.policy[each.key].arn], lookup(each.value, "iam_policy_arns", []))))
# directly attached iam policies
iam_policy_arns = lookup(each.value, "iam_policy_arns", [])

require_mfa = lookup(each.value, "role_requires_mfa", true)

Expand All @@ -120,7 +141,7 @@ module "role" {
# ----------------------------------------------------------------------------------------------------------------------

resource "aws_iam_role_policy_attachment" "policy" {
for_each = local.policies
for_each = local.create_policies
role = module.role[each.key].id
policy_arn = module.policy[each.key].arn
}
Expand Down Expand Up @@ -357,6 +378,72 @@ data "aws_iam_policy_document" "iam_manage_self" {
}
}

# ----------------------------------------------------------------------------------------------------------------------
# POLICIES - require_mfa
# ----------------------------------------------------------------------------------------------------------------------

data "aws_iam_policy_document" "require_mfa" {

# Sourced from https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_examples_aws_my-sec-creds-self-manage-mfa-only.html
statement {
sid = "AllowViewAccountInfo"
effect = "Allow"
actions = ["iam:ListVirtualMFADevices"]
resources = ["*"]
}

statement {
sid = "AllowManageOwnVirtualMFADevice"
effect = "Allow"
actions = [
"iam:CreateVirtualMFADevice",
"iam:DeleteVirtualMFADevice"
]

resources = [
"arn:aws:iam::${var.aws_account_id}:mfa/$${aws:username}",
]
}

statement {
sid = "AllowManageOwnUserMFA"
effect = "Allow"
actions = [
"iam:DeactivateMFADevice",
"iam:EnableMFADevice",
"iam:GetUser",
"iam:ListMFADevices",
"iam:ResyncMFADevice"
]
resources = [
"arn:aws:iam::${var.aws_account_id}:user/$${aws:username}",
# The mfa/${aws:username} resource is not included in the documentation
# but is required for the user to perform some of the iam:*MFA* actions
"arn:aws:iam::${var.aws_account_id}:mfa/$${aws:username}",
]
}

statement {
sid = "DenyAllExceptListedIfNoMFA"
effect = "Deny"
not_actions = [
"iam:CreateVirtualMFADevice",
"iam:EnableMFADevice",
"iam:GetUser",
"iam:ListMFADevices",
"iam:ListVirtualMFADevices",
"iam:ResyncMFADevice",
"sts:GetSessionToken"
]
resources = ["*"]
condition {
test = "Bool"
variable = "aws:MultiFactorAuthPresent"
values = ["false"]
}
}
}

# ----------------------------------------------------------------------------------------------------------------------
# POLICIES - deny_all
# ----------------------------------------------------------------------------------------------------------------------
Expand Down
34 changes: 34 additions & 0 deletions modules/aws-iam-roles/vars.tf
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ variable "aws_account_id" {
# OPTIONAL MODULE PARAMETERS
# ---------------------------------------------------------------------------------------------------------------------

variable "policy_name_static_prefix" {
description = "A static string that will be prefixed to the name of the policy."
type = string
default = ""
}

variable "role_name_static_prefix" {
description = "A static string that will be prefixed to the name of the role. This is not the same as `name_prefix` in that it does not generate a unique name, but instead provides a static string prefix to the role name."
type = string
Expand Down Expand Up @@ -220,12 +226,40 @@ variable "self_manage_policy" {
default = null
}

variable "support_from_external_accounts_policy" {
description = "A policy that will completely overwrite the default support_policy policy."
type = object({
name = string
description = string
path = string
policy = string
service_principals = list(string)
aws_principals = list(string)
federated_principals = list(string)
iam_policy_arns = list(string)
role_requires_mfa = bool
})
default = null
}

variable "developer_from_accounts" {
description = "A list of AWS principals (ARNs) permitted to assume the developer-from-external-accounts role."
type = set(string)
default = []
}

variable "self_manage_from_accounts" {
description = "A list of AWS principals (ARNs) permitted to assume the self_manage role."
type = set(string)
default = []
}

variable "support_from_accounts" {
description = "A list of AWS principals (ARNs) permitted to assume the support role."
type = set(string)
default = []
}

variable "policy_custom" {
description = "Any custom policies that should be deployed. These custom policies will be shallow merged on top of the defaults, allowing you to fully overwrite what the default policy is. Minimal defaults will be applied if not defined in the custom policy."
type = map(any)
Expand Down

0 comments on commit 35a6254

Please sign in to comment.