sshpiper/plugin/kubernetes/skel.go
Boshi Lian 706a36b40e
feat: Enhance PipeSpec and FromSpec with AuthorizedKeysSecret (#581)
* feat: Enhance PipeSpec and FromSpec with AuthorizedKeysSecret

- Added AuthorizedKeysSecret field to FromSpec for referencing Kubernetes secrets.
- Updated deepcopy functions to handle new AuthorizedKeysSecret field.
- Modified CRD definition to include the new private_key_secret field.
- Refactored fake client and lister implementations to support new fields.
- Introduced shared informers and listers for Pipe resources.
- Updated code generation scripts to reflect changes in API structure.

* refactor: Rename private_key_secret to authorized_keys_secret in CRD schema

* feat: Add support for authorized_keys from secret in skel and update k8sworkload.yaml

* feat: Enhance authorized keys handling to include secret name check

* Update plugin/kubernetes/skel.go

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-05-25 03:48:17 -07:00

299 lines
7.3 KiB
Go

package main
import (
"bytes"
"context"
"encoding/base64"
"os"
"regexp"
log "github.com/sirupsen/logrus"
"github.com/tg123/go-htpasswd"
"github.com/tg123/sshpiper/libplugin"
"github.com/tg123/sshpiper/libplugin/skel"
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
}
type skelpipeToPasswordWrapper struct {
skelpipeToWrapper
}
type skelpipeToPrivateKeyWrapper struct {
skelpipeToWrapper
}
func (s *skelpipeWrapper) From() []skel.SkelPipeFrom {
var froms []skel.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 != "" || f.AuthorizedKeysSecret.Name != "" {
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) (skel.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 {
if s.to.PrivateKeySecret.Name != "" {
return &skelpipeToPrivateKeyWrapper{
skelpipeToWrapper: skelpipeToWrapper{
plugin: s.plugin,
pipe: s.pipe,
username: targetuser,
to: s.to,
},
}, nil
}
return &skelpipeToPasswordWrapper{
skelpipeToWrapper: 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
}
if s.from.AuthorizedKeysSecret.Name != "" {
log.Debugf("mapping to %v authorized keys using secret %v", s.pipe.Spec.To.Host, s.from.AuthorizedKeysSecret.Name)
anno := s.pipe.GetAnnotations()
secret, err := s.plugin.k8sclient.Secrets(s.pipe.Namespace).Get(context.Background(), s.from.AuthorizedKeysSecret.Name, metav1.GetOptions{})
if err != nil {
return nil, err
}
for _, k := range []string{anno["sshpiper.com/authorizedkeys_field_name"], anno["authorizedkeys_field_name"], "authorized_keys", "authorizedkeys", "ssh-authorizedkeys"} {
data := secret.Data[k]
if data != nil {
log.Debugf("found authorized keys in secret %v/%v", s.from.AuthorizedKeysSecret.Name, k)
byteSlices = append(byteSlices, data)
break
}
}
}
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 *skelpipeToPrivateKeyWrapper) 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["sshpiper.com/privatekey_field_name"], 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
}
}
if anno["no_ca_publickey"] != "true" && anno["sshpiper.com/no_ca_publickey"] != "true" {
for _, k := range []string{anno["sshpiper.com/publickey_field_name"], 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 *skelpipeToPasswordWrapper) 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) ([]skel.SkelPipe, error) {
kpipes, err := p.list()
if err != nil {
return nil, err
}
var pipes []skel.SkelPipe
for _, pipe := range kpipes {
wrapper := &skelpipeWrapper{
plugin: p,
pipe: pipe,
}
pipes = append(pipes, wrapper)
}
return pipes, nil
}