add skel plugin for code sharing (#473)

* introduce plugin skel to reuse code

* Refactor code to use libplugin.NewSkelPlugin for plugin/kubernetes/main.go
Add YAML Plugin skel.go for plugin/yaml

* Fix code scanning alert no. 6: Incorrect conversion between integer types

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>

* fix gpt

* fjx password handling in SkelPlugin and skelpipeToWrapper

* go fmt

* Refactor Docker plugin to use skelpipe wrapper

* Refactor skel.go to use container username instead of client username

* revert yaml test order

* fix public and password mess up

* Refactor working dir to use skel

* revert deleted file

* Refactor skel.go to remove unused code and simplify MatchConn function

* Refactor skel.go to read userKnownHosts file in KnownHosts function

* remove workingdirbykey from goreleaser

* Refactor workingdir.go to use libplugin.SplitHostPortForSSH for parsing host and port

* Refactor skel.go to remove unused code and simplify MatchConn function

* merge doc into workingdir

---------

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
This commit is contained in:
Boshi Lian 2024-10-24 23:15:26 -07:00 committed by GitHub
parent adc5d51c33
commit 14ceadb8e8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 1200 additions and 819 deletions

View file

@ -71,6 +71,25 @@ google.com:12345
when `--strict-hostkey` is set, upstream server's public key must present in known_hosts
## Recursive mode (--recursive-search)
`--recursive-search` will search all sub directories of the `username` directory to find the `downstream` key in `authorized_keys` file.
```
├── git
│ ├── bitbucket
│ │ └── sshpiper_upstream
│ ├── github
│ │ ├── authorized_keys
│ │ ├── id_rsa
│ │ └── sshpiper_upstream
│ └── gitlab
│ └── sshpiper_upstream
├── linode....
```
## FAQ
* Q: why sshpiperd still asks for password even I disabled password auth in upstream (different behavior from `v0`)

View file

@ -1,31 +1,10 @@
package main
import (
"fmt"
"path"
"github.com/tg123/sshpiper/libplugin"
"github.com/urfave/cli/v2"
"github.com/tg123/sshpiper/plugin/internal/workingdir"
)
func createWorkingdir(c *cli.Context, user string) (*workingdir.Workingdir, error) {
if !c.Bool("allow-baduser-name") {
if !workingdir.IsUsernameSecure(user) {
return nil, fmt.Errorf("bad username: %s", user)
}
}
root := c.String("root")
return &workingdir.Workingdir{
Path: path.Join(root, user),
NoCheckPerm: c.Bool("no-check-perm"),
Strict: c.Bool("strict-hostkey"),
}, nil
}
func main() {
libplugin.CreateAndRunPluginTemplate(&libplugin.PluginTemplate{
@ -58,64 +37,33 @@ func main() {
Usage: "disable password authentication and only use public key authentication",
EnvVars: []string{"SSHPIPERD_WORKINGDIR_NOPASSWORD_AUTH"},
},
&cli.BoolFlag{
Name: "recursive-search",
Usage: "search subdirectories under user directory for upsteam",
EnvVars: []string{"SSHPIPERD_WORKINGDIR_RECURSIVESEARCH"},
},
},
CreateConfig: func(c *cli.Context) (*libplugin.SshPiperPluginConfig, error) {
return &libplugin.SshPiperPluginConfig{
fac := workdingdirFactory{
root: c.String("root"),
allowBadUsername: c.Bool("allow-baduser-name"),
noPasswordAuth: c.Bool("no-password-auth"),
noCheckPerm: c.Bool("no-check-perm"),
strictHostKey: c.Bool("strict-hostkey"),
recursiveSearch: c.Bool("recursive-search"),
}
NextAuthMethodsCallback: func(_ libplugin.ConnMetadata) ([]string, error) {
if c.Bool("no-password-auth") {
return []string{"publickey"}, nil
}
skel := libplugin.NewSkelPlugin(fac.listPipe)
config := skel.CreateConfig()
config.NextAuthMethodsCallback = func(_ libplugin.ConnMetadata) ([]string, error) {
if fac.noPasswordAuth {
return []string{"publickey"}, nil
}
return []string{"password", "publickey"}, nil
},
PasswordCallback: func(conn libplugin.ConnMetadata, password []byte) (*libplugin.Upstream, error) {
w, err := createWorkingdir(c, conn.User())
if err != nil {
return nil, err
}
u, err := w.CreateUpstream()
if err != nil {
return nil, err
}
u.Auth = libplugin.CreatePasswordAuth(password)
return u, nil
},
PublicKeyCallback: func(conn libplugin.ConnMetadata, key []byte) (*libplugin.Upstream, error) {
w, err := createWorkingdir(c, conn.User())
if err != nil {
return nil, err
}
u, err := w.CreateUpstream()
if err != nil {
return nil, err
}
k, err := w.Mapkey(key)
if err != nil {
return nil, err
}
u.Auth = libplugin.CreatePrivateKeyAuth(k)
return u, nil
},
VerifyHostKeyCallback: func(conn libplugin.ConnMetadata, hostname, netaddr string, key []byte) error {
w, err := createWorkingdir(c, conn.User())
if err != nil {
return err
}
return w.VerifyHostKey(hostname, netaddr, key)
},
}, nil
return []string{"password", "publickey"}, nil
}
return config, nil
},
})
}

