sftpd: add support for SSH user certificate authentication

This add support for PROTOCOL.certkeys vendor extension:

https://cvsweb.openbsd.org/src/usr.bin/ssh/PROTOCOL.certkeys?rev=1.8

Fixes #117

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
Nicola Murino 2020-05-15 20:08:53 +02:00
parent 82fb7f8cf0
commit 738c7ab43e
12 changed files with 346 additions and 56 deletions

View file

@ -10,6 +10,7 @@ Fully featured and highly configurable SFTP server, written in Go
- SFTP accounts are virtual accounts stored in a "data provider".
- SQLite, MySQL, PostgreSQL, bbolt (key/value store in pure Go) and in-memory data providers are supported.
- Public key and password authentication. Multiple public keys per user are supported.
- SSH user [certificate authentication](https://cvsweb.openbsd.org/src/usr.bin/ssh/PROTOCOL.certkeys?rev=1.8).
- Keyboard interactive authentication. You can easily setup a customizable multi-factor authentication.
- Partial authentication. You can configure multi-step authentication requiring, for example, the user password after successful public key authentication.
- Per user authentication methods. You can, for example, deny one or more authentication methods to one or more users.

View file

@ -61,6 +61,7 @@ func init() {
KexAlgorithms: []string{},
Ciphers: []string{},
MACs: []string{},
TrustedUserCAKeys: []string{},
LoginBannerFile: "",
EnabledSSHCommands: sftpd.GetDefaultSSHCommands(),
KeyboardInteractiveHook: "",

View file

@ -956,8 +956,13 @@ func checkUserAndPubKey(user User, pubKey []byte) (User, string, error) {
return user, "", err
}
if bytes.Equal(storedPubKey.Marshal(), pubKey) {
fp := ssh.FingerprintSHA256(storedPubKey)
return user, fp + ":" + comment, nil
certInfo := ""
cert, ok := storedPubKey.(*ssh.Certificate)
if ok {
certInfo = fmt.Sprintf(" %v ID: %v Serial: %v CA: %v", cert.Type(), cert.KeyId, cert.Serial,
ssh.FingerprintSHA256(cert.SignatureKey))
}
return user, fmt.Sprintf("%v:%v%v", ssh.FingerprintSHA256(storedPubKey), comment, certInfo), nil
}
}
return user, "", errors.New("Invalid credentials")

View file

@ -54,7 +54,8 @@ The configuration file contains the following sections:
- `private_key`, path to the private key file. It can be a path relative to the config dir or an absolute one.
- `kex_algorithms`, list of strings. Available KEX (Key Exchange) algorithms in preference order. Leave empty to use default values. The supported values can be found here: [`crypto/ssh`](https://github.com/golang/crypto/blob/master/ssh/common.go#L46 "Supported kex algos")
- `ciphers`, list of strings. Allowed ciphers. Leave empty to use default values. The supported values can be found here: [`crypto/ssh`](https://github.com/golang/crypto/blob/master/ssh/common.go#L28 "Supported ciphers")
- `macs`, list of strings. available MAC (message authentication code) algorithms in preference order. Leave empty to use default values. The supported values can be found here: [`crypto/ssh`](https://github.com/golang/crypto/blob/master/ssh/common.go#L84 "Supported MACs")
- `macs`, list of strings. Available MAC (message authentication code) algorithms in preference order. Leave empty to use default values. The supported values can be found here: [`crypto/ssh`](https://github.com/golang/crypto/blob/master/ssh/common.go#L84 "Supported MACs")
- `trusted_user_ca_keys`, list of public keys paths of certificate authorities that are trusted to sign user certificates for authentication. The paths can be absolute or relative to the configuration directory.
- `login_banner_file`, path to the login banner file. The contents of the specified file, if any, are sent to the remote user before authentication is allowed. It can be a path relative to the config dir or an absolute one. Leave empty to disable login banner.
- `setstat_mode`, integer. 0 means "normal mode": requests for changing permissions, owner/group and access/modification times are executed. 1 means "ignore mode": requests for changing permissions, owner/group and access/modification times are silently ignored.
- `enabled_ssh_commands`, list of enabled SSH commands. These SSH commands are enabled by default: `md5sum`, `sha1sum`, `cd`, `pwd`, `scp`. `*` enables all supported commands. Some commands are implemented directly inside SFTPGo, while for other commands we use system commands that need to be installed and in your system's `PATH`. For system commands we have no direct control on file creation/deletion and so we cannot support virtual folders, cloud storage filesystem, such as S3, and quota check is suboptimal: if quota is enabled, the number of files is checked at the command start and not while new files are created. The allowed size is calculated as the difference between the max quota and the used one, and it is checked against the bytes transferred via SSH. The command is aborted if it uploads more bytes than the remaining allowed size calculated at the command start. Anyway, we see the bytes that the remote command sends to the local command via SSH. These bytes contain both protocol commands and files, and so the size of the files is different from the size trasferred via SSH: for example, a command can send compressed files, or a protocol command (few bytes) could delete a big file. To mitigate this issue, quotas are recalculated at the command end with a full home directory scan. This could be heavy for big directories. If you need system commands and quotas you could consider disabling quota restrictions and periodically update quota usage yourself using the REST API. We support the following SSH commands:

View file

@ -1110,13 +1110,13 @@ components:
password:
type: string
nullable: true
description: password or public key are mandatory. If the password has no known hashing algo prefix it will be stored using argon2id. You can send a password hashed as bcrypt or pbkdf2 and it will be stored as is. For security reasons this field is omitted when you search/get users
description: password or public key/SSH user certificate are mandatory. If the password has no known hashing algo prefix it will be stored using argon2id. You can send a password hashed as bcrypt or pbkdf2 and it will be stored as is. For security reasons this field is omitted when you search/get users
public_keys:
type: array
items:
type: string
nullable: true
description: a password or at least one public key are mandatory.
description: a password or at least one public key/SSH user certificate are mandatory.
home_dir:
type: string
description: path to the user home directory. The user cannot upload or download files outside this directory. SFTPGo tries to automatically create this folder if missing. Must be an absolute path

View file

@ -496,7 +496,8 @@ def getDatetimeAsMillisSinceEpoch(dt):
def addCommonUserArguments(parser):
parser.add_argument('username', type=str)
parser.add_argument('-P', '--password', type=str, default=None, help='Default: %(default)s')
parser.add_argument('-K', '--public-keys', type=str, nargs='+', default=[], help='Default: %(default)s')
parser.add_argument('-K', '--public-keys', type=str, nargs='+', default=[], help='Public keys or SSH user certificates. ' +
'Default: %(default)s')
parser.add_argument('-H', '--home-dir', type=str, default='', help='Default: %(default)s')
parser.add_argument('--uid', type=int, default=0, help='Default: %(default)s')
parser.add_argument('--gid', type=int, default=0, help='Default: %(default)s')

View file

@ -17,6 +17,7 @@ import (
"github.com/eikenb/pipeat"
"github.com/pkg/sftp"
"github.com/stretchr/testify/assert"
"golang.org/x/crypto/ssh"
"github.com/drakkan/sftpgo/dataprovider"
"github.com/drakkan/sftpgo/utils"
@ -1734,3 +1735,41 @@ func TestProxyProtocolVersion(t *testing.T) {
_, err = c.getProxyListener(nil)
assert.Error(t, err)
}
func TestLoadHostKeys(t *testing.T) {
c := Configuration{}
c.Keys = []Key{
{
PrivateKey: "missing file",
},
}
err := c.checkAndLoadHostKeys("..", &ssh.ServerConfig{})
assert.Error(t, err)
testfile := filepath.Join(os.TempDir(), "invalidkey")
err = ioutil.WriteFile(testfile, []byte("some bytes"), 0666)
assert.NoError(t, err)
c.Keys = []Key{
{
PrivateKey: testfile,
},
}
err = c.checkAndLoadHostKeys("..", &ssh.ServerConfig{})
assert.Error(t, err)
err = os.Remove(testfile)
assert.NoError(t, err)
}
func TestCertCheckerInitErrors(t *testing.T) {
c := Configuration{}
c.TrustedUserCAKeys = append(c.TrustedUserCAKeys, "missing file")
err := c.initializeCertChecker("")
assert.Error(t, err)
testfile := filepath.Join(os.TempDir(), "invalidkey")
err = ioutil.WriteFile(testfile, []byte("some bytes"), 0666)
assert.NoError(t, err)
c.TrustedUserCAKeys = []string{testfile}
err = c.initializeCertChecker("")
assert.Error(t, err)
err = os.Remove(testfile)
assert.NoError(t, err)
}

View file

@ -1,6 +1,7 @@
package sftpd
import (
"bytes"
"encoding/hex"
"encoding/json"
"fmt"
@ -24,8 +25,9 @@ import (
)
const (
defaultPrivateRSAKeyName = "id_rsa"
defaultPrivateECDSAKeyName = "id_ecdsa"
defaultPrivateRSAKeyName = "id_rsa"
defaultPrivateECDSAKeyName = "id_ecdsa"
sourceAddressCriticalOption = "source-address"
)
var (
@ -71,6 +73,10 @@ type Configuration struct {
// MACs Specifies the available MAC (message authentication code) algorithms
// in preference order
MACs []string `json:"macs" mapstructure:"macs"`
// TrustedUserCAKeys specifies a list of public keys paths of certificate authorities
// that are trusted to sign user certificates for authentication.
// The paths can be absolute or relative to the configuration directory
TrustedUserCAKeys []string `json:"trusted_user_ca_keys" mapstructure:"trusted_user_ca_keys"`
// LoginBannerFile the contents of the specified file, if any, are sent to
// the remote user before authentication is allowed.
LoginBannerFile string `json:"login_banner_file" mapstructure:"login_banner_file"`
@ -119,12 +125,14 @@ type Configuration struct {
// connection will be accepted and the header will be ignored.
// If proxy protocol is set to 2 and we receive a proxy header from an IP that is not in the list then the
// connection will be rejected.
ProxyAllowed []string `json:"proxy_allowed" mapstructure:"proxy_allowed"`
ProxyAllowed []string `json:"proxy_allowed" mapstructure:"proxy_allowed"`
certChecker *ssh.CertChecker
parsedUserCAKeys []ssh.PublicKey
}
// Key contains information about host keys
type Key struct {
// The private key path relative to the configuration directory or absolute
// The private key path as absolute path or relative to the configuration directory
PrivateKey string `json:"private_key" mapstructure:"private_key"`
}
@ -157,7 +165,7 @@ func (c Configuration) Initialize(configDir string) error {
return sp, nil
},
PublicKeyCallback: func(conn ssh.ConnMetadata, pubKey ssh.PublicKey) (*ssh.Permissions, error) {
sp, err := c.validatePublicKeyCredentials(conn, pubKey.Marshal())
sp, err := c.validatePublicKeyCredentials(conn, pubKey)
if err == ssh.ErrPartialSuccess {
return nil, err
}
@ -178,8 +186,11 @@ func (c Configuration) Initialize(configDir string) error {
ServerVersion: fmt.Sprintf("SSH-2.0-%v", c.Banner),
}
err = c.checkAndLoadHostKeys(configDir, serverConfig)
if err != nil {
if err = c.checkAndLoadHostKeys(configDir, serverConfig); err != nil {
return err
}
if err = c.initializeCertChecker(configDir); err != nil {
return err
}
@ -336,9 +347,9 @@ func (c Configuration) AcceptInboundConnection(conn net.Conn, config *ssh.Server
var user dataprovider.User
// Unmarshal cannot fails here and even if it fails we'll have a user with no permissions
json.Unmarshal([]byte(sconn.Permissions.Extensions["user"]), &user) //nolint:errcheck
json.Unmarshal([]byte(sconn.Permissions.Extensions["sftpgo_user"]), &user) //nolint:errcheck
loginType := sconn.Permissions.Extensions["login_method"]
loginType := sconn.Permissions.Extensions["sftpgo_login_method"]
connectionID := hex.EncodeToString(sconn.SessionID())
fs, err := user.GetFilesystem(connectionID)
@ -474,8 +485,8 @@ func loginUser(user dataprovider.User, loginMethod, publicKey string, conn ssh.C
}
p := &ssh.Permissions{}
p.Extensions = make(map[string]string)
p.Extensions["user"] = string(json)
p.Extensions["login_method"] = loginMethod
p.Extensions["sftpgo_user"] = string(json)
p.Extensions["sftpgo_login_method"] = loginMethod
return p, nil
}
@ -540,26 +551,93 @@ func (c *Configuration) checkAndLoadHostKeys(configDir string, serverConfig *ssh
return nil
}
func (c Configuration) validatePublicKeyCredentials(conn ssh.ConnMetadata, pubKey []byte) (*ssh.Permissions, error) {
func (c *Configuration) initializeCertChecker(configDir string) error {
for _, keyPath := range c.TrustedUserCAKeys {
if !filepath.IsAbs(keyPath) {
keyPath = filepath.Join(configDir, keyPath)
}
keyBytes, err := ioutil.ReadFile(keyPath)
if err != nil {
logger.Warn(logSender, "", "error loading trusted user CA key %#v: %v", keyPath, err)
logger.WarnToConsole("error loading trusted user CA key %#v: %v", keyPath, err)
return err
}
parsedKey, _, _, _, err := ssh.ParseAuthorizedKey(keyBytes)
if err != nil {
logger.Warn(logSender, "", "error parsing trusted user CA key %#v: %v", keyPath, err)
logger.WarnToConsole("error parsing trusted user CA key %#v: %v", keyPath, err)
return err
}
c.parsedUserCAKeys = append(c.parsedUserCAKeys, parsedKey)
}
c.certChecker = &ssh.CertChecker{
SupportedCriticalOptions: []string{
sourceAddressCriticalOption,
},
IsUserAuthority: func(k ssh.PublicKey) bool {
for _, key := range c.parsedUserCAKeys {
if bytes.Equal(k.Marshal(), key.Marshal()) {
return true
}
}
return false
},
}
return nil
}
func (c Configuration) validatePublicKeyCredentials(conn ssh.ConnMetadata, pubKey ssh.PublicKey) (*ssh.Permissions, error) {
var err error
var user dataprovider.User
var keyID string
var sshPerm *ssh.Permissions
var certPerm *ssh.Permissions
connectionID := hex.EncodeToString(conn.SessionID())
method := dataprovider.SSHLoginMethodPublicKey
if user, keyID, err = dataprovider.CheckUserAndPubKey(dataProvider, conn.User(), pubKey); err == nil {
cert, ok := pubKey.(*ssh.Certificate)
if ok {
if cert.CertType != ssh.UserCert {
err = fmt.Errorf("ssh: cert has type %d", cert.CertType)
updateLoginMetrics(conn, method, err)
return nil, err
}
if !c.certChecker.IsUserAuthority(cert.SignatureKey) {
err = fmt.Errorf("ssh: certificate signed by unrecognized authority")
updateLoginMetrics(conn, method, err)
return nil, err
}
if err := c.certChecker.CheckCert(conn.User(), cert); err != nil {
updateLoginMetrics(conn, method, err)
return nil, err
}
// we need to check source address ourself since crypto/ssh will skip this check if we return partial success
if cert.Permissions.CriticalOptions != nil && cert.Permissions.CriticalOptions[sourceAddressCriticalOption] != "" {
if err := utils.CheckSourceAddress(conn.RemoteAddr(), cert.Permissions.CriticalOptions[sourceAddressCriticalOption]); err != nil {
updateLoginMetrics(conn, method, err)
return nil, err
}
}
certPerm = &cert.Permissions
}
if user, keyID, err = dataprovider.CheckUserAndPubKey(dataProvider, conn.User(), pubKey.Marshal()); err == nil {
if user.IsPartialAuth(method) {
logger.Debug(logSender, connectionID, "user %#v authenticated with partial success", conn.User())
return nil, ssh.ErrPartialSuccess
}
sshPerm, err = loginUser(user, method, keyID, conn)
if err == nil && certPerm != nil {
// if we have a SSH user cert we need to merge certificate permissions with our ones
// we only set Extensions, so CriticalOptions are always the ones from the certificate
sshPerm.CriticalOptions = certPerm.CriticalOptions
if certPerm.Extensions != nil {
for k, v := range certPerm.Extensions {
sshPerm.Extensions[k] = v
}
}
}
}
metrics.AddLoginAttempt(method)
if err != nil {
logger.ConnectionFailedLog(conn.User(), utils.GetIPFromRemoteAddress(conn.RemoteAddr().String()), method, err.Error())
}
metrics.AddLoginResult(method, err)
updateLoginMetrics(conn, method, err)
return sshPerm, err
}
@ -572,14 +650,10 @@ func (c Configuration) validatePasswordCredentials(conn ssh.ConnMetadata, pass [
if len(conn.PartialSuccessMethods()) == 1 {
method = dataprovider.SSHLoginMethodKeyAndPassword
}
metrics.AddLoginAttempt(method)
if user, err = dataprovider.CheckUserAndPass(dataProvider, conn.User(), string(pass)); err == nil {
sshPerm, err = loginUser(user, method, "", conn)
}
if err != nil {
logger.ConnectionFailedLog(conn.User(), utils.GetIPFromRemoteAddress(conn.RemoteAddr().String()), method, err.Error())
}
metrics.AddLoginResult(method, err)
updateLoginMetrics(conn, method, err)
return sshPerm, err
}
@ -592,13 +666,17 @@ func (c Configuration) validateKeyboardInteractiveCredentials(conn ssh.ConnMetad
if len(conn.PartialSuccessMethods()) == 1 {
method = dataprovider.SSHLoginMethodKeyAndKeyboardInt
}
metrics.AddLoginAttempt(method)
if user, err = dataprovider.CheckKeyboardInteractiveAuth(dataProvider, conn.User(), c.KeyboardInteractiveHook, client); err == nil {
sshPerm, err = loginUser(user, method, "", conn)
}
updateLoginMetrics(conn, method, err)
return sshPerm, err
}
func updateLoginMetrics(conn ssh.ConnMetadata, method string, err error) {
metrics.AddLoginAttempt(method)
if err != nil {
logger.ConnectionFailedLog(conn.User(), utils.GetIPFromRemoteAddress(conn.RemoteAddr().String()), method, err.Error())
}
metrics.AddLoginResult(method, err)
return sshPerm, err
}

View file

@ -88,24 +88,39 @@ NbbCNsVroqKlChT5wyPNGS+phi2bPARBno7WSDvshTZ7dAVEP2c9MJW0XwoSevwKlhgSdt
RLFFQ/5nclJSdzPBOmQouC0OBcMFSrYtMeknJ4VvueVvve5HcHFaEsaMc7ABAGaLYaBQOm
iixITGvaNZh/tjAAAACW5pY29sYUBwMQE=
-----END OPENSSH PRIVATE KEY-----`
configDir = ".."
permissionErrorString = "Permission Denied"
osWindows = "windows"
// test CA user key.
// % ssh-keygen -f ca_user_key
testCAUserKey = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDF5fcwZHiyixmnE6IlOZJpZhWXoh62gN+yadAA0GJ509SAEaZVLPDP8S5RsE8mUikR3wxynVshxHeqMhrkS+RlNbhSlOXDdNg94yTrq/xF8Z/PgKRInvef74k5i7bAIytza7jERzFJ/ujTEy3537T5k5EYQJ15ZQGuvzynSdv+6o99SjI4jFplyQOZ2QcYbEAmhHm5GgQlIiEFG/RlDtLksOulKZxOY3qPzP0AyQxtZJXn/5vG40aW9LTbwxCJqWlgrkFXMqAAVCbuU5YspwhiXmKt1PsldiXw23oloa4caCKN1jzbFiGuZNXEU2Ebx7JIvjQCPaUYwLjEbkRDxDqN/vmwZqBuKYiuG9Eafx+nFSQkr7QYb5b+mT+/1IFHnmeRGn38731kBqtH7tpzC/t+soRX9p2HtJM+9MYhblO2OqTSPGTlxihWUkyiRBekpAhaiHld16TsG+A3bOJHrojGcX+5g6oGarKGLAMcykL1X+rZqT993Mo6d2Z7q43MOXE= root@p1"
// this is testPubKey signed using testCAUserKey.
// % ssh-keygen -s ca_user_key -I test_user_sftp -n test_user_sftp -V always:forever -O source-address=127.0.0.1 -z 1 /tmp/test.pub
testCertValid = "ssh-rsa-cert-v01@openssh.com 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 nicola@p1"
// this is testPubKey signed using a CA user key different from testCAUserKey
testCertUntrustedCA = "ssh-rsa-cert-v01@openssh.com AAAAHHNzaC1yc2EtY2VydC12MDFAb3BlbnNzaC5jb20AAAAg8oFPWpjYy/DowMmtOjWj7Dq20d2N/4Rxzr/c710tOOUAAAADAQABAAABgQC03jj0D+djk7pxIf/0OhrxrchJTRZklofJ1NoIu4752Sq02mdXmarMVsqJ1cAjV5LBVy3D1F5U6XW4rppkXeVtd04Pxb09ehtH0pRRPaoHHlALiJt8CoMpbKYMA8b3KXPPriGxgGomvtU2T2RMURSwOZbMtpsugfjYSWenyYX+VORYhylWnSXL961LTyC21ehd6d6QnW9G7E5hYMITMY9TuQZz3bROYzXiTsgN0+g6Hn7exFQp50p45StUMfV/SftCMdCxlxuyGny2CrN/vfjO7xxOo2uv7q1qm10Q46KPWJQv+pgZ/OfL+EDjy07n5QVSKHlbx+2nT4Q0EgOSQaCTYwn3YjtABfIxWwgAFdyj6YlPulCL22qU4MYhDcA6PSBwDdf8hvxBfvsiHdM+JcSHvv8/VeJhk6CmnZxGY0fxBupov27z3yEO8nAg8k+6PaUiW1MSUfuGMF/ktB8LOstXsEPXSszuyXiOv4DaryOXUiSn7bmRqKcEFlJusO6aZP0AAAAAAAAAAAAAAAEAAAAOdGVzdF91c2VyX3NmdHAAAAASAAAADnRlc3RfdXNlcl9zZnRwAAAAAAAAAAD//////////wAAAAAAAACCAAAAFXBlcm1pdC1YMTEtZm9yd2FyZGluZwAAAAAAAAAXcGVybWl0LWFnZW50LWZvcndhcmRpbmcAAAAAAAAAFnBlcm1pdC1wb3J0LWZvcndhcmRpbmcAAAAAAAAACnBlcm1pdC1wdHkAAAAAAAAADnBlcm1pdC11c2VyLXJjAAAAAAAAAAAAAAGXAAAAB3NzaC1yc2EAAAADAQABAAABgQCqgm2gVlptULThfpRR0oCb4SAU3368ULlJaiZOUdq6b94KTfgmu4hTLs7u3a8hyZnVxrKrJ93uAVCwa/HGtgiN96CNC6JUt/QnPqTJ8LQ207RdoE9fbOe6mGwOle5z45+5JFoIi5ZZuD8JsBGodVoa92UepoMyBcNtZyl9q2GP4yT2tIYRon79dtG9AXiDYyhSgePqaObN67dn3ivMc4ZGNukK3cG07cYPic5y0wxX16wSMG3pGQDyUkAu+s4AqpnV9EWHM4PE7SYkCXE99++tUK3QALYqvGZKrLHgzmDKi6n+e14vHYUppAeGDZzwlawiY4oGP9eOW2KUfjZe2ZeL22JTFDYzH2lNV2WtUpeKRGGTSGaUblRVC9hRt6hKCT4c7qpW4kO4kPhE39JpcNPGLql7srNkw+3xXBs8xghMPtH3nOl1Rz2mxnX5tAqmPBb+KiPepnrs+pBRu7i+nCVp8az+iN87STYHy+zPtvTR+QURC8BpNraPOfXwpwM2HaMAAAGUAAAADHJzYS1zaGEyLTUxMgAAAYBnTXCL6tXUO3/Gtsm7lnH9Sulzca8FOoI4Y/4bVYhq4iUNu7Ca452m+Xr9qmCEoIyIJF0LEEcJ8jcS4rfX15e7tNNoknv7JbYXBFAbp1Y/76iqVf89FjfVcbEyH2ToAf7eyQAWzQ3gEKS8mQIkLnAwmCboUXC4GRodSIiOXiTt5Q6T02MVc8TxkhmlTA0uVLd5XgstySgE/oLBnL59lhJcwQmdhHL+m480+PaW55CtMuC36RTwk/tOyuWCDC5qMXnoveNB3yu45o3L/U4hoyJ0/5FyP5C8ahgydY0LoRZQG/mNzuraY4433rK+IfkQvZTyaDtcjhxE6hCD5F40aDDh88i6XaKAPikD6fqra6BN8PoPgLuRHzOJuqsMXBWM99s7qPgSnBbmXlekz/1jvvFiCh3zvAFTxFz2KyE4+SbDcCrhpxkNL7idw6r/ZsHaI/2+zhDcgSs5MgBwYLJEj6zUqVdp5XsF8YfC7yNZV5/qy68qY2+zXrC57SPifU2SCPE= nicola@p1"
// this is testPubKey signed as host certificate.
// % ssh-keygen -s ca_user_key -I test_user_sftp -h -n test_user_sftp -V always:forever -z 2 /tmp/test.pub
testHostCert = "ssh-rsa-cert-v01@openssh.com AAAAHHNzaC1yc2EtY2VydC12MDFAb3BlbnNzaC5jb20AAAAg7O2LDpLO1jGTX3SSzEMILoAYJb9DdggyyaUMXUUg3L4AAAADAQABAAABgQC03jj0D+djk7pxIf/0OhrxrchJTRZklofJ1NoIu4752Sq02mdXmarMVsqJ1cAjV5LBVy3D1F5U6XW4rppkXeVtd04Pxb09ehtH0pRRPaoHHlALiJt8CoMpbKYMA8b3KXPPriGxgGomvtU2T2RMURSwOZbMtpsugfjYSWenyYX+VORYhylWnSXL961LTyC21ehd6d6QnW9G7E5hYMITMY9TuQZz3bROYzXiTsgN0+g6Hn7exFQp50p45StUMfV/SftCMdCxlxuyGny2CrN/vfjO7xxOo2uv7q1qm10Q46KPWJQv+pgZ/OfL+EDjy07n5QVSKHlbx+2nT4Q0EgOSQaCTYwn3YjtABfIxWwgAFdyj6YlPulCL22qU4MYhDcA6PSBwDdf8hvxBfvsiHdM+JcSHvv8/VeJhk6CmnZxGY0fxBupov27z3yEO8nAg8k+6PaUiW1MSUfuGMF/ktB8LOstXsEPXSszuyXiOv4DaryOXUiSn7bmRqKcEFlJusO6aZP0AAAAAAAAAAgAAAAIAAAAOdGVzdF91c2VyX3NmdHAAAAASAAAADnRlc3RfdXNlcl9zZnRwAAAAAAAAAAD//////////wAAAAAAAAAAAAAAAAAAAZcAAAAHc3NoLXJzYQAAAAMBAAEAAAGBAMXl9zBkeLKLGacToiU5kmlmFZeiHraA37Jp0ADQYnnT1IARplUs8M/xLlGwTyZSKRHfDHKdWyHEd6oyGuRL5GU1uFKU5cN02D3jJOur/EXxn8+ApEie95/viTmLtsAjK3NruMRHMUn+6NMTLfnftPmTkRhAnXllAa6/PKdJ2/7qj31KMjiMWmXJA5nZBxhsQCaEebkaBCUiIQUb9GUO0uSw66UpnE5jeo/M/QDJDG1klef/m8bjRpb0tNvDEImpaWCuQVcyoABUJu5TliynCGJeYq3U+yV2JfDbeiWhrhxoIo3WPNsWIa5k1cRTYRvHski+NAI9pRjAuMRuREPEOo3++bBmoG4piK4b0Rp/H6cVJCSvtBhvlv6ZP7/UgUeeZ5EaffzvfWQGq0fu2nML+36yhFf2nYe0kz70xiFuU7Y6pNI8ZOXGKFZSTKJEF6SkCFqIeV3XpOwb4Dds4keuiMZxf7mDqgZqsoYsAxzKQvVf6tmpP33cyjp3Znurjcw5cQAAAZQAAAAMcnNhLXNoYTItNTEyAAABgHlAWMTTzNrE6pxHlkr09ZXsHgJi8U2p7eifs56DOLgklYIXVUJPEEcnzMKGdpPBnqJsvg3+PccqxgOr5L1dFuOmekQ/dGiHd1enrESiGvJOvDfm0WsuBjxEZkSNFWgC9Z2NltToMmRlhVBmb4ZRZtAmi9DAFlJ/BDV4t8ikXZ5oUsigwIeOeLkdPFx3C3x9KZIpuwuAIV4Nfmz75q1NMWY2K1hv682QCKwMYqOWSotz1vWunNmZ0yPRl9UwqAq+nqwO3AApnlrQ3MmHujWQ5tl65PyhfpI8oghhUtB6YrJIAuRXNI/S0+KewCpiYm7nbFBtv9lpecujxAeTibYBrFZ5VODEUm3sdQ/HMdTmkhi6xNgPDQVlvKFqBJAaqoO3tbhKTbEZ865tJMqhyxmZ4XY08wduvSVobrNr7s3rm42/FXLIpung+UOVXonHyeIv9zQ0iJ/bvqKQ1fOsTisZdcD0lz80ZGsjdgJt7yKfUNBnAyVbTXm048E3WsZslJIYCA== nicola@p1"
// this is testPubKey signed using testCAUserKey but with source address 172.16.34.45.
// % ssh-keygen -s ca_user_key -I test_user_sftp -n test_user_sftp -V always:forever -O source-address=172.16.34.45 -z 3 /tmp/test.pub
testCertOtherSourceAddress = "ssh-rsa-cert-v01@openssh.com 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 nicola@p1"
configDir = ".."
permissionErrorString = "Permission Denied"
osWindows = "windows"
)
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
trustedCAUserKey string
gitWrapPath string
extAuthPath string
keyIntAuthPath string
preLoginPath string
logFilePath string
)
func TestMain(m *testing.M) {
@ -178,6 +193,7 @@ func TestMain(m *testing.M) {
pubKeyPath = filepath.Join(homeBasePath, "ssh_key.pub")
privateKeyPath = filepath.Join(homeBasePath, "ssh_key")
trustedCAUserKey = filepath.Join(homeBasePath, "ca_user_key")
gitWrapPath = filepath.Join(homeBasePath, "gitwrap.sh")
extAuthPath = filepath.Join(homeBasePath, "extauth.sh")
preLoginPath = filepath.Join(homeBasePath, "prelogin.sh")
@ -194,6 +210,11 @@ func TestMain(m *testing.M) {
if err != nil {
logger.WarnToConsole("unable to save gitwrap shell script: %v", err)
}
err = ioutil.WriteFile(trustedCAUserKey, []byte(testCAUserKey), 0600)
if err != nil {
logger.WarnToConsole("unable to save trusted CA user key: %v", err)
}
sftpdConf.TrustedUserCAKeys = append(sftpdConf.TrustedUserCAKeys, trustedCAUserKey)
sftpd.SetDataProvider(dataProvider)
httpd.SetDataProvider(dataProvider)
@ -240,6 +261,7 @@ func TestMain(m *testing.M) {
os.Remove(loginBannerFile)
os.Remove(pubKeyPath)
os.Remove(privateKeyPath)
os.Remove(trustedCAUserKey)
os.Remove(gitWrapPath)
os.Remove(extAuthPath)
os.Remove(preLoginPath)
@ -751,6 +773,63 @@ func TestLogin(t *testing.T) {
assert.NoError(t, err)
}
func TestLoginUserCert(t *testing.T) {
u := getTestUser(true)
u.PublicKeys = []string{testCertValid, testCertUntrustedCA, testHostCert, testCertOtherSourceAddress}
user, _, err := httpd.AddUser(u, http.StatusOK)
assert.NoError(t, err)
// try login using a cert signed from a trusted CA
signer, err := getSignerForUserCert([]byte(testCertValid))
assert.NoError(t, err)
client, err := getCustomAuthSftpClient(user, []ssh.AuthMethod{ssh.PublicKeys(signer)})
if assert.NoError(t, err) {
defer client.Close()
assert.NoError(t, checkBasicSFTP(client))
}
// try login using a cert signed from an untrusted CA
signer, err = getSignerForUserCert([]byte(testCertUntrustedCA))
assert.NoError(t, err)
client, err = getCustomAuthSftpClient(user, []ssh.AuthMethod{ssh.PublicKeys(signer)})
if !assert.Error(t, err) {
client.Close()
}
// try login using an host certificate instead of an user certificate
signer, err = getSignerForUserCert([]byte(testHostCert))
assert.NoError(t, err)
client, err = getCustomAuthSftpClient(user, []ssh.AuthMethod{ssh.PublicKeys(signer)})
if !assert.Error(t, err) {
client.Close()
}
// try login using a user certificate with an authorized source address different from localhost
signer, err = getSignerForUserCert([]byte(testCertOtherSourceAddress))
assert.NoError(t, err)
client, err = getCustomAuthSftpClient(user, []ssh.AuthMethod{ssh.PublicKeys(signer)})
if !assert.Error(t, err) {
client.Close()
}
_, err = httpd.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
// now login with a username not in the set of valid principals for given certificate
u.Username += "1"
user, _, err = httpd.AddUser(u, http.StatusOK)
assert.NoError(t, err)
signer, err = getSignerForUserCert([]byte(testCertValid))
assert.NoError(t, err)
client, err = getCustomAuthSftpClient(user, []ssh.AuthMethod{ssh.PublicKeys(signer)})
if !assert.Error(t, err) {
client.Close()
}
_, err = httpd.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
}
func TestMultiStepLoginKeyAndPwd(t *testing.T) {
u := getTestUser(true)
u.Password = defaultPassword
@ -770,9 +849,9 @@ func TestMultiStepLoginKeyAndPwd(t *testing.T) {
if !assert.Error(t, err, "login with password is disallowed and must fail") {
client.Close()
}
key, _ := ssh.ParsePrivateKey([]byte(testPrivateKey))
signer, _ := ssh.ParsePrivateKey([]byte(testPrivateKey))
authMethods := []ssh.AuthMethod{
ssh.PublicKeys(key),
ssh.PublicKeys(signer),
ssh.Password(defaultPassword),
}
client, err = getCustomAuthSftpClient(user, authMethods)
@ -782,7 +861,7 @@ func TestMultiStepLoginKeyAndPwd(t *testing.T) {
}
authMethods = []ssh.AuthMethod{
ssh.Password(defaultPassword),
ssh.PublicKeys(key),
ssh.PublicKeys(signer),
}
_, err = getCustomAuthSftpClient(user, authMethods)
assert.Error(t, err, "multi step auth login with wrong order must fail")
@ -813,9 +892,9 @@ func TestMultiStepLoginKeyAndKeyInt(t *testing.T) {
client.Close()
}
key, _ := ssh.ParsePrivateKey([]byte(testPrivateKey))
signer, _ := ssh.ParsePrivateKey([]byte(testPrivateKey))
authMethods := []ssh.AuthMethod{
ssh.PublicKeys(key),
ssh.PublicKeys(signer),
ssh.KeyboardInteractive(func(user, instruction string, questions []string, echos []bool) ([]string, error) {
return []string{"1", "2"}, nil
}),
@ -829,13 +908,13 @@ func TestMultiStepLoginKeyAndKeyInt(t *testing.T) {
ssh.KeyboardInteractive(func(user, instruction string, questions []string, echos []bool) ([]string, error) {
return []string{"1", "2"}, nil
}),
ssh.PublicKeys(key),
ssh.PublicKeys(signer),
}
_, err = getCustomAuthSftpClient(user, authMethods)
assert.Error(t, err, "multi step auth login with wrong order must fail")
authMethods = []ssh.AuthMethod{
ssh.PublicKeys(key),
ssh.PublicKeys(signer),
ssh.Password(defaultPassword),
}
_, err = getCustomAuthSftpClient(user, authMethods)
@ -847,6 +926,47 @@ func TestMultiStepLoginKeyAndKeyInt(t *testing.T) {
assert.NoError(t, err)
}
func TestMultiStepLoginCertAndPwd(t *testing.T) {
u := getTestUser(true)
u.Password = defaultPassword
u.PublicKeys = []string{testCertValid, testCertOtherSourceAddress}
u.Filters.DeniedLoginMethods = append(u.Filters.DeniedLoginMethods, []string{
dataprovider.SSHLoginMethodKeyAndKeyboardInt,
dataprovider.SSHLoginMethodPublicKey,
dataprovider.SSHLoginMethodPassword,
dataprovider.SSHLoginMethodKeyboardInteractive,
}...)
user, _, err := httpd.AddUser(u, http.StatusOK)
assert.NoError(t, err)
signer, err := getSignerForUserCert([]byte(testCertValid))
assert.NoError(t, err)
authMethods := []ssh.AuthMethod{
ssh.PublicKeys(signer),
ssh.Password(defaultPassword),
}
client, err := getCustomAuthSftpClient(user, authMethods)
if assert.NoError(t, err) {
defer client.Close()
assert.NoError(t, checkBasicSFTP(client))
}
signer, err = getSignerForUserCert([]byte(testCertOtherSourceAddress))
assert.NoError(t, err)
authMethods = []ssh.AuthMethod{
ssh.PublicKeys(signer),
ssh.Password(defaultPassword),
}
client, err = getCustomAuthSftpClient(user, authMethods)
if !assert.Error(t, err) {
client.Close()
}
_, err = httpd.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
}
func TestLoginUserStatus(t *testing.T) {
usePubKey := true
user, _, err := httpd.AddUser(getTestUser(usePubKey), http.StatusOK)
@ -3949,6 +4069,18 @@ func runSSHCommand(command string, user dataprovider.User, usePubKey bool) ([]by
return stdout.Bytes(), err
}
func getSignerForUserCert(certBytes []byte) (ssh.Signer, error) {
signer, err := ssh.ParsePrivateKey([]byte(testPrivateKey))
if err != nil {
return nil, err
}
cert, _, _, _, err := ssh.ParseAuthorizedKey(certBytes) //nolint:dogsled
if err != nil {
return nil, err
}
return ssh.NewCertSigner(cert.(*ssh.Certificate), signer)
}
func getSftpClientWithAddr(user dataprovider.User, usePubKey bool, addr string) (*sftp.Client, error) {
var sftpClient *sftp.Client
config := &ssh.ClientConfig{
@ -3958,11 +4090,11 @@ func getSftpClientWithAddr(user dataprovider.User, usePubKey bool, addr string)
},
}
if usePubKey {
key, err := ssh.ParsePrivateKey([]byte(testPrivateKey))
signer, err := ssh.ParsePrivateKey([]byte(testPrivateKey))
if err != nil {
return nil, err
}
config.Auth = []ssh.AuthMethod{ssh.PublicKeys(key)}
config.Auth = []ssh.AuthMethod{ssh.PublicKeys(signer)}
} else {
if len(user.Password) > 0 {
config.Auth = []ssh.AuthMethod{ssh.Password(user.Password)}

View file

@ -16,6 +16,7 @@
"kex_algorithms": [],
"ciphers": [],
"macs": [],
"trusted_user_ca_keys":[],
"login_banner_file": "",
"setstat_mode": 0,
"enabled_ssh_commands": [

View file

@ -65,7 +65,7 @@
<textarea class="form-control" id="idPublicKeys" name="public_keys" rows="3"
aria-describedby="pkHelpBlock">{{range .User.PublicKeys}}{{.}}&#10;{{end}}</textarea>
<small id="pkHelpBlock" class="form-text text-muted">
One public key per line
One public key or SSH user certificate per line
</small>
</div>
</div>

View file

@ -317,3 +317,34 @@ func CleanDirInput(dirInput string) string {
}
return filepath.Clean(dirInput)
}
// CheckSourceAddress check the source address against the one defined inside an SSH user certificate
func CheckSourceAddress(addr net.Addr, sourceAddrs string) error {
if addr == nil {
return errors.New("ssh: no address known for client, but source-address match required")
}
tcpAddr, ok := addr.(*net.TCPAddr)
if !ok {
return fmt.Errorf("ssh: remote address %v is not an TCP address when checking source-address match", addr)
}
for _, sourceAddr := range strings.Split(sourceAddrs, ",") {
if allowedIP := net.ParseIP(sourceAddr); allowedIP != nil {
if allowedIP.Equal(tcpAddr.IP) {
return nil
}
} else {
_, ipNet, err := net.ParseCIDR(sourceAddr)
if err != nil {
return fmt.Errorf("ssh: error parsing source-address restriction %q: %v", sourceAddr, err)
}
if ipNet.Contains(tcpAddr.IP) {
return nil
}
}
}
return fmt.Errorf("ssh: remote address %v is not allowed because of source-address restriction", addr)
}