minio/cmd/sftp-server.go

371 lines
13 KiB
Go
Raw Normal View History

// Copyright (c) 2015-2023 MinIO, Inc.
//
// This file is part of MinIO Object Storage stack
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package cmd
import (
"bytes"
"context"
"crypto/subtle"
"errors"
"fmt"
"net"
"os"
"strconv"
"strings"
"time"
"github.com/minio/minio/internal/logger"
xsftp "github.com/minio/pkg/v3/sftp"
"github.com/pkg/sftp"
"golang.org/x/crypto/ssh"
)
type sftpLogger struct{}
func (s *sftpLogger) Info(tag xsftp.LogType, msg string) {
logger.Info(msg)
}
Allow custom SFTP algorithm selection (#19636) Algorithms are comma separated. Note that valid values does not in all cases represent default values. `--sftp=pub-key-algos=...` specifies the supported client public key authentication algorithms. Note that this doesn't include certificate types since those use the underlying algorithm. This list is sent to the client if it supports the server-sig-algs extension. Order is irrelevant. Valid values ``` ssh-ed25519 sk-ssh-ed25519@openssh.com sk-ecdsa-sha2-nistp256@openssh.com ecdsa-sha2-nistp256 ecdsa-sha2-nistp384 ecdsa-sha2-nistp521 rsa-sha2-256 rsa-sha2-512 ssh-rsa ssh-dss ``` `--sftp=kex-algos=...` specifies the supported key-exchange algorithms in preference order. Valid values: ``` curve25519-sha256 curve25519-sha256@libssh.org ecdh-sha2-nistp256 ecdh-sha2-nistp384 ecdh-sha2-nistp521 diffie-hellman-group14-sha256 diffie-hellman-group16-sha512 diffie-hellman-group14-sha1 diffie-hellman-group1-sha1 ``` `--sftp=cipher-algos=...` specifies the allowed cipher algorithms. If unspecified then a sensible default is used. Valid values: ``` aes128-ctr aes192-ctr aes256-ctr aes128-gcm@openssh.com aes256-gcm@openssh.com chacha20-poly1305@openssh.com arcfour256 arcfour128 arcfour aes128-cbc 3des-cbc ``` `--sftp=mac-algos=...` specifies a default set of MAC algorithms in preference order. This is based on RFC 4253, section 6.4, but with hmac-md5 variants removed because they have reached the end of their useful life. Valid values: ``` hmac-sha2-256-etm@openssh.com hmac-sha2-512-etm@openssh.com hmac-sha2-256 hmac-sha2-512 hmac-sha1 hmac-sha1-96 ```
2024-04-30 11:15:45 -04:00
const (
kexAlgoDH1SHA1 = "diffie-hellman-group1-sha1"
kexAlgoDH14SHA1 = "diffie-hellman-group14-sha1"
kexAlgoDH14SHA256 = "diffie-hellman-group14-sha256"
kexAlgoDH16SHA512 = "diffie-hellman-group16-sha512"
kexAlgoECDH256 = "ecdh-sha2-nistp256"
kexAlgoECDH384 = "ecdh-sha2-nistp384"
kexAlgoECDH521 = "ecdh-sha2-nistp521"
kexAlgoCurve25519SHA256LibSSH = "curve25519-sha256@libssh.org"
kexAlgoCurve25519SHA256 = "curve25519-sha256"
chacha20Poly1305ID = "chacha20-poly1305@openssh.com"
gcm256CipherID = "aes256-gcm@openssh.com"
aes128cbcID = "aes128-cbc"
tripledescbcID = "3des-cbc"
)
// https://cs.opensource.google/go/x/crypto/+/refs/tags/v0.22.0:ssh/common.go;l=46
// preferredKexAlgos specifies the default preference for key-exchange
// algorithms in preference order. The diffie-hellman-group16-sha512 algorithm
// is disabled by default because it is a bit slower than the others.
var preferredKexAlgos = []string{
kexAlgoCurve25519SHA256, kexAlgoCurve25519SHA256LibSSH,
kexAlgoECDH256, kexAlgoECDH384, kexAlgoECDH521,
kexAlgoDH14SHA256, kexAlgoDH14SHA1,
}
// supportedKexAlgos specifies the supported key-exchange algorithms in
// preference order.
// https://cs.opensource.google/go/x/crypto/+/refs/tags/v0.22.0:ssh/common.go;l=44
var supportedKexAlgos = []string{
kexAlgoCurve25519SHA256, kexAlgoCurve25519SHA256LibSSH,
// P384 and P521 are not constant-time yet, but since we don't
// reuse ephemeral keys, using them for ECDH should be OK.
kexAlgoECDH256, kexAlgoECDH384, kexAlgoECDH521,
kexAlgoDH14SHA256, kexAlgoDH16SHA512, kexAlgoDH14SHA1,
kexAlgoDH1SHA1,
}
// supportedPubKeyAuthAlgos specifies the supported client public key
// authentication algorithms. Note that this doesn't include certificate types
// since those use the underlying algorithm. This list is sent to the client if
// it supports the server-sig-algs extension. Order is irrelevant.
// https://cs.opensource.google/go/x/crypto/+/refs/tags/v0.22.0:ssh/common.go;l=142
var supportedPubKeyAuthAlgos = []string{
ssh.KeyAlgoED25519,
ssh.KeyAlgoSKED25519, ssh.KeyAlgoSKECDSA256,
ssh.KeyAlgoECDSA256, ssh.KeyAlgoECDSA384, ssh.KeyAlgoECDSA521,
ssh.KeyAlgoRSASHA256, ssh.KeyAlgoRSASHA512, ssh.KeyAlgoRSA,
ssh.KeyAlgoDSA,
}
// supportedCiphers lists ciphers we support but might not recommend.
// https://cs.opensource.google/go/x/crypto/+/refs/tags/v0.22.0:ssh/common.go;l=28
var supportedCiphers = []string{
"aes128-ctr", "aes192-ctr", "aes256-ctr",
"aes128-gcm@openssh.com", gcm256CipherID,
chacha20Poly1305ID,
"arcfour256", "arcfour128", "arcfour",
aes128cbcID,
tripledescbcID,
}
// preferredCiphers specifies the default preference for ciphers.
// https://cs.opensource.google/go/x/crypto/+/refs/tags/v0.22.0:ssh/common.go;l=37
var preferredCiphers = []string{
"aes128-gcm@openssh.com", gcm256CipherID,
chacha20Poly1305ID,
"aes128-ctr", "aes192-ctr", "aes256-ctr",
}
// supportedMACs specifies a default set of MAC algorithms in preference order.
// This is based on RFC 4253, section 6.4, but with hmac-md5 variants removed
// because they have reached the end of their useful life.
// https://cs.opensource.google/go/x/crypto/+/refs/tags/v0.22.0:ssh/common.go;l=85
var supportedMACs = []string{
"hmac-sha2-256-etm@openssh.com", "hmac-sha2-512-etm@openssh.com", "hmac-sha2-256", "hmac-sha2-512", "hmac-sha1", "hmac-sha1-96",
}
func (s *sftpLogger) Error(tag xsftp.LogType, err error) {
switch tag {
case xsftp.AcceptNetworkError:
sftpLogOnceIf(context.Background(), err, "accept-limit-sftp")
case xsftp.AcceptChannelError:
sftpLogOnceIf(context.Background(), err, "accept-channel-sftp")
case xsftp.SSHKeyExchangeError:
sftpLogOnceIf(context.Background(), err, "key-exchange-sftp")
default:
sftpLogOnceIf(context.Background(), err, "unknown-error-sftp")
}
}
Allow custom SFTP algorithm selection (#19636) Algorithms are comma separated. Note that valid values does not in all cases represent default values. `--sftp=pub-key-algos=...` specifies the supported client public key authentication algorithms. Note that this doesn't include certificate types since those use the underlying algorithm. This list is sent to the client if it supports the server-sig-algs extension. Order is irrelevant. Valid values ``` ssh-ed25519 sk-ssh-ed25519@openssh.com sk-ecdsa-sha2-nistp256@openssh.com ecdsa-sha2-nistp256 ecdsa-sha2-nistp384 ecdsa-sha2-nistp521 rsa-sha2-256 rsa-sha2-512 ssh-rsa ssh-dss ``` `--sftp=kex-algos=...` specifies the supported key-exchange algorithms in preference order. Valid values: ``` curve25519-sha256 curve25519-sha256@libssh.org ecdh-sha2-nistp256 ecdh-sha2-nistp384 ecdh-sha2-nistp521 diffie-hellman-group14-sha256 diffie-hellman-group16-sha512 diffie-hellman-group14-sha1 diffie-hellman-group1-sha1 ``` `--sftp=cipher-algos=...` specifies the allowed cipher algorithms. If unspecified then a sensible default is used. Valid values: ``` aes128-ctr aes192-ctr aes256-ctr aes128-gcm@openssh.com aes256-gcm@openssh.com chacha20-poly1305@openssh.com arcfour256 arcfour128 arcfour aes128-cbc 3des-cbc ``` `--sftp=mac-algos=...` specifies a default set of MAC algorithms in preference order. This is based on RFC 4253, section 6.4, but with hmac-md5 variants removed because they have reached the end of their useful life. Valid values: ``` hmac-sha2-256-etm@openssh.com hmac-sha2-512-etm@openssh.com hmac-sha2-256 hmac-sha2-512 hmac-sha1 hmac-sha1-96 ```
2024-04-30 11:15:45 -04:00
func filterAlgos(arg string, want []string, allowed []string) []string {
var filteredAlgos []string
found := false
for _, algo := range want {
if len(algo) == 0 {
continue
}
for _, allowedAlgo := range allowed {
algo := strings.ToLower(strings.TrimSpace(algo))
if algo == allowedAlgo {
filteredAlgos = append(filteredAlgos, algo)
found = true
break
}
}
if !found {
logger.Fatal(fmt.Errorf("unknown algorithm %q passed to --sftp=%s\nValid algorithms: %v", algo, arg, strings.Join(allowed, ", ")), "unable to start SFTP server")
}
}
if len(filteredAlgos) == 0 {
logger.Fatal(fmt.Errorf("no valid algorithms passed to --sftp=%s\nValid algorithms: %v", arg, strings.Join(allowed, ", ")), "unable to start SFTP server")
}
return filteredAlgos
}
func startSFTPServer(args []string) {
var (
port int
publicIP string
sshPrivateKey string
userCaKeyFile string
)
Allow custom SFTP algorithm selection (#19636) Algorithms are comma separated. Note that valid values does not in all cases represent default values. `--sftp=pub-key-algos=...` specifies the supported client public key authentication algorithms. Note that this doesn't include certificate types since those use the underlying algorithm. This list is sent to the client if it supports the server-sig-algs extension. Order is irrelevant. Valid values ``` ssh-ed25519 sk-ssh-ed25519@openssh.com sk-ecdsa-sha2-nistp256@openssh.com ecdsa-sha2-nistp256 ecdsa-sha2-nistp384 ecdsa-sha2-nistp521 rsa-sha2-256 rsa-sha2-512 ssh-rsa ssh-dss ``` `--sftp=kex-algos=...` specifies the supported key-exchange algorithms in preference order. Valid values: ``` curve25519-sha256 curve25519-sha256@libssh.org ecdh-sha2-nistp256 ecdh-sha2-nistp384 ecdh-sha2-nistp521 diffie-hellman-group14-sha256 diffie-hellman-group16-sha512 diffie-hellman-group14-sha1 diffie-hellman-group1-sha1 ``` `--sftp=cipher-algos=...` specifies the allowed cipher algorithms. If unspecified then a sensible default is used. Valid values: ``` aes128-ctr aes192-ctr aes256-ctr aes128-gcm@openssh.com aes256-gcm@openssh.com chacha20-poly1305@openssh.com arcfour256 arcfour128 arcfour aes128-cbc 3des-cbc ``` `--sftp=mac-algos=...` specifies a default set of MAC algorithms in preference order. This is based on RFC 4253, section 6.4, but with hmac-md5 variants removed because they have reached the end of their useful life. Valid values: ``` hmac-sha2-256-etm@openssh.com hmac-sha2-512-etm@openssh.com hmac-sha2-256 hmac-sha2-512 hmac-sha1 hmac-sha1-96 ```
2024-04-30 11:15:45 -04:00
allowPubKeys := supportedPubKeyAuthAlgos
allowKexAlgos := preferredKexAlgos
allowCiphers := preferredCiphers
allowMACs := supportedMACs
var err error
for _, arg := range args {
tokens := strings.SplitN(arg, "=", 2)
if len(tokens) != 2 {
logger.Fatal(fmt.Errorf("invalid arguments passed to --sftp=%s", arg), "unable to start SFTP server")
}
switch tokens[0] {
case "address":
host, portStr, err := net.SplitHostPort(tokens[1])
if err != nil {
logger.Fatal(fmt.Errorf("invalid arguments passed to --sftp=%s (%v)", arg, err), "unable to start SFTP server")
}
port, err = strconv.Atoi(portStr)
if err != nil {
logger.Fatal(fmt.Errorf("invalid arguments passed to --sftp=%s (%v)", arg, err), "unable to start SFTP server")
}
if port < 1 || port > 65535 {
logger.Fatal(fmt.Errorf("invalid arguments passed to --sftp=%s, (port number must be between 1 to 65535)", arg), "unable to start SFTP server")
}
publicIP = host
case "ssh-private-key":
sshPrivateKey = tokens[1]
Allow custom SFTP algorithm selection (#19636) Algorithms are comma separated. Note that valid values does not in all cases represent default values. `--sftp=pub-key-algos=...` specifies the supported client public key authentication algorithms. Note that this doesn't include certificate types since those use the underlying algorithm. This list is sent to the client if it supports the server-sig-algs extension. Order is irrelevant. Valid values ``` ssh-ed25519 sk-ssh-ed25519@openssh.com sk-ecdsa-sha2-nistp256@openssh.com ecdsa-sha2-nistp256 ecdsa-sha2-nistp384 ecdsa-sha2-nistp521 rsa-sha2-256 rsa-sha2-512 ssh-rsa ssh-dss ``` `--sftp=kex-algos=...` specifies the supported key-exchange algorithms in preference order. Valid values: ``` curve25519-sha256 curve25519-sha256@libssh.org ecdh-sha2-nistp256 ecdh-sha2-nistp384 ecdh-sha2-nistp521 diffie-hellman-group14-sha256 diffie-hellman-group16-sha512 diffie-hellman-group14-sha1 diffie-hellman-group1-sha1 ``` `--sftp=cipher-algos=...` specifies the allowed cipher algorithms. If unspecified then a sensible default is used. Valid values: ``` aes128-ctr aes192-ctr aes256-ctr aes128-gcm@openssh.com aes256-gcm@openssh.com chacha20-poly1305@openssh.com arcfour256 arcfour128 arcfour aes128-cbc 3des-cbc ``` `--sftp=mac-algos=...` specifies a default set of MAC algorithms in preference order. This is based on RFC 4253, section 6.4, but with hmac-md5 variants removed because they have reached the end of their useful life. Valid values: ``` hmac-sha2-256-etm@openssh.com hmac-sha2-512-etm@openssh.com hmac-sha2-256 hmac-sha2-512 hmac-sha1 hmac-sha1-96 ```
2024-04-30 11:15:45 -04:00
case "pub-key-algos":
allowPubKeys = filterAlgos(arg, strings.Split(tokens[1], ","), supportedPubKeyAuthAlgos)
case "kex-algos":
allowKexAlgos = filterAlgos(arg, strings.Split(tokens[1], ","), supportedKexAlgos)
case "cipher-algos":
allowCiphers = filterAlgos(arg, strings.Split(tokens[1], ","), supportedCiphers)
case "mac-algos":
allowMACs = filterAlgos(arg, strings.Split(tokens[1], ","), supportedMACs)
case "trusted-user-ca-key":
userCaKeyFile = tokens[1]
}
}
if port == 0 {
port = 8022 // Default SFTP port, since no port was given.
}
if sshPrivateKey == "" {
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")
}
privateBytes, err := os.ReadFile(sshPrivateKey)
if err != nil {
logger.Fatal(fmt.Errorf("invalid arguments passed, private key file is not accessible: %v", err), "unable to start SFTP server")
}
private, err := ssh.ParsePrivateKey(privateBytes)
if err != nil {
logger.Fatal(fmt.Errorf("invalid arguments passed, private key file is not parseable: %v", err), "unable to start SFTP server")
}
// An SSH server is represented by a ServerConfig, which holds
// certificate details and handles authentication of ServerConns.
sshConfig := &ssh.ServerConfig{
Allow custom SFTP algorithm selection (#19636) Algorithms are comma separated. Note that valid values does not in all cases represent default values. `--sftp=pub-key-algos=...` specifies the supported client public key authentication algorithms. Note that this doesn't include certificate types since those use the underlying algorithm. This list is sent to the client if it supports the server-sig-algs extension. Order is irrelevant. Valid values ``` ssh-ed25519 sk-ssh-ed25519@openssh.com sk-ecdsa-sha2-nistp256@openssh.com ecdsa-sha2-nistp256 ecdsa-sha2-nistp384 ecdsa-sha2-nistp521 rsa-sha2-256 rsa-sha2-512 ssh-rsa ssh-dss ``` `--sftp=kex-algos=...` specifies the supported key-exchange algorithms in preference order. Valid values: ``` curve25519-sha256 curve25519-sha256@libssh.org ecdh-sha2-nistp256 ecdh-sha2-nistp384 ecdh-sha2-nistp521 diffie-hellman-group14-sha256 diffie-hellman-group16-sha512 diffie-hellman-group14-sha1 diffie-hellman-group1-sha1 ``` `--sftp=cipher-algos=...` specifies the allowed cipher algorithms. If unspecified then a sensible default is used. Valid values: ``` aes128-ctr aes192-ctr aes256-ctr aes128-gcm@openssh.com aes256-gcm@openssh.com chacha20-poly1305@openssh.com arcfour256 arcfour128 arcfour aes128-cbc 3des-cbc ``` `--sftp=mac-algos=...` specifies a default set of MAC algorithms in preference order. This is based on RFC 4253, section 6.4, but with hmac-md5 variants removed because they have reached the end of their useful life. Valid values: ``` hmac-sha2-256-etm@openssh.com hmac-sha2-512-etm@openssh.com hmac-sha2-256 hmac-sha2-512 hmac-sha1 hmac-sha1-96 ```
2024-04-30 11:15:45 -04:00
Config: ssh.Config{
KeyExchanges: allowKexAlgos,
Ciphers: allowCiphers,
MACs: allowMACs,
},
PublicKeyAuthAlgorithms: allowPubKeys,
PasswordCallback: func(c ssh.ConnMetadata, pass []byte) (*ssh.Permissions, error) {
if globalIAMSys.LDAPConfig.Enabled() {
sa, _, err := globalIAMSys.getServiceAccount(context.Background(), c.User())
if err != nil && !errors.Is(err, errNoSuchServiceAccount) {
return nil, err
}
if errors.Is(err, errNoSuchServiceAccount) {
lookupResult, targetGroups, err := globalIAMSys.LDAPConfig.Bind(c.User(), string(pass))
if err != nil {
return nil, err
}
targetUser := lookupResult.NormDN
ldapPolicies, _ := globalIAMSys.PolicyDBGet(targetUser, targetGroups...)
if len(ldapPolicies) == 0 {
return nil, errAuthentication
}
criticalOptions := map[string]string{
ldapUser: targetUser,
ldapUserN: c.User(),
}
for attribKey, attribValue := range lookupResult.Attributes {
// we skip multi-value attributes here, as they cannot
// be stored in the critical options.
if len(attribValue) == 1 {
criticalOptions[ldapAttribPrefix+attribKey] = attribValue[0]
}
}
return &ssh.Permissions{
CriticalOptions: criticalOptions,
Extensions: make(map[string]string),
}, nil
}
if subtle.ConstantTimeCompare([]byte(sa.Credentials.SecretKey), pass) == 1 {
return &ssh.Permissions{
CriticalOptions: map[string]string{
"accessKey": c.User(),
},
Extensions: make(map[string]string),
}, nil
}
return nil, errAuthentication
}
ui, ok := globalIAMSys.GetUser(context.Background(), c.User())
if !ok {
return nil, errNoSuchUser
}
if subtle.ConstantTimeCompare([]byte(ui.Credentials.SecretKey), pass) == 1 {
return &ssh.Permissions{
CriticalOptions: map[string]string{
"accessKey": c.User(),
},
Extensions: make(map[string]string),
}, nil
}
return nil, errAuthentication
},
}
if userCaKeyFile != "" {
keyBytes, err := os.ReadFile(userCaKeyFile)
if err != nil {
logger.Fatal(fmt.Errorf("invalid arguments passed, trusted user certificate authority public key file is not accessible: %v", err), "unable to start SFTP server")
}
caPublicKey, _, _, _, err := ssh.ParseAuthorizedKey(keyBytes)
if err != nil {
logger.Fatal(fmt.Errorf("invalid arguments passed, trusted user certificate authority public key file is not parseable: %v", err), "unable to start SFTP server")
}
sshConfig.PublicKeyCallback = func(c ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) {
_, ok := globalIAMSys.GetUser(context.Background(), c.User())
if !ok {
return nil, errNoSuchUser
}
// Verify that client provided certificate, not only public key.
cert, ok := key.(*ssh.Certificate)
if !ok {
return nil, errSftpPublicKeyWithoutCert
}
// ssh.CheckCert called by ssh.Authenticate accepts certificates
// with empty principles list so we block those in here.
if len(cert.ValidPrincipals) == 0 {
return nil, errSftpCertWithoutPrincipals
}
// Verify that certificate provided by user is issued by trusted CA,
// username in authentication request matches to identities in certificate
// and that certificate type is correct.
checker := ssh.CertChecker{}
checker.IsUserAuthority = func(k ssh.PublicKey) bool {
return bytes.Equal(k.Marshal(), caPublicKey.Marshal())
}
_, err = checker.Authenticate(c, key)
if err != nil {
return nil, err
}
return &ssh.Permissions{
CriticalOptions: map[string]string{
"accessKey": c.User(),
},
Extensions: make(map[string]string),
}, nil
}
}
sshConfig.AddHostKey(private)
handleSFTPSession := func(channel ssh.Channel, sconn *ssh.ServerConn) {
server := sftp.NewRequestServer(channel, NewSFTPDriver(sconn.Permissions), sftp.WithRSAllocator())
defer server.Close()
server.Serve()
}
sftpServer, err := xsftp.NewServer(&xsftp.Options{
PublicIP: publicIP,
Port: port,
// OpensSSH default handshake timeout is 2 minutes.
SSHHandshakeDeadline: 2 * time.Minute,
Logger: new(sftpLogger),
SSHConfig: sshConfig,
HandleSFTPSession: handleSFTPSession,
})
if err != nil {
logger.Fatal(err, "Unable to start SFTP Server")
}
err = sftpServer.Listen()
if err != nil {
logger.Fatal(err, "SFTP Server had an unrecoverable error while accepting connections")
}
}