166
plugin/workingdir/skel.go Normal file
View file

@ -0,0 +1,166 @@
package main
import (
"fmt"
"os"
"path"
"path/filepath"
log "github.com/sirupsen/logrus"
"github.com/tg123/sshpiper/libplugin"
)
type workdingdirFactory struct {
root string
allowBadUsername bool
noPasswordAuth bool
noCheckPerm bool
strictHostKey bool
recursiveSearch bool
}
type skelpipeWrapper struct {
dir *workingdir
host string
username string
}
type skelpipeFromWrapper struct {
skelpipeWrapper
}
type skelpipePasswordWrapper struct {
skelpipeFromWrapper
}
type skelpipePublicKeyWrapper struct {
skelpipeFromWrapper
}
type skelpipeToWrapper struct {
skelpipeWrapper
}
func (s *skelpipeWrapper) From() []libplugin.SkelPipeFrom {
w := skelpipeFromWrapper{
skelpipeWrapper: *s,
}
if s.dir.Exists(userAuthorizedKeysFile) && s.dir.Exists(userKeyFile) {
return []libplugin.SkelPipeFrom{&skelpipePublicKeyWrapper{
skelpipeFromWrapper: w,
}}
} else {
return []libplugin.SkelPipeFrom{&skelpipePasswordWrapper{
skelpipeFromWrapper: w,
}}
}
}
func (s *skelpipeToWrapper) User(conn libplugin.ConnMetadata) string {
return s.username
}
func (s *skelpipeToWrapper) Host(conn libplugin.ConnMetadata) string {
return s.host
}
func (s *skelpipeToWrapper) IgnoreHostKey(conn libplugin.ConnMetadata) bool {
return !s.dir.Strict
}
func (s *skelpipeToWrapper) KnownHosts(conn libplugin.ConnMetadata) ([]byte, error) {
return s.dir.Readfile(userKnownHosts)
}
func (s *skelpipeFromWrapper) MatchConn(conn libplugin.ConnMetadata) (libplugin.SkelPipeTo, error) {
return &skelpipeToWrapper{
skelpipeWrapper: s.skelpipeWrapper,
}, nil
}
func (s *skelpipePasswordWrapper) TestPassword(conn libplugin.ConnMetadata, password []byte) (bool, error) {
return true, nil // TODO support later
}
func (s *skelpipePublicKeyWrapper) AuthorizedKeys(conn libplugin.ConnMetadata) ([]byte, error) {
return s.dir.Readfile(userAuthorizedKeysFile)
}
func (s *skelpipePublicKeyWrapper) TrustedUserCAKeys(conn libplugin.ConnMetadata) ([]byte, error) {
return nil, nil // TODO support this
}
func (s *skelpipeToWrapper) PrivateKey(conn libplugin.ConnMetadata) ([]byte, []byte, error) {
k, err := s.dir.Readfile(userKeyFile)
if err != nil {
return nil, nil, err
}
return k, nil, nil
}
func (s *skelpipeToWrapper) OverridePassword(conn libplugin.ConnMetadata) ([]byte, error) {
return nil, nil
}
func (wf *workdingdirFactory) listPipe(conn libplugin.ConnMetadata) ([]libplugin.SkelPipe, error) {
user := conn.User()
if !wf.allowBadUsername {
if !isUsernameSecure(user) {
return nil, fmt.Errorf("bad username: %s", user)
}
}
var pipes []libplugin.SkelPipe
userdir := path.Join(wf.root, conn.User())
_ = filepath.Walk(userdir, func(path string, info os.FileInfo, err error) (stop error) {
log.Infof("search upstreams in path: %v", path)
if err != nil {
log.Infof("error walking path: %v", err)
return
}
if !info.IsDir() {
return
}
if !wf.recursiveSearch {
stop = fmt.Errorf("stop")
}
w := &workingdir{
Path: path,
NoCheckPerm: wf.noCheckPerm,
Strict: wf.strictHostKey,
}
data, err := w.Readfile(userUpstreamFile)
if err != nil {
log.Infof("error reading upstream file: %v in %v", err, w.Path)
return
}
host, user, err := parseUpstreamFile(string(data))
if err != nil {
log.Infof("ignore upstream folder %v due to: %v", w.Path, err)
return
}
pipes = append(pipes, &skelpipeWrapper{
dir: w,
host: host,
username: user,
})
return
})
return pipes, nil
}

