From f9660b41bb3abd81b9383a047179b567fcfcf07f Mon Sep 17 00:00:00 2001 From: Eugene Dementiev Date: Thu, 31 Oct 2019 16:08:00 +1300 Subject: [PATCH] Add connect command to connect to an instance --- cmd/connect.go | 35 ++++++++++++++++++++++ cmd/reconf.go | 3 ++ cmd/root.go | 9 ++++-- cmd/test.go | 3 ++ go.mod | 3 +- go.sum | 3 ++ lib/connect.go | 79 ++++++++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 131 insertions(+), 4 deletions(-) create mode 100644 cmd/connect.go create mode 100644 lib/connect.go diff --git a/cmd/connect.go b/cmd/connect.go new file mode 100644 index 0000000..5314035 --- /dev/null +++ b/cmd/connect.go @@ -0,0 +1,35 @@ +package cmd + +import ( + "aws-ssh/lib" + + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +var connectCmd = &cobra.Command{ + Use: "connect [ssh command (ssh -tt user@instanceid)]", + Short: "SSH into the EC2 instance using ec2 connect feature", + Long: `aws-ssh connects to the EC2 instance using ec2 connect feature. It makes a special API call to upload +the first public key from your running ssh agent and then runs ssh command`, + Aliases: []string{"ssh"}, + Run: func(cmd *cobra.Command, args []string) { + var profile string + + profiles := viper.GetStringSlice("profiles") + if len(profiles) > 0 { + profile = profiles[0] + } + lib.ConnectEC2(profile, viper.GetString("instanceid"), viper.GetString("user"), args) + }, +} + +func init() { + connectCmd.Flags().StringP("instanceid", "i", "", "Instance ID to connect to") + connectCmd.Flags().StringP("user", "u", "ec2-user", "Existing user on the instance") + connectCmd.MarkFlagRequired("instanceid") + + viper.BindPFlag("instanceid", connectCmd.Flags().Lookup("instanceid")) + viper.BindPFlag("user", connectCmd.Flags().Lookup("user")) + rootCmd.AddCommand(connectCmd) +} diff --git a/cmd/reconf.go b/cmd/reconf.go index 0294287..6afe1e6 100644 --- a/cmd/reconf.go +++ b/cmd/reconf.go @@ -13,6 +13,9 @@ var reconfCmd = &cobra.Command{ Short: "Creates a new ssh config", Long: `Reconfigures your ssh by creating a new config for it. Only one argument is required, which is a filename. In case of any errors, the preexisting file won't be touched.`, + PreRun: func(cmd *cobra.Command, args []string) { + initConfig() + }, Run: func(cmd *cobra.Command, args []string) { lib.Reconf(viper.GetStringSlice("profiles"), args[0]) }, diff --git a/cmd/root.go b/cmd/root.go index bee5f14..877db3d 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -36,7 +36,7 @@ func Execute(version string) { } func init() { - cobra.OnInitialize(initConfig) + cobra.OnInitialize(initSettings) rootCmd.PersistentFlags().BoolP("debug", "d", false, "Show debug output") rootCmd.PersistentFlags().StringSliceP("profile", "p", []string{}, "Profiles to query. Can be specified multiple times. If not specified, goes through all profiles in ~/.aws/config and ~/.aws/credentials") @@ -45,12 +45,15 @@ func init() { viper.BindPFlag("profiles", rootCmd.PersistentFlags().Lookup("profile")) } -// initConfig reads in config file and ENV variables if set. -func initConfig() { +func initSettings() { log.SetHandler(cli.New(os.Stdout)) if viper.GetBool("debug") { log.SetLevel(log.DebugLevel) } +} + +// initConfig reads in config file and ENV variables if set. +func initConfig() { if len(viper.GetStringSlice("profiles")) == 0 { profiles, err := getProfiles() if err != nil { diff --git a/cmd/test.go b/cmd/test.go index 9b109bc..178c04a 100644 --- a/cmd/test.go +++ b/cmd/test.go @@ -16,6 +16,9 @@ var testCmd = &cobra.Command{ Allows to identify permission issues early. `, + PreRun: func(cmd *cobra.Command, args []string) { + initConfig() + }, Run: func(cmd *cobra.Command, args []string) { summaries, err := lib.TraverseProfiles(viper.GetStringSlice("profiles")) if err != nil { diff --git a/go.mod b/go.mod index 527e8f5..a1b82d3 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module aws-ssh require ( github.com/BurntSushi/toml v0.3.1 // indirect github.com/apex/log v1.1.0 - github.com/aws/aws-sdk-go v1.16.26 + github.com/aws/aws-sdk-go v1.25.23 github.com/fatih/color v1.7.0 // indirect github.com/go-ini/ini v1.48.0 github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e // indirect @@ -18,6 +18,7 @@ require ( github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c // indirect github.com/spf13/cobra v0.0.3 github.com/spf13/viper v1.3.1 + golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9 golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3 // indirect gopkg.in/ahmetb/go-linq.v3 v3.0.0 gopkg.in/ini.v1 v1.41.0 // indirect diff --git a/go.sum b/go.sum index e4f5ce3..4b51ad9 100644 --- a/go.sum +++ b/go.sum @@ -5,6 +5,8 @@ github.com/apex/log v1.1.0/go.mod h1:yA770aXIDQrhVOIGurT/pVdfCpSq1GQV/auzMN5fzvY github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/aws/aws-sdk-go v1.16.26 h1:GWkl3rkRO/JGRTWoLLIqwf7AWC4/W/1hMOUZqmX0js4= github.com/aws/aws-sdk-go v1.16.26/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= +github.com/aws/aws-sdk-go v1.25.23 h1:EJx1uSb8E/HRkDa02pOb0r/73bkDbds7qg74s57qYgs= +github.com/aws/aws-sdk-go v1.25.23/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= @@ -68,6 +70,7 @@ github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9 h1:mKdxBk7AujPs8kU4m80U72y/zjbZ3UcXC7dClwKbUI0= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3 h1:ulvT7fqt0yHWzpJwI57MezWnYDVpCAYBVuYst/L+fAY= golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= diff --git a/lib/connect.go b/lib/connect.go new file mode 100644 index 0000000..1d0c7c7 --- /dev/null +++ b/lib/connect.go @@ -0,0 +1,79 @@ +package lib + +import ( + "fmt" + "net" + "os" + "os/exec" + "strings" + "syscall" + + "github.com/apex/log" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/aws/aws-sdk-go/service/ec2instanceconnect" + "golang.org/x/crypto/ssh/agent" +) + +// ConnectEC2 connects to an EC2 instance by pushing your public key onto it first +// using EC2 connect feature and then runs ssh. +func ConnectEC2(profile, instanceID, instanceUser string, args []string) { + localSession, err := session.NewSessionWithOptions(session.Options{ + Config: aws.Config{}, + + SharedConfigState: session.SharedConfigEnable, + Profile: profile, + }) + if err != nil { + log.WithError(err).Fatal("can't get aws session") + } + ec2Svc := ec2.New(localSession) + ec2Result, err := ec2Svc.DescribeInstances(&ec2.DescribeInstancesInput{ + InstanceIds: aws.StringSlice([]string{instanceID}), + }) + if err != nil { + log.WithError(err).Fatal("can't get ec2 instance") + } + + ec2Instance := ec2Result.Reservations[0].Instances[0] + ec2ICSvc := ec2instanceconnect.New(localSession) + + log.WithField("instance_id", aws.StringValue(ec2Instance.InstanceId)).Info("Pushing SSH key...") + + sshAgent, err := net.Dial("unix", os.Getenv("SSH_AUTH_SOCK")) + + keys, err := agent.NewClient(sshAgent).List() + if err != nil || len(keys) < 1 { + log.Fatal("Can't get public keys from ssh agent. Please ensure you have the ssh-agent running and have at least one identity added (with ssh-add)") + } + pubkey := keys[0].String() + + if _, err := ec2ICSvc.SendSSHPublicKey(&ec2instanceconnect.SendSSHPublicKeyInput{ + InstanceId: ec2Instance.InstanceId, + InstanceOSUser: aws.String(instanceUser), + AvailabilityZone: ec2Instance.Placement.AvailabilityZone, + SSHPublicKey: aws.String(pubkey), + }); err != nil { + log.WithError(err).Fatal("can't push ssh key") + } + + if len(args) == 0 { + // construct default args + args = []string{ + "ssh", + "-tt", + fmt.Sprintf("%s@%s", instanceUser, instanceID), + } + } + + command, err := exec.LookPath(args[0]) + if err != nil { + log.WithError(err).Fatal("Can't find the binary in the PATH") + } + log.WithField("instance_id", aws.StringValue(ec2Instance.InstanceId)).Infof("Connecting to the instance using '%s'", strings.Join(args, " ")) + + if err := syscall.Exec(command, args, os.Environ()); err != nil { + log.WithFields(log.Fields{"command": command}).WithError(err).Fatal("can't run the command") + } +}