ssh: support MaxAuthTries on ServerConfig
This change breaks backwards compatibility. MaxAuthTries specifies the maximum number of authentication attempts permitted per connection. If set to a negative number, the server will allow unlimited authentication attempts. MaxAuthTries defaults to 6 if not specified, which is a backwards incompatible change. On exceeding maximum authentication attempts, the server will send a disconnect message to the client. This configuration property mirrors a similar property in sshd_config and prevents bad actors from continuously trying authentication. Change-Id: Ic77d2c29ee2fd2ae5c764becf7df91d29d03131b Reviewed-on: https://go-review.googlesource.com/35230 Reviewed-by: Han-Wen Nienhuys <hanwen@google.com> Run-TryBot: Han-Wen Nienhuys <hanwen@google.com> TryBot-Result: Gobot Gobot <gobot@golang.org>
This commit is contained in:
committed by
Han-Wen Nienhuys
parent
3cddcd6758
commit
9ef620b9ca
+97
-2
@@ -76,8 +76,6 @@ func tryAuth(t *testing.T, config *ClientConfig) error {
|
|||||||
}
|
}
|
||||||
return nil, errors.New("keyboard-interactive failed")
|
return nil, errors.New("keyboard-interactive failed")
|
||||||
},
|
},
|
||||||
AuthLogCallback: func(conn ConnMetadata, method string, err error) {
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
serverConfig.AddHostKey(testSigners["rsa"])
|
serverConfig.AddHostKey(testSigners["rsa"])
|
||||||
|
|
||||||
@@ -482,3 +480,100 @@ func TestClientAuthNone(t *testing.T) {
|
|||||||
t.Fatalf("server: got %q, want %q", serverConn.User(), user)
|
t.Fatalf("server: got %q, want %q", serverConn.User(), user)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Test if authentication attempts are limited on server when MaxAuthTries is set
|
||||||
|
func TestClientAuthMaxAuthTries(t *testing.T) {
|
||||||
|
user := "testuser"
|
||||||
|
|
||||||
|
serverConfig := &ServerConfig{
|
||||||
|
MaxAuthTries: 2,
|
||||||
|
PasswordCallback: func(conn ConnMetadata, pass []byte) (*Permissions, error) {
|
||||||
|
if conn.User() == "testuser" && string(pass) == "right" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, errors.New("password auth failed")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
serverConfig.AddHostKey(testSigners["rsa"])
|
||||||
|
|
||||||
|
expectedErr := fmt.Errorf("ssh: handshake failed: %v", &disconnectMsg{
|
||||||
|
Reason: 2,
|
||||||
|
Message: "too many authentication failures",
|
||||||
|
})
|
||||||
|
|
||||||
|
for tries := 2; tries < 4; tries++ {
|
||||||
|
n := tries
|
||||||
|
clientConfig := &ClientConfig{
|
||||||
|
User: user,
|
||||||
|
Auth: []AuthMethod{
|
||||||
|
RetryableAuthMethod(PasswordCallback(func() (string, error) {
|
||||||
|
n--
|
||||||
|
if n == 0 {
|
||||||
|
return "right", nil
|
||||||
|
} else {
|
||||||
|
return "wrong", nil
|
||||||
|
}
|
||||||
|
}), tries),
|
||||||
|
},
|
||||||
|
HostKeyCallback: InsecureIgnoreHostKey(),
|
||||||
|
}
|
||||||
|
|
||||||
|
c1, c2, err := netPipe()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("netPipe: %v", err)
|
||||||
|
}
|
||||||
|
defer c1.Close()
|
||||||
|
defer c2.Close()
|
||||||
|
|
||||||
|
go newServer(c1, serverConfig)
|
||||||
|
_, _, _, err = NewClientConn(c2, "", clientConfig)
|
||||||
|
if tries > 2 {
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("client: got no error, want %s", expectedErr)
|
||||||
|
} else if err.Error() != expectedErr.Error() {
|
||||||
|
t.Fatalf("client: got %s, want %s", err, expectedErr)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("client: got %s, want no error", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test if authentication attempts are correctly limited on server
|
||||||
|
// when more public keys are provided then MaxAuthTries
|
||||||
|
func TestClientAuthMaxAuthTriesPublicKey(t *testing.T) {
|
||||||
|
signers := []Signer{}
|
||||||
|
for i := 0; i < 6; i++ {
|
||||||
|
signers = append(signers, testSigners["dsa"])
|
||||||
|
}
|
||||||
|
|
||||||
|
validConfig := &ClientConfig{
|
||||||
|
User: "testuser",
|
||||||
|
Auth: []AuthMethod{
|
||||||
|
PublicKeys(append([]Signer{testSigners["rsa"]}, signers...)...),
|
||||||
|
},
|
||||||
|
HostKeyCallback: InsecureIgnoreHostKey(),
|
||||||
|
}
|
||||||
|
if err := tryAuth(t, validConfig); err != nil {
|
||||||
|
t.Fatalf("unable to dial remote side: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedErr := fmt.Errorf("ssh: handshake failed: %v", &disconnectMsg{
|
||||||
|
Reason: 2,
|
||||||
|
Message: "too many authentication failures",
|
||||||
|
})
|
||||||
|
invalidConfig := &ClientConfig{
|
||||||
|
User: "testuser",
|
||||||
|
Auth: []AuthMethod{
|
||||||
|
PublicKeys(append(signers, testSigners["rsa"])...),
|
||||||
|
},
|
||||||
|
HostKeyCallback: InsecureIgnoreHostKey(),
|
||||||
|
}
|
||||||
|
if err := tryAuth(t, invalidConfig); err == nil {
|
||||||
|
t.Fatalf("client: got no error, want %s", expectedErr)
|
||||||
|
} else if err.Error() != expectedErr.Error() {
|
||||||
|
t.Fatalf("client: got %s, want %s", err, expectedErr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -45,6 +45,12 @@ type ServerConfig struct {
|
|||||||
// authenticating.
|
// authenticating.
|
||||||
NoClientAuth bool
|
NoClientAuth bool
|
||||||
|
|
||||||
|
// MaxAuthTries specifies the maximum number of authentication attempts
|
||||||
|
// permitted per connection. If set to a negative number, the number of
|
||||||
|
// attempts are unlimited. If set to zero, the number of attempts are limited
|
||||||
|
// to 6.
|
||||||
|
MaxAuthTries int
|
||||||
|
|
||||||
// PasswordCallback, if non-nil, is called when a user
|
// PasswordCallback, if non-nil, is called when a user
|
||||||
// attempts to authenticate using a password.
|
// attempts to authenticate using a password.
|
||||||
PasswordCallback func(conn ConnMetadata, password []byte) (*Permissions, error)
|
PasswordCallback func(conn ConnMetadata, password []byte) (*Permissions, error)
|
||||||
@@ -141,6 +147,10 @@ type ServerConn struct {
|
|||||||
// Request and NewChannel channels must be serviced, or the connection
|
// Request and NewChannel channels must be serviced, or the connection
|
||||||
// will hang.
|
// will hang.
|
||||||
func NewServerConn(c net.Conn, config *ServerConfig) (*ServerConn, <-chan NewChannel, <-chan *Request, error) {
|
func NewServerConn(c net.Conn, config *ServerConfig) (*ServerConn, <-chan NewChannel, <-chan *Request, error) {
|
||||||
|
if config.MaxAuthTries == 0 {
|
||||||
|
config.MaxAuthTries = 6
|
||||||
|
}
|
||||||
|
|
||||||
fullConf := *config
|
fullConf := *config
|
||||||
fullConf.SetDefaults()
|
fullConf.SetDefaults()
|
||||||
s := &connection{
|
s := &connection{
|
||||||
@@ -267,8 +277,23 @@ func (s *connection) serverAuthenticate(config *ServerConfig) (*Permissions, err
|
|||||||
var cache pubKeyCache
|
var cache pubKeyCache
|
||||||
var perms *Permissions
|
var perms *Permissions
|
||||||
|
|
||||||
|
authFailures := 0
|
||||||
|
|
||||||
userAuthLoop:
|
userAuthLoop:
|
||||||
for {
|
for {
|
||||||
|
if authFailures >= config.MaxAuthTries && config.MaxAuthTries > 0 {
|
||||||
|
discMsg := &disconnectMsg{
|
||||||
|
Reason: 2,
|
||||||
|
Message: "too many authentication failures",
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.transport.writePacket(Marshal(discMsg)); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, discMsg
|
||||||
|
}
|
||||||
|
|
||||||
var userAuthReq userAuthRequestMsg
|
var userAuthReq userAuthRequestMsg
|
||||||
if packet, err := s.transport.readPacket(); err != nil {
|
if packet, err := s.transport.readPacket(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -289,6 +314,11 @@ userAuthLoop:
|
|||||||
if config.NoClientAuth {
|
if config.NoClientAuth {
|
||||||
authErr = nil
|
authErr = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// allow initial attempt of 'none' without penalty
|
||||||
|
if authFailures == 0 {
|
||||||
|
authFailures--
|
||||||
|
}
|
||||||
case "password":
|
case "password":
|
||||||
if config.PasswordCallback == nil {
|
if config.PasswordCallback == nil {
|
||||||
authErr = errors.New("ssh: password auth not configured")
|
authErr = errors.New("ssh: password auth not configured")
|
||||||
@@ -360,6 +390,7 @@ userAuthLoop:
|
|||||||
if isQuery {
|
if isQuery {
|
||||||
// The client can query if the given public key
|
// The client can query if the given public key
|
||||||
// would be okay.
|
// would be okay.
|
||||||
|
|
||||||
if len(payload) > 0 {
|
if len(payload) > 0 {
|
||||||
return nil, parseError(msgUserAuthRequest)
|
return nil, parseError(msgUserAuthRequest)
|
||||||
}
|
}
|
||||||
@@ -409,6 +440,8 @@ userAuthLoop:
|
|||||||
break userAuthLoop
|
break userAuthLoop
|
||||||
}
|
}
|
||||||
|
|
||||||
|
authFailures++
|
||||||
|
|
||||||
var failureMsg userAuthFailureMsg
|
var failureMsg userAuthFailureMsg
|
||||||
if config.PasswordCallback != nil {
|
if config.PasswordCallback != nil {
|
||||||
failureMsg.Methods = append(failureMsg.Methods, "password")
|
failureMsg.Methods = append(failureMsg.Methods, "password")
|
||||||
|
|||||||
Reference in New Issue
Block a user