diff --git a/cmd/sftp-server.go b/cmd/sftp-server.go index 1ebd1fdad..6e448e7ff 100644 --- a/cmd/sftp-server.go +++ b/cmd/sftp-server.go @@ -18,6 +18,7 @@ package cmd import ( + "bytes" "context" "crypto/subtle" "errors" @@ -162,6 +163,7 @@ func startSFTPServer(args []string) { port int publicIP string sshPrivateKey string + userCaKeyFile string ) allowPubKeys := supportedPubKeyAuthAlgos allowKexAlgos := preferredKexAlgos @@ -197,6 +199,8 @@ func startSFTPServer(args []string) { 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] } } @@ -278,6 +282,56 @@ func startSFTPServer(args []string) { }, } + 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) { diff --git a/cmd/typed-errors.go b/cmd/typed-errors.go index ded5735bb..ea98c480d 100644 --- a/cmd/typed-errors.go +++ b/cmd/typed-errors.go @@ -119,3 +119,9 @@ var errInvalidMaxParts = errors.New("Part number is greater than the maximum all // error returned for session policies > 2048 var errSessionPolicyTooLarge = errors.New("Session policy should not exceed 2048 characters") + +// error returned in SFTP when user used public key without certificate +var errSftpPublicKeyWithoutCert = errors.New("public key authentication without certificate is not accepted") + +// error returned in SFTP when user used certificate which does not contain principal(s) +var errSftpCertWithoutPrincipals = errors.New("certificates without principal(s) are not accepted") diff --git a/docs/ftp/README.md b/docs/ftp/README.md index 2c6f4afd3..1fb6e5795 100644 --- a/docs/ftp/README.md +++ b/docs/ftp/README.md @@ -242,3 +242,16 @@ hmac-sha1 hmac-sha1-96 ``` +### Certificate-based authentication + +`--sftp=trusted-user-ca-key=...` specifies a file containing public key of certificate authority that is trusted +to sign user certificates for authentication. + +Implementation is identical with "TrustedUserCAKeys" setting in OpenSSH server with exception that only one CA +key can be defined. + +If a certificate is presented for authentication and has its signing CA key is in this file, then it may be +used for authentication for any user listed in the certificate's principals list. + +Note that certificates that lack a list of principals will not be permitted for authentication using trusted-user-ca-key. +For more details on certificates, see the CERTIFICATES section in ssh-keygen(1).