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  }