The auth proxy was only given the user and their password or public key, so a proxy program had no way to restrict logins to particular networks, or to record where an authentication attempt came from. The JSON sent to the program now has a client_ip key holding the bare IP the client connected from, with the port stripped so IPv6 arrives as 2001:db8::1 rather than [2001:db8::1]:52344. An IPv4-mapped IPv6 address is reported as plain IPv4 so that a client arriving over a dual-stack listener still matches IPv4 networks. The key is omitted when the client has no IP address. The IP is also mixed into the backend cache key. That is needed as the program is only run on a cache miss, so a client from a non-allowlisted address presenting valid credentials within the 5 minute cache lifetime would get a cache hit and be let in without the program being consulted at all.
458 lines
14 KiB
Go
458 lines
14 KiB
Go
//go:build !plan9
|
|
|
|
package sftp
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/ecdsa"
|
|
"crypto/ed25519"
|
|
"crypto/elliptic"
|
|
"crypto/rand"
|
|
"crypto/rsa"
|
|
"crypto/subtle"
|
|
"crypto/x509"
|
|
"encoding/base64"
|
|
"encoding/pem"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/rclone/rclone/cmd/serve/proxy"
|
|
"github.com/rclone/rclone/fs"
|
|
"github.com/rclone/rclone/fs/config"
|
|
"github.com/rclone/rclone/lib/env"
|
|
"github.com/rclone/rclone/lib/file"
|
|
sdActivation "github.com/rclone/rclone/lib/sdactivation"
|
|
"github.com/rclone/rclone/vfs"
|
|
"github.com/rclone/rclone/vfs/vfscommon"
|
|
"golang.org/x/crypto/ssh"
|
|
)
|
|
|
|
// server contains everything to run the server
|
|
type server struct {
|
|
f fs.Fs
|
|
opt Options
|
|
vfs *vfs.VFS
|
|
ctx context.Context // for global config
|
|
config *ssh.ServerConfig
|
|
listener net.Listener
|
|
stopped chan struct{} // for waiting on the listener to stop
|
|
proxy *proxy.Proxy
|
|
}
|
|
|
|
func newServer(ctx context.Context, f fs.Fs, opt *Options, vfsOpt *vfscommon.Options, proxyOpt *proxy.Options) (*server, error) {
|
|
s := &server{
|
|
f: f,
|
|
ctx: ctx,
|
|
opt: *opt,
|
|
stopped: make(chan struct{}),
|
|
}
|
|
if proxy.Opt.AuthProxy != "" {
|
|
s.proxy = proxy.New(ctx, proxyOpt, vfsOpt)
|
|
} else {
|
|
s.vfs = vfs.New(ctx, f, vfsOpt)
|
|
}
|
|
err := s.configure()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("sftp configuration failed: %w", err)
|
|
}
|
|
return s, nil
|
|
}
|
|
|
|
// getVFS gets the vfs from s or the proxy
|
|
func (s *server) getVFS(what string, sshConn *ssh.ServerConn) (VFS *vfs.VFS) {
|
|
if s.proxy == nil {
|
|
return s.vfs
|
|
}
|
|
if sshConn.Permissions == nil || sshConn.Permissions.Extensions == nil {
|
|
fs.Infof(what, "SSH Permissions Extensions not found")
|
|
return nil
|
|
}
|
|
key := sshConn.Permissions.Extensions["_vfsKey"]
|
|
if key == "" {
|
|
fs.Infof(what, "VFS key not found")
|
|
return nil
|
|
}
|
|
VFS = s.proxy.Get(key)
|
|
if VFS == nil {
|
|
fs.Infof(what, "failed to read VFS from cache")
|
|
return nil
|
|
}
|
|
return VFS
|
|
}
|
|
|
|
// Accept a single connection - run in a go routine as the ssh
|
|
// authentication can block
|
|
func (s *server) acceptConnection(nConn net.Conn) {
|
|
what := describeConn(nConn)
|
|
|
|
// Before use, a handshake must be performed on the incoming net.Conn.
|
|
sshConn, chans, reqs, err := ssh.NewServerConn(nConn, s.config)
|
|
if err != nil {
|
|
fs.Errorf(what, "SSH login failed: %v", err)
|
|
return
|
|
}
|
|
|
|
fs.Infof(what, "SSH login from %s using %s", sshConn.User(), sshConn.ClientVersion())
|
|
|
|
// Discard all global out-of-band Requests
|
|
go ssh.DiscardRequests(reqs)
|
|
|
|
c := &conn{
|
|
what: what,
|
|
vfs: s.getVFS(what, sshConn),
|
|
}
|
|
if c.vfs == nil {
|
|
fs.Infof(what, "Closing unauthenticated connection (couldn't find VFS)")
|
|
_ = nConn.Close()
|
|
return
|
|
}
|
|
c.handlers = newVFSHandler(c.vfs)
|
|
|
|
// Accept all channels
|
|
go c.handleChannels(chans)
|
|
}
|
|
|
|
// Accept connections and call them in a go routine
|
|
func (s *server) acceptConnections() {
|
|
for {
|
|
nConn, err := s.listener.Accept()
|
|
if err != nil {
|
|
if strings.Contains(err.Error(), "use of closed network connection") {
|
|
return
|
|
}
|
|
fs.Errorf(nil, "Failed to accept incoming connection: %v", err)
|
|
continue
|
|
}
|
|
go s.acceptConnection(nConn)
|
|
}
|
|
}
|
|
|
|
// configure the server
|
|
//
|
|
// Based on example server code from golang.org/x/crypto/ssh and server_standalone
|
|
func (s *server) configure() (err error) {
|
|
var authorizedKeysMap map[string]struct{}
|
|
|
|
// ensure the user isn't trying to use conflicting flags
|
|
if proxy.Opt.AuthProxy != "" && s.opt.AuthorizedKeys != "" && s.opt.AuthorizedKeys != Opt.AuthorizedKeys {
|
|
return errors.New("--auth-proxy and --authorized-keys cannot be used at the same time")
|
|
}
|
|
|
|
// Load the authorized keys
|
|
if s.opt.AuthorizedKeys != "" && proxy.Opt.AuthProxy == "" {
|
|
authKeysFile := env.ShellExpand(s.opt.AuthorizedKeys)
|
|
authorizedKeysMap, err = loadAuthorizedKeys(authKeysFile)
|
|
// If user set the flag away from the default then report an error
|
|
if s.opt.AuthorizedKeys != Opt.AuthorizedKeys {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(authorizedKeysMap) == 0 {
|
|
return fmt.Errorf("failed to parse authorized keys")
|
|
}
|
|
}
|
|
fs.Logf(nil, "Loaded %d authorized keys from %q", len(authorizedKeysMap), authKeysFile)
|
|
}
|
|
|
|
if !s.opt.NoAuth && len(authorizedKeysMap) == 0 && s.opt.User == "" && s.opt.Pass == "" && s.proxy == nil {
|
|
return errors.New("no authorization found, use --user/--pass or --authorized-keys or --no-auth or --auth-proxy")
|
|
}
|
|
|
|
// An SSH server is represented by a ServerConfig, which holds
|
|
// certificate details and handles authentication of ServerConns.
|
|
s.config = &ssh.ServerConfig{
|
|
ServerVersion: "SSH-2.0-" + fs.GetConfig(s.ctx).UserAgent,
|
|
PasswordCallback: func(c ssh.ConnMetadata, pass []byte) (*ssh.Permissions, error) {
|
|
fs.Debugf(describeConn(c), "Password login attempt for %s", c.User())
|
|
if s.proxy != nil {
|
|
// query the proxy for the config
|
|
_, vfsKey, err := s.proxy.Call(c.User(), string(pass), false, c.RemoteAddr().String())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// just return the Key so we can get it back from the cache
|
|
return &ssh.Permissions{
|
|
Extensions: map[string]string{
|
|
"_vfsKey": vfsKey,
|
|
},
|
|
}, nil
|
|
} else if s.opt.User != "" && s.opt.Pass != "" {
|
|
userOK := subtle.ConstantTimeCompare([]byte(c.User()), []byte(s.opt.User))
|
|
passOK := subtle.ConstantTimeCompare(pass, []byte(s.opt.Pass))
|
|
if (userOK & passOK) == 1 {
|
|
return nil, nil
|
|
}
|
|
}
|
|
return nil, fmt.Errorf("password rejected for %q", c.User())
|
|
},
|
|
PublicKeyCallback: func(c ssh.ConnMetadata, pubKey ssh.PublicKey) (*ssh.Permissions, error) {
|
|
fs.Debugf(describeConn(c), "Public key login attempt for %s", c.User())
|
|
if s.proxy != nil {
|
|
//query the proxy for the config
|
|
_, vfsKey, err := s.proxy.Call(
|
|
c.User(),
|
|
base64.StdEncoding.EncodeToString(pubKey.Marshal()),
|
|
true,
|
|
c.RemoteAddr().String(),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// just return the Key so we can get it back from the cache
|
|
return &ssh.Permissions{
|
|
Extensions: map[string]string{
|
|
"_vfsKey": vfsKey,
|
|
},
|
|
}, nil
|
|
}
|
|
if _, ok := authorizedKeysMap[string(pubKey.Marshal())]; ok {
|
|
return &ssh.Permissions{
|
|
// Record the public key used for authentication.
|
|
Extensions: map[string]string{
|
|
"pubkey-fp": ssh.FingerprintSHA256(pubKey),
|
|
},
|
|
}, nil
|
|
}
|
|
return nil, fmt.Errorf("unknown public key for %q", c.User())
|
|
},
|
|
AuthLogCallback: func(conn ssh.ConnMetadata, method string, err error) {
|
|
status := "OK"
|
|
if err != nil {
|
|
status = err.Error()
|
|
}
|
|
fs.Debugf(describeConn(conn), "ssh auth %q from %q: %s", method, conn.ClientVersion(), status)
|
|
},
|
|
NoClientAuth: s.opt.NoAuth,
|
|
}
|
|
|
|
// Load the private key, from the cache if not explicitly configured
|
|
keyPaths := s.opt.HostKeys
|
|
cachePath := filepath.Join(config.GetCacheDir(), "serve-sftp")
|
|
if len(keyPaths) == 0 {
|
|
keyPaths = []string{
|
|
filepath.Join(cachePath, "id_rsa"),
|
|
filepath.Join(cachePath, "id_ecdsa"),
|
|
filepath.Join(cachePath, "id_ed25519"),
|
|
}
|
|
}
|
|
for _, keyPath := range keyPaths {
|
|
private, err := loadPrivateKey(keyPath)
|
|
if err != nil && len(s.opt.HostKeys) == 0 {
|
|
fs.Debugf(nil, "Failed to load %q: %v", keyPath, err)
|
|
// If loading a cached key failed, make the keys and retry
|
|
err = file.MkdirAll(cachePath, 0700)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create cache path: %w", err)
|
|
}
|
|
if strings.HasSuffix(keyPath, string(os.PathSeparator)+"id_rsa") {
|
|
const bits = 2048
|
|
fs.Logf(nil, "Generating %d bit key pair at %q", bits, keyPath)
|
|
err = makeRSASSHKeyPair(bits, keyPath+".pub", keyPath)
|
|
} else if strings.HasSuffix(keyPath, string(os.PathSeparator)+"id_ecdsa") {
|
|
fs.Logf(nil, "Generating ECDSA p256 key pair at %q", keyPath)
|
|
err = makeECDSASSHKeyPair(keyPath+".pub", keyPath)
|
|
} else if strings.HasSuffix(keyPath, string(os.PathSeparator)+"id_ed25519") {
|
|
fs.Logf(nil, "Generating Ed25519 key pair at %q", keyPath)
|
|
err = makeEd25519SSHKeyPair(keyPath+".pub", keyPath)
|
|
} else {
|
|
return fmt.Errorf("don't know how to generate key pair %q", keyPath)
|
|
}
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create SSH key pair: %w", err)
|
|
}
|
|
// reload the new key
|
|
private, err = loadPrivateKey(keyPath)
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fs.Debugf(nil, "Loaded private key from %q", keyPath)
|
|
|
|
s.config.AddHostKey(private)
|
|
}
|
|
|
|
// Once a ServerConfig has been configured, connections can be
|
|
// accepted.
|
|
var listener net.Listener
|
|
|
|
// In case we run in a socket-activated environment, listen on (the first)
|
|
// passed FD.
|
|
sdListeners, err := sdActivation.Listeners()
|
|
if err != nil {
|
|
return fmt.Errorf("unable to acquire listeners: %w", err)
|
|
}
|
|
|
|
if len(sdListeners) > 0 {
|
|
if len(sdListeners) > 1 {
|
|
fs.LogPrintf(fs.LogLevelWarning, nil, "more than one listener passed, ignoring all but the first.\n")
|
|
}
|
|
listener = sdListeners[0]
|
|
} else {
|
|
listener, err = net.Listen("tcp", s.opt.ListenAddr)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to listen for connection: %w", err)
|
|
}
|
|
}
|
|
s.listener = listener
|
|
return nil
|
|
}
|
|
|
|
// Serve SFTP until the server is Shutdown
|
|
func (s *server) Serve() (err error) {
|
|
fs.Logf(nil, "SFTP server listening on %v\n", s.listener.Addr())
|
|
s.acceptConnections()
|
|
close(s.stopped)
|
|
return nil
|
|
}
|
|
|
|
// Addr returns the address the server is listening on
|
|
func (s *server) Addr() net.Addr {
|
|
return s.listener.Addr()
|
|
}
|
|
|
|
// Wait blocks while the listener is open.
|
|
func (s *server) Wait() {
|
|
<-s.stopped
|
|
}
|
|
|
|
// Shutdown shuts the running server down
|
|
func (s *server) Shutdown() error {
|
|
err := s.listener.Close()
|
|
if errors.Is(err, io.ErrUnexpectedEOF) {
|
|
err = nil
|
|
}
|
|
s.Wait()
|
|
return err
|
|
}
|
|
|
|
func loadPrivateKey(keyPath string) (ssh.Signer, error) {
|
|
privateBytes, err := os.ReadFile(keyPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to load private key: %w", err)
|
|
}
|
|
private, err := ssh.ParsePrivateKey(privateBytes)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to parse private key: %w", err)
|
|
}
|
|
return private, nil
|
|
}
|
|
|
|
// Public key authentication is done by comparing
|
|
// the public key of a received connection
|
|
// with the entries in the authorized_keys file.
|
|
func loadAuthorizedKeys(authorizedKeysPath string) (authorizedKeysMap map[string]struct{}, err error) {
|
|
authorizedKeysBytes, err := os.ReadFile(authorizedKeysPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to load authorized keys: %w", err)
|
|
}
|
|
authorizedKeysMap = make(map[string]struct{})
|
|
for len(authorizedKeysBytes) > 0 {
|
|
pubKey, _, _, rest, err := ssh.ParseAuthorizedKey(authorizedKeysBytes)
|
|
if err == nil {
|
|
authorizedKeysMap[string(pubKey.Marshal())] = struct{}{}
|
|
authorizedKeysBytes = bytes.TrimSpace(rest)
|
|
}
|
|
}
|
|
return authorizedKeysMap, nil
|
|
}
|
|
|
|
// makeRSASSHKeyPair make a pair of public and private keys for SSH access.
|
|
// Public key is encoded in the format for inclusion in an OpenSSH authorized_keys file.
|
|
// Private Key generated is PEM encoded
|
|
//
|
|
// Originally from: https://stackoverflow.com/a/34347463/164234
|
|
func makeRSASSHKeyPair(bits int, pubKeyPath, privateKeyPath string) (err error) {
|
|
privateKey, err := rsa.GenerateKey(rand.Reader, bits)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// generate and write private key as PEM
|
|
privateKeyFile, err := os.OpenFile(privateKeyPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer fs.CheckClose(privateKeyFile, &err)
|
|
privateKeyPEM := &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(privateKey)}
|
|
if err := pem.Encode(privateKeyFile, privateKeyPEM); err != nil {
|
|
return err
|
|
}
|
|
|
|
// generate and write public key
|
|
pub, err := ssh.NewPublicKey(&privateKey.PublicKey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return os.WriteFile(pubKeyPath, ssh.MarshalAuthorizedKey(pub), 0644)
|
|
}
|
|
|
|
// makeECDSASSHKeyPair make a pair of public and private keys for ECDSA SSH access.
|
|
// Public key is encoded in the format for inclusion in an OpenSSH authorized_keys file.
|
|
// Private Key generated is PEM encoded
|
|
func makeECDSASSHKeyPair(pubKeyPath, privateKeyPath string) (err error) {
|
|
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// generate and write private key as PEM
|
|
privateKeyFile, err := os.OpenFile(privateKeyPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer fs.CheckClose(privateKeyFile, &err)
|
|
buf, err := x509.MarshalECPrivateKey(privateKey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
privateKeyPEM := &pem.Block{Type: "EC PRIVATE KEY", Bytes: buf}
|
|
if err := pem.Encode(privateKeyFile, privateKeyPEM); err != nil {
|
|
return err
|
|
}
|
|
|
|
// generate and write public key
|
|
pub, err := ssh.NewPublicKey(&privateKey.PublicKey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return os.WriteFile(pubKeyPath, ssh.MarshalAuthorizedKey(pub), 0644)
|
|
}
|
|
|
|
// makeEd25519SSHKeyPair make a pair of public and private keys for Ed25519 SSH access.
|
|
// Public key is encoded in the format for inclusion in an OpenSSH authorized_keys file.
|
|
// Private Key generated is PEM encoded
|
|
func makeEd25519SSHKeyPair(pubKeyPath, privateKeyPath string) (err error) {
|
|
publicKey, privateKey, err := ed25519.GenerateKey(rand.Reader)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// generate and write private key as PEM
|
|
privateKeyFile, err := os.OpenFile(privateKeyPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer fs.CheckClose(privateKeyFile, &err)
|
|
buf, err := x509.MarshalPKCS8PrivateKey(privateKey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
privateKeyPEM := &pem.Block{Type: "PRIVATE KEY", Bytes: buf}
|
|
if err := pem.Encode(privateKeyFile, privateKeyPEM); err != nil {
|
|
return err
|
|
}
|
|
|
|
// generate and write public key
|
|
pub, err := ssh.NewPublicKey(publicKey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return os.WriteFile(pubKeyPath, ssh.MarshalAuthorizedKey(pub), 0644)
|
|
}
|