go.mondoo.com/cnquery@v0.0.0-20231005093811-59568235f6ea/providers/os/connection/ssh/awsssmsession/session_manager.go (about) 1 // Copyright (c) Mondoo, Inc. 2 // SPDX-License-Identifier: BUSL-1.1 3 4 package awsssmsession 5 6 import ( 7 "context" 8 "encoding/json" 9 "errors" 10 "fmt" 11 "os" 12 "os/exec" 13 "runtime" 14 "strings" 15 "time" 16 17 "github.com/aws/aws-sdk-go-v2/aws" 18 "github.com/aws/aws-sdk-go-v2/service/ssm" 19 "github.com/rs/zerolog/log" 20 "go.mondoo.com/cnquery/providers-sdk/v1/inventory" 21 ) 22 23 func NewAwsSsmSessionManager(cfg aws.Config, profile string) (*AwsSsmSessionManager, error) { 24 return &AwsSsmSessionManager{ 25 profile: profile, 26 region: cfg.Region, 27 cfg: cfg, 28 }, nil 29 } 30 31 // AwsSsmSessionManager allows us to connect to a remote ec2 instance without having port 22 open 32 // 33 // References: 34 // - https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager.html 35 // - https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager-working-with-install-plugin.html 36 // - https://us-east-1.console.aws.amazon.com/systems-manager/documents/AWS-StartPortForwardingSession/description 37 type AwsSsmSessionManager struct { 38 profile string 39 region string 40 cfg aws.Config 41 } 42 43 func (a *AwsSsmSessionManager) Dial(tc *inventory.Config, localPort string, remotePort string) (*AwsSsmSessionConnection, error) { 44 return NewAwsSsmSessionConnection(a.cfg, a.profile, tc.Host, localPort, remotePort) 45 } 46 47 // NewAwsSsmSessionConnection establishes a new proxy connection via AWS Session Manager plugin. Instead of doing a 48 // tty session, we forward the ssh port from the remote machine to a local port. This ensures we have full ssh power 49 // available and the implementation with existing features stays identical. 50 // 51 // The following steps are executed: 52 // 1. Call AWS SSM StartSession to open a websocket on AWS side that forwards to the machine ssh port 53 // 2. We start the session-manager-plugin process that handles the websocket connection and maps it to a local port 54 // 55 // When the connection is closed, we kill the local process and stop the session via the AWS API 56 func NewAwsSsmSessionConnection(cfg aws.Config, profile string, instance string, localPort string, remotePort string) (*AwsSsmSessionConnection, error) { 57 ctx := context.Background() 58 conn := &AwsSsmSessionConnection{ 59 input: &ssm.StartSessionInput{ 60 DocumentName: aws.String("AWS-StartPortForwardingSession"), 61 Parameters: map[string][]string{ 62 "portNumber": {remotePort}, 63 "localPortNumber": {localPort}, 64 }, 65 Target: aws.String(instance), 66 }, 67 } 68 69 // start ssm websocket session 70 conn.client = ssm.NewFromConfig(cfg) 71 ssmSession, err := conn.client.StartSession(ctx, conn.input) 72 if err != nil { 73 return nil, err 74 } 75 conn.session = ssmSession 76 77 sessJson, err := json.Marshal(ssmSession) 78 if err != nil { 79 return nil, err 80 } 81 82 paramsJson, err := json.Marshal(conn.input) 83 if err != nil { 84 return nil, err 85 } 86 87 // proxyCommand := fmt.Sprintf("%s '%s' %s %s %s '%s'", 88 // GetSsmPluginBinaryName(), string(sessJson), cfg.Region, 89 // "StartSession", profile, string(paramsJson)) 90 91 // start aws ssm session plugin as used by the aws cli 92 // https://github.com/aws/session-manager-plugin 93 binary := GetSsmPluginBinaryName() 94 args := []string{ 95 fmt.Sprintf("'%s'", string(sessJson)), 96 cfg.Region, 97 "StartSession", 98 profile, 99 fmt.Sprintf("'%s'", string(paramsJson)), 100 } 101 102 log.Debug().Str("cmd", fmt.Sprintf("%s %s", binary, strings.Join(args, " "))).Msg("start aws session manager plugin") 103 104 cmd := exec.Command(binary, string(sessJson), cfg.Region, "StartSession", profile, string(paramsJson)) 105 cmd.Stderr = os.Stderr 106 err = cmd.Start() 107 if err != nil { 108 return nil, err 109 } 110 111 if cmd.Process == nil { 112 return nil, errors.New("could not start session-manager-plugin") 113 } 114 115 log.Debug().Int("pid", cmd.Process.Pid).Msg("aws session-manager-plugin started") 116 conn.process = cmd.Process 117 118 // TODO: we may need to implement ssh re-try, the process start takes a bit 119 time.Sleep(time.Second * 2) 120 121 return conn, nil 122 } 123 124 type AwsSsmSessionConnection struct { 125 client *ssm.Client 126 input *ssm.StartSessionInput 127 session *ssm.StartSessionOutput 128 process *os.Process 129 } 130 131 func (a *AwsSsmSessionConnection) Close() error { 132 // kill proxy command if it is still running 133 if a.process != nil { 134 a.process.Kill() 135 } 136 137 // close ssm websocket session 138 if a.client != nil { 139 _, err := a.client.TerminateSession(context.Background(), &ssm.TerminateSessionInput{ 140 SessionId: a.session.SessionId, 141 }) 142 if err != nil { 143 return err 144 } 145 } 146 return nil 147 } 148 149 // GetSsmPluginBinaryName returns filename for aws ssm plugin 150 func GetSsmPluginBinaryName() string { 151 if strings.ToLower(runtime.GOOS) == "windows" { 152 return "session-manager-plugin.exe" 153 } else { 154 return "session-manager-plugin" 155 } 156 } 157 158 // CheckPlugin runs the session-manager-plugin binary and asks for the version 159 func CheckPlugin() error { 160 name := GetSsmPluginBinaryName() 161 cmd := exec.Command(name, "--version") 162 return cmd.Run() 163 }