diff --git a/sshpiperd/sshpiperd.go b/sshpiperd/sshpiperd.go index 7225d1de..17818497 100644 --- a/sshpiperd/sshpiperd.go +++ b/sshpiperd/sshpiperd.go @@ -72,6 +72,12 @@ func installDrivers(piper *ssh.PiperConfig, config *piperdConfig, logger *log.Lo return upstream.Get(n) }, func(plugin registry.Plugin) error { + + v1, ok := plugin.(upstream.V1Provider) + if ok { + return v1.InstallUpstream(piper) + } + handler := plugin.(upstream.Provider).GetHandler() if handler == nil { diff --git a/sshpiperd/upstream/provider.go b/sshpiperd/upstream/provider.go index 4403f3e2..7659504d 100644 --- a/sshpiperd/upstream/provider.go +++ b/sshpiperd/upstream/provider.go @@ -55,6 +55,12 @@ type Provider interface { GetHandler() Handler } +// temp +type V1Provider interface { + registry.Plugin + InstallUpstream(config *ssh.PiperConfig) error +} + var ( drivers = registry.NewRegistry() ) diff --git a/sshpiperd/upstream/workingdir/config.go b/sshpiperd/upstream/workingdir/config.go index 9dc8bb45..eddee987 100644 --- a/sshpiperd/upstream/workingdir/config.go +++ b/sshpiperd/upstream/workingdir/config.go @@ -2,10 +2,11 @@ package workingdir var ( config = struct { - WorkingDir string `long:"upstream-workingdir" default:"/var/sshpiper" description:"Path to working directory" env:"SSHPIPERD_UPSTREAM_WORKINGDIR" ini-name:"upstream-workingdir"` - AllowBadUsername bool `long:"upstream-workingdir-allowbadusername" description:"Disable username check while search the working directory" env:"SSHPIPERD_UPSTREAM_WORKINGDIR_ALLOWBADUSERNAME" ini-name:"upstream-workingdir-allowbadusername"` - NoCheckPerm bool `long:"upstream-workingdir-nocheckperm" description:"Disable 0400 checking when using files in the working directory" env:"SSHPIPERD_UPSTREAM_WORKINGDIR_NOCHECKPERM" ini-name:"upstream-workingdir-nocheckperm"` - FallbackUsername string `long:"upstream-workingdir-fallbackusername" description:"Fallback to a user when user does not exists in directory" env:"SSHPIPERD_UPSTREAM_WORKINGDIR_FALLBACKUSERNAME" ini-name:"upstream-workingdir-fallbackusername"` - StrictHostKey bool `long:"upstream-workingdir-stricthostkey" description:"Upstream host public key must be in known_hosts file, otherwise drop the connection" env:"SSHPIPERD_UPSTREAM_WORKINGDIR_STRICTHOSTKEY" ini-name:"upstream-workingdir-stricthostkey"` + WorkingDir string `long:"upstream-workingdir" default:"/var/sshpiper" description:"Path to working directory" env:"SSHPIPERD_UPSTREAM_WORKINGDIR" ini-name:"upstream-workingdir"` + AllowBadUsername bool `long:"upstream-workingdir-allowbadusername" description:"Disable username check while search the working directory" env:"SSHPIPERD_UPSTREAM_WORKINGDIR_ALLOWBADUSERNAME" ini-name:"upstream-workingdir-allowbadusername"` + NoCheckPerm bool `long:"upstream-workingdir-nocheckperm" description:"Disable 0400 checking when using files in the working directory" env:"SSHPIPERD_UPSTREAM_WORKINGDIR_NOCHECKPERM" ini-name:"upstream-workingdir-nocheckperm"` + FallbackUsername string `long:"upstream-workingdir-fallbackusername" description:"Fallback to a user when user does not exists in directory" env:"SSHPIPERD_UPSTREAM_WORKINGDIR_FALLBACKUSERNAME" ini-name:"upstream-workingdir-fallbackusername"` + StrictHostKey bool `long:"upstream-workingdir-stricthostkey" description:"Upstream host public key must be in known_hosts file, otherwise drop the connection" env:"SSHPIPERD_UPSTREAM_WORKINGDIR_STRICTHOSTKEY" ini-name:"upstream-workingdir-stricthostkey"` + MatchPublicKeyInSubDir bool `long:"upstream-workingdir-matchpublickeyinsubdir" description:"Remap user in user's sub dir with publickey" env:"SSHPIPERD_UPSTREAM_WORKINGDIR_MATCHPUBLICKeYINSUBDIR" ini-name:"upstream-workingdir-matchpublickeyinsubdir"` }{} ) diff --git a/sshpiperd/upstream/workingdir/pipemgr.go b/sshpiperd/upstream/workingdir/pipemgr.go index 2ef7cb8f..3361554e 100644 --- a/sshpiperd/upstream/workingdir/pipemgr.go +++ b/sshpiperd/upstream/workingdir/pipemgr.go @@ -4,6 +4,7 @@ import ( "fmt" "io/ioutil" "os" + "path" "github.com/tg123/sshpiper/sshpiperd/upstream" ) @@ -20,7 +21,8 @@ func (p *plugin) ListPipe() ([]upstream.Pipe, error) { continue } - data, err := userUpstreamFile.read(file.Name()) + userUpstreamFile := userFile{filename: userUpstreamFile, userdir: path.Join(config.WorkingDir, file.Name())} + data, err := userUpstreamFile.read() if err != nil { continue } @@ -42,12 +44,14 @@ func (p *plugin) ListPipe() ([]upstream.Pipe, error) { } func (p *plugin) CreatePipe(opt upstream.CreatePipeOption) error { - err := os.MkdirAll(config.WorkingDir+"/"+opt.Username, 0775) + userdir := path.Join(config.WorkingDir, opt.Username) + err := os.MkdirAll(userdir, 0775) if err != nil { return err } - path := userUpstreamFile.realPath(opt.Username) + userUpstreamFile := userFile{filename: userUpstreamFile, userdir: userdir} + path := userUpstreamFile.realPath() if _, err := os.Stat(path); os.IsNotExist(err) { upuser := opt.UpstreamUsername @@ -66,7 +70,8 @@ func (p *plugin) CreatePipe(opt upstream.CreatePipeOption) error { } func (p *plugin) RemovePipe(name string) error { - path := userUpstreamFile.realPath(name) + userUpstreamFile := userFile{filename: userUpstreamFile, userdir: path.Join(config.WorkingDir, name)} + path := userUpstreamFile.realPath() if _, err := os.Stat(path); os.IsNotExist(err) { return nil } diff --git a/sshpiperd/upstream/workingdir/v1.go b/sshpiperd/upstream/workingdir/v1.go new file mode 100644 index 00000000..c3db53d0 --- /dev/null +++ b/sshpiperd/upstream/workingdir/v1.go @@ -0,0 +1,133 @@ +package workingdir + +import ( + "fmt" + "net" + "os" + "path" + "path/filepath" + + "github.com/tg123/sshpiper/sshpiperd/v0bridge" + "golang.org/x/crypto/ssh" + "golang.org/x/crypto/ssh/knownhosts" +) + +func (p *plugin) InstallUpstream(piper *ssh.PiperConfig) error { + v0bridge.InstallUpstream(piper, p.GetHandler()) + + piper.PublicKeyCallback = p.matchPublicKeyInSubDir + + return nil +} + +func (p *plugin) matchPublicKeyDir(conn ssh.ConnMetadata, key ssh.PublicKey, user, userdir string) (*ssh.Upstream, error) { + if !checkUsername(user) { + return nil, fmt.Errorf("downstream is not using a valid username") + } + + userUpstreamFile := userFile{filename: userUpstreamFile, userdir: userdir} + err := userUpstreamFile.checkPerm() + + if os.IsNotExist(err) && len(config.FallbackUsername) > 0 { + user = config.FallbackUsername + } else if err != nil { + return nil, err + } + + data, err := userUpstreamFile.read() + if err != nil { + return nil, err + } + + host, port, mappedUser, err := parseUpstreamFile(string(data)) + if err != nil { + return nil, err + } + addr := fmt.Sprintf("%v:%v", host, port) + + logger.Printf("mapping user [%v] to [%v@%v]", user, mappedUser, addr) + + c, err := net.Dial("tcp", addr) + if err != nil { + return nil, err + } + + hostKeyCallback := ssh.InsecureIgnoreHostKey() + + if config.StrictHostKey { + userKnownHosts := userFile{filename: userKnownHosts, userdir: userdir} + hostKeyCallback, err = knownhosts.New(userKnownHosts.realPath()) + + if err != nil { + return nil, err + } + } + + signer, err := mapPublicKeyFromUserfile(conn, user, userdir, key) + if err != nil { + return nil, err + } + + if signer == nil { + return nil, fmt.Errorf("cant find public key in user folder") + } + + return &ssh.Upstream{ + Conn: c, + ClientConfig: ssh.ClientConfig{ + User: mappedUser, + Auth: []ssh.AuthMethod{ssh.PublicKeys(signer)}, + HostKeyCallback: hostKeyCallback, + }, + }, nil +} + +func (p *plugin) matchPublicKeyInSubDir(conn ssh.ConnMetadata, key ssh.PublicKey, _ ssh.ChallengeContext) (*ssh.Upstream, error) { + + { + userdir := path.Join(config.WorkingDir, conn.User()) + u, err := p.matchPublicKeyDir(conn, key, conn.User(), userdir) + if err != nil && !config.MatchPublicKeyInSubDir { + logger.Errorf("cannot map private key in %v: %v", userdir, err) + return nil, err + } + + if u != nil { + return u, nil + } + } + + var upstream *ssh.Upstream + + // search in working dir + filepath.Walk(config.WorkingDir, func(path string, info os.FileInfo, err error) error { + + logger.Debug("search public key in path: %v", path) + if err != nil { + logger.Debug("error walking path: ", err) + return nil + } + + if !info.IsDir() { + return nil + } + + u, err := p.matchPublicKeyDir(conn, key, conn.User(), path) + if err != nil { + logger.Info("cannot map private key in %v: %v, search next", path, err) + } + + if u != nil { + upstream = u + return fmt.Errorf("stop") + } + + return nil + }) + + if upstream != nil { + return upstream, nil + } + + return nil, fmt.Errorf("no matching public key found in %v", config.WorkingDir) +} diff --git a/sshpiperd/upstream/workingdir/workingdir.go b/sshpiperd/upstream/workingdir/workingdir.go index edc8e49b..674e03d3 100644 --- a/sshpiperd/upstream/workingdir/workingdir.go +++ b/sshpiperd/upstream/workingdir/workingdir.go @@ -18,19 +18,24 @@ import ( "github.com/tg123/sshpiper/sshpiperd/upstream" "github.com/tg123/sshpiper/sshpiperd/v0bridge" - "golang.org/x/crypto/ssh/knownhosts" "golang.org/x/crypto/ssh" + "golang.org/x/crypto/ssh/knownhosts" ) -type userFile string +type userFile struct { + filename string + userdir string +} + +const ( + userAuthorizedKeysFile = "authorized_keys" + userKeyFile = "id_rsa" + userUpstreamFile = "sshpiper_upstream" + userKnownHosts = "known_hosts" +) var ( - userAuthorizedKeysFile userFile = "authorized_keys" - userKeyFile userFile = "id_rsa" - userUpstreamFile userFile = "sshpiper_upstream" - userKnownHosts userFile = "known_hosts" - usernameRule *regexp.Regexp ) @@ -41,21 +46,25 @@ func init() { usernameRule, _ = regexp.Compile("^[a-z_][-a-z0-9_]{0,31}$") } -func userSpecFile(user, file string) string { - return path.Join(config.WorkingDir, user, file) +func (file userFile) userSpecFile(filename string) string { + p := file.userdir + if p == "" { + p = config.WorkingDir + } + return path.Join(p, filename) } -func (file userFile) read(user string) ([]byte, error) { - return ioutil.ReadFile(userSpecFile(user, string(file))) +func (file userFile) read() ([]byte, error) { + return ioutil.ReadFile(file.userSpecFile(file.filename)) } -func (file userFile) realPath(user string) string { - return userSpecFile(user, string(file)) +func (file userFile) realPath() string { + return file.userSpecFile(file.filename) } // return error if other and group have access right -func (file userFile) checkPerm(user string) error { - filename := userSpecFile(user, string(file)) +func (file userFile) checkPerm() error { + filename := file.userSpecFile(file.filename) f, err := os.Open(filename) if err != nil { return err @@ -114,14 +123,16 @@ func parseUpstreamFile(data string) (host string, port int, user string, err err return } -func findUpstreamFromUserfile(conn ssh.ConnMetadata, challengeContext ssh.ChallengeContext) (net.Conn, *v0bridge.AuthPipe, error) { +func findUpstreamFromUserfile(conn ssh.ConnMetadata, _ ssh.ChallengeContext) (net.Conn, *v0bridge.AuthPipe, error) { user := conn.User() + userdir := path.Join(config.WorkingDir, conn.User()) if !checkUsername(user) { return nil, nil, fmt.Errorf("downstream is not using a valid username") } - err := userUpstreamFile.checkPerm(user) + userUpstreamFile := userFile{filename: userUpstreamFile, userdir: userdir} + err := userUpstreamFile.checkPerm() if os.IsNotExist(err) && len(config.FallbackUsername) > 0 { user = config.FallbackUsername @@ -129,7 +140,7 @@ func findUpstreamFromUserfile(conn ssh.ConnMetadata, challengeContext ssh.Challe return nil, nil, err } - data, err := userUpstreamFile.read(user) + data, err := userUpstreamFile.read() if err != nil { return nil, nil, err } @@ -150,7 +161,8 @@ func findUpstreamFromUserfile(conn ssh.ConnMetadata, challengeContext ssh.Challe hostKeyCallback := ssh.InsecureIgnoreHostKey() if config.StrictHostKey { - hostKeyCallback, err = knownhosts.New(userKnownHosts.realPath(user)) + userKnownHosts := userFile{filename: userKnownHosts, userdir: userdir} + hostKeyCallback, err = knownhosts.New(userKnownHosts.realPath()) if err != nil { return nil, nil, err @@ -161,7 +173,7 @@ func findUpstreamFromUserfile(conn ssh.ConnMetadata, challengeContext ssh.Challe User: mappedUser, PublicKeyCallback: func(conn ssh.ConnMetadata, key ssh.PublicKey) (v0bridge.AuthPipeType, ssh.AuthMethod, error) { - signer, err := mapPublicKeyFromUserfile(conn, key) + signer, err := mapPublicKeyFromUserfile(conn, user, userdir, key) if err != nil || signer == nil { // try one @@ -175,20 +187,15 @@ func findUpstreamFromUserfile(conn ssh.ConnMetadata, challengeContext ssh.Challe }, nil } -func mapPublicKeyFromUserfile(conn ssh.ConnMetadata, key ssh.PublicKey) (signer ssh.Signer, err error) { - user := conn.User() - - if !checkUsername(user) { - return nil, fmt.Errorf("downstream is not using a valid username") - } - +func mapPublicKeyFromUserfile(conn ssh.ConnMetadata, user, userdir string, key ssh.PublicKey) (signer ssh.Signer, err error) { defer func() { // print error when func exit if err != nil { logger.Printf("mapping private key error: %v, public key auth denied for [%v] from [%v]", err, user, conn.RemoteAddr()) } }() - err = userAuthorizedKeysFile.checkPerm(user) + userAuthorizedKeysFile := userFile{filename: userAuthorizedKeysFile, userdir: userdir} + err = userAuthorizedKeysFile.checkPerm() if os.IsNotExist(err) && len(config.FallbackUsername) > 0 { err = nil @@ -200,11 +207,12 @@ func mapPublicKeyFromUserfile(conn ssh.ConnMetadata, key ssh.PublicKey) (signer keydata := key.Marshal() var rest []byte - rest, err = userAuthorizedKeysFile.read(user) + rest, err = userAuthorizedKeysFile.read() if err != nil { return nil, err } + userKeyFile := userFile{filename: userKeyFile, userdir: userdir} var authedPubkey ssh.PublicKey for len(rest) > 0 { @@ -215,13 +223,13 @@ func mapPublicKeyFromUserfile(conn ssh.ConnMetadata, key ssh.PublicKey) (signer } if bytes.Equal(authedPubkey.Marshal(), keydata) { - err = userKeyFile.checkPerm(user) + err = userKeyFile.checkPerm() if err != nil { return nil, err } var privateBytes []byte - privateBytes, err = userKeyFile.read(user) + privateBytes, err = userKeyFile.read() if err != nil { return nil, err } @@ -233,7 +241,7 @@ func mapPublicKeyFromUserfile(conn ssh.ConnMetadata, key ssh.PublicKey) (signer } // in log may see this twice, one is for query the other is real sign again - logger.Printf("auth succ, using mapped private key [%v] for user [%v] from [%v]", userKeyFile.realPath(user), user, conn.RemoteAddr()) + logger.Printf("auth succ, using mapped private key [%v] for user [%v] from [%v]", userKeyFile.realPath(), user, conn.RemoteAddr()) return private, nil } } diff --git a/sshpiperd/v0bridge/v0bridge.go b/sshpiperd/v0bridge/v0bridge.go index b39e0437..216c796f 100644 --- a/sshpiperd/v0bridge/v0bridge.go +++ b/sshpiperd/v0bridge/v0bridge.go @@ -73,7 +73,7 @@ func (p *proxy) CreateChallengeContext(conn ssh.ConnMetadata) (ssh.ChallengeCont return p, nil } -func (p *proxy) createUpstream(conn net.Conn, pipe *AuthPipe, authType AuthPipeType, oldMethod, mappedMethod ssh.AuthMethod) (*ssh.Upstream, error) { +func CreateUpstream(conn net.Conn, pipe *AuthPipe, authType AuthPipeType, oldMethod, mappedMethod ssh.AuthMethod) (*ssh.Upstream, error) { clientConfig := ssh.ClientConfig{ User: pipe.User, @@ -130,7 +130,7 @@ func (p *proxy) NoneAuthCallback(conn ssh.ConnMetadata, challengeCtx ssh.Challen } if pipe.NoneAuthCallback == nil { - return p.createUpstream(c, pipe, AuthPipeTypePassThrough, ssh.NoneAuth(), nil) + return CreateUpstream(c, pipe, AuthPipeTypePassThrough, ssh.NoneAuth(), nil) } t, m, err := pipe.NoneAuthCallback(conn) @@ -138,7 +138,7 @@ func (p *proxy) NoneAuthCallback(conn ssh.ConnMetadata, challengeCtx ssh.Challen return nil, err } - return p.createUpstream(c, pipe, t, ssh.NoneAuth(), m) + return CreateUpstream(c, pipe, t, ssh.NoneAuth(), m) } func (p *proxy) PasswordCallback(conn ssh.ConnMetadata, password []byte, challengeCtx ssh.ChallengeContext) (*ssh.Upstream, error) { @@ -148,7 +148,7 @@ func (p *proxy) PasswordCallback(conn ssh.ConnMetadata, password []byte, challen } if pipe.PasswordCallback == nil { - return p.createUpstream(c, pipe, AuthPipeTypePassThrough, ssh.Password(string(password)), nil) + return CreateUpstream(c, pipe, AuthPipeTypePassThrough, ssh.Password(string(password)), nil) } t, m, err := pipe.PasswordCallback(conn, password) @@ -156,7 +156,7 @@ func (p *proxy) PasswordCallback(conn ssh.ConnMetadata, password []byte, challen return nil, err } - return p.createUpstream(c, pipe, t, ssh.Password(string(password)), m) + return CreateUpstream(c, pipe, t, ssh.Password(string(password)), m) } func (p *proxy) PublicKeyCallback(conn ssh.ConnMetadata, key ssh.PublicKey, challengeCtx ssh.ChallengeContext) (*ssh.Upstream, error) { @@ -166,7 +166,7 @@ func (p *proxy) PublicKeyCallback(conn ssh.ConnMetadata, key ssh.PublicKey, chal } if pipe.PublicKeyCallback == nil { - return p.createUpstream(c, pipe, AuthPipeTypePassThrough, ssh.NoneAuth(), nil) + return CreateUpstream(c, pipe, AuthPipeTypePassThrough, ssh.NoneAuth(), nil) } t, m, err := pipe.PublicKeyCallback(conn, key) @@ -174,10 +174,13 @@ func (p *proxy) PublicKeyCallback(conn ssh.ConnMetadata, key ssh.PublicKey, chal return nil, err } - return p.createUpstream(c, pipe, t, ssh.NoneAuth(), m) // cannt passthrough public key, use none instead + return CreateUpstream(c, pipe, t, ssh.NoneAuth(), m) // cannt passthrough public key, use none instead } -func InstallUpstream(config *ssh.PiperConfig, handler func(conn ssh.ConnMetadata, challengeContext ssh.ChallengeContext) (net.Conn, *AuthPipe, error)) { +func InstallUpstream(config *ssh.PiperConfig, handler func(conn ssh.ConnMetadata, challengeContext ssh.ChallengeContext) (net.Conn, *AuthPipe, error)) error { + if handler == nil { + return fmt.Errorf("handler is nil") + } p := &proxy{ handler: handler, @@ -192,4 +195,6 @@ func InstallUpstream(config *ssh.PiperConfig, handler func(conn ssh.ConnMetadata config.NoneAuthCallback = p.NoneAuthCallback config.PasswordCallback = p.PasswordCallback config.PublicKeyCallback = p.PublicKeyCallback + + return nil }