From 48a723aca91b30f33b1b7a6f8db7108dd2d34754 Mon Sep 17 00:00:00 2001 From: Mahmoud Mousa Date: Sat, 5 Oct 2024 08:27:55 +0900 Subject: [PATCH 1/6] Add key lookup sequence --- render/render.go | 27 +++++++++ utils/auth.go | 140 +++++++++++++++++++++++++++++++++++++++++++++++ utils/utils.go | 119 ---------------------------------------- 3 files changed, 167 insertions(+), 119 deletions(-) create mode 100644 utils/auth.go diff --git a/render/render.go b/render/render.go index 346045e..891f4d1 100644 --- a/render/render.go +++ b/render/render.go @@ -15,6 +15,8 @@ limitations under the License. package render import ( + "os" + "github.com/pterm/pterm" "github.com/pterm/pterm/putils" ) @@ -28,3 +30,28 @@ func RenderSidekickBig() { pterm.DefaultCenter.Println(s) } + +func RenderKeyValidation(resultLines []string, keyHash string, hostname string) { + startColor := pterm.NewRGB(0, 255, 255) + endColor := pterm.NewRGB(255, 0, 255) + + pterm.DefaultCenter.Print(keyHash) + for i := 0; i < len(resultLines[1:]); i++ { + fadeFactor := float32(i) / float32(20) + currentColor := startColor.Fade(0, 1, fadeFactor, endColor) + pterm.DefaultCenter.Print(currentColor.Sprint(resultLines[1:][i])) + } + prompt := pterm.DefaultInteractiveContinue + + pterm.DefaultCenter.Printf(pterm.FgYellow.Sprintf("This is the ASCII art and fingerprint of your VPS's public key at %s", hostname)) + pterm.DefaultCenter.Printf(pterm.FgYellow.Sprint("Please confirm you want to continue with the connection")) + pterm.DefaultCenter.Printf(pterm.FgYellow.Sprint("Sidekick will add this host/key pair to known_hosts")) + pterm.Println() + + prompt.DefaultText = "Would you like to proceed?" + prompt.Options = []string{"yes", "no"} + if result, _ := prompt.Show(); result != "yes" { + pterm.Error.Println("In order to continue, you need to accept this.") + os.Exit(0) + } +} diff --git a/utils/auth.go b/utils/auth.go new file mode 100644 index 0000000..9606f7f --- /dev/null +++ b/utils/auth.go @@ -0,0 +1,140 @@ +package utils + +import ( + "errors" + "fmt" + "log" + "net" + "os" + "os/exec" + "os/user" + "path" + "strings" + + "github.com/mightymoud/sidekick/render" + "github.com/skeema/knownhosts" + "golang.org/x/crypto/ssh" + "golang.org/x/crypto/ssh/agent" +) + +func getKeyFilesAuth() ([]ssh.AuthMethod, error) { + user, err := user.Current() + if err != nil { + return nil, err + } + sshDir := path.Join(user.HomeDir, ".ssh") + keyFiles := []string{ + "id_rsa", + "id_ecdsa", + "id_ed25519", + } + + var authMethods []ssh.AuthMethod + + for _, keyFile := range keyFiles { + keyPath := path.Join(sshDir, keyFile) + if _, err := os.Stat(keyPath); os.IsNotExist(err) { + continue + } + + privateKey, err := os.ReadFile(keyPath) + if err != nil { + continue + } + + signer, err := ssh.ParsePrivateKey(privateKey) + if err != nil { + continue + } + + authMethods = append(authMethods, ssh.PublicKeys(signer)) + } + + return authMethods, nil +} + +func inspectServerPublicKey(key ssh.PublicKey, hostname string) { + sshKeyCmd := exec.Command("sh", "-s", "-", string(ssh.MarshalAuthorizedKey(key))) + sshKeyCmd.Stdin = strings.NewReader(sshKeyScript) + result, sshKeyCmdErr := sshKeyCmd.Output() + if sshKeyCmdErr != nil { + panic(sshKeyCmdErr) + } + resultLines := strings.Split(string(result), "\n") + keyHash := resultLines[0] + + render.RenderKeyValidation(resultLines, keyHash, hostname) + +} + +func GetSshClient(server string, sshUser string) (*ssh.Client, error) { + sshPort := "22" + sshAgentSock := os.Getenv("SSH_AUTH_SOCK") + if sshAgentSock == "" { + log.Fatal("No SSH SOCK AVAILABLE") + return nil, errors.New("Error happened connecting to ssh-agent") + } + + conn, err := net.Dial("unix", sshAgentSock) + if err != nil { + log.Fatalf("Failed to connect to SSH agent: %s", err) + return nil, err + } + defer conn.Close() + + agentClient := agent.NewClient(conn) + + // Get auth of standard keys not in agent + authMethods, _ := getKeyFilesAuth() + + authMethods = append(authMethods, ssh.PublicKeysCallback(agentClient.Signers)) + + cb := ssh.HostKeyCallback(func(hostname string, remote net.Addr, key ssh.PublicKey) error { + currentUser, _ := user.Current() + khPath := fmt.Sprintf("%s/.ssh/known_hosts", currentUser.HomeDir) + kh, knErr := knownhosts.NewDB(khPath) + if knErr != nil { + log.Fatalf("Failed to read known_hosts: %s", err) + os.Exit(1) + } + + innerCallback := kh.HostKeyCallback() + err := innerCallback(hostname, remote, key) + if knownhosts.IsHostKeyChanged(err) { + return fmt.Errorf("REMOTE HOST IDENTIFICATION HAS CHANGED for host %s! This may indicate a MitM attack.", hostname) + } else if knownhosts.IsHostUnknown(err) { + inspectServerPublicKey(key, hostname) + f, ferr := os.OpenFile(khPath, os.O_APPEND|os.O_WRONLY, 0600) + if ferr == nil { + defer f.Close() + ferr = knownhosts.WriteKnownHost(f, hostname, remote, key) + } else { + log.Printf("Failed to add host %s to known_hosts: %v\n", hostname, ferr) + } + return nil + } + return err + }) + // now that we have our key, we need to start ssh client sesssion + config := &ssh.ClientConfig{ + User: sshUser, + Auth: authMethods, + HostKeyCallback: cb, + } + + // create SSH client with the said config and connect to server + client, sshClientErr := ssh.Dial("tcp", fmt.Sprintf("%s:%s", server, sshPort), config) + if sshClientErr != nil { + log.Fatalf("Failed to create ssh client to the server: %v", sshClientErr) + } + + return client, nil +} + +func Login(server string, user string) (*ssh.Client, error) { + sshClient, err := GetSshClient(server, user) + if err != nil { + return nil, err + } + return sshClient, nil +} diff --git a/utils/utils.go b/utils/utils.go index c09c8be..2530428 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -20,20 +20,16 @@ import ( "errors" "fmt" "log" - "net" "os" "os/exec" - "os/user" "regexp" "strings" "time" "github.com/pterm/pterm" - "github.com/skeema/knownhosts" "github.com/spf13/cobra" "github.com/spf13/viper" "golang.org/x/crypto/ssh" - "golang.org/x/crypto/ssh/agent" "gopkg.in/yaml.v3" ) @@ -43,118 +39,6 @@ type CommandsStage struct { SpinnerFailMessage string } -func inspectServerPublicKey(key ssh.PublicKey) { - sshKeyCmd := exec.Command("sh", "-s", "-", string(ssh.MarshalAuthorizedKey(key))) - sshKeyCmd.Stdin = strings.NewReader(sshKeyScript) - result, sshKeyCmdErr := sshKeyCmd.Output() - if sshKeyCmdErr != nil { - panic(sshKeyCmdErr) - } - resultLines := strings.Split(string(result), "\n") - keyHash := resultLines[0] - - startColor := pterm.NewRGB(0, 255, 255) - endColor := pterm.NewRGB(255, 0, 255) - - pterm.DefaultCenter.Print(keyHash) - for i := 0; i < len(resultLines[1:]); i++ { - fadeFactor := float32(i) / float32(20) - currentColor := startColor.Fade(0, 1, fadeFactor, endColor) - pterm.DefaultCenter.Print(currentColor.Sprint(resultLines[1:][i])) - } - -} - -func GetSshClient(server string, sshUser string) (*ssh.Client, error) { - sshPort := "22" - // connect to local ssh-agent to grab all keys - sshAgentSock := os.Getenv("SSH_AUTH_SOCK") - if sshAgentSock == "" { - log.Fatal("No SSH SOCK AVAILABLE") - return nil, errors.New("Error happened connecting to ssh-agent") - } - // make a connection to SSH agent over unix protocl - conn, err := net.Dial("unix", sshAgentSock) - if err != nil { - log.Fatalf("Failed to connect to SSH agent: %s", err) - return nil, err - } - defer conn.Close() - - // make a ssh agent out of the connection - agentClient := agent.NewClient(conn) - - // Check that we can get all the public keys added to the agent properly - _, signersErr := agentClient.Signers() - if signersErr != nil { - log.Fatalf("Failed to get signers from SSH agent: %v", signersErr) - return nil, err - } - - cb := ssh.HostKeyCallback(func(hostname string, remote net.Addr, key ssh.PublicKey) error { - currentUser, _ := user.Current() - khPath := fmt.Sprintf("%s/.ssh/known_hosts", currentUser.HomeDir) - kh, knErr := knownhosts.NewDB(khPath) - if knErr != nil { - log.Fatalf("Failed to read known_hosts: %s", err) - } - - innerCallback := kh.HostKeyCallback() - err := innerCallback(hostname, remote, key) - if knownhosts.IsHostKeyChanged(err) { - return fmt.Errorf("REMOTE HOST IDENTIFICATION HAS CHANGED for host %s! This may indicate a MitM attack.", hostname) - } else if knownhosts.IsHostUnknown(err) { - inspectServerPublicKey(key) - prompt := pterm.DefaultInteractiveContinue - - pterm.DefaultCenter.Printf(pterm.FgYellow.Sprintf("This is the ASCII art and fingerprint of your VPS's public key at %s", hostname)) - pterm.DefaultCenter.Printf(pterm.FgYellow.Sprint("Please confirm you want to continue with the connection")) - pterm.DefaultCenter.Printf(pterm.FgYellow.Sprint("Sidekick will add this host/key pair to known_hosts")) - pterm.Println() - - prompt.DefaultText = "Would you like to proceed?" - prompt.Options = []string{"yes", "no"} - if result, _ := prompt.Show(); result != "yes" { - pterm.Error.Println("In order to continue, you need to accept this.") - os.Exit(0) - } - f, ferr := os.OpenFile(khPath, os.O_APPEND|os.O_WRONLY, 0600) - if ferr == nil { - defer f.Close() - ferr = knownhosts.WriteKnownHost(f, hostname, remote, key) - } else { - log.Printf("Failed to add host %s to known_hosts: %v\n", hostname, ferr) - } - return nil - } - return err - }) - // now that we have our key, we need to start ssh client sesssion - config := &ssh.ClientConfig{ - User: sshUser, - Auth: []ssh.AuthMethod{ - ssh.PublicKeysCallback(agentClient.Signers), - }, - HostKeyCallback: cb, - } - - // create SSH client with the said config and connect to server - client, sshClientErr := ssh.Dial("tcp", fmt.Sprintf("%s:%s", server, sshPort), config) - if sshClientErr != nil { - log.Fatalf("Failed to create ssh client to the server: %v", sshClientErr) - } - - return client, nil -} - -func Login(server string, user string) (*ssh.Client, error) { - sshClient, err := GetSshClient(server, user) - if err != nil { - return nil, err - } - return sshClient, nil -} - func RunCommand(client *ssh.Client, cmd string) (chan string, error) { session, err := client.NewSession() errChannel := make(chan string) @@ -192,7 +76,6 @@ func RunCommand(client *ssh.Client, cmd string) (chan string, error) { } }() - // fmt.Printf("\033[35m Running the command: \033[0m %s\n", cmd) if err := session.Run(cmd); err != nil { session.Close() errString := <-errChannel @@ -200,7 +83,6 @@ func RunCommand(client *ssh.Client, cmd string) (chan string, error) { } time.Sleep(time.Millisecond * 500) - // fmt.Println("Ran command successfully!") return stdOutChannel, nil } @@ -212,7 +94,6 @@ func RunCommands(client *ssh.Client, commands []string) error { } } - // fmt.Println("Ran all commands successfully") return nil } From 815b28b68ba0d016060f051bb44813a12de75c1b Mon Sep 17 00:00:00 2001 From: Mahmoud Mousa Date: Sat, 5 Oct 2024 15:09:52 +0900 Subject: [PATCH 2/6] Add version flag and integrate with goreleaser --- .goreleaser.yaml | 2 ++ cmd/root.go | 11 +++++++---- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/.goreleaser.yaml b/.goreleaser.yaml index dc2893e..a07ffa3 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -14,6 +14,8 @@ before: - go mod tidy builds: + - ldflags: + - "-X 'github.com/mightymoud/sidekick/cmd.version={{.Tag}}'" - env: - CGO_ENABLED=0 goos: diff --git a/cmd/root.go b/cmd/root.go index 9161948..a4e78c1 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -21,10 +21,13 @@ import ( "github.com/spf13/cobra" ) +var version = "dev" + var rootCmd = &cobra.Command{ - Use: "sidekick", - Short: "CLI to self-host all your apps on a single VPS without vendor locking", - Long: `With sidekick you can deploy any number of applications to a single VPS, connect multiple domains and much more.`, + Use: "sidekick", + Version: version, + Short: "CLI to self-host all your apps on a single VPS without vendor locking", + Long: `With sidekick you can deploy any number of applications to a single VPS, connect multiple domains and much more.`, } func Execute() { @@ -35,6 +38,6 @@ func Execute() { } func init() { + rootCmd.SetVersionTemplate(`{{println .Version}}`) rootCmd.AddCommand(preview.PreviewCmd) - rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") } From 74bdd3862cb5a801f03010b65abc65644550d32b Mon Sep 17 00:00:00 2001 From: Mahmoud Mousa Date: Sat, 5 Oct 2024 15:17:30 +0900 Subject: [PATCH 3/6] chore: Fix goreleaser config --- .goreleaser.yaml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.goreleaser.yaml b/.goreleaser.yaml index a07ffa3..9d04fd6 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -14,10 +14,10 @@ before: - go mod tidy builds: - - ldflags: - - "-X 'github.com/mightymoud/sidekick/cmd.version={{.Tag}}'" - env: - CGO_ENABLED=0 + ldflags: + - "-X 'github.com/mightymoud/sidekick/cmd.version={{.Tag}}'" goos: - linux - windows @@ -44,3 +44,4 @@ changelog: exclude: - "^docs:" - "^test:" + - "^chore:" From 1ead99ff62c3ebdb0d67fa1b1fc9b492fbb0b08a Mon Sep 17 00:00:00 2001 From: Mahmoud Mousa Date: Sat, 5 Oct 2024 15:41:47 +0900 Subject: [PATCH 4/6] docs: Update after last release --- README.md | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index ecf5e18..5ea9282 100644 --- a/README.md +++ b/README.md @@ -77,8 +77,10 @@ Then you need to enter the following: After that Sidekick will setup many things on your VPS - Usually takes around 2 mins +You can use flags instead. Read more [in the docs](https://www.sidekickdeploy.com/docs/command/init/). +
- What does Sidekick do when I run this command + What does Sidekick do when I run this command? * Login with `root` user * Make a new user `sidekick` and grant sudo access @@ -94,14 +96,20 @@ After that Sidekick will setup many things on your VPS - Usually takes around 2
- If you are on a Mac make sure to: - -* Run `ssh-add --apple-use-keychain ~/.ssh/YOUR_KEY` first before running this command. + Which SSH key will Sidekick use to login? -This will be fixed soon in our next release where Sidekick will check the default key before grabbing the keys from the ssh agent. +Sidekick will look up the default keys in your default .ssh directory in the following order: + +- id_rsa.pub +- id_ecdsa.pub +- id_ed25519.pub + +Sidekick will also get all keys from the `ssh-agent` and try them as well. If you want to use a custom key and not a default one, you would need to add the to your agent first by running `ssh-add KEY_FILE`
+Read more details about flags and other options for this command [on the docs](https://www.sidekickdeploy.com/docs/command/init/) + ### Launch a new application
From 5ce79b3a07bfcafd7a04382dc9a898a76ad9f8f3 Mon Sep 17 00:00:00 2001 From: Mahmoud Mousa Date: Sat, 5 Oct 2024 15:45:52 +0900 Subject: [PATCH 5/6] docs: Remove go install --- README.md | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/README.md b/README.md index 5ea9282..6c429f0 100644 --- a/README.md +++ b/README.md @@ -33,18 +33,12 @@ I'm tired of the complexity involved in hosting my side projects. While some pla ## Installation -On a Mac: +Using brew: ```bash brew install sidekick ``` -Linux/Windows you need GO installed on your system then you need to run: - -```bash -go install github.com/mightymoud/sidekick@latest -``` - ## Usage Sidekick helps you along all the steps of deployment on your VPS. From basic setup to zero downtime deploys, we got you! ✊ From d70beb3239ffcee74862414e99dc4a3e32349b6c Mon Sep 17 00:00:00 2001 From: Mahmoud Mousa Date: Mon, 7 Oct 2024 14:36:15 +0900 Subject: [PATCH 6/6] Patch SSH key trials --- .goreleaser.yaml | 2 -- cmd/root.go | 2 +- utils/auth.go | 33 ++++++++++++++++++++++----------- 3 files changed, 23 insertions(+), 14 deletions(-) diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 9d04fd6..bfb8fc8 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -16,8 +16,6 @@ before: builds: - env: - CGO_ENABLED=0 - ldflags: - - "-X 'github.com/mightymoud/sidekick/cmd.version={{.Tag}}'" goos: - linux - windows diff --git a/cmd/root.go b/cmd/root.go index a4e78c1..e0a5664 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -21,7 +21,7 @@ import ( "github.com/spf13/cobra" ) -var version = "dev" +var version = "v0.6.2" var rootCmd = &cobra.Command{ Use: "sidekick", diff --git a/utils/auth.go b/utils/auth.go index 9606f7f..c85609b 100644 --- a/utils/auth.go +++ b/utils/auth.go @@ -10,6 +10,7 @@ import ( "os/user" "path" "strings" + "time" "github.com/mightymoud/sidekick/render" "github.com/skeema/knownhosts" @@ -115,19 +116,29 @@ func GetSshClient(server string, sshUser string) (*ssh.Client, error) { } return err }) - // now that we have our key, we need to start ssh client sesssion - config := &ssh.ClientConfig{ - User: sshUser, - Auth: authMethods, - HostKeyCallback: cb, - } - // create SSH client with the said config and connect to server - client, sshClientErr := ssh.Dial("tcp", fmt.Sprintf("%s:%s", server, sshPort), config) - if sshClientErr != nil { - log.Fatalf("Failed to create ssh client to the server: %v", sshClientErr) - } + var client *ssh.Client + + // This error will be thrown when one method/key doesn't work + var expectedClientErr = errors.New("ssh: handshake failed: ssh: unable to authenticate, attempted methods [none publickey], no supported methods remain") + for _, method := range authMethods { + config := &ssh.ClientConfig{ + User: sshUser, + Auth: []ssh.AuthMethod{method}, + HostKeyCallback: cb, + Timeout: 5 * time.Second, + } + workingClient, sshClientErr := ssh.Dial("tcp", fmt.Sprintf("%s:%s", server, sshPort), config) + if sshClientErr != nil { + if sshClientErr.Error() != expectedClientErr.Error() { + log.Fatalf("Failed to create ssh client to the server: %v", sshClientErr) + } + continue + } + client = workingClient + break + } return client, nil }