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:
provider "aws"
clauseprovider "aws"
clauseprovider "aws"
clauses, each with a different alias, and would want to run one command with one instance, another command with the otherFurther, 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:
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
.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.
aws_caller_identity
See the examples above.
Thanks!
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
Oh, I _do_ like it!
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?
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.
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.
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
}
}
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.
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
}
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"
}
}
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._
Most helpful comment
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.