diff --git a/cmd/envd-ssh/main.go b/cmd/envd-ssh/main.go index d8efe92c3..8d67efcb7 100644 --- a/cmd/envd-ssh/main.go +++ b/cmd/envd-ssh/main.go @@ -26,17 +26,17 @@ import ( "github.com/sirupsen/logrus" cli "github.com/urfave/cli/v2" + "github.com/tensorchord/envd/pkg/config" "github.com/tensorchord/envd/pkg/remote/sshd" "github.com/tensorchord/envd/pkg/ssh" "github.com/tensorchord/envd/pkg/version" ) const ( - authorizedKeysPath = "/var/envd/remote/authorized_keys" - envPort = "envd_SSH_PORT" - flagDebug = "debug" - flagAuthKey = "authorized-keys" - flagNoAuth = "no-auth" + envPort = "envd_SSH_PORT" + flagDebug = "debug" + flagAuthKey = "authorized-keys" + flagNoAuth = "no-auth" ) func main() { @@ -55,8 +55,8 @@ func main() { }, &cli.StringFlag{ Name: flagAuthKey, - Usage: "path to authorized keys file, defaults to " + authorizedKeysPath, - Value: authorizedKeysPath, + Usage: "path to authorized keys file, defaults to " + config.ContainerauthorizedKeysPath, + Value: config.ContainerauthorizedKeysPath, Aliases: []string{"a"}, }, &cli.BoolFlag{ diff --git a/cmd/envd/build.go b/cmd/envd/build.go index ceee49fcd..52954bebb 100644 --- a/cmd/envd/build.go +++ b/cmd/envd/build.go @@ -25,6 +25,7 @@ import ( "github.com/tensorchord/envd/pkg/builder" "github.com/tensorchord/envd/pkg/flag" "github.com/tensorchord/envd/pkg/home" + sshconfig "github.com/tensorchord/envd/pkg/ssh/config" "github.com/tensorchord/envd/pkg/util/fileutil" ) @@ -50,6 +51,12 @@ var CommandBuild = &cli.Command{ Aliases: []string{"p"}, Value: ".", }, + &cli.PathFlag{ + Name: "public-key", + Usage: "Path to the public key", + Aliases: []string{"pubk"}, + Value: sshconfig.GetPublicKey(), + }, }, Action: build, @@ -92,5 +99,5 @@ func build(clicontext *cli.Context) error { if err != nil { return errors.Wrap(err, "failed to create the builder") } - return builder.Build(clicontext.Context) + return builder.Build(clicontext.Path("public-key"), clicontext.Context) } diff --git a/cmd/envd/destroy.go b/cmd/envd/destroy.go index ab0ca1b2c..288ea6cbb 100644 --- a/cmd/envd/destroy.go +++ b/cmd/envd/destroy.go @@ -22,7 +22,7 @@ import ( cli "github.com/urfave/cli/v2" "github.com/tensorchord/envd/pkg/docker" - "github.com/tensorchord/envd/pkg/ssh" + sshconfig "github.com/tensorchord/envd/pkg/ssh/config" "github.com/tensorchord/envd/pkg/util/fileutil" ) @@ -59,7 +59,7 @@ func destroy(clicontext *cli.Context) error { return errors.Wrapf(err, "failed to destroy the environment: %s", ctr) } - if err = ssh.RemoveEntry(ctr); err != nil { + if err = sshconfig.RemoveEntry(ctr); err != nil { logrus.Infof("failed to remove entry %s from your SSH config file: %s", ctr, err) return errors.Wrap(err, "failed to remove entry from your SSH config file") } diff --git a/cmd/envd/up.go b/cmd/envd/up.go index eaffd381c..8f333d7b7 100644 --- a/cmd/envd/up.go +++ b/cmd/envd/up.go @@ -30,6 +30,7 @@ import ( "github.com/tensorchord/envd/pkg/home" "github.com/tensorchord/envd/pkg/lang/ir" "github.com/tensorchord/envd/pkg/ssh" + sshconfig "github.com/tensorchord/envd/pkg/ssh/config" "github.com/tensorchord/envd/pkg/util/fileutil" ) @@ -60,16 +61,22 @@ var CommandUp = &cli.Command{ Aliases: []string{"f"}, Value: "build.envd", }, - &cli.BoolFlag{ - Name: "auth", - Usage: "Enable authentication for ssh", - Value: false, - }, + // &cli.BoolFlag{ + // Name: "auth", + // Usage: "Enable authentication for ssh", + // Value: false, + // }, &cli.PathFlag{ Name: "private-key", Usage: "Path to the private key", Aliases: []string{"k"}, - Value: "~/.ssh/id_rsa", + Value: sshconfig.GetPrivateKey(), + }, + &cli.PathFlag{ + Name: "public-key", + Usage: "Path to the public key", + Aliases: []string{"pubk"}, + Value: sshconfig.GetPublicKey(), }, &cli.DurationFlag{ Name: "timeout", @@ -129,7 +136,7 @@ func up(clicontext *cli.Context) error { return errors.Wrap(err, "failed to create the builder") } - if err := builder.Build(clicontext.Context); err != nil { + if err := builder.Build(clicontext.Path("public-key"), clicontext.Context); err != nil { return err } gpu := builder.GPUEnabled() @@ -138,6 +145,7 @@ func up(clicontext *cli.Context) error { if err != nil { return err } + containerID, containerIP, err := dockerClient.StartEnvd(clicontext.Context, tag, ctr, buildContext, gpu, *ir.DefaultGraph, clicontext.Duration("timeout"), clicontext.StringSlice("volume")) @@ -147,14 +155,14 @@ func up(clicontext *cli.Context) error { logrus.Debugf("container %s is running", containerID) logrus.Debugf("Add entry %s to SSH config. at %s", buildContext, containerIP) - if err = ssh.AddEntry(ctr, containerIP, ssh.DefaultSSHPort); err != nil { + if err = sshconfig.AddEntry(ctr, containerIP, ssh.DefaultSSHPort, clicontext.Path("private-key")); err != nil { logrus.Infof("failed to add entry %s to your SSH config file: %s", ctr, err) return errors.Wrap(err, "failed to add entry to your SSH config file") } if !detach { sshClient, err := ssh.NewClient( - containerIP, "root", ssh.DefaultSSHPort, clicontext.Bool("auth"), clicontext.Path("private-key"), "") + containerIP, "envd", ssh.DefaultSSHPort, true, clicontext.Path("private-key"), "") if err != nil { return err } diff --git a/pkg/builder/builder.go b/pkg/builder/builder.go index 0bfc90198..c2d29ac22 100644 --- a/pkg/builder/builder.go +++ b/pkg/builder/builder.go @@ -36,7 +36,7 @@ import ( ) type Builder interface { - Build(ctx context.Context) error + Build(pub string, ctx context.Context) error GPUEnabled() bool } @@ -81,8 +81,8 @@ func (b generalBuilder) GPUEnabled() bool { return ir.GPUEnabled() } -func (b generalBuilder) Build(ctx context.Context) error { - def, err := b.compile(ctx) +func (b generalBuilder) Build(pub string, ctx context.Context) error { + def, err := b.compile(pub, ctx) if err != nil { return errors.Wrap(err, "failed to compile") } @@ -110,11 +110,11 @@ func (b generalBuilder) interpret() error { return nil } -func (b generalBuilder) compile(ctx context.Context) (*llb.Definition, error) { +func (b generalBuilder) compile(pub string, ctx context.Context) (*llb.Definition, error) { if err := b.interpret(); err != nil { return nil, errors.Wrap(err, "failed to interpret") } - def, err := ir.Compile(ctx, fileutil.Base(b.buildContextDir)) + def, err := ir.Compile(ctx, fileutil.Base(b.buildContextDir), pub) if err != nil { return nil, errors.Wrap(err, "failed to compile build.envd") } diff --git a/pkg/builder/builder_test.go b/pkg/builder/builder_test.go index a3fd3deaf..20ccc2270 100644 --- a/pkg/builder/builder_test.go +++ b/pkg/builder/builder_test.go @@ -34,6 +34,7 @@ import ( "github.com/tensorchord/envd/pkg/progress/compileui" compileuimock "github.com/tensorchord/envd/pkg/progress/compileui/mock" "github.com/tensorchord/envd/pkg/progress/progresswriter" + sshconfig "github.com/tensorchord/envd/pkg/ssh/config" ) var _ = Describe("Builder", func() { @@ -90,7 +91,8 @@ var _ = Describe("Builder", func() { b.Interpreter.(*mockstarlark.MockInterpreter).EXPECT().ExecFile( gomock.Eq(configFilePath), ).Return(nil, expected) - err := b.Build(context.TODO()) + pub := sshconfig.GetPublicKey() + err := b.Build(pub, context.TODO()) Expect(err).To(HaveOccurred()) }) }) @@ -98,13 +100,14 @@ var _ = Describe("Builder", func() { When("failed to interpret manifest", func() { It("should get an error", func() { expected := errors.New("failed to interpret manifest") + pub := sshconfig.GetPublicKey() b.Interpreter.(*mockstarlark.MockInterpreter).EXPECT().ExecFile( gomock.Eq(configFilePath), ).Return(nil, nil) b.Interpreter.(*mockstarlark.MockInterpreter).EXPECT().ExecFile( gomock.Eq(b.manifestFilePath), ).Return(nil, expected) - err := b.Build(context.TODO()) + err := b.Build(pub, context.TODO()) Expect(err).To(HaveOccurred()) }) }) diff --git a/pkg/config/config.go b/pkg/config/config.go new file mode 100644 index 000000000..068448fbe --- /dev/null +++ b/pkg/config/config.go @@ -0,0 +1,33 @@ +// Copyright 2022 The envd Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package config + +import ( + "path/filepath" + + "github.com/adrg/xdg" +) + +type UpState string + +const ( + PrivateKeyFile = "id_rsa_envd" + PublicKeyFile = "id_rsa_envd.pub" + ContainerauthorizedKeysPath = "/var/envd/authorized_keys" + envdFolderName = ".envd" +) + +func GetEnvdHome() string { + return filepath.Join(xdg.ConfigHome, envdFolderName) +} diff --git a/pkg/docker/entrypoint.go b/pkg/docker/entrypoint.go index a0503c64f..7dff99d17 100644 --- a/pkg/docker/entrypoint.go +++ b/pkg/docker/entrypoint.go @@ -18,13 +18,14 @@ import ( "fmt" "strings" + "github.com/tensorchord/envd/pkg/config" "github.com/tensorchord/envd/pkg/editor/jupyter" "github.com/tensorchord/envd/pkg/lang/ir" ) const ( template = `set -e -/var/envd/bin/envd-ssh --no-auth & +/var/envd/bin/envd-ssh --authorized-keys %s & %s wait -n` ) @@ -32,7 +33,9 @@ wait -n` func entrypointSH(g ir.Graph, workingDir string) string { if g.JupyterConfig != nil { cmds := jupyter.GenerateCommand(g, workingDir) - return fmt.Sprintf(template, strings.Join(cmds, " ")) + return fmt.Sprintf(template, + config.ContainerauthorizedKeysPath, strings.Join(cmds, " ")) } - return fmt.Sprintf(template, "") + return fmt.Sprintf(template, + config.ContainerauthorizedKeysPath, "") } diff --git a/pkg/home/manager.go b/pkg/home/manager.go index 7aa961f84..0c4c9912e 100644 --- a/pkg/home/manager.go +++ b/pkg/home/manager.go @@ -23,6 +23,7 @@ import ( "github.com/adrg/xdg" "github.com/cockroachdb/errors" "github.com/sirupsen/logrus" + sshconfig "github.com/tensorchord/envd/pkg/ssh/config" "github.com/tensorchord/envd/pkg/util/fileutil" ) @@ -85,7 +86,7 @@ func (m generalManager) Cached(key string) bool { func (m *generalManager) dumpCacheStatus() error { file, err := os.Create(m.cacheStatusFile) if err != nil { - return errors.Wrap(err, "failed to open cache status file") + return errors.Wrap(err, "failed to create cache status file") } defer file.Close() @@ -120,9 +121,14 @@ func (m *generalManager) init() error { if err != nil { if os.IsNotExist(err) { logrus.WithField("filename", m.cacheStatusFile).Debug("Creating file") - if _, err := os.Create(m.cacheStatusFile); err != nil { + file, err := os.Create(m.cacheStatusFile) + if err != nil { return errors.Wrap(err, "failed to create file") } + err = file.Close() + if err != nil { + return errors.Wrap(err, "failed to close file") + } if err := m.dumpCacheStatus(); err != nil { return errors.Wrap(err, "failed to dump cache status") } @@ -131,6 +137,11 @@ func (m *generalManager) init() error { } } + // Generate SSH keys when init + if err := sshconfig.GenerateKeys(); err != nil { + return errors.Wrap(err, "failed to generate ssh key") + } + file, err := os.Open(m.cacheStatusFile) if err != nil { return errors.Wrap(err, "failed to open cache status file") diff --git a/pkg/lang/frontend/starlark/interpreter.go b/pkg/lang/frontend/starlark/interpreter.go index 55bc9a149..07f3f4e60 100644 --- a/pkg/lang/frontend/starlark/interpreter.go +++ b/pkg/lang/frontend/starlark/interpreter.go @@ -44,7 +44,7 @@ func NewInterpreter() Interpreter { } func (s generalInterpreter) ExecFile(filename string) (interface{}, error) { - logrus.WithField("filename", filename).Debug("inperprete the file") + logrus.WithField("filename", filename).Debug("interprete the file") var src interface{} globals, err := starlark.ExecFile(s.Thread, filename, src, nil) if err != nil { diff --git a/pkg/lang/ir/compile.go b/pkg/lang/ir/compile.go index 7549a846f..71a667571 100644 --- a/pkg/lang/ir/compile.go +++ b/pkg/lang/ir/compile.go @@ -54,13 +54,14 @@ func GPUEnabled() bool { return DefaultGraph.CUDA != nil } -func Compile(ctx context.Context, cachePrefix string) (*llb.Definition, error) { +func Compile(ctx context.Context, cachePrefix string, pub string) (*llb.Definition, error) { w, err := compileui.New(ctx, os.Stdout, "auto") if err != nil { return nil, errors.Wrap(err, "failed to create compileui") } DefaultGraph.Writer = w DefaultGraph.CachePrefix = cachePrefix + DefaultGraph.PublicKeyPath = pub state, err := DefaultGraph.Compile() if err != nil { return nil, err @@ -109,7 +110,11 @@ func (g Graph) Compile() (llb.State, error) { diffShellStage := llb.Diff(builtinSystemStage, shellStage, llb.WithCustomName("install shell")) pypiStage := llb.Diff(builtinSystemStage, g.compilePyPIPackages(builtinSystemStage), llb.WithCustomName("install PyPI packages")) systemStage := llb.Diff(builtinSystemStage, g.compileSystemPackages(builtinSystemStage), llb.WithCustomName("install system packages")) - sshStage := g.copyEnvdSSHServer() + sshStage, err := g.copyEnvdSSHServerWithKey() + + if err != nil { + return llb.State{}, errors.Wrap(err, "failed to copy SSH key") + } vscodeStage, err := g.compileVSCode() if err != nil { diff --git a/pkg/lang/ir/system.go b/pkg/lang/ir/system.go index b8aa07e7d..14e0f36ce 100644 --- a/pkg/lang/ir/system.go +++ b/pkg/lang/ir/system.go @@ -16,12 +16,15 @@ package ir import ( "fmt" + "os" "path/filepath" "strings" + "github.com/cockroachdb/errors" "github.com/moby/buildkit/client/llb" "github.com/sirupsen/logrus" "github.com/spf13/viper" + "github.com/tensorchord/envd/pkg/config" "github.com/tensorchord/envd/pkg/flag" ) @@ -135,11 +138,19 @@ func (g *Graph) compileBase() llb.State { return base } -func (g Graph) copyEnvdSSHServer() llb.State { +func (g Graph) copyEnvdSSHServerWithKey() (llb.State, error) { // TODO(gaocegege): Remove global var ssh image. + public := DefaultGraph.PublicKeyPath + bdat, err := os.ReadFile(public) + dat := strings.TrimSuffix(string(bdat), "\n") + if err != nil { + return llb.State{}, errors.Wrap(err, "Cannot read public SSH key") + } run := llb.Image(viper.GetString(flag.FlagSSHImage)). File(llb.Copy(llb.Image(viper.GetString(flag.FlagSSHImage)), "usr/bin/envd-ssh", "/var/envd/bin/envd-ssh", - &llb.CopyInfo{CreateDestPath: true}), llb.WithCustomName("install envd-ssh")) - return run + &llb.CopyInfo{CreateDestPath: true}), llb.WithCustomName("install envd-ssh")). + File(llb.Mkfile(config.ContainerauthorizedKeysPath, + 0644, []byte(dat+" envd"), llb.WithUIDGID(defaultUID, defaultGID)), llb.WithCustomName("install ssh keys")) + return run, nil } diff --git a/pkg/lang/ir/types.go b/pkg/lang/ir/types.go index 9de932dce..2118dea37 100644 --- a/pkg/lang/ir/types.go +++ b/pkg/lang/ir/types.go @@ -31,6 +31,8 @@ type Graph struct { UbuntuAPTSource *string PyPIMirror *string + PublicKeyPath string + BuiltinSystemPackages []string PyPIPackages []string SystemPackages []string diff --git a/pkg/ssh/config/key.go b/pkg/ssh/config/key.go new file mode 100644 index 000000000..2d4c3e32c --- /dev/null +++ b/pkg/ssh/config/key.go @@ -0,0 +1,162 @@ +// Copyright 2022 The envd Authors +// Copyright 2022 The Okteto Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package sshconfig + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "fmt" + "os" + + "github.com/adrg/xdg" + "github.com/cockroachdb/errors" + "github.com/sirupsen/logrus" + "github.com/tensorchord/envd/pkg/config" + "github.com/tensorchord/envd/pkg/util/fileutil" + "golang.org/x/crypto/ssh" +) + +const ( + bitSize = 4096 +) + +// KeyExists returns true if the okteto key pair exists +func KeyExists(public, private string) bool { + // public, private := getKeyPaths() + publicKeyExists, _ := fileutil.FileExists(public) + if !publicKeyExists { + logrus.Infof("%s doesn't exist", public) + return false + } + + logrus.Infof("%s already present", public) + + privateKeyExists, _ := fileutil.FileExists(private) + if !privateKeyExists { + logrus.Infof("%s doesn't exist", private) + return false + } + + logrus.Infof("%s already present", private) + return true +} + +// GenerateKeys generates a SSH key pair on path +func GenerateKeys() error { + publicKeyPath, privateKeyPath, err := getKeyPaths() + if err != nil { + return err + } + return generateKeys(publicKeyPath, privateKeyPath, bitSize) +} + +func generateKeys(public, private string, bitSize int) error { + privateKey, err := generatePrivateKey(bitSize) + if err != nil { + return fmt.Errorf("failed to generate private SSH key: %s", err) + } + + publicKeyBytes, err := generatePublicKey(&privateKey.PublicKey) + if err != nil { + return fmt.Errorf("failed to generate public SSH key: %s", err) + } + + privateKeyBytes := encodePrivateKeyToPEM(privateKey) + + if err := os.WriteFile(public, publicKeyBytes, 0600); err != nil { + return fmt.Errorf("failed to write public SSH key: %s", err) + } + + if err := os.WriteFile(private, privateKeyBytes, 0600); err != nil { + return fmt.Errorf("failed to write private SSH key: %s", err) + } + + logrus.Infof("created ssh keypair at %s and %s", public, private) + return nil +} + +func generatePrivateKey(bitSize int) (*rsa.PrivateKey, error) { + // Private Key generation + privateKey, err := rsa.GenerateKey(rand.Reader, bitSize) + if err != nil { + return nil, err + } + + // Validate Private Key + err = privateKey.Validate() + if err != nil { + return nil, err + } + + return privateKey, nil +} + +func encodePrivateKeyToPEM(privateKey *rsa.PrivateKey) []byte { + privDER := x509.MarshalPKCS1PrivateKey(privateKey) + + privBlock := pem.Block{ + Type: "RSA PRIVATE KEY", + Headers: nil, + Bytes: privDER, + } + + privatePEM := pem.EncodeToMemory(&privBlock) + + return privatePEM +} + +func generatePublicKey(privatekey *rsa.PublicKey) ([]byte, error) { + publicRsaKey, err := ssh.NewPublicKey(privatekey) + if err != nil { + return nil, err + } + + pubKeyBytes := ssh.MarshalAuthorizedKey(publicRsaKey) + + return pubKeyBytes, nil +} + +func getKeyPaths() (string, string, error) { + public, err := xdg.ConfigFile("envd/" + config.PublicKeyFile) + if err != nil { + return "", "", errors.Wrap(err, "Cannot get public key path") + } + + private, err := xdg.ConfigFile("envd/" + config.PrivateKeyFile) + if err != nil { + return "", "", errors.Wrap(err, "Cannot get private key path") + } + return public, private, nil +} + +// GetPublicKey returns the path to the public key +func GetPublicKey() string { + pub, _, err := getKeyPaths() + if err != nil { + logrus.Fatal("Cannot get public key path") + } + return pub +} + +// GetPrivateKey returns the path to the private key +func GetPrivateKey() string { + _, pri, err := getKeyPaths() + if err != nil { + logrus.Fatal("Cannot get private key path") + } + return pri +} diff --git a/pkg/ssh/ssh_config.go b/pkg/ssh/config/ssh_config.go similarity index 95% rename from pkg/ssh/ssh_config.go rename to pkg/ssh/config/ssh_config.go index db50b3586..dab463e89 100644 --- a/pkg/ssh/ssh_config.go +++ b/pkg/ssh/config/ssh_config.go @@ -1,6 +1,6 @@ // Copyright 2022 The envd Authors // Copyright 2022 The Okteto Authors -// based on https://github.com/havoc-io/ssh_config +// based on https://github.com/havoc-io/sshconfig // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package ssh +package sshconfig import ( "bufio" @@ -47,7 +47,6 @@ type ( } ) -//nolint //will use auth fields in the future const ( forwardAgentKeyword = "ForwardAgent" @@ -238,7 +237,7 @@ func (config *sshConfig) writeToFilepath(p string) error { return fmt.Errorf("failed to get info on %s: %s", p, err) } - // default for ssh_config + // default for sshconfig mode = 0600 } else { mode = stat.Mode() @@ -302,11 +301,11 @@ func buildHostname(name string) string { } // AddEntry adds an entry to the user's sshconfig -func AddEntry(name, iface string, port int) error { - return add(getSSHConfigPath(), buildHostname(name), iface, port) +func AddEntry(name, iface string, port int, privateKeyPath string) error { + return add(getSSHConfigPath(), buildHostname(name), iface, port, privateKeyPath) } -func add(path, name, iface string, port int) error { +func add(path, name, iface string, port int, privateKeyPath string) error { cfg, err := getConfig(path) if err != nil { return err @@ -320,13 +319,13 @@ func add(path, name, iface string, port int) error { host := newHost([]string{name}, []string{"entry generated by envd"}) host.params = []*param{ newParam(forwardAgentKeyword, []string{"yes"}, nil), - // newParam(pubkeyAcceptedKeyTypesKeyword, []string{"+ssh-rsa"}, nil), - // newParam(hostKeyAlgorithms, []string{"+ssh-rsa"}, nil), + newParam(pubkeyAcceptedKeyTypesKeyword, []string{"+ssh-rsa"}, nil), + newParam(hostKeyAlgorithms, []string{"+ssh-rsa"}, nil), newParam(hostNameKeyword, []string{iface}, nil), newParam(portKeyword, []string{strconv.Itoa(port)}, nil), newParam(strictHostKeyCheckingKeyword, []string{"no"}, nil), newParam(userKnownHostsFileKeyword, []string{"/dev/null"}, nil), - // newParam(identityFile, []string{"\"" + privateKey + "\""}, nil), + newParam(identityFile, []string{"\"" + privateKeyPath + "\""}, nil), } cfg.hosts = append(cfg.hosts, host)