hashicorp / terraform-provider-aws

The AWS Provider enables Terraform to manage AWS resources.
https://registry.terraform.io/providers/hashicorp/aws
Mozilla Public License 2.0
9.71k stars 9.07k forks source link

Allow adding or modifying Trusted Role policy (aws_iam_role.assume_role_policy) to an existing IAM Role #7922

Open ghost opened 5 years ago

ghost commented 5 years ago

This issue was originally opened by @rajeshwar-nu as hashicorp/terraform#20665. It was migrated here as a result of the provider split. The original body of the issue is below.


aws_iam_role allows creating a role with a trusted policy specified in assume_role_policy. However, if one tries to configure an IAM role for EMRFS as mentioned at https://docs.aws.amazon.com/emr/latest/ManagementGuide/emr-emrfs-iam-roles.html#emrfs-seccfg-json, they will soon realize that terraform will fail due to a Cycle error. This is because the trust policy of IAM roles of both EC2 and EMFRS require the other's ARN for correct configuration. A simple example is

# IAM Role for EC2 Instance Profile
data "template_file" "iam_emr_ec2_role_trust_policy_json" {
  template = "${file("${path.module}/policies/iam_emr_ec2_role_trust_policy.json")}"

  vars {
    emrfs_role = "${aws_iam_role.iam_emrfs_role.arn}"
  }
}

resource "aws_iam_role" "iam_emr_ec2_role" {
  name               = "iam_emr_ec2_role_${var.env}"
  assume_role_policy = "${data.template_file.iam_emr_ec2_role_trust_policy_json.rendered}"
}

and the corresponding IAM role config for EMRFS is

# IAM role for EMRFS
data "template_file" "iam_emrfs_role_trust_policy_json" {
  template = "${file("${path.module}/policies/iam_emrfs_role_trust_policy.json")}"

  vars {
    emr_ec2_role = "${aws_iam_role.iam_emr_ec2_role.arn}"
  }
}

resource "aws_iam_role" "iam_emrfs_role" {
  name               = "iam_emrfs_role_${var.env}"
  assume_role_policy = "${data.template_file.iam_emrfs_role_trust_policy_json.rendered}"
}

This will result in a Cycle error. The template file in each uses the variable to fill the ARN or other role in their policy json document

An easy way to solve this problem is to allow attaching or modifying trust policy on an existing role.

marczis commented 4 years ago

Any updates on this?

johngtam commented 3 years ago

+1 about this -- this would be terrific, I've been running into the boostrapping problem described above, and I can't create the bi-directional trust relationships I need. Having this would be great!

Edit: I think I realize the problem here (and it may not even be within Terraform itself). In the example in OP's post, if I were to attempt to re-create it with the AWS console, it's impossible without some backtracking?

  1. If I were to create the iam_emr_ec2_role role on the console, it would require a trusted entity. However, that trusted entity doesn't even exist. So I'd have to temporarily add in some other trusted entity temporarily.

  2. Now I'd be able to create the iam_emrfs_role role, because the trusted entity will have been created in step 1 (the iam_emr_ec2_role role).

  3. Then I'd have to go back to the iam_emr_ec2_role role, delete the temporary trusted entity, and add iam_emrfs_role as a trusted entity.

From the way AWS functions, if this were to be translated over to Terraform, Terraform would have to be able to interpret this scenario and perform similar steps like above to get over this bootstrapping problem. Does anybody know any example of where Terraform overcomes this bootstrapping problem?

cooltoast commented 3 years ago

Posting my workaround here that I showed @johngtam

You just need to:

Code, which can remain static

locals {
  role1_name = "role1"
  role2_name = "role2"

  role1_arn = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:role/${local.role1_name}"
  role2_arn = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:role/${local.role2_name}"
}

variable "bootstrap" {
  default = false
}

data "aws_caller_identity" "current" {}

# Role 1
data "aws_iam_policy_document" "role1_trust" {
  statement {
    actions = ["sts:AssumeRole"]
    principals {
      type        = "AWS"
      identifiers = [
        local.role2_arn
      ]
    }
  }
}

