support password match/ca publickey for kubernetes (#211)
* add testcase * refine testcases * fix tab * happy ql * inline timeout * add ca support for k8s
This commit is contained in:
parent
13fcc8c0a8
commit
694d09affd
15 changed files with 391 additions and 154 deletions
|
|
@ -23,12 +23,16 @@ type FromSpec struct {
|
|||
Username string `json:"username"`
|
||||
UsernameRegexMatch bool `json:"username_regex_match,omitempty"`
|
||||
AuthorizedKeysData string `json:"authorized_keys_data,omitempty"`
|
||||
HtpasswdData string `json:"htpasswd_data,omitempty"`
|
||||
AuthorizedKeysFile string `json:"authorized_keys_file,omitempty"`
|
||||
HtpasswdFile string `json:"htpasswd_file,omitempty"`
|
||||
}
|
||||
|
||||
type ToSpec struct {
|
||||
Username string `json:"username,omitempty"`
|
||||
Host string `json:"host"`
|
||||
PrivateKeySecret corev1.LocalObjectReference `json:"private_key_secret,omitempty"`
|
||||
PasswordSecret corev1.LocalObjectReference `json:"password_secret,omitempty"`
|
||||
KnownHostsData string `json:"known_hosts_data,omitempty"`
|
||||
IgnoreHostkey bool `json:"ignore_hostkey,omitempty"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,8 +34,14 @@ spec:
|
|||
properties:
|
||||
authorized_keys_data:
|
||||
type: string
|
||||
authorized_keys_file:
|
||||
type: string
|
||||
username:
|
||||
type: string
|
||||
htpasswd_data:
|
||||
type: string
|
||||
htpasswd_file:
|
||||
type: string
|
||||
username_regex_match:
|
||||
type: boolean
|
||||
required:
|
||||
|
|
@ -59,6 +65,15 @@ spec:
|
|||
TODO: Add other useful fields. apiVersion, kind, uid?'
|
||||
type: string
|
||||
type: object
|
||||
password_secret:
|
||||
description: LocalObjectReference contains enough information
|
||||
to let you locate the referenced object inside the same namespace.
|
||||
properties:
|
||||
name:
|
||||
description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
|
||||
TODO: Add other useful fields. apiVersion, kind, uid?'
|
||||
type: string
|
||||
type: object
|
||||
username:
|
||||
type: string
|
||||
required:
|
||||
|
|
|
|||
|
|
@ -1,4 +0,0 @@
|
|||
// Code generated by client-gen. DO NOT EDIT.
|
||||
|
||||
// This package has the automatically generated clientset.
|
||||
package versioned
|
||||
|
|
@ -8,7 +8,6 @@ import (
|
|||
v1beta1 "github.com/tg123/sshpiper/plugin/kubernetes/apis/sshpiper/v1beta1"
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
labels "k8s.io/apimachinery/pkg/labels"
|
||||
schema "k8s.io/apimachinery/pkg/runtime/schema"
|
||||
types "k8s.io/apimachinery/pkg/types"
|
||||
watch "k8s.io/apimachinery/pkg/watch"
|
||||
testing "k8s.io/client-go/testing"
|
||||
|
|
@ -20,9 +19,9 @@ type FakePipes struct {
|
|||
ns string
|
||||
}
|
||||
|
||||
var pipesResource = schema.GroupVersionResource{Group: "sshpiper", Version: "v1beta1", Resource: "pipes"}
|
||||
var pipesResource = v1beta1.SchemeGroupVersion.WithResource("pipes")
|
||||
|
||||
var pipesKind = schema.GroupVersionKind{Group: "sshpiper", Version: "v1beta1", Kind: "Pipe"}
|
||||
var pipesKind = v1beta1.SchemeGroupVersion.WithKind("Pipe")
|
||||
|
||||
// Get takes name of the pipe, and returns the corresponding pipe object, and an error if there is any.
|
||||
func (c *FakePipes) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1beta1.Pipe, err error) {
|
||||
|
|
|
|||
|
|
@ -5,10 +5,13 @@ import (
|
|||
"context"
|
||||
"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"
|
||||
sshpiper "github.com/tg123/sshpiper/plugin/kubernetes/generated/clientset/versioned"
|
||||
|
|
@ -93,11 +96,15 @@ func (p *plugin) supportedMethods() ([]string, error) {
|
|||
|
||||
for _, pipe := range pipes {
|
||||
for _, from := range pipe.Spec.From {
|
||||
if from.AuthorizedKeysData != "" {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -140,30 +147,92 @@ func (p *plugin) createUpstream(conn libplugin.ConnMetadata, pipe *piperv1beta1.
|
|||
IgnoreHostKey: to.IgnoreHostkey,
|
||||
}
|
||||
|
||||
if originPassword != "" {
|
||||
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{"ssh-privatekey", "privatekey", anno["privatekey_field_name"]} {
|
||||
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{"ssh-publickey", "publickey", anno["publickey_field_name"]} {
|
||||
data := secret.Data[k]
|
||||
if data != nil {
|
||||
log.Debugf("found publickey key 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{"password", anno["password_field_name"]} {
|
||||
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
|
||||
}
|
||||
|
||||
secret, err := p.k8sclient.Secrets(pipe.Namespace).Get(context.Background(), to.PrivateKeySecret.Name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
anno := pipe.GetAnnotations()
|
||||
for _, k := range []string{"ssh-privatekey", "privatekey", anno["privatekey_field_name"]} {
|
||||
data := secret.Data[k]
|
||||
if data != nil {
|
||||
u.Auth = libplugin.CreatePrivateKeyAuth(data)
|
||||
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()
|
||||
|
||||
|
|
@ -185,23 +254,48 @@ func (p *plugin) findAndCreateUpstream(conn libplugin.ConnMetadata, password str
|
|||
}
|
||||
|
||||
if publicKey == nil && password != "" {
|
||||
return p.createUpstream(conn, pipe, password)
|
||||
}
|
||||
|
||||
rest, err := base64.StdEncoding.DecodeString(from.AuthorizedKeysData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var authedPubkey ssh.PublicKey
|
||||
for len(rest) > 0 {
|
||||
authedPubkey, _, _, rest, err = ssh.ParseAuthorizedKey(rest)
|
||||
pwds, err := loadStringAndFile(from.HtpasswdData, from.HtpasswdFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if bytes.Equal(authedPubkey.Marshal(), publicKey) {
|
||||
return p.createUpstream(conn, pipe, "")
|
||||
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 bytes.Equal(authedPubkey.Marshal(), publicKey) {
|
||||
return p.createUpstream(conn, pipe, "")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,8 +15,8 @@ CODEGEN_PKG=${REPO_ROOT}/vendor/k8s.io/code-generator
|
|||
# k8s.io/kubernetes. The output-base is needed for the generators to output into the vendor dir
|
||||
# instead of the $GOPATH directly. For normal projects this can be dropped.
|
||||
# generators deepcopy,client,informer,lister
|
||||
chmod +x "${CODEGEN_PKG}"/generate-groups.sh
|
||||
"${CODEGEN_PKG}"/generate-groups.sh \
|
||||
chmod +x "${CODEGEN_PKG}"/kube_codegen.sh
|
||||
"${CODEGEN_PKG}"/kube_codegen.sh \
|
||||
"deepcopy,client,lister" \
|
||||
github.com/tg123/sshpiper/plugin/kubernetes/generated \
|
||||
github.com/tg123/sshpiper/plugin/kubernetes/apis \
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue