Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pass credentials to local-exec OR extract credentials via properties #8242

Closed
deitch opened this issue Apr 8, 2019 · 16 comments
Closed

Pass credentials to local-exec OR extract credentials via properties #8242

deitch opened this issue Apr 8, 2019 · 16 comments
Labels
enhancement Requests to existing resources that expand the functionality or scope.

Comments

@deitch
Copy link

deitch commented Apr 8, 2019

Community Note

  • Please vote on this issue by adding a 👍 reaction to the original issue to help the community and maintainers prioritize this request
  • Please do not leave "+1" or "me too" comments, they generate extra noise for issue followers and do not help prioritize the request
  • If you are interested in working on this issue or have submitted a pull request, please leave a comment

Description

If you need to execute a local command, via local-exec provisioner - whether part of a standard aws resource or via null_resource - the AWS credentials are not passed to that command. Almost all implementations of AWS CLI/SDK accept the usage of AWS_PROFILE,AWS_ACCESS_KEY_ID,AWS_SECRET_ACCESS_KEY, so any could consume it.

Reasoning: I might have something I want to execute outside of terraform due to unsupported resources (e.g. directory upload #3020 ) or to keep some things out of terraform state, or to go through compliance.

In theory, those commands should have access to the AWS vars with which I launched terraform, but that usually isn't good enough for several reasons:

  • I might have assumed a role inside my provider "aws" clause
  • I might have picked a different profile inside my provider "aws" clause
  • I might have multiple provider "aws" clauses, each with a different alias, and would want to run one command with one instance, another command with the other

Further, provider "aws" already has all of the support for fixed credentials, profiles, and even assuming roles to get temporary credentials via sts. A local command should, in principle, run just like any other resource.

I see two ways of doing this:

  1. When executing local-exec provisioners, pass the relevant credentials - AWS_SECRET_ACCESS_KEY / AWS_ACCESS_KEY_ID - into the environment. I am not sure this can be done directly. If it can, I believe it only would work if the local-exec provisioner is attached to an aws_* resource, and not a null_resource.
  2. Provide a way to extract the credentials used for the connection. The best candidate appears to be extending aws_caller_identity, although there might be a better way.

The first case would be straightforward:

resource "aws_launch_configuration" "my_config" {
    # lots of other stuff
    provisioner "local-exec" {
      command = "some-command"
      environment {
        # automatically includes AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY
      }
}

The second case is just explicit:

resource "null_resource" "my_resource" {
  provisioner "local-exec" {
    command = "some-command"
    environment {
      AWS_ACCESS_KEY_ID = "${data.aws_caller_identity.current.access_key}"
      AWS_ACCESS_KEY_ID = "${data.aws_caller_identity.current.secret_key}"
    }
  }
}

For this to work, we would need to ensure that access_key and secret_key are the actual ones used, e.g. temporary credentials via assume role or similar, or ones retrieved via using a profile, and not ones passed in via the provider "aws" config, unless those are the ones actually being used.

New or Affected Resource(s)

  • provider itself
  • possible aws_caller_identity

Potential Terraform Configuration

See the examples above.

Thanks!

@deitch deitch added the enhancement Requests to existing resources that expand the functionality or scope. label Apr 8, 2019
@twang817
Copy link

twang817 commented May 3, 2019

I took a crack at implementing option 2. See #8517

I've also attached some linux64 binaries for those who might want to help test it out:

https://github.com/twang817/terraform-provider-aws/releases/tag/v2.8.0-8517

@deitch
Copy link
Author

deitch commented May 4, 2019

Oh, I do like it!

@twang817
Copy link

twang817 commented May 6, 2019

Note, that the PR does result in the access/secret keys being stored in the state file.

The docs seem to make warnings that state data may be sensitive:

https://www.terraform.io/docs/state/sensitive-data.html

I do not believe it is necessary to take any more measures to protect secrets from the state file -- as state files are already deemed potentially sensitive and that there already exists a mechanism to protect the state file via backend encryption (at least, when remote state is being used).

It may be worth it, however, to move this into a separate data source. This allows users to continue using CallerIdentity without worrying about sensitive data in the state file. Furthermore, suddenly introducing this change into CallerIdentity may not be a pleasant surprise for all the existing plans that currently use CallerIdentity and have no need for credentials. Users that want access to the credentials must decide for themselves whether the risks of keeping keys in the state file are acceptable.

Thoughts?

@egarbi
Copy link
Contributor

egarbi commented May 7, 2019

Related to sensitive data stored on state files, if one use assumed roles to perform these tasks within the provider, the credentials stored will be temporary ones. (key, secret and token)
In my case, I need this to do a one time task (VPC authorization and association) and those expired credentials will remain there forever without compromising security at all.

@twang817
Copy link

twang817 commented May 7, 2019

I am also in an environment where credentials are temporary and the security risk for a stored credential exists only for a few hours up until the credential expires.

Nonetheless, there are many people who use Terraform with permanent credentials (and perhaps even local state files) and the storage of credentials in state files may be of concern. This should ultimately be up to every engineer/organization to weigh the pros and cons and decide for themselves. It is probably worthwhile, however, to be very explicit about this risk in the docs.

As far as using aws_caller_identity, I have split out the credentials into a new data source: aws_provider_credentials. I have done some simple testing and it seems to work. I'll push it up as soon as I can get the make test command to complete (Docker for Mac has horrible, horrible disk IO). I have only tested it using environment variables (AWS_ACCESS_KEY_ID, etc). I still need to test it out using the various methods of supplying credentials. It might also be worthwhile to check if it works with provider aliases -- I have never used that feature, so it may take a couple moments for me to figure it out.

@bertonha
Copy link

bertonha commented Feb 16, 2020

Until we don't have a option on aws-cli to assume role or a way to inject the AWS_ACCESS_KEY_ID,AWS_SECRET_ACCESS_KEY on environment on local-exec I wrote the following code. I hope it helps someone.

resource "null_resource" "call-db-migrate" {
  provisioner "local-exec" {
    interpreter = ["/bin/bash", "-c"]
    command = <<EOF
set -e
CREDENTIALS=(`aws sts assume-role \
  --role-arn ${var.aws_role} \
  --role-session-name "db-migration-cli" \
  --query "[Credentials.AccessKeyId,Credentials.SecretAccessKey,Credentials.SessionToken]" \
  --output text`)

unset AWS_PROFILE
export AWS_DEFAULT_REGION=us-east-1
export AWS_ACCESS_KEY_ID="$${CREDENTIALS[0]}"
export AWS_SECRET_ACCESS_KEY="$${CREDENTIALS[1]}"
export AWS_SESSION_TOKEN="$${CREDENTIALS[2]}"

aws sts get-caller-identity
EOF
  }
}

@Gowiem
Copy link

Gowiem commented Sep 22, 2020

For those of you using aws-vault for your IAM user and then using @bertonha's local-exec script: Be sure to unset AWS_SECURITY_TOKEN as well. aws-vault sets that env var and without unsetting it, AWS will continue to complain about "The security token included in the request is invalid", which it is. But if you're like me, you'll continue to think that is referring to the AWS_SESSION_TOKEN var and bang your head against it before understanding that it is complaining about the aws-vault artifact.

@colinhoglund
Copy link

colinhoglund commented Oct 17, 2020

I use assume_role quite a bit so option 2 sounds interesting. It would be nice if aws_caller_identity also produced a friendlier way to consume a role_arn to pass to external scripts.

In the meantime, the following workaround that constructs an assumable role_arn from aws_caller_identity.arn seems to do the trick:

data aws_caller_identity this {}

locals {
  # arn:aws:iam::000000000000:user/username
  # arn:aws:sts::000000000000:assumed-role/role/0000000000000000000
  caller        = regex("arn:aws:[^:]*::(?P<account>[^:]*):(?P<type>[^/]*)/(?P<name>[^/]*)", data.aws_caller_identity.this.arn)
  role_arn      = "arn:aws:iam::${local.caller.account}:role/${local.caller.name}"
  # this is a contrived example, your external script is responsible for setting up the session properly
  cmd           = ["echo", "{}"]
  cmd_with_role = ["echo", "{\"role_arn\": \"${local.role_arn}\"}"]
}

data external this {
  program = local.caller.type == "assumed-role" ? local.cmd_with_role : local.cmd
}

output this {
  value = data.external.this.result
}

@lmmattr
Copy link

lmmattr commented Oct 28, 2020

What about a different option? If there was a generic resource that just passed the parameters to the API/CLI using the credentials in the provider, it could provide the ability to do pretty much anything not currently implemented as its own dedicated resource.

Here's what i'm working with at the moment as an example:

resource "null_resource" "refresh_instances" {
  triggers = {
    image = aws_launch_template.ecs.latest_version
  }

  provisioner "local-exec" {
    interpreter = ["/bin/sh", "-c"]
    environment = {
      AWS_DEFAULT_REGION = "eu-west-1"
    }
    command = <<EOF
set -e
$(aws sts assume-role --role-arn ${local.role} --role-session-name terraform_run_instance_refresh --query 'Credentials.[`export#AWS_ACCESS_KEY_ID=`,AccessKeyId,`#AWS_SECRET_ACCESS_KEY=`,SecretAccessKey,`#AWS_SESSION_TOKEN=`,SessionToken]' --output text | sed $'s/\t//g' | sed 's/#/ /g')

instances=$(aws ecs list-container-instances \
  --cluster ${aws_ecs_cluster.main.id} \
  --query 'containerInstanceArns' \
  --output text)

aws ecs update-container-instances-state \
  --cluster ${aws_ecs_cluster.main.id} \
  --container-instances $instances \
  --status DRAINING
EOF
  }
}

It could just look something like this:

resource "aws_cli" "instance_list" {
  command = "ecs"
  subcommand = "list-container-instances"

  parameters {
    cluster = aws_ecs_cluster.main.id
    container-instances = aws_ecs_cluster.main.id
    status = "DRAINING"
    query  = "containerInstanceArns"
    output = "text"
  }

}

resource "aws_cli" "instance_refresh" {
  command = "ecs"
  subcommand = "update-container-instances-state"

  parameters {
    cluster = aws_ecs_cluster.main.id
    container-instances = aws_cli.instance_list.output
    status = "DRAINING"
  }
}

@Omarimcblack
Copy link
Contributor

Omarimcblack commented Nov 27, 2020

I have a working solution, would like to hear if anyone has any further recommendations or simple a 👍

I went for something similar to the second case suggested by @deitch (providing a way to extract the credentials from a given provider) however i done so using a new data source d/aws_credentials, leaving d/aws_caller_identity untouched.

resource "null_resource" "this" {
  provisioner "local-exec" {
    command = "some-aws-command"
    environment {
      AWS_SESSION_TOKEN     = data.aws_credentials.default.token
      AWS_SECRET_ACCESS_KEY = data.aws_credentials.default.secret_key
      AWS_ACCESS_KEY_ID     = data.aws_credentials.default.access_key
      AWS_SECURITY_TOKEN    = data.aws_credentials.default.token
    }
  }
}

small print: exposes credentials into the state so i wouldn't suggest using alongside long life credentials nor an insecure backend. In light of that it may not ever be able to be merged but if the cons don't scare you compile it locally for your use case.

@tmccombs
Copy link
Contributor

tmccombs commented Feb 7, 2022

It would be great to have a solution that doesn't store the credentials in the state.

Also, regarding @Omarimcblack's PR, since the PR thread is closed:

you can create a separate provider that reads the same information and contains this data source and then publish it on the registry.

I think you would have to replace the entire AWS provider, because a separate provider wouldn't be able to access the credentials of the hashicorp aws provider.

@dimisjim
Copy link
Contributor

looks related to #26043 & #386

@julialawrence
Copy link

julialawrence commented Mar 22, 2023

I use assume_role quite a bit so option 2 sounds interesting. It would be nice if aws_caller_identity also produced a friendlier way to consume a role_arn to pass to external scripts.

In the meantime, the following workaround that constructs an assumable role_arn from aws_caller_identity.arn seems to do the trick:

data aws_caller_identity this {}

locals {
  # arn:aws:iam::000000000000:user/username
  # arn:aws:sts::000000000000:assumed-role/role/0000000000000000000
  caller        = regex("arn:aws:[^:]*::(?P<account>[^:]*):(?P<type>[^/]*)/(?P<name>[^/]*)", data.aws_caller_identity.this.arn)
  role_arn      = "arn:aws:iam::${local.caller.account}:role/${local.caller.name}"
  # this is a contrived example, your external script is responsible for setting up the session properly
  cmd           = ["echo", "{}"]
  cmd_with_role = ["echo", "{\"role_arn\": \"${local.role_arn}\"}"]
}

data external this {
  program = local.caller.type == "assumed-role" ? local.cmd_with_role : local.cmd
}

output this {
  value = data.external.this.result
}

For anyone looking for a "terraformy" way of getting the role arn, 'aws_session_context' data source will convert session arn into a role arn and save wrestling with regular expressions and string formatting.

@millermatt
Copy link

For anyone looking for a "terraformy" way of getting the role arn, 'aws_session_context' data source will convert session arn into a role arn and save wrestling with regular expressions and string formatting.

Should be aws_iam_session_context rather than aws_session_context

@johnsonaj
Copy link
Contributor

We appreciate the interest, continued discussion, and suggested workarounds here. After extensive discussion, the Terraform AWS Provider maintainers have opted to close this request due to its potential security implications, which could inadvertently affect all users of the provider.

As it stands, the local-exec provisioner operates after Terraform has provisioned resources, thereby lacking access to the AWS Provider's session credentials for those resources. The proposed solution would entail adding these credentials to the state, thereby exposing them to any entity with access to that file.

Several workarounds within this issue have shown success in utilizing credentials with local-exec. While these approaches may not be ideal, the maintainers believe they are safer alternatives compared to potentially exposing plaintext credentials to malicious use.

Thank you for your understanding.

Copy link

I'm going to lock this issue because it has been closed for 30 days ⏳. This helps our maintainers find and focus on the active issues.
If you have found a problem that seems similar to this, please open a new issue and complete the issue template so we can capture all the details necessary to investigate further.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Apr 21, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
enhancement Requests to existing resources that expand the functionality or scope.
Projects
None yet