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

Regression in 1.1: script_path in remote-exec no longer uploads file to ~ paths #30243

Closed
lorengordon opened this issue Dec 22, 2021 · 12 comments · Fixed by #30297
Closed

Regression in 1.1: script_path in remote-exec no longer uploads file to ~ paths #30243

lorengordon opened this issue Dec 22, 2021 · 12 comments · Fixed by #30297
Assignees
Labels
bug confirmed a Terraform Core team member has reproduced this issue provisioner/remote-exec v1.1 Issues (primarily bugs) reported against v1.1 releases

Comments

@lorengordon
Copy link
Contributor

Terraform Version

❯ terraform -version
Terraform v1.1.2
on linux_amd64
+ provider registry.terraform.io/hashicorp/aws v3.70.0
+ provider registry.terraform.io/hashicorp/http v2.1.0
+ provider registry.terraform.io/hashicorp/tls v3.1.0

Terraform Configuration Files

data "http" "ip" {
  url = "http://ipv4.icanhazip.com"
}

data "aws_ami" "focal" {
  most_recent = true

  filter {
    name   = "virtualization-type"
    values = ["hvm"]
  }

  filter {
    name   = "name"
    values = ["ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server*"]
  }

  owners = ["099720109477"]
}

resource "aws_key_pair" "test" {
  key_name_prefix = "test-terraform-provisioner-"
  public_key      = tls_private_key.test.public_key_openssh
}

resource "tls_private_key" "test" {
  algorithm = "RSA"
}

