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
|
|
@ -116,20 +116,6 @@ builds:
|
||||||
binary: plugins/failtoban
|
binary: plugins/failtoban
|
||||||
tags:
|
tags:
|
||||||
- full
|
- full
|
||||||
- id: plugin_workingdirbykey
|
|
||||||
env:
|
|
||||||
- CGO_ENABLED=0
|
|
||||||
goos:
|
|
||||||
- linux
|
|
||||||
- windows
|
|
||||||
# - darwin
|
|
||||||
goarch:
|
|
||||||
- amd64
|
|
||||||
- arm64
|
|
||||||
main: ./plugin/workingdirbykey
|
|
||||||
binary: plugins/workingdirbykey
|
|
||||||
tags:
|
|
||||||
- full
|
|
||||||
- id: plugin_totp
|
- id: plugin_totp
|
||||||
env:
|
env:
|
||||||
- CGO_ENABLED=0
|
- CGO_ENABLED=0
|
||||||
|
|
|
||||||
312
libplugin/skel.go
Normal file
312
libplugin/skel.go
Normal file
|
|
@ -0,0 +1,312 @@
|
||||||
|
package libplugin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/subtle"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/patrickmn/go-cache"
|
||||||
|
"golang.org/x/crypto/ssh"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SkelPlugin struct {
|
||||||
|
cache *cache.Cache
|
||||||
|
listPipe func(ConnMetadata) ([]SkelPipe, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSkelPlugin(listPipe func(ConnMetadata) ([]SkelPipe, error)) *SkelPlugin {
|
||||||
|
return &SkelPlugin{
|
||||||
|
cache: cache.New(1*time.Minute, 10*time.Minute),
|
||||||
|
listPipe: listPipe,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type SkelPipe interface {
|
||||||
|
From() []SkelPipeFrom
|
||||||
|
}
|
||||||
|
|
||||||
|
type SkelPipeFrom interface {
|
||||||
|
MatchConn(conn ConnMetadata) (SkelPipeTo, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type SkelPipeFromPassword interface {
|
||||||
|
SkelPipeFrom
|
||||||
|
|
||||||
|
TestPassword(conn ConnMetadata, password []byte) (bool, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type SkelPipeFromPublicKey interface {
|
||||||
|
SkelPipeFrom
|
||||||
|
|
||||||
|
AuthorizedKeys(conn ConnMetadata) ([]byte, error)
|
||||||
|
TrustedUserCAKeys(conn ConnMetadata) ([]byte, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type SkelPipeTo interface {
|
||||||
|
Host(conn ConnMetadata) string
|
||||||
|
User(conn ConnMetadata) string
|
||||||
|
IgnoreHostKey(conn ConnMetadata) bool
|
||||||
|
KnownHosts(conn ConnMetadata) ([]byte, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type SkelPipeToPassword interface {
|
||||||
|
SkelPipeTo
|
||||||
|
|
||||||
|
OverridePassword(conn ConnMetadata) ([]byte, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type SkelPipeToPrivateKey interface {
|
||||||
|
SkelPipeTo
|
||||||
|
|
||||||
|
PrivateKey(conn ConnMetadata) ([]byte, []byte, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *SkelPlugin) CreateConfig() *SshPiperPluginConfig {
|
||||||
|
return &SshPiperPluginConfig{
|
||||||
|
NextAuthMethodsCallback: p.SupportedMethods,
|
||||||
|
PasswordCallback: p.PasswordCallback,
|
||||||
|
PublicKeyCallback: p.PublicKeyCallback,
|
||||||
|
VerifyHostKeyCallback: p.VerifyHostKeyCallback,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *SkelPlugin) SupportedMethods(conn ConnMetadata) ([]string, error) {
|
||||||
|
set := make(map[string]bool)
|
||||||
|
|
||||||
|
pipes, err := p.listPipe(conn)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, pipe := range pipes {
|
||||||
|
for _, from := range pipe.From() {
|
||||||
|
|
||||||
|
switch from.(type) {
|
||||||
|
case SkelPipeFromPublicKey:
|
||||||
|
set["publickey"] = true
|
||||||
|
default:
|
||||||
|
set["password"] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(set) == 2 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var methods []string
|
||||||
|
for k := range set {
|
||||||
|
methods = append(methods, k)
|
||||||
|
}
|
||||||
|
|
||||||
|
return methods, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *SkelPlugin) VerifyHostKeyCallback(conn ConnMetadata, hostname, netaddr string, key []byte) error {
|
||||||
|
item, found := p.cache.Get(conn.UniqueID())
|
||||||
|
if !found {
|
||||||
|
log.Warnf("connection expired when verifying host key for conn [%v]", conn.UniqueID())
|
||||||
|
return fmt.Errorf("connection expired")
|
||||||
|
}
|
||||||
|
|
||||||
|
to := item.(SkelPipeTo)
|
||||||
|
|
||||||
|
data, err := to.KnownHosts(conn)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return VerifyHostKeyFromKnownHosts(bytes.NewBuffer(data), hostname, netaddr, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *SkelPlugin) match(conn ConnMetadata, verify func(SkelPipeFrom) (bool, error)) (SkelPipeFrom, SkelPipeTo, error) {
|
||||||
|
pipes, err := p.listPipe(conn)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, pipe := range pipes {
|
||||||
|
for _, from := range pipe.From() {
|
||||||
|
|
||||||
|
to, err := from.MatchConn(conn)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if to == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
ok, err := verify(from)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if ok {
|
||||||
|
return from, to, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil, fmt.Errorf("no matching pipe for username [%v] found", conn.User())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *SkelPlugin) PasswordCallback(conn ConnMetadata, password []byte) (*Upstream, error) {
|
||||||
|
_, to, err := p.match(conn, func(from SkelPipeFrom) (bool, error) {
|
||||||
|
frompass, ok := from.(SkelPipeFromPassword)
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return frompass.TestPassword(conn, password)
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
u, err := p.createUpstream(conn, to)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
toPass, ok := to.(SkelPipeToPassword)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("pipe to does not support password")
|
||||||
|
}
|
||||||
|
|
||||||
|
overridepassword, err := toPass.OverridePassword(conn)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if overridepassword != nil {
|
||||||
|
u.Auth = CreatePasswordAuth(overridepassword)
|
||||||
|
} else {
|
||||||
|
u.Auth = CreatePasswordAuth(password)
|
||||||
|
}
|
||||||
|
|
||||||
|
return u, nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *SkelPlugin) PublicKeyCallback(conn ConnMetadata, publicKey []byte) (*Upstream, error) {
|
||||||
|
|
||||||
|
pubKey, err := ssh.ParsePublicKey(publicKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
pkcert, isCert := pubKey.(*ssh.Certificate)
|
||||||
|
if isCert {
|
||||||
|
// ensure cert is valid first
|
||||||
|
|
||||||
|
if pkcert.CertType != ssh.UserCert {
|
||||||
|
return nil, fmt.Errorf("only user certificates are supported, cert type: %v", pkcert.CertType)
|
||||||
|
}
|
||||||
|
|
||||||
|
certChecker := ssh.CertChecker{}
|
||||||
|
if err := certChecker.CheckCert(conn.User(), pkcert); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_, to, err := p.match(conn, func(from SkelPipeFrom) (bool, error) {
|
||||||
|
// verify public key
|
||||||
|
fromPubKey, ok := from.(SkelPipeFromPublicKey)
|
||||||
|
if !ok {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
verified := false
|
||||||
|
|
||||||
|
if isCert {
|
||||||
|
rest, err := fromPubKey.TrustedUserCAKeys(conn)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf("trusted user ca keys: %v", rest)
|
||||||
|
|
||||||
|
var trustedca ssh.PublicKey
|
||||||
|
for len(rest) > 0 {
|
||||||
|
trustedca, _, _, rest, err = ssh.ParseAuthorizedKey(rest)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if subtle.ConstantTimeCompare(trustedca.Marshal(), pkcert.SignatureKey.Marshal()) == 1 {
|
||||||
|
verified = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
rest, err := fromPubKey.AuthorizedKeys(conn)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var authedPubkey ssh.PublicKey
|
||||||
|
for len(rest) > 0 {
|
||||||
|
authedPubkey, _, _, rest, err = ssh.ParseAuthorizedKey(rest)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if subtle.ConstantTimeCompare(authedPubkey.Marshal(), publicKey) == 1 {
|
||||||
|
verified = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return verified, nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
u, err := p.createUpstream(conn, to)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
toPrivateKey, ok := to.(SkelPipeToPrivateKey)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("pipe to does not support private key")
|
||||||
|
}
|
||||||
|
|
||||||
|
priv, cert, err := toPrivateKey.PrivateKey(conn)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
u.Auth = CreatePrivateKeyAuth(priv, cert)
|
||||||
|
|
||||||
|
return u, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *SkelPlugin) createUpstream(conn ConnMetadata, to SkelPipeTo) (*Upstream, error) {
|
||||||
|
host, port, err := SplitHostPortForSSH(to.Host(conn))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
user := to.User(conn)
|
||||||
|
if user == "" {
|
||||||
|
user = conn.User()
|
||||||
|
}
|
||||||
|
|
||||||
|
p.cache.SetDefault(conn.UniqueID(), to)
|
||||||
|
|
||||||
|
return &Upstream{
|
||||||
|
Host: host,
|
||||||
|
Port: int32(port), // port is already checked to be within int32 range in SplitHostPortForSSH
|
||||||
|
UserName: user,
|
||||||
|
IgnoreHostKey: to.IgnoreHostKey(conn),
|
||||||
|
}, err
|
||||||
|
}
|
||||||
|
|
@ -67,11 +67,12 @@ func SplitHostPortForSSH(addr string) (host string, port int, err error) {
|
||||||
h, p, err := net.SplitHostPort(host)
|
h, p, err := net.SplitHostPort(host)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
host = h
|
host = h
|
||||||
port, err = strconv.Atoi(p)
|
var parsedPort int64
|
||||||
|
parsedPort, err = strconv.ParseInt(p, 10, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
port = int(parsedPort)
|
||||||
} else if host != "" {
|
} else if host != "" {
|
||||||
// test valid after concat :22
|
// test valid after concat :22
|
||||||
if _, _, err = net.SplitHostPort(host + ":22"); err == nil {
|
if _, _, err = net.SplitHostPort(host + ":22"); err == nil {
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,6 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/subtle"
|
|
||||||
"encoding/base64"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
|
|
||||||
|
|
@ -13,8 +11,6 @@ import (
|
||||||
"github.com/docker/docker/api/types/network"
|
"github.com/docker/docker/api/types/network"
|
||||||
"github.com/docker/docker/client"
|
"github.com/docker/docker/client"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"github.com/tg123/sshpiper/libplugin"
|
|
||||||
"golang.org/x/crypto/ssh"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type pipe struct {
|
type pipe struct {
|
||||||
|
|
@ -39,7 +35,7 @@ func newDockerPlugin() (*plugin, error) {
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *plugin) listPipes() ([]pipe, error) {
|
func (p *plugin) list() ([]pipe, error) {
|
||||||
// filter := filters.NewArgs()
|
// filter := filters.NewArgs()
|
||||||
// filter.Add("label", fmt.Sprintf("sshpiper.username=%v", username))
|
// filter.Add("label", fmt.Sprintf("sshpiper.username=%v", username))
|
||||||
|
|
||||||
|
|
@ -115,107 +111,3 @@ func (p *plugin) listPipes() ([]pipe, error) {
|
||||||
|
|
||||||
return pipes, nil
|
return pipes, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *plugin) supportedMethods() ([]string, error) {
|
|
||||||
pipes, err := p.listPipes()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
set := make(map[string]bool)
|
|
||||||
|
|
||||||
for _, pipe := range pipes {
|
|
||||||
if pipe.AuthorizedKeys != "" {
|
|
||||||
set["publickey"] = true // found authorized_keys, so we support publickey
|
|
||||||
} else {
|
|
||||||
set["password"] = true // no authorized_keys, so we support password
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var methods []string
|
|
||||||
for k := range set {
|
|
||||||
methods = append(methods, k)
|
|
||||||
}
|
|
||||||
return methods, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *plugin) createUpstream(conn libplugin.ConnMetadata, to pipe, originPassword string) (*libplugin.Upstream, error) {
|
|
||||||
host, port, err := libplugin.SplitHostPortForSSH(to.Host)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
u := &libplugin.Upstream{
|
|
||||||
Host: host,
|
|
||||||
Port: int32(port),
|
|
||||||
UserName: to.ContainerUsername,
|
|
||||||
IgnoreHostKey: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
// password found
|
|
||||||
if originPassword != "" {
|
|
||||||
u.Auth = libplugin.CreatePasswordAuth([]byte(originPassword))
|
|
||||||
return u, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// try private key
|
|
||||||
data, err := base64.StdEncoding.DecodeString(to.PrivateKey)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if data != nil {
|
|
||||||
u.Auth = libplugin.CreatePrivateKeyAuth(data)
|
|
||||||
return u, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, fmt.Errorf("no password or private key found")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *plugin) findAndCreateUpstream(conn libplugin.ConnMetadata, password string, publicKey []byte) (*libplugin.Upstream, error) {
|
|
||||||
user := conn.User()
|
|
||||||
|
|
||||||
pipes, err := p.listPipes()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, pipe := range pipes {
|
|
||||||
|
|
||||||
// test password
|
|
||||||
if publicKey == nil && password != "" {
|
|
||||||
if pipe.ClientUsername != user {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
return p.createUpstream(conn, pipe, password)
|
|
||||||
}
|
|
||||||
|
|
||||||
// test public key
|
|
||||||
if pipe.ClientUsername != "" {
|
|
||||||
if pipe.ClientUsername != user {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ignore username and match all
|
|
||||||
rest, err := base64.StdEncoding.DecodeString(pipe.AuthorizedKeys)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var authedPubkey ssh.PublicKey
|
|
||||||
for len(rest) > 0 {
|
|
||||||
authedPubkey, _, _, rest, err = ssh.ParseAuthorizedKey(rest)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if subtle.ConstantTimeCompare(authedPubkey.Marshal(), publicKey) == 1 {
|
|
||||||
return p.createUpstream(conn, pipe, "")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, fmt.Errorf("no matching pipe for username [%v] found", user)
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -18,20 +18,8 @@ func main() {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return &libplugin.SshPiperPluginConfig{
|
skel := libplugin.NewSkelPlugin(plugin.listPipe)
|
||||||
|
return skel.CreateConfig(), nil
|
||||||
NextAuthMethodsCallback: func(_ libplugin.ConnMetadata) ([]string, error) {
|
|
||||||
return plugin.supportedMethods()
|
|
||||||
},
|
|
||||||
|
|
||||||
PasswordCallback: func(conn libplugin.ConnMetadata, password []byte) (*libplugin.Upstream, error) {
|
|
||||||
return plugin.findAndCreateUpstream(conn, string(password), nil)
|
|
||||||
},
|
|
||||||
|
|
||||||
PublicKeyCallback: func(conn libplugin.ConnMetadata, key []byte) (*libplugin.Upstream, error) {
|
|
||||||
return plugin.findAndCreateUpstream(conn, "", key)
|
|
||||||
},
|
|
||||||
}, nil
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
130
plugin/docker/skel.go
Normal file
130
plugin/docker/skel.go
Normal file
|
|
@ -0,0 +1,130 @@
|
||||||
|
//go:build full || e2e
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
|
||||||
|
"github.com/tg123/sshpiper/libplugin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type skelpipeWrapper struct {
|
||||||
|
plugin *plugin
|
||||||
|
|
||||||
|
pipe *pipe
|
||||||
|
}
|
||||||
|
|
||||||
|
type skelpipeFromWrapper struct {
|
||||||
|
skelpipeWrapper
|
||||||
|
}
|
||||||
|
|
||||||
|
type skelpipePasswordWrapper struct {
|
||||||
|
skelpipeFromWrapper
|
||||||
|
}
|
||||||
|
|
||||||
|
type skelpipePublicKeyWrapper struct {
|
||||||
|
skelpipeFromWrapper
|
||||||
|
}
|
||||||
|
|
||||||
|
type skelpipeToWrapper struct {
|
||||||
|
skelpipeWrapper
|
||||||
|
|
||||||
|
username string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *skelpipeWrapper) From() []libplugin.SkelPipeFrom {
|
||||||
|
|
||||||
|
w := skelpipeFromWrapper{
|
||||||
|
skelpipeWrapper: *s,
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.pipe.PrivateKey != "" || s.pipe.AuthorizedKeys != "" {
|
||||||
|
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.pipe.Host
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *skelpipeToWrapper) IgnoreHostKey(conn libplugin.ConnMetadata) bool {
|
||||||
|
return true // TODO support this
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *skelpipeToWrapper) KnownHosts(conn libplugin.ConnMetadata) ([]byte, error) {
|
||||||
|
return nil, nil // TODO support this
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *skelpipeFromWrapper) MatchConn(conn libplugin.ConnMetadata) (libplugin.SkelPipeTo, error) {
|
||||||
|
user := conn.User()
|
||||||
|
|
||||||
|
matched := s.pipe.ClientUsername == user || s.pipe.ClientUsername == ""
|
||||||
|
targetuser := s.pipe.ContainerUsername
|
||||||
|
|
||||||
|
if targetuser == "" {
|
||||||
|
targetuser = user
|
||||||
|
}
|
||||||
|
|
||||||
|
if matched {
|
||||||
|
return &skelpipeToWrapper{
|
||||||
|
skelpipeWrapper: s.skelpipeWrapper,
|
||||||
|
username: targetuser,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *skelpipePasswordWrapper) TestPassword(conn libplugin.ConnMetadata, password []byte) (bool, error) {
|
||||||
|
return true, nil // do not test input password
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *skelpipePublicKeyWrapper) AuthorizedKeys(conn libplugin.ConnMetadata) ([]byte, error) {
|
||||||
|
return base64.StdEncoding.DecodeString(s.pipe.AuthorizedKeys)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 := base64.StdEncoding.DecodeString(s.pipe.PrivateKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return k, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *skelpipeToWrapper) OverridePassword(conn libplugin.ConnMetadata) ([]byte, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *plugin) listPipe(_ libplugin.ConnMetadata) ([]libplugin.SkelPipe, error) {
|
||||||
|
dpipes, err := p.list()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var pipes []libplugin.SkelPipe
|
||||||
|
for _, pipe := range dpipes {
|
||||||
|
wrapper := &skelpipeWrapper{
|
||||||
|
plugin: p,
|
||||||
|
pipe: &pipe,
|
||||||
|
}
|
||||||
|
pipes = append(pipes, wrapper)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return pipes, nil
|
||||||
|
}
|
||||||
|
|
@ -1,23 +1,9 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"crypto/subtle"
|
|
||||||
"encoding/base64"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"regexp"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
gocache "github.com/patrickmn/go-cache"
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
"github.com/tg123/go-htpasswd"
|
|
||||||
"github.com/tg123/sshpiper/libplugin"
|
|
||||||
piperv1beta1 "github.com/tg123/sshpiper/plugin/kubernetes/apis/sshpiper/v1beta1"
|
piperv1beta1 "github.com/tg123/sshpiper/plugin/kubernetes/apis/sshpiper/v1beta1"
|
||||||
sshpiper "github.com/tg123/sshpiper/plugin/kubernetes/generated/clientset/versioned"
|
sshpiper "github.com/tg123/sshpiper/plugin/kubernetes/generated/clientset/versioned"
|
||||||
piperlister "github.com/tg123/sshpiper/plugin/kubernetes/generated/listers/sshpiper/v1beta1"
|
piperlister "github.com/tg123/sshpiper/plugin/kubernetes/generated/listers/sshpiper/v1beta1"
|
||||||
"golang.org/x/crypto/ssh"
|
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/fields"
|
"k8s.io/apimachinery/pkg/fields"
|
||||||
"k8s.io/apimachinery/pkg/labels"
|
"k8s.io/apimachinery/pkg/labels"
|
||||||
|
|
@ -31,7 +17,6 @@ type plugin struct {
|
||||||
k8sclient corev1.CoreV1Interface
|
k8sclient corev1.CoreV1Interface
|
||||||
lister piperlister.PipeLister
|
lister piperlister.PipeLister
|
||||||
stop chan<- struct{}
|
stop chan<- struct{}
|
||||||
cache *gocache.Cache
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func newKubernetesPlugin(allNamespaces bool, kubeConfigPath string) (*plugin, error) {
|
func newKubernetesPlugin(allNamespaces bool, kubeConfigPath string) (*plugin, error) {
|
||||||
|
|
@ -77,7 +62,6 @@ func newKubernetesPlugin(allNamespaces bool, kubeConfigPath string) (*plugin, er
|
||||||
k8sclient: k8sclient.CoreV1(),
|
k8sclient: k8sclient.CoreV1(),
|
||||||
lister: lister,
|
lister: lister,
|
||||||
stop: stop,
|
stop: stop,
|
||||||
cache: gocache.New(1*time.Minute, 10*time.Minute),
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -88,221 +72,3 @@ func (p *plugin) Stop() {
|
||||||
func (p *plugin) list() ([]*piperv1beta1.Pipe, error) {
|
func (p *plugin) list() ([]*piperv1beta1.Pipe, error) {
|
||||||
return p.lister.List(labels.Everything())
|
return p.lister.List(labels.Everything())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *plugin) supportedMethods() ([]string, error) {
|
|
||||||
pipes, err := p.list()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
set := make(map[string]bool)
|
|
||||||
|
|
||||||
for _, pipe := range pipes {
|
|
||||||
for _, from := range pipe.Spec.From {
|
|
||||||
if from.AuthorizedKeysData != "" || from.AuthorizedKeysFile != "" {
|
|
||||||
set["publickey"] = true // found authorized_keys, so we support publickey
|
|
||||||
} else {
|
|
||||||
set["password"] = true // no authorized_keys, so we support password
|
|
||||||
}
|
|
||||||
|
|
||||||
if from.HtpasswdData != "" || from.HtpasswdFile != "" {
|
|
||||||
set["password"] = true // found htpasswd, so we support password
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var methods []string
|
|
||||||
for k := range set {
|
|
||||||
methods = append(methods, k)
|
|
||||||
}
|
|
||||||
return methods, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *plugin) verifyHostKey(conn libplugin.ConnMetadata, hostname, netaddr string, key []byte) error {
|
|
||||||
item, found := p.cache.Get(conn.UniqueID())
|
|
||||||
|
|
||||||
if !found {
|
|
||||||
return fmt.Errorf("connection expired")
|
|
||||||
}
|
|
||||||
|
|
||||||
pipe := item.(*piperv1beta1.Pipe)
|
|
||||||
to := pipe.Spec.To
|
|
||||||
|
|
||||||
data, err := base64.StdEncoding.DecodeString(to.KnownHostsData)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return libplugin.VerifyHostKeyFromKnownHosts(bytes.NewBuffer(data), hostname, netaddr, key)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *plugin) createUpstream(conn libplugin.ConnMetadata, pipe *piperv1beta1.Pipe, originPassword string) (*libplugin.Upstream, error) {
|
|
||||||
to := pipe.Spec.To
|
|
||||||
host, port, err := libplugin.SplitHostPortForSSH(to.Host)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
u := &libplugin.Upstream{
|
|
||||||
Host: host,
|
|
||||||
Port: int32(port),
|
|
||||||
UserName: to.Username,
|
|
||||||
IgnoreHostKey: to.IgnoreHostkey,
|
|
||||||
}
|
|
||||||
|
|
||||||
if to.PrivateKeySecret.Name != "" {
|
|
||||||
log.Debugf("mapping to %v private key using secret %v", to.Host, to.PrivateKeySecret.Name)
|
|
||||||
secret, err := p.k8sclient.Secrets(pipe.Namespace).Get(context.Background(), to.PrivateKeySecret.Name, metav1.GetOptions{})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
anno := pipe.GetAnnotations()
|
|
||||||
var publicKey []byte
|
|
||||||
var privateKey []byte
|
|
||||||
|
|
||||||
for _, k := range []string{anno["privatekey_field_name"], "ssh-privatekey", "privatekey"} {
|
|
||||||
data := secret.Data[k]
|
|
||||||
if data != nil {
|
|
||||||
log.Debugf("found private key in secret %v/%v", to.PrivateKeySecret.Name, k)
|
|
||||||
privateKey = data
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, k := range []string{anno["publickey_field_name"], "ssh-publickey-cert", "publickey-cert", "ssh-publickey", "publickey"} {
|
|
||||||
data := secret.Data[k]
|
|
||||||
if data != nil {
|
|
||||||
log.Debugf("found publickey key cert in secret %v/%v", to.PrivateKeySecret.Name, k)
|
|
||||||
publicKey = data
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if privateKey != nil {
|
|
||||||
u.Auth = libplugin.CreatePrivateKeyAuth(privateKey, publicKey)
|
|
||||||
p.cache.Set(conn.UniqueID(), pipe, gocache.DefaultExpiration)
|
|
||||||
return u, nil
|
|
||||||
}
|
|
||||||
} else if to.PasswordSecret.Name != "" {
|
|
||||||
log.Debugf("mapping to %v password using secret %v", to.Host, to.PasswordSecret.Name)
|
|
||||||
secret, err := p.k8sclient.Secrets(pipe.Namespace).Get(context.Background(), to.PasswordSecret.Name, metav1.GetOptions{})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
anno := pipe.GetAnnotations()
|
|
||||||
for _, k := range []string{anno["password_field_name"], "password"} {
|
|
||||||
data := secret.Data[k]
|
|
||||||
if data != nil {
|
|
||||||
log.Debugf("found password in secret %v/%v", to.PasswordSecret.Name, k)
|
|
||||||
u.Auth = libplugin.CreatePasswordAuth(data)
|
|
||||||
p.cache.Set(conn.UniqueID(), pipe, gocache.DefaultExpiration)
|
|
||||||
return u, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if originPassword != "" {
|
|
||||||
log.Debugf("mapping to %v using user input password", to.Host)
|
|
||||||
u.Auth = libplugin.CreatePasswordAuth([]byte(originPassword))
|
|
||||||
p.cache.Set(conn.UniqueID(), pipe, gocache.DefaultExpiration)
|
|
||||||
return u, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, fmt.Errorf("no password or private key found")
|
|
||||||
}
|
|
||||||
|
|
||||||
func loadStringAndFile(base64orraw string, filepath string) ([][]byte, error) {
|
|
||||||
|
|
||||||
all := make([][]byte, 0, 2)
|
|
||||||
|
|
||||||
if base64orraw != "" {
|
|
||||||
data, err := base64.StdEncoding.DecodeString(base64orraw)
|
|
||||||
if err != nil {
|
|
||||||
data = []byte(base64orraw)
|
|
||||||
}
|
|
||||||
|
|
||||||
all = append(all, data)
|
|
||||||
}
|
|
||||||
|
|
||||||
if filepath != "" {
|
|
||||||
data, err := os.ReadFile(filepath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
all = append(all, data)
|
|
||||||
}
|
|
||||||
|
|
||||||
return all, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *plugin) findAndCreateUpstream(conn libplugin.ConnMetadata, password string, publicKey []byte) (*libplugin.Upstream, error) {
|
|
||||||
user := conn.User()
|
|
||||||
|
|
||||||
pipes, err := p.list()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, pipe := range pipes {
|
|
||||||
for _, from := range pipe.Spec.From {
|
|
||||||
matched := from.Username == user
|
|
||||||
|
|
||||||
if from.UsernameRegexMatch {
|
|
||||||
matched, _ = regexp.MatchString(from.Username, user)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !matched {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if publicKey == nil && password != "" {
|
|
||||||
|
|
||||||
pwds, err := loadStringAndFile(from.HtpasswdData, from.HtpasswdFile)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
pwdmatched := len(pwds) == 0
|
|
||||||
|
|
||||||
for _, data := range pwds {
|
|
||||||
log.Debugf("try to match password using htpasswd")
|
|
||||||
auth, err := htpasswd.NewFromReader(bytes.NewReader(data), htpasswd.DefaultSystems, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if auth.Match(user, password) {
|
|
||||||
pwdmatched = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if pwdmatched {
|
|
||||||
return p.createUpstream(conn, pipe, password)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Debugf("try to match public using authorized key")
|
|
||||||
pubkeydata, err := loadStringAndFile(from.AuthorizedKeysData, from.AuthorizedKeysFile)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, rest := range pubkeydata {
|
|
||||||
var authedPubkey ssh.PublicKey
|
|
||||||
for len(rest) > 0 {
|
|
||||||
authedPubkey, _, _, rest, err = ssh.ParseAuthorizedKey(rest)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if subtle.ConstantTimeCompare(authedPubkey.Marshal(), publicKey) == 1 {
|
|
||||||
return p.createUpstream(conn, pipe, "")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, fmt.Errorf("no matching pipe for username [%v] found", user)
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -28,23 +28,8 @@ func main() {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return &libplugin.SshPiperPluginConfig{
|
skel := libplugin.NewSkelPlugin(plugin.listPipe)
|
||||||
NextAuthMethodsCallback: func(_ libplugin.ConnMetadata) ([]string, error) {
|
return skel.CreateConfig(), nil
|
||||||
return plugin.supportedMethods()
|
|
||||||
},
|
|
||||||
|
|
||||||
PasswordCallback: func(conn libplugin.ConnMetadata, password []byte) (*libplugin.Upstream, error) {
|
|
||||||
return plugin.findAndCreateUpstream(conn, string(password), nil)
|
|
||||||
},
|
|
||||||
|
|
||||||
PublicKeyCallback: func(conn libplugin.ConnMetadata, key []byte) (*libplugin.Upstream, error) {
|
|
||||||
return plugin.findAndCreateUpstream(conn, "", key)
|
|
||||||
},
|
|
||||||
|
|
||||||
VerifyHostKeyCallback: func(conn libplugin.ConnMetadata, hostname, netaddr string, key []byte) error {
|
|
||||||
return plugin.verifyHostKey(conn, hostname, netaddr, key)
|
|
||||||
},
|
|
||||||
}, nil
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
257
plugin/kubernetes/skel.go
Normal file
257
plugin/kubernetes/skel.go
Normal file
|
|
@ -0,0 +1,257 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
|
"os"
|
||||||
|
"regexp"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"github.com/tg123/go-htpasswd"
|
||||||
|
"github.com/tg123/sshpiper/libplugin"
|
||||||
|
piperv1beta1 "github.com/tg123/sshpiper/plugin/kubernetes/apis/sshpiper/v1beta1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
type skelpipeWrapper struct {
|
||||||
|
plugin *plugin
|
||||||
|
|
||||||
|
pipe *piperv1beta1.Pipe
|
||||||
|
}
|
||||||
|
|
||||||
|
type skelpipeFromWrapper struct {
|
||||||
|
plugin *plugin
|
||||||
|
|
||||||
|
pipe *piperv1beta1.Pipe
|
||||||
|
from *piperv1beta1.FromSpec
|
||||||
|
to *piperv1beta1.ToSpec
|
||||||
|
}
|
||||||
|
|
||||||
|
type skelpipePasswordWrapper struct {
|
||||||
|
skelpipeFromWrapper
|
||||||
|
}
|
||||||
|
|
||||||
|
type skelpipePublicKeyWrapper struct {
|
||||||
|
skelpipeFromWrapper
|
||||||
|
}
|
||||||
|
|
||||||
|
type skelpipeToWrapper struct {
|
||||||
|
plugin *plugin
|
||||||
|
|
||||||
|
pipe *piperv1beta1.Pipe
|
||||||
|
username string
|
||||||
|
to *piperv1beta1.ToSpec
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *skelpipeWrapper) From() []libplugin.SkelPipeFrom {
|
||||||
|
var froms []libplugin.SkelPipeFrom
|
||||||
|
for _, f := range s.pipe.Spec.From {
|
||||||
|
|
||||||
|
w := &skelpipeFromWrapper{
|
||||||
|
plugin: s.plugin,
|
||||||
|
pipe: s.pipe,
|
||||||
|
from: &f,
|
||||||
|
to: &s.pipe.Spec.To,
|
||||||
|
}
|
||||||
|
|
||||||
|
if f.AuthorizedKeysData != "" || f.AuthorizedKeysFile != "" {
|
||||||
|
froms = append(froms, &skelpipePublicKeyWrapper{
|
||||||
|
skelpipeFromWrapper: *w,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
froms = append(froms, &skelpipePasswordWrapper{
|
||||||
|
skelpipeFromWrapper: *w,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return froms
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *skelpipeToWrapper) User(conn libplugin.ConnMetadata) string {
|
||||||
|
return s.username
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *skelpipeToWrapper) Host(conn libplugin.ConnMetadata) string {
|
||||||
|
return s.to.Host
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *skelpipeToWrapper) IgnoreHostKey(conn libplugin.ConnMetadata) bool {
|
||||||
|
return s.to.IgnoreHostkey
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *skelpipeToWrapper) KnownHosts(conn libplugin.ConnMetadata) ([]byte, error) {
|
||||||
|
return base64.StdEncoding.DecodeString(s.to.KnownHostsData)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *skelpipeFromWrapper) MatchConn(conn libplugin.ConnMetadata) (libplugin.SkelPipeTo, error) {
|
||||||
|
user := conn.User()
|
||||||
|
|
||||||
|
matched := s.from.Username == user
|
||||||
|
targetuser := s.to.Username
|
||||||
|
|
||||||
|
if targetuser == "" {
|
||||||
|
targetuser = user
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.from.UsernameRegexMatch {
|
||||||
|
re, err := regexp.Compile(s.from.Username)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
matched = re.MatchString(user)
|
||||||
|
|
||||||
|
if matched {
|
||||||
|
targetuser = re.ReplaceAllString(user, s.to.Username)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if matched {
|
||||||
|
return &skelpipeToWrapper{
|
||||||
|
plugin: s.plugin,
|
||||||
|
pipe: s.pipe,
|
||||||
|
username: targetuser,
|
||||||
|
to: s.to,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *skelpipePasswordWrapper) TestPassword(conn libplugin.ConnMetadata, password []byte) (bool, error) {
|
||||||
|
|
||||||
|
pwds, err := loadStringAndFile(s.from.HtpasswdData, s.from.HtpasswdFile)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
pwdmatched := len(pwds) == 0
|
||||||
|
|
||||||
|
for _, data := range pwds {
|
||||||
|
log.Debugf("try to match password using htpasswd")
|
||||||
|
auth, err := htpasswd.NewFromReader(bytes.NewReader(data), htpasswd.DefaultSystems, nil)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if auth.Match(conn.User(), string(password)) {
|
||||||
|
pwdmatched = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return pwdmatched, nil // yaml do not test input password
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *skelpipePublicKeyWrapper) AuthorizedKeys(conn libplugin.ConnMetadata) ([]byte, error) {
|
||||||
|
byteSlices, err := loadStringAndFile(s.from.AuthorizedKeysData, s.from.AuthorizedKeysFile)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return bytes.Join(byteSlices, []byte("\n")), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *skelpipePublicKeyWrapper) TrustedUserCAKeys(conn libplugin.ConnMetadata) ([]byte, error) {
|
||||||
|
return nil, nil // TODO support trusted_user_ca_keys
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *skelpipeToWrapper) PrivateKey(conn libplugin.ConnMetadata) ([]byte, []byte, error) {
|
||||||
|
|
||||||
|
log.Debugf("mapping to %v private key using secret %v", s.to.Host, s.to.PrivateKeySecret.Name)
|
||||||
|
secret, err := s.plugin.k8sclient.Secrets(s.pipe.Namespace).Get(context.Background(), s.to.PrivateKeySecret.Name, metav1.GetOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
anno := s.pipe.GetAnnotations()
|
||||||
|
var publicKey []byte
|
||||||
|
var privateKey []byte
|
||||||
|
|
||||||
|
for _, k := range []string{anno["privatekey_field_name"], "ssh-privatekey", "privatekey"} {
|
||||||
|
data := secret.Data[k]
|
||||||
|
if data != nil {
|
||||||
|
log.Debugf("found private key in secret %v/%v", s.to.PrivateKeySecret.Name, k)
|
||||||
|
privateKey = data
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, k := range []string{anno["publickey_field_name"], "ssh-publickey-cert", "publickey-cert", "ssh-publickey", "publickey"} {
|
||||||
|
data := secret.Data[k]
|
||||||
|
if data != nil {
|
||||||
|
log.Debugf("found publickey key cert in secret %v/%v", s.to.PrivateKeySecret.Name, k)
|
||||||
|
publicKey = data
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return privateKey, publicKey, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *skelpipeToWrapper) OverridePassword(conn libplugin.ConnMetadata) ([]byte, error) {
|
||||||
|
if s.to.PasswordSecret.Name != "" {
|
||||||
|
log.Debugf("mapping to %v password using secret %v", s.to.Host, s.to.PasswordSecret.Name)
|
||||||
|
secret, err := s.plugin.k8sclient.Secrets(s.pipe.Namespace).Get(context.Background(), s.to.PasswordSecret.Name, metav1.GetOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
anno := s.pipe.GetAnnotations()
|
||||||
|
for _, k := range []string{anno["password_field_name"], "password"} {
|
||||||
|
data := secret.Data[k]
|
||||||
|
if data != nil {
|
||||||
|
log.Debugf("found password in secret %v/%v", s.to.PasswordSecret.Name, k)
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Warnf("password field not found in secret %v", s.to.PasswordSecret.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadStringAndFile(base64orraw string, filepath string) ([][]byte, error) {
|
||||||
|
|
||||||
|
all := make([][]byte, 0, 2)
|
||||||
|
|
||||||
|
if base64orraw != "" {
|
||||||
|
data, err := base64.StdEncoding.DecodeString(base64orraw)
|
||||||
|
if err != nil {
|
||||||
|
data = []byte(base64orraw)
|
||||||
|
}
|
||||||
|
|
||||||
|
all = append(all, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
if filepath != "" {
|
||||||
|
data, err := os.ReadFile(filepath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
all = append(all, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
return all, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *plugin) listPipe(_ libplugin.ConnMetadata) ([]libplugin.SkelPipe, error) {
|
||||||
|
kpipes, err := p.list()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var pipes []libplugin.SkelPipe
|
||||||
|
for _, pipe := range kpipes {
|
||||||
|
wrapper := &skelpipeWrapper{
|
||||||
|
plugin: p,
|
||||||
|
pipe: pipe,
|
||||||
|
}
|
||||||
|
pipes = append(pipes, wrapper)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return pipes, nil
|
||||||
|
}
|
||||||
|
|
@ -71,6 +71,25 @@ google.com:12345
|
||||||
|
|
||||||
when `--strict-hostkey` is set, upstream server's public key must present in known_hosts
|
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
|
## FAQ
|
||||||
* Q: why sshpiperd still asks for password even I disabled password auth in upstream (different behavior from `v0`)
|
* Q: why sshpiperd still asks for password even I disabled password auth in upstream (different behavior from `v0`)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,31 +1,10 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"path"
|
|
||||||
|
|
||||||
"github.com/tg123/sshpiper/libplugin"
|
"github.com/tg123/sshpiper/libplugin"
|
||||||
"github.com/urfave/cli/v2"
|
"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() {
|
func main() {
|
||||||
|
|
||||||
libplugin.CreateAndRunPluginTemplate(&libplugin.PluginTemplate{
|
libplugin.CreateAndRunPluginTemplate(&libplugin.PluginTemplate{
|
||||||
|
|
@ -58,64 +37,33 @@ func main() {
|
||||||
Usage: "disable password authentication and only use public key authentication",
|
Usage: "disable password authentication and only use public key authentication",
|
||||||
EnvVars: []string{"SSHPIPERD_WORKINGDIR_NOPASSWORD_AUTH"},
|
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) {
|
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) {
|
skel := libplugin.NewSkelPlugin(fac.listPipe)
|
||||||
if c.Bool("no-password-auth") {
|
config := skel.CreateConfig()
|
||||||
return []string{"publickey"}, nil
|
config.NextAuthMethodsCallback = func(_ libplugin.ConnMetadata) ([]string, error) {
|
||||||
}
|
if fac.noPasswordAuth {
|
||||||
|
return []string{"publickey"}, nil
|
||||||
|
}
|
||||||
|
|
||||||
return []string{"password", "publickey"}, nil
|
return []string{"password", "publickey"}, nil
|
||||||
},
|
}
|
||||||
|
return config, 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
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
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
|
||||||
|
}
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
# Publickey based Working Directory plugin for sshpiperd
|
|
||||||
|
|
||||||
This plugin is smilar to the [workingdir](../workingdir/) plugin, but it uses public key to route.
|
|
||||||
|
|
||||||
workingdir tree
|
|
||||||
|
|
||||||
```
|
|
||||||
├── git
|
|
||||||
│ ├── bitbucket
|
|
||||||
│ │ └── sshpiper_upstream
|
|
||||||
│ ├── github
|
|
||||||
│ │ ├── authorized_keys
|
|
||||||
│ │ ├── id_rsa
|
|
||||||
│ │ └── sshpiper_upstream
|
|
||||||
│ └── gitlab
|
|
||||||
│ └── sshpiper_upstream
|
|
||||||
├── linode....
|
|
||||||
```
|
|
||||||
|
|
||||||
The plugin will search across all sub directories of the `username` directory to see if the `downstream` key is in `authorized_keys` file.
|
|
||||||
The first matched sub directory will be used to route to the upstream.
|
|
||||||
|
|
@ -1,100 +0,0 @@
|
||||||
//go:build full
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"path"
|
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
"github.com/tg123/sshpiper/libplugin"
|
|
||||||
"github.com/urfave/cli/v2"
|
|
||||||
|
|
||||||
"github.com/tg123/sshpiper/plugin/internal/workingdir"
|
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
|
|
||||||
libplugin.CreateAndRunPluginTemplate(&libplugin.PluginTemplate{
|
|
||||||
Name: "workingdir",
|
|
||||||
Usage: "sshpiperd workingdir plugin",
|
|
||||||
Flags: []cli.Flag{
|
|
||||||
&cli.StringFlag{
|
|
||||||
Name: "root",
|
|
||||||
Usage: "path to root working directory",
|
|
||||||
Value: "/var/sshpiper",
|
|
||||||
EnvVars: []string{"SSHPIPERD_WORKINGDIR_ROOT"},
|
|
||||||
},
|
|
||||||
&cli.BoolFlag{
|
|
||||||
Name: "allow-baduser-name",
|
|
||||||
Usage: "allow bad username",
|
|
||||||
EnvVars: []string{"SSHPIPERD_WORKINGDIR_ALLOWBADUSERNAME"},
|
|
||||||
},
|
|
||||||
&cli.BoolFlag{
|
|
||||||
Name: "no-check-perm",
|
|
||||||
Usage: "disable 0400 checking",
|
|
||||||
EnvVars: []string{"SSHPIPERD_WORKINGDIR_NOCHECKPERM"},
|
|
||||||
},
|
|
||||||
&cli.BoolFlag{
|
|
||||||
Name: "strict-hostkey",
|
|
||||||
Usage: "upstream host public key must be in known_hosts file, otherwise drop the connection",
|
|
||||||
EnvVars: []string{"SSHPIPERD_WORKINGDIR_STRICTHOSTKEY"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
CreateConfig: func(c *cli.Context) (*libplugin.SshPiperPluginConfig, error) {
|
|
||||||
|
|
||||||
root := c.String("root")
|
|
||||||
|
|
||||||
return &libplugin.SshPiperPluginConfig{
|
|
||||||
PublicKeyCallback: func(conn libplugin.ConnMetadata, key []byte) (*libplugin.Upstream, error) {
|
|
||||||
|
|
||||||
userdir := path.Join(root, conn.User())
|
|
||||||
|
|
||||||
var upstream *libplugin.Upstream
|
|
||||||
|
|
||||||
_ = filepath.Walk(userdir, func(path string, info os.FileInfo, err error) error {
|
|
||||||
|
|
||||||
log.Infof("search public key in path: %v", path)
|
|
||||||
if err != nil {
|
|
||||||
log.Infof("error walking path: %v", err)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if !info.IsDir() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
w := &workingdir.Workingdir{
|
|
||||||
Path: path,
|
|
||||||
NoCheckPerm: c.Bool("no-check-perm"),
|
|
||||||
Strict: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
u, err := w.CreateUpstream()
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
k, err := w.Mapkey(key)
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
u.Auth = libplugin.CreatePrivateKeyAuth(k)
|
|
||||||
upstream = u
|
|
||||||
return fmt.Errorf("stop")
|
|
||||||
})
|
|
||||||
|
|
||||||
if upstream != nil {
|
|
||||||
return upstream, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, fmt.Errorf("no matching public key found in %v", userdir)
|
|
||||||
},
|
|
||||||
}, nil
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
@ -8,7 +8,6 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|
||||||
plugin := newYamlPlugin()
|
plugin := newYamlPlugin()
|
||||||
|
|
||||||
libplugin.CreateAndRunPluginTemplate(&libplugin.PluginTemplate{
|
libplugin.CreateAndRunPluginTemplate(&libplugin.PluginTemplate{
|
||||||
|
|
@ -30,25 +29,8 @@ func main() {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
CreateConfig: func(c *cli.Context) (*libplugin.SshPiperPluginConfig, error) {
|
CreateConfig: func(c *cli.Context) (*libplugin.SshPiperPluginConfig, error) {
|
||||||
|
skel := libplugin.NewSkelPlugin(plugin.listPipe)
|
||||||
return &libplugin.SshPiperPluginConfig{
|
return skel.CreateConfig(), nil
|
||||||
|
|
||||||
NextAuthMethodsCallback: func(_ libplugin.ConnMetadata) ([]string, error) {
|
|
||||||
return plugin.supportedMethods()
|
|
||||||
},
|
|
||||||
|
|
||||||
PasswordCallback: func(conn libplugin.ConnMetadata, password []byte) (*libplugin.Upstream, error) {
|
|
||||||
return plugin.findAndCreateUpstream(conn, string(password), nil)
|
|
||||||
},
|
|
||||||
|
|
||||||
PublicKeyCallback: func(conn libplugin.ConnMetadata, key []byte) (*libplugin.Upstream, error) {
|
|
||||||
return plugin.findAndCreateUpstream(conn, "", key)
|
|
||||||
},
|
|
||||||
|
|
||||||
VerifyHostKeyCallback: func(conn libplugin.ConnMetadata, hostname, netaddr string, key []byte) error {
|
|
||||||
return plugin.verifyHostKey(conn, hostname, netaddr, key)
|
|
||||||
},
|
|
||||||
}, nil
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
163
plugin/yaml/skel.go
Normal file
163
plugin/yaml/skel.go
Normal file
|
|
@ -0,0 +1,163 @@
|
||||||
|
//go:build full || e2e
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"regexp"
|
||||||
|
|
||||||
|
"github.com/tg123/sshpiper/libplugin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type skelpipeWrapper struct {
|
||||||
|
plugin *plugin
|
||||||
|
|
||||||
|
pipe *yamlPipe
|
||||||
|
}
|
||||||
|
type skelpipeFromWrapper struct {
|
||||||
|
plugin *plugin
|
||||||
|
|
||||||
|
from *yamlPipeFrom
|
||||||
|
to *yamlPipeTo
|
||||||
|
}
|
||||||
|
type skelpipePasswordWrapper struct {
|
||||||
|
skelpipeFromWrapper
|
||||||
|
}
|
||||||
|
|
||||||
|
type skelpipePublicKeyWrapper struct {
|
||||||
|
skelpipeFromWrapper
|
||||||
|
}
|
||||||
|
|
||||||
|
type skelpipeToWrapper struct {
|
||||||
|
plugin *plugin
|
||||||
|
|
||||||
|
username string
|
||||||
|
to *yamlPipeTo
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *skelpipeWrapper) From() []libplugin.SkelPipeFrom {
|
||||||
|
var froms []libplugin.SkelPipeFrom
|
||||||
|
for _, f := range s.pipe.From {
|
||||||
|
|
||||||
|
w := &skelpipeFromWrapper{
|
||||||
|
plugin: s.plugin,
|
||||||
|
from: &f,
|
||||||
|
to: &s.pipe.To,
|
||||||
|
}
|
||||||
|
|
||||||
|
if f.SupportPublicKey() {
|
||||||
|
froms = append(froms, &skelpipePublicKeyWrapper{
|
||||||
|
skelpipeFromWrapper: *w,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
froms = append(froms, &skelpipePasswordWrapper{
|
||||||
|
skelpipeFromWrapper: *w,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return froms
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *skelpipeToWrapper) User(conn libplugin.ConnMetadata) string {
|
||||||
|
return s.username
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *skelpipeToWrapper) Host(conn libplugin.ConnMetadata) string {
|
||||||
|
return s.to.Host
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *skelpipeToWrapper) IgnoreHostKey(conn libplugin.ConnMetadata) bool {
|
||||||
|
return s.to.IgnoreHostkey
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *skelpipeToWrapper) KnownHosts(conn libplugin.ConnMetadata) ([]byte, error) {
|
||||||
|
return s.plugin.loadFileOrDecodeMany(s.to.KnownHosts, s.to.KnownHostsData, map[string]string{
|
||||||
|
"DOWNSTREAM_USER": conn.User(),
|
||||||
|
"UPSTREAM_USER": s.username,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *skelpipeFromWrapper) MatchConn(conn libplugin.ConnMetadata) (libplugin.SkelPipeTo, error) {
|
||||||
|
user := conn.User()
|
||||||
|
|
||||||
|
matched := s.from.Username == user
|
||||||
|
targetuser := s.to.Username
|
||||||
|
|
||||||
|
if targetuser == "" {
|
||||||
|
targetuser = user
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.from.UsernameRegexMatch {
|
||||||
|
re, err := regexp.Compile(s.from.Username)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
matched = re.MatchString(user)
|
||||||
|
|
||||||
|
if matched {
|
||||||
|
targetuser = re.ReplaceAllString(user, s.to.Username)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if matched {
|
||||||
|
return &skelpipeToWrapper{
|
||||||
|
plugin: s.plugin,
|
||||||
|
username: targetuser,
|
||||||
|
to: s.to,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *skelpipePasswordWrapper) TestPassword(conn libplugin.ConnMetadata, password []byte) (bool, error) {
|
||||||
|
return true, nil // yaml do not test input password
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *skelpipePublicKeyWrapper) AuthorizedKeys(conn libplugin.ConnMetadata) ([]byte, error) {
|
||||||
|
return s.plugin.loadFileOrDecodeMany(s.from.AuthorizedKeys, s.from.AuthorizedKeysData, map[string]string{
|
||||||
|
"DOWNSTREAM_USER": conn.User(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *skelpipePublicKeyWrapper) TrustedUserCAKeys(conn libplugin.ConnMetadata) ([]byte, error) {
|
||||||
|
return s.plugin.loadFileOrDecodeMany(s.from.TrustedUserCAKeys, s.from.TrustedUserCAKeysData, map[string]string{
|
||||||
|
"DOWNSTREAM_USER": conn.User(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *skelpipeToWrapper) PrivateKey(conn libplugin.ConnMetadata) ([]byte, []byte, error) {
|
||||||
|
p, err := s.plugin.loadFileOrDecode(s.to.PrivateKey, s.to.PrivateKeyData, map[string]string{
|
||||||
|
"DOWNSTREAM_USER": conn.User(),
|
||||||
|
"UPSTREAM_USER": s.username,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return p, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *skelpipeToWrapper) OverridePassword(conn libplugin.ConnMetadata) ([]byte, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *plugin) listPipe(_ libplugin.ConnMetadata) ([]libplugin.SkelPipe, error) {
|
||||||
|
config, err := p.loadConfig()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var pipes []libplugin.SkelPipe
|
||||||
|
for _, pipe := range config.Pipes {
|
||||||
|
wrapper := &skelpipeWrapper{
|
||||||
|
plugin: p,
|
||||||
|
pipe: &pipe,
|
||||||
|
}
|
||||||
|
pipes = append(pipes, wrapper)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return pipes, nil
|
||||||
|
}
|
||||||
|
|
@ -4,21 +4,15 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"crypto/subtle"
|
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/patrickmn/go-cache"
|
|
||||||
"github.com/tg123/sshpiper/libplugin"
|
|
||||||
"golang.org/x/crypto/ssh"
|
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
type pipeConfigFrom struct {
|
type yamlPipeFrom struct {
|
||||||
Username string `yaml:"username"`
|
Username string `yaml:"username"`
|
||||||
UsernameRegexMatch bool `yaml:"username_regex_match,omitempty"`
|
UsernameRegexMatch bool `yaml:"username_regex_match,omitempty"`
|
||||||
AuthorizedKeys listOrString `yaml:"authorized_keys,omitempty"`
|
AuthorizedKeys listOrString `yaml:"authorized_keys,omitempty"`
|
||||||
|
|
@ -27,7 +21,11 @@ type pipeConfigFrom struct {
|
||||||
TrustedUserCAKeysData listOrString `yaml:"trusted_user_ca_keys_data,omitempty"`
|
TrustedUserCAKeysData listOrString `yaml:"trusted_user_ca_keys_data,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type pipeConfigTo struct {
|
func (f yamlPipeFrom) SupportPublicKey() bool {
|
||||||
|
return f.AuthorizedKeys.Any() || f.AuthorizedKeysData.Any() || f.TrustedUserCAKeys.Any() || f.TrustedUserCAKeysData.Any()
|
||||||
|
}
|
||||||
|
|
||||||
|
type yamlPipeTo struct {
|
||||||
Username string `yaml:"username,omitempty"`
|
Username string `yaml:"username,omitempty"`
|
||||||
Host string `yaml:"host"`
|
Host string `yaml:"host"`
|
||||||
Password string `yaml:"password,omitempty"`
|
Password string `yaml:"password,omitempty"`
|
||||||
|
|
@ -70,27 +68,23 @@ func (l *listOrString) UnmarshalYAML(value *yaml.Node) error {
|
||||||
return fmt.Errorf("Failed to unmarshal OneOfType")
|
return fmt.Errorf("Failed to unmarshal OneOfType")
|
||||||
}
|
}
|
||||||
|
|
||||||
type pipeConfig struct {
|
type yamlPipe struct {
|
||||||
From []pipeConfigFrom `yaml:"from,flow"`
|
From []yamlPipeFrom `yaml:"from,flow"`
|
||||||
To pipeConfigTo `yaml:"to,flow"`
|
To yamlPipeTo `yaml:"to,flow"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type piperConfig struct {
|
type piperConfig struct {
|
||||||
Version string `yaml:"version"`
|
Version string `yaml:"version"`
|
||||||
Pipes []pipeConfig `yaml:"pipes,flow"`
|
Pipes []yamlPipe `yaml:"pipes,flow"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type plugin struct {
|
type plugin struct {
|
||||||
File string
|
File string
|
||||||
NoCheckPerm bool
|
NoCheckPerm bool
|
||||||
|
|
||||||
cache *cache.Cache
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func newYamlPlugin() *plugin {
|
func newYamlPlugin() *plugin {
|
||||||
return &plugin{
|
return &plugin{}
|
||||||
cache: cache.New(1*time.Minute, 10*time.Minute),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *plugin) checkPerm() error {
|
func (p *plugin) checkPerm() error {
|
||||||
|
|
@ -191,199 +185,3 @@ func (p *plugin) loadFileOrDecodeMany(files listOrString, base64data listOrStrin
|
||||||
|
|
||||||
return bytes.Join(byteSlices, []byte("\n")), nil
|
return bytes.Join(byteSlices, []byte("\n")), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *plugin) supportedMethods() ([]string, error) {
|
|
||||||
config, err := p.loadConfig()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
set := make(map[string]bool)
|
|
||||||
|
|
||||||
for _, pipe := range config.Pipes {
|
|
||||||
for _, from := range pipe.From {
|
|
||||||
if from.AuthorizedKeys.Any() || from.AuthorizedKeysData.Any() || from.TrustedUserCAKeys.Any() || from.TrustedUserCAKeysData.Any() {
|
|
||||||
set["publickey"] = true // found authorized_keys, so we support publickey
|
|
||||||
} else {
|
|
||||||
set["password"] = true // no authorized_keys, so we support password
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var methods []string
|
|
||||||
for k := range set {
|
|
||||||
methods = append(methods, k)
|
|
||||||
}
|
|
||||||
return methods, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *plugin) verifyHostKey(conn libplugin.ConnMetadata, hostname, netaddr string, key []byte) error {
|
|
||||||
item, found := p.cache.Get(conn.UniqueID())
|
|
||||||
|
|
||||||
if !found {
|
|
||||||
return fmt.Errorf("connection expired")
|
|
||||||
}
|
|
||||||
|
|
||||||
to := item.(*pipeConfigTo)
|
|
||||||
|
|
||||||
data, err := p.loadFileOrDecodeMany(to.KnownHosts, to.KnownHostsData, map[string]string{
|
|
||||||
"DOWNSTREAM_USER": conn.User(),
|
|
||||||
"UPSTREAM_USER": to.Username,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return libplugin.VerifyHostKeyFromKnownHosts(bytes.NewBuffer(data), hostname, netaddr, key)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *plugin) createUpstream(conn libplugin.ConnMetadata, to pipeConfigTo, originPassword string) (*libplugin.Upstream, error) {
|
|
||||||
|
|
||||||
host, port, err := libplugin.SplitHostPortForSSH(to.Host)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
u := &libplugin.Upstream{
|
|
||||||
Host: host,
|
|
||||||
Port: int32(port),
|
|
||||||
UserName: to.Username,
|
|
||||||
IgnoreHostKey: to.IgnoreHostkey,
|
|
||||||
}
|
|
||||||
|
|
||||||
pass := to.Password
|
|
||||||
if pass == "" {
|
|
||||||
pass = originPassword
|
|
||||||
}
|
|
||||||
|
|
||||||
// password found
|
|
||||||
if pass != "" {
|
|
||||||
u.Auth = libplugin.CreatePasswordAuth([]byte(pass))
|
|
||||||
p.cache.Set(conn.UniqueID(), &to, cache.DefaultExpiration)
|
|
||||||
return u, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// try private key
|
|
||||||
data, err := p.loadFileOrDecode(to.PrivateKey, to.PrivateKeyData, map[string]string{
|
|
||||||
"DOWNSTREAM_USER": conn.User(),
|
|
||||||
"UPSTREAM_USER": to.Username,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if data != nil {
|
|
||||||
u.Auth = libplugin.CreatePrivateKeyAuth(data)
|
|
||||||
p.cache.Set(conn.UniqueID(), &to, cache.DefaultExpiration)
|
|
||||||
return u, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, fmt.Errorf("no password or private key found")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *plugin) findAndCreateUpstream(conn libplugin.ConnMetadata, password string, publicKey []byte) (*libplugin.Upstream, error) {
|
|
||||||
user := conn.User()
|
|
||||||
|
|
||||||
config, err := p.loadConfig()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var isCert bool
|
|
||||||
var pkcert *ssh.Certificate
|
|
||||||
|
|
||||||
if publicKey != nil {
|
|
||||||
pubKey, err := ssh.ParsePublicKey(publicKey)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
pkcert, isCert = pubKey.(*ssh.Certificate)
|
|
||||||
if isCert {
|
|
||||||
// ensure cert is valid first
|
|
||||||
|
|
||||||
if pkcert.CertType != ssh.UserCert {
|
|
||||||
return nil, fmt.Errorf("only user certificates are supported, cert type: %v", pkcert.CertType)
|
|
||||||
}
|
|
||||||
|
|
||||||
certChecker := ssh.CertChecker{}
|
|
||||||
if err := certChecker.CheckCert(conn.User(), pkcert); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, pipe := range config.Pipes {
|
|
||||||
for _, from := range pipe.From {
|
|
||||||
matched := from.Username == user
|
|
||||||
|
|
||||||
if pipe.To.Username == "" {
|
|
||||||
pipe.To.Username = user
|
|
||||||
}
|
|
||||||
|
|
||||||
if from.UsernameRegexMatch {
|
|
||||||
re, err := regexp.Compile(from.Username)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
matched = re.MatchString(user)
|
|
||||||
|
|
||||||
if matched {
|
|
||||||
pipe.To.Username = re.ReplaceAllString(user, pipe.To.Username)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !matched {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if publicKey == nil && password != "" {
|
|
||||||
return p.createUpstream(conn, pipe.To, password)
|
|
||||||
}
|
|
||||||
|
|
||||||
if isCert {
|
|
||||||
rest, err := p.loadFileOrDecodeMany(from.TrustedUserCAKeys, from.TrustedUserCAKeysData, map[string]string{
|
|
||||||
"DOWNSTREAM_USER": user,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var trustedca ssh.PublicKey
|
|
||||||
for len(rest) > 0 {
|
|
||||||
trustedca, _, _, rest, err = ssh.ParseAuthorizedKey(rest)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if subtle.ConstantTimeCompare(trustedca.Marshal(), pkcert.SignatureKey.Marshal()) == 1 {
|
|
||||||
return p.createUpstream(conn, pipe.To, "")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
rest, err := p.loadFileOrDecodeMany(from.AuthorizedKeys, from.AuthorizedKeysData, map[string]string{
|
|
||||||
"DOWNSTREAM_USER": user,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var authedPubkey ssh.PublicKey
|
|
||||||
for len(rest) > 0 {
|
|
||||||
authedPubkey, _, _, rest, err = ssh.ParseAuthorizedKey(rest)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if subtle.ConstantTimeCompare(authedPubkey.Marshal(), publicKey) == 1 {
|
|
||||||
return p.createUpstream(conn, pipe.To, "")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, fmt.Errorf("no matching pipe for username [%v] found", user)
|
|
||||||
}
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue