Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extend k8s plugin #77

Closed
wants to merge 14 commits into from
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ require (
github.com/jinzhu/gorm v1.9.16
github.com/json-iterator/go v1.1.12 // indirect
github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect
github.com/pockost/sshpipe-k8s-lib v0.0.3
github.com/pockost/sshpipe-k8s-lib v0.0.5
github.com/tg123/remotesigner v0.0.0-20210928104451-7c20285909d1
github.com/tg123/sshkey v0.0.0-20201202190454-3bb356f89f1f
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519
Expand Down
9 changes: 9 additions & 0 deletions sshpiperd/upstream/kubernetes/crd.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,15 @@ spec:
description: The service port
type: integer
default: 22
user:
description: The login name
type: string
namespace:
description: The namespace
type: string
secretName:
description: The Kubernetes Secret name with authorizedKeys and privateKey data keys
type: string
# either Namespaced or Cluster
scope: Namespaced
names:
Expand Down
104 changes: 92 additions & 12 deletions sshpiperd/upstream/kubernetes/kubernetes.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"net"
//"path/filepath"
//"strings"
"bytes"

"github.com/tg123/sshpiper/sshpiperd/upstream"
"golang.org/x/crypto/ssh"
Expand All @@ -14,11 +15,15 @@ import (
//"k8s.io/client-go/util/homedir"
//"k8s.io/client-go/tools/clientcmd"
sshpipeclientset "github.com/pockost/sshpipe-k8s-lib/pkg/client/clientset/versioned"
"k8s.io/client-go/kubernetes"
)

type pipeConfig struct {
Username string `kubernetes:"username"`
TargetUser string `kubernetes:"username"`
UpstreamHost string `kubernetes:"upstream_host"`
Namespace string `kubernetes:"namespace"`
SecretName string `kubernetes:"secret_name"`
}

// type createPipeCtx struct {
Expand All @@ -27,7 +32,7 @@ type pipeConfig struct {
// challengeContext ssh.AdditionalChallengeContext
// }

func (p *plugin) getClientSet() (*sshpipeclientset.Clientset, error) {
func (p *plugin) getClientSet() (*sshpipeclientset.Clientset, *kubernetes.Clientset, error) {
/*
var kubeconfig string
home := homedir.HomeDir()
Expand All @@ -41,16 +46,21 @@ func (p *plugin) getClientSet() (*sshpipeclientset.Clientset, error) {
*/
config, err := rest.InClusterConfig()
if err != nil {
return nil, err
return nil, nil, err
}

// create the clientset
clientset, err := sshpipeclientset.NewForConfig(config)
if err != nil {
return nil, err
return nil, nil, err
}

k8sclient, err := kubernetes.NewForConfig(config)
if err != nil {
return nil, nil, err
}

return clientset, nil
return clientset, k8sclient, nil
}

func (p *plugin) getConfig(clientset *sshpipeclientset.Clientset) ([]pipeConfig, error) {
Expand All @@ -67,14 +77,30 @@ func (p *plugin) getConfig(clientset *sshpipeclientset.Clientset) ([]pipeConfig,

var config []pipeConfig
for _, pipe := range pipes.Items {
targetHost := fmt.Sprintf("%s.%s", pipe.Spec.Target.Name, pipe.ObjectMeta.Namespace)
var targetHost string
namespace := pipe.ObjectMeta.Namespace
targetNamespace := pipe.Spec.Target.Namespace
if targetNamespace == "" {
targetNamespace = namespace
}
targetHost = fmt.Sprintf("%s.%s", pipe.Spec.Target.Name, targetNamespace)
mappedUser := pipe.Spec.Target.User
secretName := pipe.Spec.SecretName

for _, username := range pipe.Spec.Users {
var targetUser string = mappedUser
if targetUser == "" {
targetUser = username
}

config = append(
config,
pipeConfig{
Username: username,
UpstreamHost: targetHost,
Username: username,
TargetUser: targetUser,
UpstreamHost: targetHost,
Namespace: namespace,
SecretName: secretName,
},
)
}
Expand All @@ -83,25 +109,79 @@ func (p *plugin) getConfig(clientset *sshpipeclientset.Clientset) ([]pipeConfig,
return config, nil
}

func (p *plugin) createAuthPipe(pipe pipeConfig, conn ssh.ConnMetadata, challengeContext ssh.AdditionalChallengeContext) (*ssh.AuthPipe, error) {
func (p *plugin) mapPublicKeyFromPipe(conn ssh.ConnMetadata, key ssh.PublicKey, k8sclient *kubernetes.Clientset, pipe pipeConfig) (signer ssh.Signer, err error) {
user := conn.User()

defer func() { // print error when func exit
if err != nil {
p.logger.Printf("mapping private key error: %v, public key auth denied for [%v] from [%v]", err, user, conn.RemoteAddr())
}
}()

secret, err := k8sclient.CoreV1().Secrets(pipe.Namespace).Get(context.TODO(), pipe.SecretName, metav1.GetOptions{})
if err != nil {
return nil, err
}
var authorizedKeys []byte = secret.Data["authorizedKeys"]

var authedPubkey ssh.PublicKey

authedPubkey, _, _, authorizedKeys, err = ssh.ParseAuthorizedKey(authorizedKeys)
if err != nil {
return nil, err
}

if bytes.Equal(authedPubkey.Marshal(), key.Marshal()) {
var privateKey []byte = secret.Data["privateKey"]

var private ssh.Signer
private, err = ssh.ParsePrivateKey(privateKey)
if err != nil {
return nil, err
}

// in log may see this twice, one is for query the other is real sign again
p.logger.Printf("auth succ, using mapped private key for user [%v] from [%v]", user, conn.RemoteAddr())
return private, nil
}

p.logger.Printf("public key auth failed user [%v] from [%v]", conn.User(), conn.RemoteAddr())

return nil, nil
}

func (p *plugin) createAuthPipe(pipe pipeConfig, conn ssh.ConnMetadata, challengeContext ssh.AdditionalChallengeContext, k8sclient *kubernetes.Clientset) (*ssh.AuthPipe, error) {
hostKeyCallback := ssh.InsecureIgnoreHostKey()

a := &ssh.AuthPipe{
User: pipe.Username,
User: pipe.TargetUser,
UpstreamHostKeyCallback: hostKeyCallback,
}

a.PasswordCallback = func(conn ssh.ConnMetadata, password []byte) (ssh.AuthPipeType, ssh.AuthMethod, error) {
return ssh.AuthPipeTypePassThrough, nil, nil
}

if pipe.SecretName != "" {
a.PublicKeyCallback = func(conn ssh.ConnMetadata, key ssh.PublicKey) (ssh.AuthPipeType, ssh.AuthMethod, error) {
signer, err := p.mapPublicKeyFromPipe(conn, key, k8sclient, pipe)

if err != nil || signer == nil {
// try one
return ssh.AuthPipeTypeNone, nil, nil
}

return ssh.AuthPipeTypeMap, ssh.PublicKeys(signer), nil
}
}

return a, nil
}

func (p *plugin) findUpstream(conn ssh.ConnMetadata, challengeContext ssh.AdditionalChallengeContext) (net.Conn, *ssh.AuthPipe, error) {
user := conn.User()

clientset, err := p.getClientSet()
clientset, k8sclient, err := p.getClientSet()
if err != nil {
return nil, nil, err
}
Expand All @@ -122,12 +202,12 @@ func (p *plugin) findUpstream(conn ssh.ConnMetadata, challengeContext ssh.Additi
return nil, nil, err
}

a, err := p.createAuthPipe(pipe, conn, challengeContext)
a, err := p.createAuthPipe(pipe, conn, challengeContext, k8sclient)
if err != nil {
return nil, nil, err
}

p.logger.Printf("Forwarding connection to [%v] for user [%v]", pipe.UpstreamHost, pipe.Username)
p.logger.Printf("Forwarding connection to [%v] for user [%v]", pipe.UpstreamHost, pipe.TargetUser)
return c, a, nil
}
}
Expand Down