Skip to content

Commit c7b9050

Browse files
dudymasNuru
andauthored
fix: tfstate-backend cold-start regression (#1093)
Co-authored-by: Nuru <[email protected]>
1 parent 6a4eff3 commit c7b9050

File tree

3 files changed

+81
-13
lines changed

3 files changed

+81
-13
lines changed

modules/tfstate-backend/README.md

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,14 @@ wish to restrict who can read the production Terraform state backend S3 bucket.
1010
all Terraform users require read access to the most sensitive accounts, such as `root` and `audit`, in order to read
1111
security configuration information, so careful planning is required when architecting backend splits.
1212

13-
> [!TIP]
14-
>
15-
> Part of cold start, so it has to initially be run with `SuperAdmin`, multiple times: to create the S3 bucket and then
16-
> to move the state into it. Follow the guide
17-
> **[here](https://docs.cloudposse.com/reference-architecture/how-to-guides/implementation/enterprise/implement-aws-cold-start/#provision-tfstate-backend-component)**
18-
> to get started.
13+
## Prerequisites
14+
15+
- This component assumes you are using the `aws-teams` and `aws-team-roles` components.
16+
- Before the `account` and `account-map` components are deployed for the first time, you'll want to run this component with `access_roles_enabled` set to `false` to
17+
prevent errors due to missing IAM Role ARNs.
18+
This will enable only enough access to the Terraform state for you to finish provisioning accounts and roles.
19+
After those components have been deployed, you will want to
20+
run this component again with `access_roles_enabled` set to `true` to provide the complete access as configured in the stacks.
1921

2022
### Access Control
2123

@@ -141,15 +143,18 @@ terraform:
141143
| Name | Type |
142144
|------|------|
143145
| [aws_iam_role.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource |
146+
| [aws_arn.cold_start_access](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/arn) | data source |
147+
| [aws_iam_policy_document.cold_start_assume_role](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source |
144148
| [aws_iam_policy_document.tfstate](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source |
149+
| [aws_partition.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/partition) | data source |
145150
| [awsutils_caller_identity.current](https://registry.terraform.io/providers/cloudposse/awsutils/latest/docs/data-sources/caller_identity) | data source |
146151
147152
## Inputs
148153
149154
| Name | Description | Type | Default | Required |
150155
|------|-------------|------|---------|:--------:|
151156
| <a name="input_access_roles"></a> [access\_roles](#input\_access\_roles) | Map of access roles to create (key is role name, use "default" for same as component). See iam-assume-role-policy module for details. | <pre>map(object({<br> write_enabled = bool<br> allowed_roles = map(list(string))<br> denied_roles = map(list(string))<br> allowed_principal_arns = list(string)<br> denied_principal_arns = list(string)<br> allowed_permission_sets = map(list(string))<br> denied_permission_sets = map(list(string))<br> }))</pre> | `{}` | no |
152-
| <a name="input_access_roles_enabled"></a> [access\_roles\_enabled](#input\_access\_roles\_enabled) | Enable creation of access roles. Set false for cold start (before account-map has been created). | `bool` | `true` | no |
157+
| <a name="input_access_roles_enabled"></a> [access\_roles\_enabled](#input\_access\_roles\_enabled) | Enable access roles to be assumed. Set `false` for cold start (before account-map has been created),<br>because the role to ARN mapping has not yet been created.<br>Note that the current caller and any `allowed_principal_arns` will always be allowed to assume the role. | `bool` | `true` | no |
153158
| <a name="input_additional_tag_map"></a> [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.<br>This is for some rare cases where resources want additional configuration of tags<br>and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no |
154159
| <a name="input_attributes"></a> [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,<br>in the order they appear in the list. New attributes are appended to the<br>end of the list. The elements of the list are joined by the `delimiter`<br>and treated as a single ID element. | `list(string)` | `[]` | no |
155160
| <a name="input_context"></a> [context](#input\_context) | Single object for setting entire context at once.<br>See description of individual variables for details.<br>Leave string and numeric variables as `null` to use default value.<br>Individual variable settings (non-null) override settings in context object,<br>except for attributes, tags, and additional\_tag\_map, which are merged. | `any` | <pre>{<br> "additional_tag_map": {},<br> "attributes": [],<br> "delimiter": null,<br> "descriptor_formats": {},<br> "enabled": true,<br> "environment": null,<br> "id_length_limit": null,<br> "label_key_case": null,<br> "label_order": [],<br> "label_value_case": null,<br> "labels_as_tags": [<br> "unset"<br> ],<br> "name": null,<br> "namespace": null,<br> "regex_replace_chars": null,<br> "stage": null,<br> "tags": {},<br> "tenant": null<br>}</pre> | no |

modules/tfstate-backend/iam.tf

Lines changed: 64 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,28 @@
11
locals {
2-
access_roles = local.enabled && var.access_roles_enabled ? {
2+
access_roles = local.enabled ? {
33
for k, v in var.access_roles : (
44
length(split(module.this.delimiter, k)) > 1 ? k : module.label[k].id
55
) => v
66
} : {}
7-
access_roles_enabled = module.this.enabled && length(keys(local.access_roles)) > 0
87

8+
access_roles_enabled = local.enabled && var.access_roles_enabled
9+
cold_start_access_enabled = local.enabled && !var.access_roles_enabled
10+
11+
# Technically, `eks_role_arn` is incorrect, becuse it strips any path from the ARN,
12+
# but since we do not expect there to be a path in the role ARN (as opposed to perhaps an attached IAM policy),
13+
# it is OK. The advantage of using `eks_role_arn` is that it converts and Assumed Role ARN from STS, like
14+
# arn:aws:sts::123456789012:assumed-role/acme-core-gbl-root-admin/aws-go-sdk-1722029959251053170
15+
# to the IAM Role ARN, like
16+
# arn:aws:iam::123456789012:role/acme-core-gbl-root-admin
917
caller_arn = coalesce(data.awsutils_caller_identity.current.eks_role_arn, data.awsutils_caller_identity.current.arn)
1018
}
1119

1220
data "awsutils_caller_identity" "current" {}
21+
data "aws_partition" "current" {}
1322

1423

1524
module "label" {
16-
for_each = var.access_roles
25+
for_each = local.enabled ? var.access_roles : {}
1726
source = "cloudposse/label/null"
1827
version = "0.25.0" # requires Terraform >= 0.13.0
1928

@@ -28,7 +37,7 @@ module "label" {
2837
}
2938

3039
module "assume_role" {
31-
for_each = local.access_roles
40+
for_each = local.access_roles_enabled ? local.access_roles : {}
3241
source = "../account-map/modules/team-assume-role-policy"
3342

3443
allowed_roles = each.value.allowed_roles
@@ -74,7 +83,7 @@ resource "aws_iam_role" "default" {
7483

7584
name = each.key
7685
description = "${each.value.write_enabled ? "Access" : "Read-only access"} role for ${module.this.id}"
77-
assume_role_policy = module.assume_role[each.key].policy_document
86+
assume_role_policy = var.access_roles_enabled ? module.assume_role[each.key].policy_document : data.aws_iam_policy_document.cold_start_assume_role[each.key].json
7887
tags = merge(module.this.tags, { Name = each.key })
7988

8089
inline_policy {
@@ -83,3 +92,53 @@ resource "aws_iam_role" "default" {
8392
}
8493
managed_policy_arns = []
8594
}
95+
96+
locals {
97+
all_cold_start_access_principals = local.cold_start_access_enabled ? toset(concat([local.caller_arn],
98+
flatten([for k, v in local.access_roles : v.allowed_principal_arns]))) : toset([])
99+
cold_start_access_principal_arns = local.cold_start_access_enabled ? { for k, v in local.access_roles : k => distinct(concat(
100+
[local.caller_arn], v.allowed_principal_arns
101+
)) } : {}
102+
cold_start_access_principals = local.cold_start_access_enabled ? {
103+
for k, v in local.cold_start_access_principal_arns : k => formatlist("arn:%v:iam::%v:root", data.aws_partition.current.partition, distinct([
104+
for arn in v : data.aws_arn.cold_start_access[arn].account
105+
]))
106+
} : {}
107+
108+
}
109+
110+
data "aws_arn" "cold_start_access" {
111+
for_each = local.all_cold_start_access_principals
112+
arn = each.value
113+
}
114+
115+
# This is a basic policy that allows the caller and explicitly allowed principals to assume the role
116+
# during the period roles are being set up (cold start).
117+
data "aws_iam_policy_document" "cold_start_assume_role" {
118+
for_each = local.cold_start_access_enabled ? local.access_roles : {}
119+
120+
statement {
121+
sid = "ColdStartRoleAssumeRole"
122+
123+
effect = "Allow"
124+
# These actions need to be kept in sync with the actions in the assume_role module
125+
actions = [
126+
"sts:AssumeRole",
127+
"sts:SetSourceIdentity",
128+
"sts:TagSession",
129+
]
130+
131+
condition {
132+
test = "ArnLike"
133+
variable = "aws:PrincipalArn"
134+
values = local.cold_start_access_principal_arns[each.key]
135+
}
136+
137+
principals {
138+
type = "AWS"
139+
# Principals is a required field, so we allow any principal in any of the accounts, restricted by the assumed Role ARN in the condition clauses.
140+
# This allows us to allow non-existent (yet to be created) roles, which would not be allowed if directly specified in `principals`.
141+
identifiers = local.cold_start_access_principals[each.key]
142+
}
143+
}
144+
}

modules/tfstate-backend/variables.tf

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ variable "access_roles" {
4343

4444
variable "access_roles_enabled" {
4545
type = bool
46-
description = "Enable creation of access roles. Set false for cold start (before account-map has been created)."
46+
description = <<-EOT
47+
Enable access roles to be assumed. Set `false` for cold start (before account-map has been created),
48+
because the role to ARN mapping has not yet been created.
49+
Note that the current caller and any `allowed_principal_arns` will always be allowed to assume the role.
50+
EOT
4751
default = true
4852
}

0 commit comments

Comments
 (0)