resource "aws_security_group" "test" {
  name_prefix = "test-terraform-provisioner-"

  ingress {
    from_port   = "22"
    to_port     = "22"
    protocol    = "tcp"
    cidr_blocks = ["${chomp(data.http.ip.body)}/32"]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

resource "aws_instance" "test" {
  ami                    = data.aws_ami.focal.id
  key_name               = aws_key_pair.test.id
  vpc_security_group_ids = [aws_security_group.test.id]
  instance_type          = "t3.micro"

  connection {
    type        = "ssh"
    host        = self.public_ip
    user        = "ubuntu"
    port        = "22"
    private_key = tls_private_key.test.private_key_pem
  }

  provisioner "remote-exec" {
    inline = [
      "echo foo"
    ]

    connection {
      script_path = "~/foo.sh"
    }
  }
}

Expected Behavior

Expected terraform to create the instance, connect to it, and run the inline script. This worked fine in Terraform 1.0, but it fails in Terraform 1.1.

Actual Behavior

Provisioner fails to create the inline script in the specified location. chmod executes and resolves the ~, but then cannot find the file, and the provisioner exits with that error.

aws_instance.test (remote-exec): Connected!
╷
│ Error: remote-exec provisioner error
│
│   with aws_instance.test,
│   on main.tf line 62, in resource "aws_instance" "test":
│   62:   provisioner "remote-exec" {
│
│ Failed to upload script: Error chmodding script file to 0777 in remote machine error executing "chmod 0777 ~/foo.sh": Process exited with status 1: chmod: cannot access '/home/ubuntu/foo.sh': No such file or directory

Steps to Reproduce

  1. terraform init
  2. terraform apply
@lorengordon lorengordon added bug new new issue not yet triaged labels Dec 22, 2021
@lorengordon
Copy link
Contributor Author

lorengordon commented Dec 22, 2021

Relevant debug output using terraform 1.1.2:

2021-12-22T08:08:36.576-0800 [DEBUG] Connecting to <public-ip>:22 for SSH
2021-12-22T08:08:36.680-0800 [DEBUG] Connection established. Handshaking for user ubuntu
aws_instance.test (remote-exec): Connected!
2021-12-22T08:08:37.547-0800 [DEBUG] starting ssh KeepAlives
2021-12-22T08:08:37.547-0800 [DEBUG] opening new ssh session
2021-12-22T08:08:38.878-0800 [DEBUG] Starting remote scp process:  'scp' -vt \~
2021-12-22T08:08:38.982-0800 [DEBUG] Started SCP session, beginning transfers...
2021-12-22T08:08:38.982-0800 [DEBUG] Beginning file upload...
2021-12-22T08:08:39.080-0800 [DEBUG] SCP session complete, closing stdin pipe.
2021-12-22T08:08:39.080-0800 [DEBUG] Waiting for SSH session to complete.
2021-12-22T08:08:39.179-0800 [ERROR] scp stderr: "Sink: C0644 19 foo.sh\n"
2021-12-22T08:08:39.179-0800 [DEBUG] opening new ssh session
2021-12-22T08:08:39.391-0800 [DEBUG] starting remote command: chmod 0777 ~/foo.sh
2021-12-22T08:08:39.495-0800 [DEBUG] remote command exited with '1': chmod 0777 ~/foo.sh
2021-12-22T08:08:39.496-0800 [WARN]  Errors while provisioning aws_instance.test with "remote-exec", so aborting
2021-12-22T08:08:39.519-0800 [ERROR] vertex "aws_instance.test" error: remote-exec provisioner error

@lorengordon
Copy link
Contributor Author

And here's the same section of debug output using terraform 1.0.11:

2021-12-22T08:14:28.443-0800 [DEBUG] Connecting to <public-ip>:22 for SSH
2021-12-22T08:14:28.552-0800 [DEBUG] Connection established. Handshaking for user ubuntu
aws_instance.test (remote-exec): Connected!
2021-12-22T08:14:29.378-0800 [DEBUG] starting ssh KeepAlives
2021-12-22T08:14:29.378-0800 [DEBUG] opening new ssh session
2021-12-22T08:14:31.197-0800 [DEBUG] Starting remote scp process:  scp -vt ~
2021-12-22T08:14:31.302-0800 [DEBUG] Started SCP session, beginning transfers...
2021-12-22T08:14:31.302-0800 [DEBUG] Beginning file upload...
2021-12-22T08:14:31.408-0800 [DEBUG] SCP session complete, closing stdin pipe.
2021-12-22T08:14:31.408-0800 [DEBUG] Waiting for SSH session to complete.
2021-12-22T08:14:31.510-0800 [ERROR] scp stderr: "Sink: C0644 19 foo.sh\n"
2021-12-22T08:14:31.510-0800 [DEBUG] opening new ssh session
2021-12-22T08:14:31.730-0800 [DEBUG] starting remote command: chmod 0777 ~/foo.sh
2021-12-22T08:14:31.838-0800 [DEBUG] remote command exited with '0': chmod 0777 ~/foo.sh
2021-12-22T08:14:31.838-0800 [DEBUG] opening new ssh session
2021-12-22T08:14:32.040-0800 [DEBUG] starting remote command: ~/foo.sh
aws_instance.test (remote-exec): foo
2021-12-22T08:14:32.145-0800 [DEBUG] remote command exited with '0': ~/foo.sh
2021-12-22T08:14:32.145-0800 [DEBUG] opening new ssh session
2021-12-22T08:14:32.250-0800 [DEBUG] Starting remote scp process:  scp -vt ~
2021-12-22T08:14:32.351-0800 [DEBUG] Started SCP session, beginning transfers...
2021-12-22T08:14:32.352-0800 [DEBUG] Copying input data into temporary file so we can read the length
2021-12-22T08:14:32.360-0800 [DEBUG] Beginning file upload...
2021-12-22T08:14:32.457-0800 [DEBUG] SCP session complete, closing stdin pipe.
2021-12-22T08:14:32.457-0800 [DEBUG] Waiting for SSH session to complete.
2021-12-22T08:14:32.606-0800 [ERROR] scp stderr: "Sink: C0644 0 foo.sh\n"
aws_instance.test: Creation complete after 25s [id=i-099dc92e451cf4b47]
2021-12-22T08:14:32.620-0800 [DEBUG] provider: plugin process exited: path=.terraform/providers/registry.terraform.io/hashicorp/aws/3.70.0/linux_amd64/terraform-provider-aws_v3.70.0_x5 pid=4157
2021-12-22T08:14:32.620-0800 [DEBUG] provider.stdio: received EOF, stopping recv loop: err="rpc error: code = Unavailable desc = transport is closing"
2021-12-22T08:14:32.620-0800 [DEBUG] provider: plugin exited

@lorengordon
Copy link
Contributor Author

I also checked the trace logs using terraform 1.1.2 to see if there were any more details about the file upload and why/how it failed, but there was no trace output around the file upload.

@lorengordon
Copy link
Contributor Author

For comparison to a success case with terraform 1.1.2, I modified the connection to specify the full path and grabbed the same section of debug output:

    connection {
      script_path = "/home/ubuntu/foo.sh"
    }
2021-12-22T08:23:43.624-0800 [DEBUG] Connecting to <public-ip>:22 for SSH
2021-12-22T08:23:43.728-0800 [DEBUG] Connection established. Handshaking for user ubuntu
aws_instance.test (remote-exec): Connected!
2021-12-22T08:23:44.533-0800 [DEBUG] starting ssh KeepAlives
2021-12-22T08:23:44.533-0800 [DEBUG] opening new ssh session
2021-12-22T08:23:45.920-0800 [DEBUG] Starting remote scp process:  scp -vt /home/ubuntu
2021-12-22T08:23:46.025-0800 [DEBUG] Started SCP session, beginning transfers...
2021-12-22T08:23:46.025-0800 [DEBUG] Beginning file upload...
2021-12-22T08:23:46.124-0800 [DEBUG] SCP session complete, closing stdin pipe.
2021-12-22T08:23:46.125-0800 [DEBUG] Waiting for SSH session to complete.
2021-12-22T08:23:46.235-0800 [ERROR] scp stderr: "Sink: C0644 19 foo.sh\n"
2021-12-22T08:23:46.235-0800 [DEBUG] opening new ssh session
2021-12-22T08:23:46.439-0800 [DEBUG] starting remote command: chmod 0777 /home/ubuntu/foo.sh
2021-12-22T08:23:46.551-0800 [DEBUG] remote command exited with '0': chmod 0777 /home/ubuntu/foo.sh
2021-12-22T08:23:46.551-0800 [DEBUG] opening new ssh session
2021-12-22T08:23:46.754-0800 [DEBUG] starting remote command: /home/ubuntu/foo.sh
aws_instance.test (remote-exec): foo
2021-12-22T08:23:46.856-0800 [DEBUG] remote command exited with '0': /home/ubuntu/foo.sh
2021-12-22T08:23:46.856-0800 [DEBUG] opening new ssh session
2021-12-22T08:23:46.965-0800 [DEBUG] Starting remote scp process:  scp -vt /home/ubuntu
2021-12-22T08:23:47.065-0800 [DEBUG] Started SCP session, beginning transfers...
2021-12-22T08:23:47.066-0800 [DEBUG] Copying input data into temporary file so we can read the length
2021-12-22T08:23:47.075-0800 [DEBUG] Beginning file upload...
2021-12-22T08:23:47.180-0800 [DEBUG] SCP session complete, closing stdin pipe.
2021-12-22T08:23:47.180-0800 [DEBUG] Waiting for SSH session to complete.
2021-12-22T08:23:47.285-0800 [ERROR] scp stderr: "Sink: C0644 0 foo.sh\n"
aws_instance.test: Creation complete after 29s [id=i-07e1923a09c70a8d2]
2021-12-22T08:23:47.304-0800 [DEBUG] provider.stdio: received EOF, stopping recv loop: err="rpc error: code = Unavailable desc = transport is closing"
2021-12-22T08:23:47.312-0800 [DEBUG] provider: plugin process exited: path=.terraform/providers/registry.terraform.io/hashicorp/aws/3.70.0/linux_amd64/terraform-provider-aws_v3.70.0_x5 pid=4355
2021-12-22T08:23:47.312-0800 [DEBUG] provider: plugin exited

@lorengordon
Copy link
Contributor Author

lorengordon commented Dec 22, 2021

Comparing the two terraform 1.1.2 runs, the key difference appears to be that the command is now being escaped incorrectly?

Success using full path:

2021-12-22T08:23:45.920-0800 [DEBUG] Starting remote scp process:  scp -vt /home/ubuntu

Failure using ~:

2021-12-22T08:08:38.878-0800 [DEBUG] Starting remote scp process:  'scp' -vt \~

vs success using ~ in terraform 1.0.11:

2021-12-22T08:14:31.197-0800 [DEBUG] Starting remote scp process:  scp -vt ~

@lorengordon lorengordon changed the title Regression in 1.1: script_path in remote-exec no longer expands ~ paths Regression in 1.1: script_path in remote-exec no longer uploads file to ~ paths Dec 22, 2021
@jbardin
Copy link
Member

jbardin commented Dec 22, 2021

Hi @lorengordon,

Thanks for reporting the issue! I am fairly certain it was a security patch which changed this behavior from #28626.

The provisioner's use of the remote scp command was not intended to allow for shell expansion (I don't think we ever documented that as working), however as you've shown here the remote scp call was expanding ~, so we are faced with a change in behavior between the versions.

@jbardin jbardin added confirmed a Terraform Core team member has reproduced this issue provisioner/remote-exec v1.1 Issues (primarily bugs) reported against v1.1 releases and removed new new issue not yet triaged labels Dec 22, 2021
@lorengordon
Copy link
Contributor Author

Fwiw, our use case around ~ was building across a few different platforms that each use a different user. So each build has a different path to the home directory. Using ~ avoids needing to map a user directory for each platform into the logic.

dghubble added a commit to poseidon/typhoon that referenced this issue Dec 28, 2021
* Terraform v1.1 changed the behavior of provisioners and
`remote-exec` in a way that breaks support for expansions
in commands (including file provisioner, where `destination`
is part of an `scp` command)
* Terraform will likely revert the change eventually, but I
suspect it will take a while
* Instead, we can stop relying on Terraform's expansion
behavior. `/home/core` is a suitable choice for `$HOME` on
both Flatcar Linux and Fedora CoreOS (harldink `/var/home/core`)

Rel: hashicorp/terraform#30243
dghubble added a commit to poseidon/typhoon that referenced this issue Dec 28, 2021
* Terraform v1.1 changed the behavior of provisioners and
`remote-exec` in a way that breaks support for expansions
in commands (including file provisioner, where `destination`
is part of an `scp` command)
* Terraform will likely revert the change eventually, but I
suspect it will take a while
* Instead, we can stop relying on Terraform's expansion
behavior. `/home/core` is a suitable choice for `$HOME` on
both Flatcar Linux and Fedora CoreOS (harldink `/var/home/core`)

Rel: hashicorp/terraform#30243
dghubble added a commit to poseidon/typhoon that referenced this issue Dec 28, 2021
* Terraform v1.1 changed the behavior of provisioners and
`remote-exec` in a way that breaks support for expansions
in commands (including file provisioner, where `destination`
is part of an `scp` command)
* Terraform will likely revert the change eventually, but I
suspect it will take a while
* Instead, we can stop relying on Terraform's expansion
behavior. `/home/core` is a suitable choice for `$HOME` on
both Flatcar Linux and Fedora CoreOS (harldink `/var/home/core`)

Rel: hashicorp/terraform#30243
dghubble-robot pushed a commit to poseidon/terraform-digitalocean-kubernetes that referenced this issue Dec 28, 2021
* Terraform v1.1 changed the behavior of provisioners and
`remote-exec` in a way that breaks support for expansions
in commands (including file provisioner, where `destination`
is part of an `scp` command)
* Terraform will likely revert the change eventually, but I
suspect it will take a while
* Instead, we can stop relying on Terraform's expansion
behavior. `/home/core` is a suitable choice for `$HOME` on
both Flatcar Linux and Fedora CoreOS (harldink `/var/home/core`)

Rel: hashicorp/terraform#30243
dghubble-robot pushed a commit to poseidon/terraform-aws-kubernetes that referenced this issue Dec 28, 2021
* Terraform v1.1 changed the behavior of provisioners and
`remote-exec` in a way that breaks support for expansions
in commands (including file provisioner, where `destination`
is part of an `scp` command)
* Terraform will likely revert the change eventually, but I
suspect it will take a while
* Instead, we can stop relying on Terraform's expansion
behavior. `/home/core` is a suitable choice for `$HOME` on
both Flatcar Linux and Fedora CoreOS (harldink `/var/home/core`)

Rel: hashicorp/terraform#30243
dghubble-robot pushed a commit to poseidon/terraform-google-kubernetes that referenced this issue Dec 28, 2021
* Terraform v1.1 changed the behavior of provisioners and
`remote-exec` in a way that breaks support for expansions
in commands (including file provisioner, where `destination`
is part of an `scp` command)
* Terraform will likely revert the change eventually, but I
suspect it will take a while
* Instead, we can stop relying on Terraform's expansion
behavior. `/home/core` is a suitable choice for `$HOME` on
both Flatcar Linux and Fedora CoreOS (harldink `/var/home/core`)

Rel: hashicorp/terraform#30243
dghubble-robot pushed a commit to poseidon/terraform-onprem-kubernetes that referenced this issue Dec 28, 2021
* Terraform v1.1 changed the behavior of provisioners and
`remote-exec` in a way that breaks support for expansions
in commands (including file provisioner, where `destination`
is part of an `scp` command)
* Terraform will likely revert the change eventually, but I
suspect it will take a while
* Instead, we can stop relying on Terraform's expansion
behavior. `/home/core` is a suitable choice for `$HOME` on
both Flatcar Linux and Fedora CoreOS (harldink `/var/home/core`)

Rel: hashicorp/terraform#30243
dghubble-robot pushed a commit to poseidon/terraform-azure-kubernetes that referenced this issue Dec 28, 2021
* Terraform v1.1 changed the behavior of provisioners and
`remote-exec` in a way that breaks support for expansions
in commands (including file provisioner, where `destination`
is part of an `scp` command)
* Terraform will likely revert the change eventually, but I
suspect it will take a while
* Instead, we can stop relying on Terraform's expansion
behavior. `/home/core` is a suitable choice for `$HOME` on
both Flatcar Linux and Fedora CoreOS (harldink `/var/home/core`)

Rel: hashicorp/terraform#30243
@apparentlymart
Copy link
Contributor

Hi all! Sorry for the quiet period here while we were out for holidays.

The change here was an intentional one with a security-related motivation, as discussed over in #28626. However, it does look like we missed including this in the "upgrade notes" section of the changelog and in the Terraform v1.1 Upgrade Guide, and we're sorry for not being more explicit about this change.

Security-related changes are one of the pragmatic exceptions we allow in our v1.0 Compatibility Promises, with this change being made in response to explicit security feedback sent through our security reporting channels. Because of that, our internal discussion here has focused on finding a compromise to still meet this issue's stated use-case, which we understand as a need to upload into the home directory of the target user without necessarily knowing the absolute path of that home directory.

Under the default configuration of sshd and scp, a relative destination path will be interpreted as relative to the target user's home directory without the need for any special metacharacters or environment variable substitutions, and Terraform's internal use of scp is consistent with the normal CLI usage in that regard, and so the new pattern we'll recommend for this situation is to remove the ~/ or $HOME/ or ${HOME}/ prefixes and just specify the relative path alone instead, like this:

  provisioner "remote-exec" {
    inline = [
      "echo foo"
    ]

    connection {
      script_path = "foo.sh"
    }
  }

This issue was originally discussing the effect of Terraform's implementation detail of executing scripts by first uploading them into the remote filesystem using scp, but this also applies to the more explicit use of scp in the file provisioner which also supports this relative path pattern as follows:

   provisioner "file" {
     content     = "example"
     destination = "example.txt"
   }

In both of these cases the relative path will be passed verbatim to the remote scp process, without any prior shell evaluation, and so it'll be the responsibility of that process to interpret the path. As noted above, the default behavior of scp in OpenSSH is to run in the home directory of the selected user, and that is why paths are relative to that directory.

In order to resolve this issue, we're intending to:

  • Add a section about this to the Terraform v1.1 Upgrade Guide, including guidance similar to the above. (We cannot retroactively update the changelog because it belongs to a now-immutable git tag in this repository.)
  • Add a new section to the connection block documentation which describes the detail that Terraform provisioners can use scp internally to transfer scripts to run, that script_path allows specifying where they will be written, and noting that relative paths are the way to write into the user's home directory.
  • Add similar new content to the file provisioner's documentation being explicit that it's valid to use a relative path in destination and that it will have the effect I described above.

We'll work on the steps I described above shortly, and close this issue once we've completed that work. Thanks again for reporting this!

@lorengordon
Copy link
Contributor Author

Under the default configuration of sshd and scp, a relative destination path will be interpreted as relative to the target user's home directory

TIL. That's an interesting detail I was not aware of. Thanks for the workaround!

@dprosper
Copy link

dprosper commented Jan 5, 2022

@apparentlymart I tested but looking to confirm, the connection block as well as the remote-exec provisioner are not impacted by this change, i.e. we can continue to use

resource "null_resource" "file_copy" {

  connection {
    type = "ssh"
    host = "x.x.x.x"
    user = "root"
    private_key = file("~/.ssh/id_rsa")
  }

  provisioner "file" {
    source = "main.tf"
    destination = ".test/blah.txt"
  }

    provisioner "remote-exec" {
    inline = [
      "chmod 400 ~/.test/blah.txt",
      "echo 'hi' > ~/.test/blah2.txt",
      "chmod 600 ~/.test/blah2.txt",
    ]
  }
}

@apparentlymart
Copy link
Contributor

apparentlymart commented Jan 5, 2022

Hi @dprosper,

Indeed, the two other examples you showed here have some different handling and thus different effective behavior:

  • The file function is evaluated locally inside the Terraform CLI process, and so the Terraform language runtime itself is the one interpreting the ~/ sequence in that context, rather than any shell, and the expansion in that case is your local user's home directory, as opposed to any user in a remote system. There is no change to that behavior in Terraform v1.1. (For completeness, I'll note that this local handling is specific to the ~ prefix and doesn't support any other typical shell expansion sequences, by design.)
  • Because the remote-exec provisioner's explicit purpose is to run commands in a remote shell, the inline argument and the files that script or scripts arguments refer to are uploaded by the provisioner verbatim to the remote system and then executed with the remote user's default shell. Therefore the interpretation of those strings is whatever the remote shell supports, and Terraform makes no attempt to escape them or disable any shell behaviors.

This change only applies to situations where Terraform is internally using Secure Copy Protocol, and specifically to the path arguments passed to the remote scp service when performing those operations. From a Terraform language perspective, that currently means only the paths in the two specific arguments I showed in my previous comment.

The security concern reported here is that allowing arbitrary command execution during file upload is an unexpected/unintended avenue for code execution that module authors may reasonably not consider when attempting to "harden" a Terraform module against unintended handling of externally-provided data.

@github-actions
Copy link

github-actions bot commented Feb 6, 2022

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 Feb 6, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
bug confirmed a Terraform Core team member has reproduced this issue provisioner/remote-exec v1.1 Issues (primarily bugs) reported against v1.1 releases
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants