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 }