data "aws_iam_policy_document" "role1_bootstrap" {
  statement {
    actions = ["sts:AssumeRole"]
    principals {
      type        = "Service"
      identifiers = ["ec2.amazonaws.com"]
    }
  }
}

resource "aws_iam_role" "role1" {
  name = local.role1_name

  assume_role_policy = var.bootstrap ? data.aws_iam_policy_document.role1_bootstrap.json : data.aws_iam_policy_document.role1_trust.json
}

# Role 2
data "aws_iam_policy_document" "role2_trust" {
  statement {
    actions = ["sts:AssumeRole"]
    principals {
      type        = "AWS"
      identifiers = [
        local.role1_arn
      ]
    }
  }
}

resource "aws_iam_role" "role2" {
  name = local.role2_name

  assume_role_policy = data.aws_iam_policy_document.role2_trust.json
}

First Apply

$ TF_VAR_bootstrap=true terraform apply

Terraform will perform the following actions:

  # aws_iam_role.role1 will be created
  + resource "aws_iam_role" "role1" {
      + arn                   = (known after apply)
      + assume_role_policy    = jsonencode(
            {
              + Statement = [
                  + {
                      + Action    = "sts:AssumeRole"
                      + Effect    = "Allow"
                      + Principal = {
                          + Service = "ec2.amazonaws.com"
                        }
                      + Sid       = ""
                    },
                ]
              + Version   = "2012-10-17"
            }
        )
      + create_date           = (known after apply)
      + force_detach_policies = false
      + id                    = (known after apply)
      + max_session_duration  = 3600
      + name                  = "role1"
      + path                  = "/"
      + unique_id             = (known after apply)
    }

  # aws_iam_role.role2 will be created
  + resource "aws_iam_role" "role2" {
      + arn                   = (known after apply)
      + assume_role_policy    = jsonencode(
            {
              + Statement = [
                  + {
                      + Action    = "sts:AssumeRole"
                      + Effect    = "Allow"
                      + Principal = {
                          + AWS = "arn:aws:iam::<ACCOUNT_ID>:role/role1"
                        }
                      + Sid       = ""
                    },
                ]
              + Version   = "2012-10-17"
            }
        )
      + create_date           = (known after apply)
      + force_detach_policies = false
      + id                    = (known after apply)
      + max_session_duration  = 3600
      + name                  = "role2"
      + path                  = "/"
      + unique_id             = (known after apply)
    }

Plan: 2 to add, 0 to change, 0 to destroy.

Second Apply

$ terraform apply

Terraform will perform the following actions:

  # aws_iam_role.role1 will be updated in-place
  ~ resource "aws_iam_role" "role1" {
      ~ assume_role_policy    = jsonencode(
          ~ {
              ~ Statement = [
                  ~ {
                      ~ Principal = {
                          + AWS     = "arn:aws:iam::<ACCOUNT_ID>:role/role2"
                          - Service = "ec2.amazonaws.com" -> null
                        }
                        # (3 unchanged elements hidden)
                    },
                ]
                # (1 unchanged element hidden)
            }
        )
        id                    = "role1"
        name                  = "role1"
        tags                  = {}
        # (6 unchanged attributes hidden)
    }

Plan: 0 to add, 1 to change, 0 to destroy.
tomasbackman commented 3 years ago

Nice workaround.

I still hope this improvement gets added to the provider soonish though, it is always nicer with "proper" support than workarounds =) And This circular issue is pretty annoying. @aeschright

It also feels like a resource to modify a trust policy would give more flexibility. For example if a new user or other role or something gets created later that need to be able to assume an existing role. Then I think it would be much nicer to "attach" a new trust policy to the old role, so that it can be done in the same module as the other new stuff. Instead of having to rerun the terraform module that created the module earlier, and through the new data/input update the trust policy that way. I prefer more self sufficient modules.

jatcwang commented 2 years ago

When trying to manage Snowflake infrastructure (including S3 access) we have the same issue where we run into a cyclic dependency because we do not know the Snowflake IAM user it'll use to assume our S3 read role until we have created the Storage Integration in Snowflake

In this case there's no workaround because we cannot manually construct the Snowflake IAM user's ARN.

kallangerard commented 2 years ago

Any update on this?

runsnowflake commented 2 years ago

Any update on this?

