diff --git a/.gitignore b/.gitignore index 4df0dc1..79fe8d3 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,5 @@ .terraform/* logs +X +Y diff --git a/CHANGELOG.md b/CHANGELOG.md index a648c63..a10c9fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ * v1.0.1 -- 20210301 - add docs - - comment out ec2* role stuff + - comment out ec2 role stuff - add check for ldap provider * v1.0.2 -- 20210402 @@ -98,3 +98,7 @@ tag: 2.0.1 * 2.3.3 -- 2024-06-17 - enable override_prefixes to actually work + +* 2.4.0 -- 2025-07-29 + - move things to common + - make submodule rolesanywhere diff --git a/common/data.tf b/common/data.tf new file mode 100644 index 0000000..30905b7 --- /dev/null +++ b/common/data.tf @@ -0,0 +1,23 @@ +data "aws_caller_identity" "current" {} + +data "aws_arn" "current" { + arn = data.aws_caller_identity.current.arn +} + +data "aws_region" "current" {} + +# output "caller_account_id" { +# value = data.aws_caller_identity.current.account_id +# } +# +# output "account_caller_arn" { +# value = data.aws_caller_identity.current.arn +# } +# +# output "account_caller_arn_partition" { +# value = data.aws_arn.current.partition +# } +# +# output "account_region"name" { +# value = data.aws_region.current.name +# } diff --git a/common/variables.common.tf b/common/variables.common.tf new file mode 100644 index 0000000..2f1a864 --- /dev/null +++ b/common/variables.common.tf @@ -0,0 +1,26 @@ +#--- +# account info +#--- +variable "account_id" { + description = "AWS Account ID (default will pull from current user)" + type = string + default = "" +} + +variable "account_alias" { + description = "AWS Account Alias" + type = string + default = "" +} + +variable "override_prefixes" { + description = "Override built-in prefixes by component (role, policy). This should be used primarily for common infrastructure things" + type = map(string) + default = {} +} + +variable "tags" { + description = "AWS Tags to apply to appropriate resources (S3, KMS). Do not include safeguard tags here, use the data_safeguard field for such things." + type = map(string) + default = {} +} diff --git a/common/version.tf b/common/version.tf new file mode 100644 index 0000000..f403a49 --- /dev/null +++ b/common/version.tf @@ -0,0 +1,3 @@ +locals { + _module_version = "2.4.0" +} diff --git a/common/versions.tf b/common/versions.tf new file mode 100644 index 0000000..53db328 --- /dev/null +++ b/common/versions.tf @@ -0,0 +1,18 @@ +# tf 0.13+ +terraform { + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 3.66.0" + } + ldap = { + source = "trevex/ldap" + version = ">= 0.5.4" + } + external = { + source = "hashicorp/external" + version = ">= 2.2.0" + } + } + required_version = ">= 0.13" +} diff --git a/data.tf b/data.tf deleted file mode 100644 index 30905b7..0000000 --- a/data.tf +++ /dev/null @@ -1,23 +0,0 @@ -data "aws_caller_identity" "current" {} - -data "aws_arn" "current" { - arn = data.aws_caller_identity.current.arn -} - -data "aws_region" "current" {} - -# output "caller_account_id" { -# value = data.aws_caller_identity.current.account_id -# } -# -# output "account_caller_arn" { -# value = data.aws_caller_identity.current.arn -# } -# -# output "account_caller_arn_partition" { -# value = data.aws_arn.current.partition -# } -# -# output "account_region"name" { -# value = data.aws_region.current.name -# } diff --git a/data.tf b/data.tf new file mode 120000 index 0000000..9b14937 --- /dev/null +++ b/data.tf @@ -0,0 +1 @@ +common/data.tf \ No newline at end of file diff --git a/rolesanywhere/aws_config.tf b/rolesanywhere/aws_config.tf new file mode 100644 index 0000000..36ffd0d --- /dev/null +++ b/rolesanywhere/aws_config.tf @@ -0,0 +1,13 @@ +resource "local_file" "aws_config_file" { + filename = format("%v/%v.%v", "./certs", local.role_name, "aws_config") + file_permission = "0644" + directory_permission = "0755" + content = templatefile("aws_config.tpl", { + account_alias = var.account_alias + role_name = local.role_name + role_arn = aws_iam_role.role.arn + trust_anchor_arn = local.this_trust_arn + profile_arn = aws_rolesanywhere_profile.role.arn + region = var.region + }) +} diff --git a/rolesanywhere/aws_config.tpl b/rolesanywhere/aws_config.tpl new file mode 100644 index 0000000..351a0e8 --- /dev/null +++ b/rolesanywhere/aws_config.tpl @@ -0,0 +1,3 @@ +[profile ${account_alias}.${role_name}] +region = ${region} +credential_process = aws_signing_helper credential-process --certificate CERTPATH/${role_name}.crt --private-key CERTPATH/${role_name}.key --trust-anchor-arn ${trust_anchor_arn} --profile-arn ${profile_arn} --role-arn ${role_arn} --region ${region} diff --git a/rolesanywhere/certificate.tf b/rolesanywhere/certificate.tf new file mode 100644 index 0000000..09b3e0f --- /dev/null +++ b/rolesanywhere/certificate.tf @@ -0,0 +1,28 @@ +module "certificate" { + source = "git@github.e.it.census.gov:terraform-modules/aws-certificates//acmpca-iam-rolesanywhere" + + role_name = local.role_name + contact_email = var.contact_group_email + certificate_subject_ou = local.certificate_subject_ou["x509Subject/OU"] + validity_days = var.validity_days +} + +locals { + this_trust_arn = try(([for k, v in local.trust_ca[var.region] : v if v.ca_name == module.certificate.certificate_authority_name])[0].trust_arn, null) +} + +## output "certificate_subject" { +## value = module.certificate.certificate_subject +## } +## +## output "certificate_details" { +## value = module.certificate.certificate_details +## } +## +## output "certificate_issuer_details" { +## value = module.certificate.certificate_issuer_details +## } +## +## output "certificate_issuer_subject" { +## value = module.certificate.certificate_issuer_subject +## } diff --git a/rolesanywhere/data.ssm.tf b/rolesanywhere/data.ssm.tf new file mode 100644 index 0000000..26de7b8 --- /dev/null +++ b/rolesanywhere/data.ssm.tf @@ -0,0 +1,42 @@ +data "aws_ssm_parameters_by_path" "trust_east" { + provider = aws.east + path = "/enterprise/rolesanywhere/trustanchor/" + recursive = true +} + +data "aws_ssm_parameters_by_path" "trust_west" { + provider = aws.west + path = "/enterprise/rolesanywhere/trustanchor/" + recursive = true +} + +locals { + trust_east_arns = zipmap(data.aws_ssm_parameters_by_path.trust_east.names, data.aws_ssm_parameters_by_path.trust_east.arns) + trust_east_values = zipmap(data.aws_ssm_parameters_by_path.trust_east.names, nonsensitive(data.aws_ssm_parameters_by_path.trust_east.values)) + _trust_east_ca = { for v in data.aws_ssm_parameters_by_path.trust_east.names : v => slice(reverse(split("/", v)), 0, 2) } + trust_east_ca = { for k, v in local._trust_east_ca : k => { + label = k + region = "us-gov-east-1" + ca_name = v[0] + ssm_parameter_name = k + ssm_paraemter_arn = local.trust_east_arns[k] + trust_arn = local.trust_east_values[k] + } } + + trust_west_arns = zipmap(data.aws_ssm_parameters_by_path.trust_west.names, data.aws_ssm_parameters_by_path.trust_west.arns) + trust_west_values = zipmap(data.aws_ssm_parameters_by_path.trust_west.names, nonsensitive(data.aws_ssm_parameters_by_path.trust_west.values)) + _trust_west_ca = { for v in data.aws_ssm_parameters_by_path.trust_west.names : v => slice(reverse(split("/", v)), 0, 2) } + trust_west_ca = { for k, v in local._trust_west_ca : k => { + label = k + region = "us-gov-west-1" + ca_name = v[0] + ssm_parameter_name = k + ssm_paraemter_arn = local.trust_west_arns[k] + trust_arn = local.trust_west_values[k] + } } + + trust_ca = { + "us-gov-east-1" = local.trust_east_ca + "us-gov-west-1" = local.trust_west_ca + } +} diff --git a/rolesanywhere/data.tf b/rolesanywhere/data.tf new file mode 120000 index 0000000..995624d --- /dev/null +++ b/rolesanywhere/data.tf @@ -0,0 +1 @@ +../common/data.tf \ No newline at end of file diff --git a/rolesanywhere/main.tf b/rolesanywhere/main.tf new file mode 100644 index 0000000..bca1dd9 --- /dev/null +++ b/rolesanywhere/main.tf @@ -0,0 +1,16 @@ +locals { + base_tags = { + "boc:tf_module_version" = local._module_version + "boc:created_by" = "terraform" + } +} + +locals { + account_id = var.account_id != "" ? var.account_id : data.aws_caller_identity.current.account_id + region = data.aws_region.current.name + account_environment = data.aws_arn.current.partition == "aws-us-gov" ? "gov" : "ew" + account_alias = var.account_alias != "" && var.account_alias != null ? var.account_alias : "none" + + role_name = format("%v%v", lookup(local._prefixes, "role", ""), var.role_name) + role_description = var.role_description == "" ? format("%vRole for %v", local.saml_string, var.role_name) : var.role_description +} diff --git a/rolesanywhere/outputs.tf b/rolesanywhere/outputs.tf new file mode 100644 index 0000000..a8b899a --- /dev/null +++ b/rolesanywhere/outputs.tf @@ -0,0 +1,9 @@ +output "role_arn" { + description = "Created role ARN" + value = aws_iam_role.role.arn +} + +output "role_name" { + description = "Created role name" + value = aws_iam_role.role.name +} diff --git a/rolesanywhere/region.tf b/rolesanywhere/region.tf new file mode 100644 index 0000000..f617506 --- /dev/null +++ b/rolesanywhere/region.tf @@ -0,0 +1,3 @@ +locals { + region = var.region +} diff --git a/rolesanywhere/role.tf b/rolesanywhere/role.tf new file mode 100644 index 0000000..e95a518 --- /dev/null +++ b/rolesanywhere/role.tf @@ -0,0 +1,95 @@ +locals { + certificate_subject_ou = { "x509Subject/OU" = lookup(var.certificate_conditions, "x509Subject/OU", format("IAM RolesAnywhere %v", data.aws_caller_identity.current.account_id)) } + certificate_conditions = merge( + local.certificate_subject_ou, + var.certificate_conditions, + { "x509Subject/CN" = local.role_name }, + ) +} + +resource "aws_rolesanywhere_profile" "role" { + name = local.role_name + enabled = true + duration_seconds = var.max_session_duration + role_arns = [aws_iam_role.role.arn] + + tags = merge( + local.base_tags, + var.tags, + { Name = local.role_name }, + ) +} + +resource "aws_iam_role" "role" { + name = local.role_name + description = local.role_description + force_detach_policies = local._defaults["force_detach_policies"] + max_session_duration = var.max_session_duration + assume_role_policy = data.aws_iam_policy_document.role_anywhere_assume.json + managed_policy_arns = var.managed_policy_arns + + + tags = merge( + local.base_tags, + var.tags, + lookup(var.component_tags, "role", {}), + { Name = local.role_name }, + ) +} + +resource "aws_iam_role_policy_attachment" "role" { + for_each = var.create ? toset(var.attached_policies) : toset([]) + role = var.create ? aws_iam_role.role[0].name : "" + policy_arn = each.value +} + +resource "aws_iam_role_policy" "role" { + for_each = { for p in var.inline_policies : p.name => p.policy } + role = aws_iam_role.role.arn + name = each.key + policy = each.value +} + + +data "aws_iam_policy_document" "role_anywhere_assume" { + statement { + sid = "RolesAnywhereTrust" + effect = "Allow" + actions = [ + "sts:AssumeRole", + "sts:TagSession", + "sts:SetSourceIdentity", + ] + principals { + type = "Service" + identifiers = ["rolesanywhere.amazonaws.com"] + } + condition { + test = "ForAnyValue:StringEquals" + variable = "aws:SourceArn" + values = concat(nonsensitive(data.aws_ssm_parameters_by_path.trust_east.values), nonsensitive(data.aws_ssm_parameters_by_path.trust_west.values)) + } + # include condition for certificate + dynamic "condition" { + for_each = local.certificate_conditions + iterator = c + content { + test = "StringEquals" + variable = format("aws:PrincipalTag/%v", c.key) + values = [c.value] + } + } + } +} + + +## "Condition": { +## "StringEquals": { +## "aws:PrincipalTag/x509Subject/CN": "onpremsrv01", +## "aws:PrincipalTag/x509Subject/OU": "SecOps" +## } +## } +# https://aws.amazon.com/blogs/security/extend-aws-iam-roles-to-workloads-outside-of-aws-with-iam-roles-anywhere/ +# https://docs.aws.amazon.com/rolesanywhere/latest/userguide/workload-identities.html +# https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_temp_control-access_monitor.html + diff --git a/rolesanywhere/variables.common.tf b/rolesanywhere/variables.common.tf new file mode 120000 index 0000000..7439ed8 --- /dev/null +++ b/rolesanywhere/variables.common.tf @@ -0,0 +1 @@ +../common/variables.common.tf \ No newline at end of file diff --git a/rolesanywhere/variables.rolesanywhere.tf b/rolesanywhere/variables.rolesanywhere.tf new file mode 100644 index 0000000..4bd756f --- /dev/null +++ b/rolesanywhere/variables.rolesanywhere.tf @@ -0,0 +1,27 @@ +variable "certificate_conditions" { + description = "Map of certificate conditions to be merged with x509Subject/CN={role_name}" + type = map(string) + default = {} +} + +variable "contact_group_email" { + description = "Email of contact group" + type = string +} + +variable "contact_users" { + description = "Username of contact(s)" + type = list(string) + default = [] +} + +variable "validity_days" { + description = "Number of days for which the certificate is valid (default: 365, max: 365)" + type = number + default = 365 + + validation { + condition = var.validity_days > 0 && var.validity_days <= 365 + error_message = "validity_days must be between 1 and 365" + } +} diff --git a/rolesanywhere/variables.tf b/rolesanywhere/variables.tf new file mode 100644 index 0000000..9befa9e --- /dev/null +++ b/rolesanywhere/variables.tf @@ -0,0 +1,46 @@ +variable "role_name" { + description = "Role/application name without prefix" + type = string +} + +variable "role_description" { + description = "Role/application description" + type = string + default = "" +} + +variable "assume_policy_document" { + description = "JSON policy document for role to assume (i.e., the SAML assume document)" + type = string + default = null +} + +variable "attached_policies" { + description = "List of IAM Policy ARNs to attach to this role" + type = list(string) + default = [] +} + +variable "managed_policy_arns" { + description = "List of IAM Managed Policy ARNs to attach to this role" + type = list(string) + default = [] +} + +variable "inline_policies" { + description = "List of IAM Policy Document objects to include in this role. Format is {name=name,policy=policy-json}" + type = list(object({ name = string, policy = string })) + default = [] +} + +variable "max_session_duration" { + description = "Override the maximum session duration from the default (3600)" + type = number + default = 3600 +} + +variable "component_tags" { + description = "Additional tags for Components (role, policy)" + type = map(map(string)) + default = { "role" = {}, "policy" = {} } +} diff --git a/rolesanywhere/version.tf b/rolesanywhere/version.tf new file mode 120000 index 0000000..b83c5b7 --- /dev/null +++ b/rolesanywhere/version.tf @@ -0,0 +1 @@ +../common/version.tf \ No newline at end of file diff --git a/rolesanywhere/versions.tf b/rolesanywhere/versions.tf new file mode 100644 index 0000000..ab01aa2 --- /dev/null +++ b/rolesanywhere/versions.tf @@ -0,0 +1,33 @@ +terraform { + required_version = ">= 1.0.0" + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 5.0" + } + # ldap = { + # source = "trevex/ldap" + # version = ">= 0.5.4" + # } + # external = { + # source = "hashicorp/external" + # version = ">= 1.0" + # } + # null = { + # source = "hashicorp/null" + # version = ">= 1.0" + # } + # random = { + # source = "hashicorp/random" + # version = ">= 1.0" + # } + # template = { + # source = "hashicorp/template" + # version = ">= 1.0" + # } + # infoblox = { + # source = "infobloxopen/infoblox" + # version = ">= 2.1.0" + # } + } +} diff --git a/variables.common.tf b/variables.common.tf deleted file mode 100644 index 2f1a864..0000000 --- a/variables.common.tf +++ /dev/null @@ -1,26 +0,0 @@ -#--- -# account info -#--- -variable "account_id" { - description = "AWS Account ID (default will pull from current user)" - type = string - default = "" -} - -variable "account_alias" { - description = "AWS Account Alias" - type = string - default = "" -} - -variable "override_prefixes" { - description = "Override built-in prefixes by component (role, policy). This should be used primarily for common infrastructure things" - type = map(string) - default = {} -} - -variable "tags" { - description = "AWS Tags to apply to appropriate resources (S3, KMS). Do not include safeguard tags here, use the data_safeguard field for such things." - type = map(string) - default = {} -} diff --git a/variables.common.tf b/variables.common.tf new file mode 120000 index 0000000..eb0bc7b --- /dev/null +++ b/variables.common.tf @@ -0,0 +1 @@ +common/variables.common.tf \ No newline at end of file diff --git a/version.tf b/version.tf deleted file mode 100644 index 19b3325..0000000 --- a/version.tf +++ /dev/null @@ -1,4 +0,0 @@ -locals { - # _module_version = "1.4.0" - _module_version = "2.3.3" -} diff --git a/version.tf b/version.tf new file mode 120000 index 0000000..fb90c0e --- /dev/null +++ b/version.tf @@ -0,0 +1 @@ +common/version.tf \ No newline at end of file diff --git a/versions.tf b/versions.tf deleted file mode 100644 index 53db328..0000000 --- a/versions.tf +++ /dev/null @@ -1,18 +0,0 @@ -# tf 0.13+ -terraform { - required_providers { - aws = { - source = "hashicorp/aws" - version = ">= 3.66.0" - } - ldap = { - source = "trevex/ldap" - version = ">= 0.5.4" - } - external = { - source = "hashicorp/external" - version = ">= 2.2.0" - } - } - required_version = ">= 0.13" -} diff --git a/versions.tf b/versions.tf new file mode 120000 index 0000000..7b425c1 --- /dev/null +++ b/versions.tf @@ -0,0 +1 @@ +common/versions.tf \ No newline at end of file