github.com/minio/minio@v0.0.0-20240328213742-3f72439b8a27/cmd/sftp-server.go (about)

     1  // Copyright (c) 2015-2023 MinIO, Inc.
     2  //
     3  // This file is part of MinIO Object Storage stack
     4  //
     5  // This program is free software: you can redistribute it and/or modify
     6  // it under the terms of the GNU Affero General Public License as published by
     7  // the Free Software Foundation, either version 3 of the License, or
     8  // (at your option) any later version.
     9  //
    10  // This program is distributed in the hope that it will be useful
    11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13  // GNU Affero General Public License for more details.
    14  //
    15  // You should have received a copy of the GNU Affero General Public License
    16  // along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17  
    18  package cmd
    19  
    20  import (
    21  	"context"
    22  	"crypto/subtle"
    23  	"errors"
    24  	"fmt"
    25  	"net"
    26  	"os"
    27  	"strconv"
    28  	"strings"
    29  	"time"
    30  
    31  	"github.com/minio/minio/internal/logger"
    32  	xsftp "github.com/minio/pkg/v2/sftp"
    33  	"github.com/pkg/sftp"
    34  	"golang.org/x/crypto/ssh"
    35  )
    36  
    37  type sftpLogger struct{}
    38  
    39  func (s *sftpLogger) Info(tag xsftp.LogType, msg string) {
    40  	logger.Info(msg)
    41  }
    42  
    43  func (s *sftpLogger) Error(tag xsftp.LogType, err error) {
    44  	switch tag {
    45  	case xsftp.AcceptNetworkError:
    46  		logger.LogOnceIf(context.Background(), err, "accept-limit-sftp")
    47  	case xsftp.AcceptChannelError:
    48  		logger.LogOnceIf(context.Background(), err, "accept-channel-sftp")
    49  	case xsftp.SSHKeyExchangeError:
    50  		logger.LogOnceIf(context.Background(), err, "key-exchange-sftp")
    51  	default:
    52  		logger.LogOnceIf(context.Background(), err, "unknown-error-sftp")
    53  	}
    54  }
    55  
    56  func startSFTPServer(args []string) {
    57  	var (
    58  		port          int
    59  		publicIP      string
    60  		sshPrivateKey string
    61  	)
    62  
    63  	var err error
    64  	for _, arg := range args {
    65  		tokens := strings.SplitN(arg, "=", 2)
    66  		if len(tokens) != 2 {
    67  			logger.Fatal(fmt.Errorf("invalid arguments passed to --sftp=%s", arg), "unable to start SFTP server")
    68  		}
    69  		switch tokens[0] {
    70  		case "address":
    71  			host, portStr, err := net.SplitHostPort(tokens[1])
    72  			if err != nil {
    73  				logger.Fatal(fmt.Errorf("invalid arguments passed to --sftp=%s (%v)", arg, err), "unable to start SFTP server")
    74  			}
    75  			port, err = strconv.Atoi(portStr)
    76  			if err != nil {
    77  				logger.Fatal(fmt.Errorf("invalid arguments passed to --sftp=%s (%v)", arg, err), "unable to start SFTP server")
    78  			}
    79  			if port < 1 || port > 65535 {
    80  				logger.Fatal(fmt.Errorf("invalid arguments passed to --sftp=%s, (port number must be between 1 to 65535)", arg), "unable to start SFTP server")
    81  			}
    82  			publicIP = host
    83  		case "ssh-private-key":
    84  			sshPrivateKey = tokens[1]
    85  		}
    86  	}
    87  
    88  	if port == 0 {
    89  		port = 8022 // Default SFTP port, since no port was given.
    90  	}
    91  
    92  	if sshPrivateKey == "" {
    93  		logger.Fatal(fmt.Errorf("invalid arguments passed, private key file is mandatory for --sftp='ssh-private-key=path/to/id_ecdsa'"), "unable to start SFTP server")
    94  	}
    95  
    96  	privateBytes, err := os.ReadFile(sshPrivateKey)
    97  	if err != nil {
    98  		logger.Fatal(fmt.Errorf("invalid arguments passed, private key file is not accessible: %v", err), "unable to start SFTP server")
    99  	}
   100  
   101  	private, err := ssh.ParsePrivateKey(privateBytes)
   102  	if err != nil {
   103  		logger.Fatal(fmt.Errorf("invalid arguments passed, private key file is not parseable: %v", err), "unable to start SFTP server")
   104  	}
   105  
   106  	// An SSH server is represented by a ServerConfig, which holds
   107  	// certificate details and handles authentication of ServerConns.
   108  	sshConfig := &ssh.ServerConfig{
   109  		PasswordCallback: func(c ssh.ConnMetadata, pass []byte) (*ssh.Permissions, error) {
   110  			if globalIAMSys.LDAPConfig.Enabled() {
   111  				sa, _, err := globalIAMSys.getServiceAccount(context.Background(), c.User())
   112  				if err != nil && !errors.Is(err, errNoSuchServiceAccount) {
   113  					return nil, err
   114  				}
   115  				if errors.Is(err, errNoSuchServiceAccount) {
   116  					targetUser, targetGroups, err := globalIAMSys.LDAPConfig.Bind(c.User(), string(pass))
   117  					if err != nil {
   118  						return nil, err
   119  					}
   120  					ldapPolicies, _ := globalIAMSys.PolicyDBGet(targetUser, targetGroups...)
   121  					if len(ldapPolicies) == 0 {
   122  						return nil, errAuthentication
   123  					}
   124  					return &ssh.Permissions{
   125  						CriticalOptions: map[string]string{
   126  							ldapUser:  targetUser,
   127  							ldapUserN: c.User(),
   128  						},
   129  						Extensions: make(map[string]string),
   130  					}, nil
   131  				}
   132  				if subtle.ConstantTimeCompare([]byte(sa.Credentials.SecretKey), pass) == 1 {
   133  					return &ssh.Permissions{
   134  						CriticalOptions: map[string]string{
   135  							"accessKey": c.User(),
   136  						},
   137  						Extensions: make(map[string]string),
   138  					}, nil
   139  				}
   140  				return nil, errAuthentication
   141  			}
   142  
   143  			ui, ok := globalIAMSys.GetUser(context.Background(), c.User())
   144  			if !ok {
   145  				return nil, errNoSuchUser
   146  			}
   147  
   148  			if subtle.ConstantTimeCompare([]byte(ui.Credentials.SecretKey), pass) == 1 {
   149  				return &ssh.Permissions{
   150  					CriticalOptions: map[string]string{
   151  						"accessKey": c.User(),
   152  					},
   153  					Extensions: make(map[string]string),
   154  				}, nil
   155  			}
   156  			return nil, errAuthentication
   157  		},
   158  	}
   159  
   160  	sshConfig.AddHostKey(private)
   161  
   162  	handleSFTPSession := func(channel ssh.Channel, sconn *ssh.ServerConn) {
   163  		server := sftp.NewRequestServer(channel, NewSFTPDriver(sconn.Permissions), sftp.WithRSAllocator())
   164  		defer server.Close()
   165  		server.Serve()
   166  	}
   167  
   168  	sftpServer, err := xsftp.NewServer(&xsftp.Options{
   169  		PublicIP: publicIP,
   170  		Port:     port,
   171  		// OpensSSH default handshake timeout is 2 minutes.
   172  		SSHHandshakeDeadline: 2 * time.Minute,
   173  		Logger:               new(sftpLogger),
   174  		SSHConfig:            sshConfig,
   175  		HandleSFTPSession:    handleSFTPSession,
   176  	})
   177  	if err != nil {
   178  		logger.Fatal(err, "Unable to start SFTP Server")
   179  	}
   180  
   181  	err = sftpServer.Listen()
   182  	if err != nil {
   183  		logger.Fatal(err, "SFTP Server had an unrecoverable error while accepting connections")
   184  	}
   185  }