From eb4816fd29187a15a1f1f3807b98f4f34a73f27e Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Sun, 6 Apr 2025 21:17:23 -0700 Subject: [PATCH] Add infisical ssh connect command --- cli/go.mod | 2 +- cli/go.sum | 4 +- cli/packages/cmd/ssh.go | 128 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 131 insertions(+), 3 deletions(-) diff --git a/cli/go.mod b/cli/go.mod index 5f3992e17..f86a1f9a7 100644 --- a/cli/go.mod +++ b/cli/go.mod @@ -12,7 +12,7 @@ require ( github.com/fatih/semgroup v1.2.0 github.com/gitleaks/go-gitdiff v0.8.0 github.com/h2non/filetype v1.1.3 - github.com/infisical/go-sdk v0.5.1 + github.com/infisical/go-sdk v0.5.3 github.com/infisical/infisical-kmip v0.3.5 github.com/mattn/go-isatty v0.0.20 github.com/muesli/ansi v0.0.0-20221106050444-61f0cd9a192a diff --git a/cli/go.sum b/cli/go.sum index da221fd6f..078fadd2d 100644 --- a/cli/go.sum +++ b/cli/go.sum @@ -277,8 +277,8 @@ github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1: github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc= github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/infisical/go-sdk v0.5.1 h1:bl0D4A6CmvfL8RwEQTcZh39nsxC6q3HSs76/4J8grWY= -github.com/infisical/go-sdk v0.5.1/go.mod h1:ExjqFLRz7LSpZpGluqDLvFl6dFBLq5LKyLW7GBaMAIs= +github.com/infisical/go-sdk v0.5.3 h1:6qQCkxR1NqeoYu/6gL9BvQARr/apupX7+Ei5adCRTCU= +github.com/infisical/go-sdk v0.5.3/go.mod h1:ExjqFLRz7LSpZpGluqDLvFl6dFBLq5LKyLW7GBaMAIs= github.com/infisical/infisical-kmip v0.3.5 h1:QM3s0e18B+mYv3a9HQNjNAlbwZJBzXq5BAJM2scIeiE= github.com/infisical/infisical-kmip v0.3.5/go.mod h1:bO1M4YtKyutNg1bREPmlyZspC5duSR7hyQ3lPmLzrIs= github.com/jedib0t/go-pretty v4.3.0+incompatible h1:CGs8AVhEKg/n9YbUenWmNStRW2PHJzaeDodcfvRAbIo= diff --git a/cli/packages/cmd/ssh.go b/cli/packages/cmd/ssh.go index a3d10fc91..3b4b61778 100644 --- a/cli/packages/cmd/ssh.go +++ b/cli/packages/cmd/ssh.go @@ -8,6 +8,7 @@ import ( "fmt" "net" "os" + "os/exec" "path/filepath" "strings" "time" @@ -17,6 +18,7 @@ import ( "github.com/Infisical/infisical-merge/packages/util" infisicalSdk "github.com/infisical/go-sdk" infisicalSdkUtil "github.com/infisical/go-sdk/packages/util" + "github.com/manifoldco/promptui" "github.com/spf13/cobra" "golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh/agent" @@ -48,6 +50,12 @@ var sshSignKeyCmd = &cobra.Command{ Run: signKey, } +var sshConnectCmd = &cobra.Command{ + Use: "connect", + Short: "Connect to an SSH host using issued credentials", + Run: sshConnect, +} + var algoToFileName = map[infisicalSdkUtil.CertKeyAlgorithm]string{ infisicalSdkUtil.RSA2048: "id_rsa_2048", infisicalSdkUtil.RSA4096: "id_rsa_4096", @@ -595,6 +603,122 @@ func signKey(cmd *cobra.Command, args []string) { fmt.Println("Successfully wrote SSH certificate to:", signedKeyPath) } +func sshConnect(cmd *cobra.Command, args []string) { + token, err := util.GetInfisicalToken(cmd) + if err != nil { + util.HandleError(err, "Unable to parse token") + } + + var infisicalToken string + if token != nil && (token.Type == util.SERVICE_TOKEN_IDENTIFIER || token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER) { + infisicalToken = token.Token + } else { + util.RequireLogin() + util.RequireLocalWorkspaceFile() + + loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails(true) + if err != nil { + util.HandleError(err, "Unable to authenticate") + } + + if loggedInUserDetails.LoginExpired { + util.PrintErrorMessageAndExit("Your login session has expired, please run [infisical login] and try again") + } + + infisicalToken = loggedInUserDetails.UserCredentials.JTWToken + } + + customHeaders, err := util.GetInfisicalCustomHeadersMap() + if err != nil { + util.HandleError(err, "Unable to get custom headers") + } + + infisicalClient := infisicalSdk.NewInfisicalClient(context.Background(), infisicalSdk.Config{ + SiteUrl: config.INFISICAL_URL, + UserAgent: api.USER_AGENT, + AutoTokenRefresh: false, + CustomHeaders: customHeaders, + }) + infisicalClient.Auth().SetAccessToken(infisicalToken) + + // Fetch SSH Hosts + hosts, err := infisicalClient.Ssh().GetSshHosts(infisicalSdk.GetSshHostsOptions{}) + if err != nil { + util.HandleError(err, "Failed to fetch SSH hosts") + } + if len(hosts) == 0 { + util.PrintErrorMessageAndExit("You do not have access to any SSH hosts") + } + + // Prompt to select host + hostNames := make([]string, len(hosts)) + for i, h := range hosts { + hostNames[i] = h.Hostname + } + + hostPrompt := promptui.Select{ + Label: "Select an SSH Host", + Items: hostNames, + Size: 10, + } + hostIdx, _, err := hostPrompt.Run() + if err != nil { + util.HandleError(err, "Prompt failed") + } + selectedHost := hosts[hostIdx] + + // Prompt to select login user + if len(selectedHost.LoginMappings) == 0 { + util.PrintErrorMessageAndExit("No login users available for selected host") + } + + loginUsers := make([]string, len(selectedHost.LoginMappings)) + for i, m := range selectedHost.LoginMappings { + loginUsers[i] = m.LoginUser + } + + loginPrompt := promptui.Select{ + Label: "Select Login User", + Items: loginUsers, + Size: 5, + } + loginIdx, _, err := loginPrompt.Run() + if err != nil { + util.HandleError(err, "Prompt failed") + } + selectedLoginUser := selectedHost.LoginMappings[loginIdx].LoginUser + + // Issue SSH creds for host + creds, err := infisicalClient.Ssh().IssueCredentialsFromHost(selectedHost.ID, infisicalSdk.IssueSshCredsFromHostOptions{ + LoginUser: selectedLoginUser, + }) + if err != nil { + util.HandleError(err, "Failed to issue SSH credentials") + } + + // Load credentials into SSH agent + err = addCredentialsToAgent(creds.PrivateKey, creds.SignedKey) + if err != nil { + util.HandleError(err, "Failed to add credentials to SSH agent") + } + fmt.Println("✔ SSH credentials successfully added to agent") + + // Connect to host using system ssh and agent + target := fmt.Sprintf("%s@%s", selectedLoginUser, selectedHost.Hostname) + fmt.Printf("Connecting to %s...\n", target) + + sshCmd := exec.Command("ssh", target) + sshCmd.Stdin = os.Stdin + sshCmd.Stdout = os.Stdout + sshCmd.Stderr = os.Stderr + + err = sshCmd.Run() + if err != nil { + util.HandleError(err, "SSH connection failed") + } +} + + func init() { sshSignKeyCmd.Flags().String("token", "", "Issue SSH certificate using machine identity access token") sshSignKeyCmd.Flags().String("certificateTemplateId", "", "The ID of the SSH certificate template to issue the SSH certificate for") @@ -617,5 +741,9 @@ func init() { sshIssueCredentialsCmd.Flags().String("outFilePath", "", "The path to write the SSH credentials to such as ~/.ssh, ./some_folder, ./some_folder/id_rsa-cert.pub. If not provided, the credentials will be saved to the current working directory") sshIssueCredentialsCmd.Flags().Bool("addToAgent", false, "Whether to add issued SSH credentials to the SSH agent") sshCmd.AddCommand(sshIssueCredentialsCmd) + + sshConnectCmd.Flags().String("token", "", "Use a machine identity access token") + sshCmd.AddCommand(sshConnectCmd) rootCmd.AddCommand(sshCmd) + }