flaupretre/terraform-ssh-tunnel

Does this work on Terraform Cloud?

Opened this issue · 3 comments

I'm trying to see if I can set up a tunnel via SSM so that I can keep my resources in a private subnet, but still use Terraform cloud. When trying to get the PG provider to connect, it fails with Error connecting to PostgreSQL server 127.0.0.1 (scheme: postgres): dial tcp 127.0.0.1:25252: connect: connection refused

I have added a data.external script to check the file descriptor on the local port (and nothing shows up).

I really love the idea in #36, but when trying that, I ran into an issue SessionManagerPlugin is not found.

This led to me implement my own Go binary for setting up the tunnel. Note, to get this working, I had to replace the go.mod
github.com/twinj/uuid value with github.com/twinj/uuid v0.0.0-20151029044442-89173bcdda19 (see: aws/session-manager-plugin#73)

package main

import (
	"context"
	"encoding/json"
	"flag"
	"fmt"
	"log"
	"os"
	"strconv"

	"github.com/aws/aws-sdk-go-v2/config"
	"github.com/aws/aws-sdk-go-v2/service/ssm"
	"github.com/aws/aws-sdk-go/aws"
	pluginSession "github.com/aws/session-manager-plugin/src/sessionmanagerplugin/session"
	_ "github.com/aws/session-manager-plugin/src/sessionmanagerplugin/session/portsession"
)

type serverConfig struct {
	target     string
	region     string
	remoteHost string
	remotePort int
	localPort  int
}

func main() {
	var cfg serverConfig

	flag.StringVar(&cfg.target, "target", "", "Instance for SSM to connect to")
	flag.StringVar(&cfg.region, "region", "", "Region in which the target instance is located")
	flag.StringVar(&cfg.remoteHost, "remoteHost", "", "Remote host to connect to")
	flag.IntVar(&cfg.remotePort, "remotePort", 0, "Port on remote host to connect to")
	flag.IntVar(&cfg.localPort, "localPort", 0, "Local port which will be forwarded to remote host")
	flag.Parse()

	if cfg.target == "" || cfg.remoteHost == "" || cfg.remotePort == 0 || cfg.localPort == 0 || cfg.region == "" {
		log.Fatal("target, region, remoteHost, remotePort, and localPort must be set")
	}

	ctx := context.Background()

	awsCfg, err := config.LoadDefaultConfig(ctx, config.WithRegion(cfg.region))
	if err != nil {
		log.Fatal(err)
	}
	svc := ssm.NewFromConfig(awsCfg)

	startSessionInput := ssm.StartSessionInput{
		Target:       &cfg.target,
		DocumentName: aws.String("AWS-StartPortForwardingSessionToRemoteHost"),
		Parameters: map[string][]string{
			"host": {
				cfg.remoteHost,
			},
			"portNumber": {
				strconv.Itoa(cfg.remotePort),
			},
			"localPortNumber": {
				strconv.Itoa(cfg.localPort),
			},
		},
	}

	startSessionOutput, err := svc.StartSession(ctx, &startSessionInput)
	if err != nil {
		log.Fatal(err)
	}

	startSessionOuputJson, err := json.Marshal(startSessionOutput)
	if err != nil {
		log.Fatal(err)
	}

	args := []string{
		"session-manager-plugin",
		string(startSessionOuputJson),
		cfg.region,
		"StartSession",
		"",
		fmt.Sprintf("{\"Target\": \"%s\"}", cfg.target),
		fmt.Sprintf("https://ssm.%s.amazonaws.com", cfg.region),
	}

	pluginSession.ValidateInputAndStartSession(args, os.Stdout)
}

I had to build it for amd64 linux to get it to run on Terraform cloud.

This succeeds in running, and SSM shows a connection. But checking the file descriptors on the local port and the pid for this binary come back with empty results after running.

My suspicious are, either Terraform cloud terminates the data.external.... after it returns an output, or the data.external.... processes are run in some sort of isolation which prevents them from exposing the local port for use.

I know this was a pretty big brain dump, but I would love to figure out how to get SSM tunnels working with Terraform cloud because that would be awesome from a security perspective.

In the future, if this go program is the way to do it, i think it should be turned into a Terraform provider.

You need to install the session manager AWS CLI integration to use this repo, otherwise the bash scripts that call AWS cli don't work.

See this docs from AWS:
https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager-working-with-install-plugin.html

This is a separate installation from the AWS-CLI tool, so you need to have both installed. I'm unsure how this can be done on Terraform Cloud.

We should nevertheless add this requirement to this repo README.

This is a separate installation from the AWS-CLI tool, so you need to have both installed. I'm unsure how this can be done on Terraform Cloud.

That is why I made that Go program which uses the AWS session-manager-plugin Go library. This avoids the need for the installation as the binary has the logic in it (and that logic comes from AWS's implementation which is built into the plugin one normally installs).

Im trying to turn this into a Terraform provider to see if that works in the cloud. I worry that Terraform cloud won't allow a Provider to listen on a local port, where that local port is reachable from another provider (e.g. the postgres provider).

I just got around to building and releasing a wrapped version of SSM Session Manager Plugin as a provider. It works in TF Cloud for both EKS + RDS: https://registry.terraform.io/providers/ComplyCo/aws-ssm-tunnels. I think the developer experience could be a bit better because I needed to add a keepalive resource to keep the tunnel from getting prematurely shut down.