AriLFrankel commented 1 year ago

As of this update that requires roles to explicitly trust themselves, I am running into a related issue terraforming new IAM roles. It's not possible to terraform the role to trust itself before the role exists. https://aws.amazon.com/blogs/security/announcing-an-update-to-iam-role-trust-policy-behavior/

ashuec90 commented 1 year ago

I am also running into the same issue, as it is working fine where role is already present but not where we are creating the new role.

Swaps76 commented 1 year ago

I'm also running into this issue.. to be able to amend an existing trust policy would be great.. If you hard code it it doesn't help as you get malformed policy.

ashuec90 commented 1 year ago

As a work around i have adjust the code as below, as this will be applied for the first time when the role gets created also if the role already exist then also it will be applied in a single terraform run.

##This data source will be used to add any other roles to be assumed. Add another statement to add the role.
data "aws_iam_policy_document" "worker_node_assume_role_policy_itself" {
  depends_on = [
    aws_iam_role.worker_node
  ]
  statement {

    principals {
      type        = "Service"
      identifiers = ["ec2.amazonaws.com"]
    }
    actions = ["sts:AssumeRole"]
  }
  statement {

    principals {
      type        = "AWS"
      identifiers = ["arn:aws:iam::<ACCOUNT_ID>:role/rolename"]
    }
    actions = ["sts:AssumeRole"]
  }
}

data "aws_iam_policy_document" "worker_node_assume_role_policy" {
  statement {
    actions = ["sts:AssumeRole"]

    principals {
      type        = "Service"
      identifiers = ["ec2.amazonaws.com"]
    }

  }
}

resource "aws_iam_role" "worker_node" {
  name = "roleName"
  # Ignore changes to assume_role_policy.
  # This will allow to create the role for the first time using default policy document, so that we can control the trust policy using below null resource.
  # This will preveent this role to get reverted in the next subsequent terraform run.
  lifecycle {
    ignore_changes = [
      assume_role_policy,
    ]
  }  
  assume_role_policy = data.aws_iam_policy_document.worker_node_assume_role_policy.json  
}

## This null resource will update the trust policy for eks worker role.
resource "null_resource" "add_worker_node_assume_role_policy" {
  depends_on = [
    data.aws_iam_policy_document.worker_node_assume_role_policy_itself
  ]
  provisioner "local-exec" {
    command = "sleep 5;aws iam update-assume-role-policy --role-name rolename --policy-document '${self.triggers.updated_policy_json}' "
  }
  triggers = {
    updated_policy_json = (replace(replace(data.aws_iam_policy_document.worker_node_assume_role_policy_itself.json,"\n", "")," ", ""))
    "after" = aws_iam_role.worker_node.assume_role_policy
  }
}
vk7416 commented 1 year ago

@ashuec90 How safe is it to run this null_resource in a production pipeline, multiple time to create multiple resources, In the example you have created only one resource right, in the same way i want to use null_resource for multiple resources multiple times in the same module.

mickhan commented 1 year ago

I found this page while looking for a solution to the circular reference issue with AssumeRole. And then I found a way to solve the problem. Condition can be used to match all IAM roles instead of specifying them in the principal. Please refer to 'Wildcarding principals' section in https://aws.amazon.com/blogs/security/how-to-use-trust-policies-with-iam-roles/. This method solved my problem, hope it can be helpful to you as well.

eugeneyarovoi commented 6 months ago

Would be very helpful for cases when an external module you don't want to modify creates a role, but you want to give that role additional trust relationships.

kmacmcfarlane commented 5 months ago

To solve this, I found I could synthesize the IAM role arn before the AWS role actually exists. This way two roles can have policies that reference one another. In my case, I have a module that constructs the role, so I am passing out the role arn as an output.

locals {
  role_name = join("-", [var.name_prefix, var.domain, var.name])
  role_arn  = "arn:aws:iam::${var.aws_account_id}:role/${local.role_name}"
}

resource "aws_iam_role" "role" {
  name               = local.role_name
  ...
}

output "arn" {
  value = local.role_arn
}
jportertfg commented 4 months ago

the aws CLI has an iam update-assume-role-policy endpoint is there a reason this is still blocked from being implemented?