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:
parent
adc5d51c33
commit
14ceadb8e8
18 changed files with 1200 additions and 819 deletions
|
|
@ -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`)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
166
plugin/workingdir/skel.go
Normal 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
|
||||
}
|
||||
109
plugin/workingdir/workingdir.go
Normal file
109
plugin/workingdir/workingdir.go
Normal 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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue