sftp: add support for ssh keys
This commit is contained in:
parent
2a370a8776
commit
d3360f0fd9
|
@ -70,6 +70,7 @@ type SftpAuthRequest struct {
|
|||
IP string `json:"ip"`
|
||||
SessionID []byte `json:"session_id"`
|
||||
ClientVersion []byte `json:"client_version"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
// SftpAuthResponse is returned by the Panel when a pair of SFTP credentials
|
||||
|
@ -77,8 +78,8 @@ type SftpAuthRequest struct {
|
|||
// matched as well as the permissions that are assigned to the authenticated
|
||||
// user for the SFTP subsystem.
|
||||
type SftpAuthResponse struct {
|
||||
SSHKeys []string `json:"ssh_keys"`
|
||||
Server string `json:"server"`
|
||||
Token string `json:"token"`
|
||||
Permissions []string `json:"permissions"`
|
||||
}
|
||||
|
||||
|
|
|
@ -118,7 +118,7 @@ func (h *Handler) Filewrite(request *sftp.Request) (io.WriterAt, error) {
|
|||
return f, nil
|
||||
}
|
||||
|
||||
// Filecmd hander for basic SFTP system calls related to files, but not anything to do with reading
|
||||
// Filecmd handler for basic SFTP system calls related to files, but not anything to do with reading
|
||||
// or writing to those files.
|
||||
func (h *Handler) Filecmd(request *sftp.Request) error {
|
||||
if h.ro {
|
||||
|
|
233
sftp/server.go
233
sftp/server.go
|
@ -1,7 +1,11 @@
|
|||
package sftp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/ecdsa"
|
||||
"crypto/ed25519"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
|
@ -50,28 +54,21 @@ func New(m *server.Manager) *SFTPServer {
|
|||
|
||||
// Starts the SFTP server and add a persistent listener to handle inbound SFTP connections.
|
||||
func (c *SFTPServer) Run() error {
|
||||
if _, err := os.Stat(path.Join(c.BasePath, ".sftp/id_rsa")); os.IsNotExist(err) {
|
||||
if err := c.generatePrivateKey(); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if err != nil {
|
||||
return errors.Wrap(err, "sftp/server: could not stat private key file")
|
||||
}
|
||||
pb, err := ioutil.ReadFile(path.Join(c.BasePath, ".sftp/id_rsa"))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "sftp/server: could not read private key file")
|
||||
}
|
||||
private, err := ssh.ParsePrivateKey(pb)
|
||||
keys, err := c.loadPrivateKeys()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
conf := &ssh.ServerConfig{
|
||||
NoClientAuth: false,
|
||||
MaxAuthTries: 6,
|
||||
PasswordCallback: c.passwordCallback,
|
||||
NoClientAuth: false,
|
||||
MaxAuthTries: 6,
|
||||
PasswordCallback: c.passwordCallback,
|
||||
PublicKeyCallback: c.publicKeyCallback,
|
||||
}
|
||||
|
||||
for _, k := range keys {
|
||||
conf.AddHostKey(k)
|
||||
}
|
||||
conf.AddHostKey(private)
|
||||
|
||||
listener, err := net.Listen("tcp", c.Listen)
|
||||
if err != nil {
|
||||
|
@ -104,7 +101,7 @@ func (c *SFTPServer) AcceptInbound(conn net.Conn, config *ssh.ServerConfig) {
|
|||
// If its not a session channel we just move on because its not something we
|
||||
// know how to handle at this point.
|
||||
if ch.ChannelType() != "session" {
|
||||
ch.Reject(ssh.UnknownChannelType, "unknown channel type")
|
||||
_ = ch.Reject(ssh.UnknownChannelType, "unknown channel type")
|
||||
continue
|
||||
}
|
||||
|
||||
|
@ -118,7 +115,7 @@ func (c *SFTPServer) AcceptInbound(conn net.Conn, config *ssh.ServerConfig) {
|
|||
// Channels have a type that is dependent on the protocol. For SFTP
|
||||
// this is "subsystem" with a payload that (should) be "sftp". Discard
|
||||
// anything else we receive ("pty", "shell", etc)
|
||||
req.Reply(req.Type == "subsystem" && string(req.Payload[4:]) == "sftp", nil)
|
||||
_ = req.Reply(req.Type == "subsystem" && string(req.Payload[4:]) == "sftp", nil)
|
||||
}
|
||||
}(requests)
|
||||
|
||||
|
@ -136,6 +133,7 @@ func (c *SFTPServer) AcceptInbound(conn net.Conn, config *ssh.ServerConfig) {
|
|||
return s.ID() == uuid
|
||||
})
|
||||
if srv == nil {
|
||||
_ = conn.Close()
|
||||
continue
|
||||
}
|
||||
|
||||
|
@ -143,31 +141,153 @@ func (c *SFTPServer) AcceptInbound(conn net.Conn, config *ssh.ServerConfig) {
|
|||
// them access to the underlying filesystem.
|
||||
handler := sftp.NewRequestServer(channel, NewHandler(sconn, srv.Filesystem()).Handlers())
|
||||
if err := handler.Serve(); err == io.EOF {
|
||||
handler.Close()
|
||||
_ = handler.Close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Generates a private key that will be used by the SFTP server.
|
||||
func (c *SFTPServer) generatePrivateKey() error {
|
||||
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
func (c *SFTPServer) loadPrivateKeys() ([]ssh.Signer, error) {
|
||||
if _, err := os.Stat(path.Join(c.BasePath, ".sftp/id_rsa")); err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := c.generateRSAPrivateKey(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
rsaBytes, err := ioutil.ReadFile(path.Join(c.BasePath, ".sftp/id_rsa"))
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
return nil, errors.Wrap(err, "sftp/server: could not read private key file")
|
||||
}
|
||||
rsaPrivateKey, err := ssh.ParsePrivateKey(rsaBytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if _, err := os.Stat(path.Join(c.BasePath, ".sftp/id_ecdsa")); err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := c.generateECDSAPrivateKey(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
ecdsaBytes, err := ioutil.ReadFile(path.Join(c.BasePath, ".sftp/id_ecdsa"))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "sftp/server: could not read private key file")
|
||||
}
|
||||
ecdsaPrivateKey, err := ssh.ParsePrivateKey(ecdsaBytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if _, err := os.Stat(path.Join(c.BasePath, ".sftp/id_ed25519")); err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := c.generateEd25519PrivateKey(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
ed25519Bytes, err := ioutil.ReadFile(path.Join(c.BasePath, ".sftp/id_ed25519"))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "sftp/server: could not read private key file")
|
||||
}
|
||||
ed25519PrivateKey, err := ssh.ParsePrivateKey(ed25519Bytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return []ssh.Signer{
|
||||
rsaPrivateKey,
|
||||
ecdsaPrivateKey,
|
||||
ed25519PrivateKey,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// generateRSAPrivateKey generates a RSA-4096 private key that will be used by the SFTP server.
|
||||
func (c *SFTPServer) generateRSAPrivateKey() error {
|
||||
key, err := rsa.GenerateKey(rand.Reader, 4096)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.MkdirAll(path.Join(c.BasePath, ".sftp"), 0755); err != nil {
|
||||
return errors.Wrap(err, "sftp/server: could not create .sftp directory")
|
||||
}
|
||||
o, err := os.OpenFile(path.Join(c.BasePath, ".sftp/id_rsa"), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
return err
|
||||
}
|
||||
defer o.Close()
|
||||
|
||||
err = pem.Encode(o, &pem.Block{
|
||||
if err := pem.Encode(o, &pem.Block{
|
||||
Type: "RSA PRIVATE KEY",
|
||||
Bytes: x509.MarshalPKCS1PrivateKey(key),
|
||||
})
|
||||
return errors.WithStack(err)
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// generateECDSAPrivateKey generates a ECDSA-P256 private key that will be used by the SFTP server.
|
||||
func (c *SFTPServer) generateECDSAPrivateKey() error {
|
||||
key, err := ecdsa.GenerateKey(elliptic.P521(), rand.Reader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.MkdirAll(path.Join(c.BasePath, ".sftp"), 0755); err != nil {
|
||||
return errors.Wrap(err, "sftp/server: could not create .sftp directory")
|
||||
}
|
||||
o, err := os.OpenFile(path.Join(c.BasePath, ".sftp/id_ecdsa"), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer o.Close()
|
||||
|
||||
privBytes, err := x509.MarshalPKCS8PrivateKey(key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := pem.Encode(o, &pem.Block{
|
||||
Type: "PRIVATE KEY",
|
||||
Bytes: privBytes,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// generateEd25519PrivateKey generates a ed25519 private key that will be used by the SFTP server.
|
||||
func (c *SFTPServer) generateEd25519PrivateKey() error {
|
||||
_, key, err := ed25519.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.MkdirAll(path.Join(c.BasePath, ".sftp"), 0755); err != nil {
|
||||
return errors.Wrap(err, "sftp/server: could not create .sftp directory")
|
||||
}
|
||||
o, err := os.OpenFile(path.Join(c.BasePath, ".sftp/id_ed25519"), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer o.Close()
|
||||
|
||||
privBytes, err := x509.MarshalPKCS8PrivateKey(key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := pem.Encode(o, &pem.Block{
|
||||
Type: "PRIVATE KEY",
|
||||
Bytes: privBytes,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// A function capable of validating user credentials with the Panel API.
|
||||
|
@ -178,6 +298,7 @@ func (c *SFTPServer) passwordCallback(conn ssh.ConnMetadata, pass []byte) (*ssh.
|
|||
IP: conn.RemoteAddr().String(),
|
||||
SessionID: conn.SessionID(),
|
||||
ClientVersion: conn.ClientVersion(),
|
||||
Type: "password",
|
||||
}
|
||||
|
||||
logger := log.WithFields(log.Fields{"subsystem": "sftp", "username": conn.User(), "ip": conn.RemoteAddr().String()})
|
||||
|
@ -188,6 +309,11 @@ func (c *SFTPServer) passwordCallback(conn ssh.ConnMetadata, pass []byte) (*ssh.
|
|||
return nil, &remote.SftpInvalidCredentialsError{}
|
||||
}
|
||||
|
||||
if len(pass) < 1 {
|
||||
logger.Warn("failed to validate user credentials (invalid format)")
|
||||
return nil, &remote.SftpInvalidCredentialsError{}
|
||||
}
|
||||
|
||||
resp, err := c.manager.Client().ValidateSftpCredentials(context.Background(), request)
|
||||
if err != nil {
|
||||
if _, ok := err.(*remote.SftpInvalidCredentialsError); ok {
|
||||
|
@ -209,3 +335,56 @@ func (c *SFTPServer) passwordCallback(conn ssh.ConnMetadata, pass []byte) (*ssh.
|
|||
|
||||
return sshPerm, nil
|
||||
}
|
||||
|
||||
func (c *SFTPServer) publicKeyCallback(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) {
|
||||
request := remote.SftpAuthRequest{
|
||||
User: conn.User(),
|
||||
Pass: "KEKW",
|
||||
IP: conn.RemoteAddr().String(),
|
||||
SessionID: conn.SessionID(),
|
||||
ClientVersion: conn.ClientVersion(),
|
||||
Type: "publicKey",
|
||||
}
|
||||
|
||||
logger := log.WithFields(log.Fields{"subsystem": "sftp", "username": conn.User(), "ip": conn.RemoteAddr().String()})
|
||||
logger.Debug("validating public key for SFTP connection")
|
||||
|
||||
if !validUsernameRegexp.MatchString(request.User) {
|
||||
logger.Warn("failed to validate user credentials (invalid format)")
|
||||
return nil, &remote.SftpInvalidCredentialsError{}
|
||||
}
|
||||
|
||||
resp, err := c.manager.Client().ValidateSftpCredentials(context.Background(), request)
|
||||
if err != nil {
|
||||
if _, ok := err.(*remote.SftpInvalidCredentialsError); ok {
|
||||
logger.Warn("failed to validate user credentials (invalid username or password)")
|
||||
} else {
|
||||
logger.WithField("error", err).Error("encountered an error while trying to validate user credentials")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(resp.SSHKeys) < 1 {
|
||||
return nil, &remote.SftpInvalidCredentialsError{}
|
||||
}
|
||||
|
||||
for _, k := range resp.SSHKeys {
|
||||
storedPublicKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(k))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !bytes.Equal(key.Marshal(), storedPublicKey.Marshal()) {
|
||||
continue
|
||||
}
|
||||
|
||||
return &ssh.Permissions{
|
||||
Extensions: map[string]string{
|
||||
"uuid": resp.Server,
|
||||
"user": conn.User(),
|
||||
"permissions": strings.Join(resp.Permissions, ","),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
return nil, &remote.SftpInvalidCredentialsError{}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user