View file

@ -0,0 +1,109 @@
package main
import (
"bufio"
"fmt"
"os"
"path"
"regexp"
"strings"
"github.com/tg123/sshpiper/libplugin"
)
type workingdir struct {
Path string
NoCheckPerm bool
Strict bool
}
var (
// Base username validation on Debians default: https://sources.debian.net/src/adduser/3.113%2Bnmu3/adduser.conf/#L85
// -> NAME_REGEX="^[a-z][-a-z0-9_]*\$"
// The length is limited to 32 characters. See man 8 useradd: https://linux.die.net/man/8/useradd
usernameRule *regexp.Regexp = regexp.MustCompile("^[a-z_][-a-z0-9_]{0,31}$")
)
const (
userAuthorizedKeysFile = "authorized_keys"
userKeyFile = "id_rsa"
userUpstreamFile = "sshpiper_upstream"
userKnownHosts = "known_hosts"
)
func isUsernameSecure(user string) bool {
return usernameRule.MatchString(user)
}
func (w *workingdir) checkPerm(file string) error {
filename := path.Join(w.Path, file)
f, err := os.Open(filename)
if err != nil {
return err
}
defer f.Close()
fi, err := f.Stat()
if err != nil {
return err
}
if w.NoCheckPerm {
return nil
}
if fi.Mode().Perm()&0077 != 0 {
return fmt.Errorf("%v's perm is too open", filename)
}
return nil
}
func (w *workingdir) fullpath(file string) string {
return path.Join(w.Path, file)
}
func (w *workingdir) Readfile(file string) ([]byte, error) {
if err := w.checkPerm(file); err != nil {
return nil, err
}
return os.ReadFile(w.fullpath(file))
}
func (w *workingdir) Exists(file string) bool {
info, err := os.Stat(w.fullpath(file))
if os.IsNotExist(err) {
return false
}
return !info.IsDir()
}
// TODO refactor this
func parseUpstreamFile(data string) (host string, user string, err error) {
r := bufio.NewReader(strings.NewReader(data))
for {
host, err = r.ReadString('\n')
if err != nil {
break
}
host = strings.TrimSpace(host)
if host != "" && host[0] != '#' {
break
}
}
t := strings.SplitN(host, "@", 2)
if len(t) > 1 {
user = t[0]
host = t[1]
}
_, _, err = libplugin.SplitHostPortForSSH(host)
return
}