From 7e86085f7100619423adecdd8edabc053a7a21c9 Mon Sep 17 00:00:00 2001 From: Kashif Saadat Date: Wed, 15 May 2024 18:33:01 +0100 Subject: [PATCH 1/2] feat: allow customisation of statefile name for repos deploying multiple environments --- modules/role/README.md | 15 ++++++--------- modules/role/locals.tf | 1 + modules/role/policies.tf | 4 ++-- modules/role/variables.tf | 8 +++++++- 4 files changed, 16 insertions(+), 12 deletions(-) diff --git a/modules/role/README.md b/modules/role/README.md index ef0d6ff..01f5690 100644 --- a/modules/role/README.md +++ b/modules/role/README.md @@ -108,17 +108,11 @@ No modules. | Name | Type | |------|------| -| [aws_iam_policy.tfstate_apply](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource | -| [aws_iam_policy.tfstate_plan](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource | -| [aws_iam_policy.tfstate_remote](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource | | [aws_iam_role.ro](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | | [aws_iam_role.rw](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | | [aws_iam_role.sr](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | | [aws_iam_role_policy_attachment.ro](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | | [aws_iam_role_policy_attachment.rw](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | -| [aws_iam_role_policy_attachment.tfstate_apply](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | -| [aws_iam_role_policy_attachment.tfstate_plan](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | -| [aws_iam_role_policy_attachment.tfstate_remote](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | | [aws_caller_identity.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/caller_identity) | data source | | [aws_iam_openid_connect_provider.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_openid_connect_provider) | data source | | [aws_iam_policy_document.base](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | @@ -136,12 +130,14 @@ No modules. | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| | [additional\_audiences](#input\_additional\_audiences) | Additional audiences to be allowed in the OIDC federation mapping | `list(string)` | `[]` | no | -| [common\_provider](#input\_common\_provider) | The name of a common OIDC provider to be used as the trust for the role | `string` | `""` | no | +| [common\_provider](#input\_common\_provider) | The name of a common OIDC provider to be used as the trust for the role | `string` | `"github"` | no | | [custom\_provider](#input\_custom\_provider) | An object representing an `aws_iam_openid_connect_provider` resource |
object({
url = string
audiences = list(string)
subject_reader_mapping = string
subject_branch_mapping = string
subject_tag_mapping = string
})
| `null` | no | | [description](#input\_description) | Description of the role being created | `string` | n/a | yes | +| [enable\_branch\_suffix\_on\_statefile](#input\_enable\_branch\_suffix\_on\_statefile) | Add the protected branch as a suffix on the statefile name, e.g. -.tfstate | `bool` | `false` | no | | [force\_detach\_policies](#input\_force\_detach\_policies) | Flag to force detachment of policies attached to the IAM role. | `bool` | `null` | no | | [name](#input\_name) | Name of the role to create | `string` | n/a | yes | -| [permission\_boundary](#input\_permission\_boundary) | The name of the policy that is used to set the permissions boundary for the IAM role | `string` | n/a | yes | +| [permission\_boundary](#input\_permission\_boundary) | The name of the policy that is used to set the permissions boundary for the IAM role | `string` | `null` | no | +| [permission\_boundary\_arn](#input\_permission\_boundary\_arn) | The full ARN of the permission boundary to attach to the role | `string` | `null` | no | | [protected\_branch](#input\_protected\_branch) | The name of the protected branch under which the read-write role can be assumed | `string` | `"main"` | no | | [protected\_tag](#input\_protected\_tag) | The name of the protected tag under which the read-write role can be assume | `string` | `"*"` | no | | [read\_only\_inline\_policies](#input\_read\_only\_inline\_policies) | Inline policies map with policy name as key and json as value. | `map(string)` | `{}` | no | @@ -150,7 +146,8 @@ No modules. | [read\_write\_inline\_policies](#input\_read\_write\_inline\_policies) | Inline policies map with policy name as key and json as value. | `map(string)` | `{}` | no | | [read\_write\_max\_session\_duration](#input\_read\_write\_max\_session\_duration) | The maximum session duration (in seconds) that you want to set for the specified role | `number` | `null` | no | | [read\_write\_policy\_arns](#input\_read\_write\_policy\_arns) | List of IAM policy ARNs to attach to the read-write role | `list(string)` | `[]` | no | -| [repository](#input\_repository) | List of repositories to be allowed i nthe OIDC federation mapping | `string` | n/a | yes | +| [region](#input\_region) | The region in which the role will be used (defaulting to the provider region) | `string` | `null` | no | +| [repository](#input\_repository) | List of repositories to be allowed in the OIDC federation mapping | `string` | n/a | yes | | [role\_path](#input\_role\_path) | Path under which to create IAM role. | `string` | `null` | no | | [shared\_repositories](#input\_shared\_repositories) | List of repositories to provide read access to the remote state | `list(string)` | `[]` | no | | [tags](#input\_tags) | Tags to apply resoures created by this module | `map(string)` | n/a | yes | diff --git a/modules/role/locals.tf b/modules/role/locals.tf index 1dc9446..3f1d927 100644 --- a/modules/role/locals.tf +++ b/modules/role/locals.tf @@ -49,4 +49,5 @@ locals { template_keys_regex = "{(repo|type|ref)}" # The prefix for the terraform state key in the S3 bucket tf_state_prefix = format("%s-%s", local.account_id, local.region) + tf_state_suffix = var.enable_branch_suffix_on_statefile ? format("-%s", var.protected_branch) : "" } diff --git a/modules/role/policies.tf b/modules/role/policies.tf index b2cd7fb..6851052 100644 --- a/modules/role/policies.tf +++ b/modules/role/policies.tf @@ -20,7 +20,7 @@ data "aws_iam_policy_document" "base" { ] resources = [ - format("arn:aws:s3:::%s-tfstate/%s.tfstate", local.tf_state_prefix, local.repo_name) + format("arn:aws:s3:::%s-tfstate/%s%s.tfstate", local.tf_state_prefix, local.repo_name, local.tf_state_suffix), ] } } @@ -56,7 +56,7 @@ data "aws_iam_policy_document" "tfstate_apply" { ] resources = [ - format("arn:aws:s3:::%s-tfstate/%s.tfstate", local.tf_state_prefix, local.repo_name) + format("arn:aws:s3:::%s-tfstate/%s%s.tfstate", local.tf_state_prefix, local.repo_name, local.tf_state_suffix) ] } } diff --git a/modules/role/variables.tf b/modules/role/variables.tf index 75a1944..d8849bc 100644 --- a/modules/role/variables.tf +++ b/modules/role/variables.tf @@ -39,9 +39,15 @@ variable "additional_audiences" { description = "Additional audiences to be allowed in the OIDC federation mapping" } +variable "enable_branch_suffix_on_statefile" { + type = bool + default = false + description = "Add the protected branch as a suffix on the statefile name, e.g. -.tfstate" +} + variable "repository" { type = string - description = "List of repositories to be allowed i nthe OIDC federation mapping" + description = "List of repositories to be allowed in the OIDC federation mapping" } variable "shared_repositories" { From 5ff99b251f13e36e993211d049f82a3f3ec92d10 Mon Sep 17 00:00:00 2001 From: Kashif Saadat Date: Thu, 16 May 2024 17:34:37 +0100 Subject: [PATCH 2/2] feat: add support to restrict iam role use based on github deployment environment --- modules/role/README.md | 7 +++---- modules/role/checks.tf | 22 ++++++++++++++++++++++ modules/role/locals.tf | 11 +++++------ modules/role/main.tf | 23 +++++++++++++++-------- modules/role/variables.tf | 31 ++++++++++++++++++------------- 5 files changed, 63 insertions(+), 31 deletions(-) diff --git a/modules/role/README.md b/modules/role/README.md index 01f5690..78f4b85 100644 --- a/modules/role/README.md +++ b/modules/role/README.md @@ -131,15 +131,13 @@ No modules. |------|-------------|------|---------|:--------:| | [additional\_audiences](#input\_additional\_audiences) | Additional audiences to be allowed in the OIDC federation mapping | `list(string)` | `[]` | no | | [common\_provider](#input\_common\_provider) | The name of a common OIDC provider to be used as the trust for the role | `string` | `"github"` | no | -| [custom\_provider](#input\_custom\_provider) | An object representing an `aws_iam_openid_connect_provider` resource |
object({
url = string
audiences = list(string)
subject_reader_mapping = string
subject_branch_mapping = string
subject_tag_mapping = string
})
| `null` | no | +| [custom\_provider](#input\_custom\_provider) | An object representing an `aws_iam_openid_connect_provider` resource |
object({
url = string
audiences = list(string)
subject_reader_mapping = string
subject_branch_mapping = string
subject_env_mapping = string
subject_tag_mapping = string
})
| `null` | no | | [description](#input\_description) | Description of the role being created | `string` | n/a | yes | -| [enable\_branch\_suffix\_on\_statefile](#input\_enable\_branch\_suffix\_on\_statefile) | Add the protected branch as a suffix on the statefile name, e.g. -.tfstate | `bool` | `false` | no | | [force\_detach\_policies](#input\_force\_detach\_policies) | Flag to force detachment of policies attached to the IAM role. | `bool` | `null` | no | | [name](#input\_name) | Name of the role to create | `string` | n/a | yes | | [permission\_boundary](#input\_permission\_boundary) | The name of the policy that is used to set the permissions boundary for the IAM role | `string` | `null` | no | | [permission\_boundary\_arn](#input\_permission\_boundary\_arn) | The full ARN of the permission boundary to attach to the role | `string` | `null` | no | -| [protected\_branch](#input\_protected\_branch) | The name of the protected branch under which the read-write role can be assumed | `string` | `"main"` | no | -| [protected\_tag](#input\_protected\_tag) | The name of the protected tag under which the read-write role can be assume | `string` | `"*"` | no | +| [protected\_by](#input\_protected\_by) | The branch, environment and/or tag to protect the role against |
object({
branch = optional(string)
environment = optional(string)
tag = optional(string)
})
|
{
"branch": "main",
"environment": "production",
"tag": "*"
}
| no | | [read\_only\_inline\_policies](#input\_read\_only\_inline\_policies) | Inline policies map with policy name as key and json as value. | `map(string)` | `{}` | no | | [read\_only\_max\_session\_duration](#input\_read\_only\_max\_session\_duration) | The maximum session duration (in seconds) that you want to set for the specified role | `number` | `null` | no | | [read\_only\_policy\_arns](#input\_read\_only\_policy\_arns) | List of IAM policy ARNs to attach to the read-only role | `list(string)` | `[]` | no | @@ -151,6 +149,7 @@ No modules. | [role\_path](#input\_role\_path) | Path under which to create IAM role. | `string` | `null` | no | | [shared\_repositories](#input\_shared\_repositories) | List of repositories to provide read access to the remote state | `list(string)` | `[]` | no | | [tags](#input\_tags) | Tags to apply resoures created by this module | `map(string)` | n/a | yes | +| [tf\_state\_suffix](#input\_tf\_state\_suffix) | A suffix for the terraform statefile, e.g. -.tfstate | `string` | `""` | no | ## Outputs diff --git a/modules/role/checks.tf b/modules/role/checks.tf index 72c895d..2206b9c 100644 --- a/modules/role/checks.tf +++ b/modules/role/checks.tf @@ -10,6 +10,28 @@ check "provider_config" { } } +check "protected_by_config" { + assert { + condition = !(var.protected_by.branch == null && var.protected_by.environment == null && var.protected_by.tag == null) + error_message = "At least one of 'protected_by.branch', 'protected_by.environment', or 'protected_by.tag' must be specified" + } + + assert { + condition = !(var.protected_by.branch == "") + error_message = "'protected_by.branch' must not be an empty string" + } + + assert { + condition = !(var.protected_by.environment == "") + error_message = "'protected_by.environment' must not be an empty string" + } + + assert { + condition = !(var.protected_by.tag == "") + error_message = "'protected_by.tag' must not be an empty string" + } +} + check "policy_config" { assert { condition = !(length(var.read_only_policy_arns) == 0 && length(var.read_only_inline_policies) == 0) diff --git a/modules/role/locals.tf b/modules/role/locals.tf index 3f1d927..d63ed1d 100644 --- a/modules/role/locals.tf +++ b/modules/role/locals.tf @@ -13,6 +13,7 @@ locals { subject_reader_mapping = "repo:{repo}:*" subject_branch_mapping = "repo:{repo}:ref:refs/heads/{ref}" + subject_env_mapping = "repo:{repo}:environment:{env}" subject_tag_mapping = "repo:{repo}:ref:refs/tags/{ref}" } @@ -37,17 +38,15 @@ locals { } locals { - selected_provider = coalesce( - var.custom_provider, - lookup(local.common_providers, var.common_provider, null), - ) + common_provider = lookup(local.common_providers, var.common_provider, null) + selected_provider = var.custom_provider != null ? var.custom_provider : local.common_provider # Extract just the repository name part of the full path repo_name = element(split("/", var.repository), length(split("/", var.repository)) - 1) # Keys to search for in the subject mapping template - template_keys_regex = "{(repo|type|ref)}" + template_keys_regex = "{(repo|type|ref|env)}" # The prefix for the terraform state key in the S3 bucket tf_state_prefix = format("%s-%s", local.account_id, local.region) - tf_state_suffix = var.enable_branch_suffix_on_statefile ? format("-%s", var.protected_branch) : "" + tf_state_suffix = var.tf_state_suffix != "" ? format("-%s", var.tf_state_suffix) : "" } diff --git a/modules/role/main.tf b/modules/role/main.tf index 0e3a6a4..cc3e1dc 100644 --- a/modules/role/main.tf +++ b/modules/role/main.tf @@ -101,23 +101,30 @@ data "aws_iam_policy_document" "rw" { condition { test = "StringLike" variable = format("%s:sub", trimprefix(local.selected_provider.url, "https://")) - values = [ - format(replace(local.selected_provider.subject_branch_mapping, format("/%s/", local.template_keys_regex), "%s"), [ + values = compact([ + var.protected_by.branch != null ? format(replace(local.selected_provider.subject_branch_mapping, format("/%s/", local.template_keys_regex), "%s"), [ for v in flatten(regexall(local.template_keys_regex, local.selected_provider.subject_branch_mapping)) : { repo = var.repository type = "branch" - ref = var.protected_branch + ref = var.protected_by.branch + }[v] + ]...) : "", + + var.protected_by.environment != null ? format(replace(local.selected_provider.subject_env_mapping, format("/%s/", local.template_keys_regex), "%s"), [ + for v in flatten(regexall(local.template_keys_regex, local.selected_provider.subject_env_mapping)) : { + repo = var.repository + env = var.protected_by.environment }[v] - ]...), + ]...) : "", - format(replace(local.selected_provider.subject_tag_mapping, format("/%s/", local.template_keys_regex), "%s"), [ + var.protected_by.tag != null ? format(replace(local.selected_provider.subject_tag_mapping, format("/%s/", local.template_keys_regex), "%s"), [ for v in flatten(regexall(local.template_keys_regex, local.selected_provider.subject_tag_mapping)) : { repo = var.repository type = "tag" - ref = var.protected_tag + ref = var.protected_by.tag }[v] - ]...) - ] + ]...) : "" + ]) } } } diff --git a/modules/role/variables.tf b/modules/role/variables.tf index d8849bc..e718a8b 100644 --- a/modules/role/variables.tf +++ b/modules/role/variables.tf @@ -26,6 +26,7 @@ variable "custom_provider" { audiences = list(string) subject_reader_mapping = string subject_branch_mapping = string + subject_env_mapping = string subject_tag_mapping = string }) @@ -39,10 +40,10 @@ variable "additional_audiences" { description = "Additional audiences to be allowed in the OIDC federation mapping" } -variable "enable_branch_suffix_on_statefile" { - type = bool - default = false - description = "Add the protected branch as a suffix on the statefile name, e.g. -.tfstate" +variable "tf_state_suffix" { + type = string + default = "" + description = "A suffix for the terraform statefile, e.g. -.tfstate" } variable "repository" { @@ -56,16 +57,20 @@ variable "shared_repositories" { description = "List of repositories to provide read access to the remote state" } -variable "protected_branch" { - type = string - default = "main" - description = "The name of the protected branch under which the read-write role can be assumed" -} +variable "protected_by" { + type = object({ + branch = optional(string) + environment = optional(string) + tag = optional(string) + }) -variable "protected_tag" { - type = string - default = "*" - description = "The name of the protected tag under which the read-write role can be assume" + default = { + branch = "main" + environment = "production" + tag = "*" + } + + description = "The branch, environment and/or tag to protect the role against" } variable "role_path" {