Support for HAProxy PROXY protocol

you can proxy and/or load balance the SFTP/SCP service without losing
the information about the client's address.
This commit is contained in:
Nicola Murino 2020-02-27 09:21:30 +01:00
parent 637463a068
commit 830e3d1f64
8 changed files with 95 additions and 23 deletions

View file

@ -5,7 +5,8 @@ os:
- osx
go:
- "1.13.x"
- 1.13.x
- 1.14.x
env:
- GO111MODULE=on

View file

@ -27,6 +27,7 @@ Full featured and highly configurable SFTP server
- SCP and rsync are supported.
- Support for serving local filesystem, S3 Compatible Object Storage and Google Cloud Storage over SFTP/SCP.
- Prometheus metrics are exposed.
- Support for HAProxy PROXY protocol: you can proxy and/or load balance the SFTP/SCP service without losing the information about the client's address.
- REST API for users management, backup, restore and real time reports of the active connections with possibility of forcibly closing a connection.
- Web based interface to easily manage users and connections.
- Easy migration from Linux system user accounts.
@ -159,6 +160,7 @@ The `sftpgo` configuration file contains the following sections:
- `git-receive-pack`, `git-upload-pack`, `git-upload-archive`. These commands enable support for Git repositories over SSH, they need to be installed and in your system's `PATH`. Git commands are not allowed inside virtual folders.
- `rsync`. The `rsync` command need to be installed and in your system's `PATH`. We cannot avoid that rsync create symlinks so if the user has the permission to create symlinks we add the option `--safe-links` to the received rsync command if it is not already set. This should prevent to create symlinks that point outside the home dir. If the user cannot create symlinks we add the option `--munge-links`, if it is not already set. This should make symlinks unusable (but manually recoverable). The `rsync` command interacts with the filesystem directly and it is not aware about virtual folders, so it will be automatically disabled for users with virtual folders.
- `keyboard_interactive_auth_program`, string. Absolute path to an external program to use for keyboard interactive authentication. See the "Keyboard Interactive Authentication" paragraph for more details.
- `proxy_protocol`, integer. Support for [HAProxy PROXY protocol](https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt). If you are running SFTPGo behind a proxy server such as HAProxy, AWS ELB or NGNIX, you can enable the proxy protocol. It provides a convenient way to safely transport connection information such as a client's address across multiple layers of NAT or TCP proxies to get the real client IP address instead of the proxy IP. Set to 1 to enable proxy protocol, default 0. You have to enable the protocol in your proxy configuration too, for example for HAProxy add `send-proxy` or `send-proxy-v2` to each server configuration line
- **"data_provider"**, the configuration for the data provider
- `driver`, string. Supported drivers are `sqlite`, `mysql`, `postgresql`, `bolt`, `memory`
- `name`, string. Database name. For driver `sqlite` this can be the database name relative to the config dir or the absolute path to the SQLite database. For driver `memory` this is the (optional) path relative to the config dir or the absolute path to the users dump to load.
@ -219,7 +221,8 @@ Here is a full example showing the default config in JSON format:
"login_banner_file": "",
"setstat_mode": 0,
"enabled_ssh_commands": ["md5sum", "sha1sum", "cd", "pwd"],
"keyboard_interactive_auth_program": ""
"keyboard_interactive_auth_program": "",
"proxy_protocol": 0
},
"data_provider": {
"driver": "sqlite",
@ -909,6 +912,7 @@ There is an open [issue](https://github.com/drakkan/sftpgo/issues/69) with some
- [ZeroConf](https://github.com/grandcat/zeroconf)
- [SB Admin 2](https://github.com/BlackrockDigital/startbootstrap-sb-admin-2)
- [shlex](https://github.com/google/shlex)
- [go-proxyproto](https://github.com/pires/go-proxyproto)
Some code was initially taken from [Pterodactyl sftp server](https://github.com/pterodactyl/sftp-server)

View file

@ -62,6 +62,7 @@ func init() {
LoginBannerFile: "",
EnabledSSHCommands: sftpd.GetDefaultSSHCommands(),
KeyboardInteractiveProgram: "",
ProxyProtocol: 0,
},
ProviderConf: dataprovider.Config{
Driver: "sqlite",
@ -85,7 +86,7 @@ func init() {
ExternalAuthProgram: "",
ExternalAuthScope: 0,
CredentialsPath: "credentials",
PreLoginProgram: "",
PreLoginProgram: "",
},
HTTPDConfig: httpd.Conf{
BindPort: 8080,

1
go.mod
View file

@ -17,6 +17,7 @@ require (
github.com/mattn/go-sqlite3 v2.0.3+incompatible
github.com/nathanaelle/password v1.0.0
github.com/pelletier/go-toml v1.6.0 // indirect
github.com/pires/go-proxyproto v0.0.0-20200213100827-833e5d06d8f0
github.com/pkg/sftp v1.11.0
github.com/prometheus/client_golang v1.4.1
github.com/prometheus/procfs v0.0.10 // indirect

2
go.sum
View file

@ -164,6 +164,8 @@ github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pelletier/go-toml v1.6.0 h1:aetoXYr0Tv7xRU/V4B4IZJ2QcbtMUFoNb3ORp7TzIK4=
github.com/pelletier/go-toml v1.6.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys=
github.com/pires/go-proxyproto v0.0.0-20200213100827-833e5d06d8f0 h1:iIf32tEZ9PiKn9rPH6kloHsKshbnv8rZ8NWPfJjXj7o=
github.com/pires/go-proxyproto v0.0.0-20200213100827-833e5d06d8f0/go.mod h1:Odh9VFOZJCf9G8cLW5o435Xf1J95Jw9Gw5rnCjcwzAY=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=

View file

@ -16,6 +16,7 @@ import (
"github.com/drakkan/sftpgo/logger"
"github.com/drakkan/sftpgo/metrics"
"github.com/drakkan/sftpgo/utils"
"github.com/pires/go-proxyproto"
"github.com/pkg/sftp"
"golang.org/x/crypto/ssh"
)
@ -99,6 +100,15 @@ type Configuration struct {
// Absolute path to an external program to use for keyboard interactive authentication.
// Leave empty to disable this authentication mode.
KeyboardInteractiveProgram string `json:"keyboard_interactive_auth_program" mapstructure:"keyboard_interactive_auth_program"`
// Support for HAProxy PROXY protocol.
// If you are running SFTPGo behind a proxy server such as HAProxy, AWS ELB or NGNIX, you can enable
// the proxy protocol. It provides a convenient way to safely transport connection information
// such as a client's address across multiple layers of NAT or TCP proxies to get the real
// client IP address instead of the proxy IP.
// Set to 1 to enable proxy protocol.
// You have to enable the protocol in your proxy configuration too, for example for HAProxy
// add "send-proxy" or "send-proxy-v2" to each server configuration line
ProxyProtocol int `json:"proxy_protocol" mapstructure:"proxy_protocol"`
}
// Key contains information about host keys
@ -183,23 +193,38 @@ func (c Configuration) Initialize(configDir string) error {
logger.Warn(logSender, "", "error starting listener on address %s:%d: %v", c.BindAddress, c.BindPort, err)
return err
}
var proxyListener *proxyproto.Listener
if c.ProxyProtocol == 1 {
proxyListener = &proxyproto.Listener{
Listener: listener,
}
}
actions = c.Actions
uploadMode = c.UploadMode
setstatMode = c.SetstatMode
logger.Info(logSender, "", "server listener registered address: %v", listener.Addr().String())
if c.IdleTimeout > 0 {
startIdleTimer(time.Duration(c.IdleTimeout) * time.Minute)
}
c.checkIdleTimer()
for {
conn, _ := listener.Accept()
if conn != nil {
var conn net.Conn
if proxyListener != nil {
conn, err = proxyListener.Accept()
} else {
conn, err = listener.Accept()
}
if conn != nil && err == nil {
go c.AcceptInboundConnection(conn, serverConfig)
}
}
}
func (c Configuration) checkIdleTimer() {
if c.IdleTimeout > 0 {
startIdleTimer(time.Duration(c.IdleTimeout) * time.Minute)
}
}
func (c Configuration) configureSecurityOptions(serverConfig *ssh.ServerConfig) {
if len(c.KexAlgorithms) > 0 {
serverConfig.KeyExchanges = c.KexAlgorithms

View file

@ -89,18 +89,18 @@ iixITGvaNZh/tjAAAACW5pY29sYUBwMQE=
)
var (
allPerms = []string{dataprovider.PermAny}
homeBasePath string
scpPath string
gitPath string
sshPath string
pubKeyPath string
privateKeyPath string
gitWrapPath string
extAuthPath string
keyIntAuthPath string
preLoginPath string
logFilePath string
allPerms = []string{dataprovider.PermAny}
homeBasePath string
scpPath string
gitPath string
sshPath string
pubKeyPath string
privateKeyPath string
gitWrapPath string
extAuthPath string
keyIntAuthPath string
preLoginPath string
logFilePath string
)
func TestMain(m *testing.M) {
@ -205,6 +205,17 @@ func TestMain(m *testing.M) {
waitTCPListening(fmt.Sprintf("%s:%d", sftpdConf.BindAddress, sftpdConf.BindPort))
waitTCPListening(fmt.Sprintf("%s:%d", httpdConf.BindAddress, httpdConf.BindPort))
sftpdConf.BindPort = 2222
sftpdConf.ProxyProtocol = 1
go func() {
logger.Debug(logSender, "", "initializing SFTP server with config %+v", sftpdConf)
if err := sftpdConf.Initialize(configDir); err != nil {
logger.Error(logSender, "", "could not start SFTP server: %v", err)
}
}()
waitTCPListening(fmt.Sprintf("%s:%d", sftpdConf.BindAddress, sftpdConf.BindPort))
exitCode := m.Run()
os.Remove(logFilePath)
os.Remove(loginBannerFile)
@ -312,6 +323,28 @@ func TestBasicSFTPHandling(t *testing.T) {
os.RemoveAll(user.GetHomeDir())
}
func TestProxyProtocol(t *testing.T) {
usePubKey := false
user, _, err := httpd.AddUser(getTestUser(usePubKey), http.StatusOK)
if err != nil {
t.Errorf("unable to add user: %v", err)
}
// remove the home dir to test auto creation
os.RemoveAll(user.HomeDir)
client, err := getSftpClientWithAddr(user, usePubKey, "127.0.0.1:2222")
if err != nil {
t.Errorf("unable to create sftp client: %v", err)
} else {
defer client.Close()
_, err = client.Getwd()
if err != nil {
t.Errorf("error mkdir: %v", err)
}
}
httpd.RemoveUser(user, http.StatusOK)
os.RemoveAll(user.GetHomeDir())
}
func TestUploadResume(t *testing.T) {
usePubKey := false
u := getTestUser(usePubKey)
@ -4626,7 +4659,7 @@ func runSSHCommand(command string, user dataprovider.User, usePubKey bool) ([]by
return stdout.Bytes(), err
}
func getSftpClient(user dataprovider.User, usePubKey bool) (*sftp.Client, error) {
func getSftpClientWithAddr(user dataprovider.User, usePubKey bool, addr string) (*sftp.Client, error) {
var sftpClient *sftp.Client
config := &ssh.ClientConfig{
User: user.Username,
@ -4647,7 +4680,7 @@ func getSftpClient(user dataprovider.User, usePubKey bool) (*sftp.Client, error)
config.Auth = []ssh.AuthMethod{ssh.Password(defaultPassword)}
}
}
conn, err := ssh.Dial("tcp", sftpServerAddr, config)
conn, err := ssh.Dial("tcp", addr, config)
if err != nil {
return sftpClient, err
}
@ -4655,6 +4688,10 @@ func getSftpClient(user dataprovider.User, usePubKey bool) (*sftp.Client, error)
return sftpClient, err
}
func getSftpClient(user dataprovider.User, usePubKey bool) (*sftp.Client, error) {
return getSftpClientWithAddr(user, usePubKey, sftpServerAddr)
}
func getKeyboardInteractiveSftpClient(user dataprovider.User, answers []string) (*sftp.Client, error) {
var sftpClient *sftp.Client
config := &ssh.ClientConfig{

View file

@ -20,7 +20,8 @@
"login_banner_file": "",
"setstat_mode": 0,
"enabled_ssh_commands": ["md5sum", "sha1sum", "cd", "pwd"],
"keyboard_interactive_auth_program": ""
"keyboard_interactive_auth_program": "",
"proxy_protocol": 0
},
"data_provider": {
"driver": "sqlite",