Skip to content

Commit cd5a44f

Browse files
authored
Merge pull request #10882 from alexandrejbr/alexandrejbr/ssh-max-auth-tries
OTP-20263 ssh: max_auth_tries option for server role
2 parents 9b155fc + 0d0f791 commit cd5a44f

14 files changed

Lines changed: 755 additions & 140 deletions

lib/ssh/doc/guides/hardening.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,11 @@ increase the resilence. The options to use are:
7171
true, the number of simultaneous login attempts are limited by the value of
7272
the [max_sessions](`m:ssh#hardening_daemon_options-max_sessions`) option.
7373

74+
- **[max_auth_tries](`m:ssh#hardening_daemon_options-max_auth_tries`)** - The
75+
maximum number of authentication attempts permitted per connection. When a
76+
client exceeds this number of failed attempts, the daemon disconnects it. The
77+
default is 6.
78+
7479
### Timeouts
7580

7681
- **[hello_timeout](`t:ssh:hello_timeout_daemon_option/0`)** - If the client

lib/ssh/src/ssh.hrl

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1288,14 +1288,21 @@ in the User's Guide chapter.
12881288
is the maximum allowed packet size, 262144 bytes,
12891289
which is the same as no check being made,
12901290
since maximum allowed packet size check is performed earlier.
1291+
1292+
- **`max_auth_tries`{: #hardening_daemon_options-max_auth_tries }** - The
1293+
maximum number of authentication attempts permitted per connection. When a
1294+
client exceeds this number of failed attempts, the daemon disconnects it.
1295+
Accepted values are a positive integer or the atom `infinity` to disable the
1296+
limit. The default value is `6`.
12911297
""".
12921298
-doc(#{group => <<"Daemon Options">>}).
12931299
-type hardening_daemon_options() ::
12941300
{max_sessions, pos_integer()}
12951301
| {max_channels, pos_integer()}
12961302
| {parallel_login, boolean()}
12971303
| {minimal_remote_max_packet_size, pos_integer()}
1298-
| {max_auth_request_size, pos_integer()}.
1304+
| {max_auth_request_size, pos_integer()}
1305+
| {max_auth_tries, pos_integer() | infinity}.
12991306

13001307
-doc """
13011308
- **`connectfun`** - Provides a fun to implement your own logging when a user
@@ -1406,7 +1413,8 @@ Experimental options that should not to be used in products.
14061413
userauth_methods, % list( string() ) eg ["keyboard-interactive", "password"]
14071414
userauth_supported_methods, % string() eg "keyboard-interactive,password"
14081415
userauth_pubkeys,
1409-
kb_tries_left = 0, % integer(), num tries left for "keyboard-interactive"
1416+
auth_tries_left = 0, % integer()|infinity, auth tries left (max_auth_tries)
1417+
auth_attempts = 0, % non_neg_integer(), userauth requests seen (OpenSSH "attempt")
14101418
userauth_preference,
14111419
available_host_keys,
14121420
pwdfun_user_state,

lib/ssh/src/ssh_auth.erl

Lines changed: 102 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -235,72 +235,68 @@ handle_userauth_request(#ssh_msg_service_request{name = Name = "ssh-userauth"},
235235

236236
handle_userauth_request(#ssh_msg_userauth_request{user = User,
237237
service = "ssh-connection",
238-
method = "password",
238+
method = "password" = Method,
239239
data = <<?FALSE, ?UINT32(Sz), Password:Sz/binary>>}, _,
240-
#ssh{userauth_supported_methods = Methods} = Ssh) ->
240+
Ssh) ->
241241
case check_password(User, Password, Ssh) of
242242
{true,Ssh1} ->
243243
{authorized, User,
244244
{#ssh_msg_userauth_success{}, Ssh1}
245245
};
246246
{false,Ssh1} ->
247-
{not_authorized, {User, {error,"Bad user or password"}},
248-
{#ssh_msg_userauth_failure{authentications = Methods,
249-
partial_success = false}, Ssh1}
250-
}
247+
userauth_failure(User, Method, {error,"Bad user or password"}, Ssh1)
251248
end;
252249

253250
handle_userauth_request(#ssh_msg_userauth_request{user = User,
254251
service = "ssh-connection",
255-
method = "password",
252+
method = "password" = Method,
256253
data = <<?TRUE,
257254
_/binary
258255
%% ?UINT32(Sz1), OldBinPwd:Sz1/binary,
259256
%% ?UINT32(Sz2), NewBinPwd:Sz2/binary
260257
>>
261-
}, _,
262-
#ssh{userauth_supported_methods = Methods} = Ssh) ->
258+
}, _, Ssh) ->
263259
%% Password change without us having sent SSH_MSG_USERAUTH_PASSWD_CHANGEREQ (because we never do)
264260
%% RFC 4252 says:
265261
%% SSH_MSG_USERAUTH_FAILURE without partial success - The password
266262
%% has not been changed. Either password changing was not supported,
267263
%% or the old password was bad.
268264

269-
{not_authorized, {User, {error,"Password change not supported"}},
270-
{#ssh_msg_userauth_failure{authentications = Methods,
271-
partial_success = false}, Ssh}
272-
};
265+
userauth_failure(User, Method, {error, "Password change not supported"}, Ssh);
273266

274267
handle_userauth_request(#ssh_msg_userauth_request{user = User,
275268
service = "ssh-connection",
276269
method = "none"}, _,
277270
#ssh{userauth_supported_methods = Methods,
271+
auth_attempts = Attempts,
278272
opts = Opts} = Ssh) ->
279273
case ?GET_OPT(no_auth_needed, Opts) of
280-
false ->
281-
%% The normal case
282-
{not_authorized, {User, undefined},
283-
{#ssh_msg_userauth_failure{authentications = Methods,
284-
partial_success = false}, Ssh}
285-
};
286274
true ->
287275
%% RFC 4252 5.2
288276
{authorized, User,
289277
{#ssh_msg_userauth_success{}, Ssh}
290-
}
278+
};
279+
false when Attempts =< 1 ->
280+
%% RFC 4252 5.4: the client's first "none" request is used to query
281+
%% the available methods; like OpenSSH this first attempt is free.
282+
{not_authorized, {User, undefined},
283+
{#ssh_msg_userauth_failure{authentications = Methods,
284+
partial_success = false}, Ssh}
285+
};
286+
false ->
287+
%% Any subsequent "none" counts as a failed attempt.
288+
userauth_failure(User, "none", undefined, Ssh)
291289
end;
292290

293291
handle_userauth_request(#ssh_msg_userauth_request{user = User,
294292
service = "ssh-connection",
295-
method = "publickey",
293+
method = "publickey" = Method,
296294
data = <<?BYTE(?FALSE),
297295
?UINT32(ALen), BAlg:ALen/binary,
298296
?UINT32(KLen), KeyBlob:KLen/binary,
299297
_/binary
300298
>>
301-
},
302-
_SessionId,
303-
#ssh{userauth_supported_methods = Methods} = Ssh0) ->
299+
}, _SessionId, Ssh0) ->
304300
Ssh =
305301
case check_user(User, Ssh0) of
306302
{true,Ssh01} -> Ssh01#ssh{user=User};
@@ -313,26 +309,21 @@ handle_userauth_request(#ssh_msg_userauth_request{user = User,
313309
true ->
314310
{not_authorized, {User, undefined},
315311
{#ssh_msg_userauth_pk_ok{algorithm_name = binary_to_list(BAlg),
316-
key_blob = KeyBlob}, Ssh}
312+
key_blob = KeyBlob}, Ssh}
317313
};
318314
false ->
319-
{not_authorized, {User, undefined},
320-
{#ssh_msg_userauth_failure{authentications = Methods,
321-
partial_success = false}, Ssh}
322-
}
315+
userauth_failure(User, Method, undefined, Ssh)
323316
end;
324317

325318
handle_userauth_request(#ssh_msg_userauth_request{user = User,
326319
service = "ssh-connection",
327-
method = "publickey",
320+
method = "publickey" = Method,
328321
data = <<?BYTE(?TRUE),
329322
?UINT32(ALen), BAlg:ALen/binary,
330323
?UINT32(KLen), KeyBlob:KLen/binary,
331324
SigWLen/binary>>
332325
},
333-
SessionId,
334-
#ssh{user = PreVerifyUser,
335-
userauth_supported_methods = Methods} = Ssh0) ->
326+
SessionId, #ssh{user = PreVerifyUser} = Ssh0) ->
336327

337328
{UserOk,Ssh} = check_user(User, Ssh0),
338329
case
@@ -345,81 +336,63 @@ handle_userauth_request(#ssh_msg_userauth_request{user = User,
345336
{#ssh_msg_userauth_success{}, Ssh}
346337
};
347338
false ->
348-
{not_authorized, {User, undefined},
349-
{#ssh_msg_userauth_failure{authentications = Methods,
350-
partial_success = false}, Ssh}
351-
}
339+
userauth_failure(User, Method, undefined, Ssh)
352340
end;
353341

354342
handle_userauth_request(#ssh_msg_userauth_request{user = User,
355343
service = "ssh-connection",
356344
method = "keyboard-interactive",
357345
data = _},
358-
_, #ssh{opts = Opts,
359-
kb_tries_left = KbTriesLeft,
360-
userauth_supported_methods = Methods} = Ssh) ->
361-
case KbTriesLeft of
362-
N when N<1 ->
363-
{not_authorized, {User, {authmethod, "keyboard-interactive"}},
364-
{#ssh_msg_userauth_failure{authentications = Methods,
365-
partial_success = false}, Ssh}
366-
};
367-
368-
_ ->
369-
%% RFC4256
370-
%% The data field contains:
371-
%% - language tag (deprecated). If =/=[] SHOULD use it however. We skip
372-
%% it for simplicity.
373-
%% - submethods. "... the user can give a hint of which actual methods
374-
%% he wants to use. ...". It's a "MAY use" so we skip
375-
%% it. It also needs an understanding between the client
376-
%% and the server.
377-
%%
378-
%% "The server MUST reply with an SSH_MSG_USERAUTH_SUCCESS,
379-
%% SSH_MSG_USERAUTH_FAILURE, or SSH_MSG_USERAUTH_INFO_REQUEST message."
380-
Default = {"SSH server",
381-
"Enter password for \""++User++"\"",
382-
"password: ",
383-
false},
384-
385-
{Name, Instruction, Prompt, Echo} =
386-
case ?GET_OPT(auth_method_kb_interactive_data, Opts) of
387-
undefined ->
388-
Default;
389-
{_,_,_,_}=V ->
390-
V;
391-
F when is_function(F, 4) ->
392-
{_,PeerName} = Ssh#ssh.peer,
393-
F(PeerName, User, "ssh-connection", Ssh#ssh.pwdfun_user_state);
394-
F when is_function(F) ->
395-
{_,PeerName} = Ssh#ssh.peer,
396-
F(PeerName, User, "ssh-connection")
397-
end,
398-
EchoEnc = case Echo of
399-
true -> <<?TRUE>>;
400-
false -> <<?FALSE>>
401-
end,
402-
Msg = #ssh_msg_userauth_info_request{name = unicode:characters_to_list(Name),
403-
instruction = unicode:characters_to_list(Instruction),
404-
language_tag = "",
405-
num_prompts = 1,
406-
data = <<?STRING(unicode:characters_to_binary(Prompt)),
407-
EchoEnc/binary
408-
>>
409-
},
410-
{not_authorized, {User, undefined},
411-
{Msg, Ssh#ssh{user = User}}
412-
}
413-
end;
346+
_, #ssh{opts = Opts} = Ssh) ->
347+
%% RFC4256
348+
%% The data field contains:
349+
%% - language tag (deprecated). If =/=[] SHOULD use it however. We skip
350+
%% it for simplicity.
351+
%% - submethods. "... the user can give a hint of which actual methods
352+
%% he wants to use. ...". It's a "MAY use" so we skip
353+
%% it. It also needs an understanding between the client
354+
%% and the server.
355+
%%
356+
%% "The server MUST reply with an SSH_MSG_USERAUTH_SUCCESS,
357+
%% SSH_MSG_USERAUTH_FAILURE, or SSH_MSG_USERAUTH_INFO_REQUEST message."
358+
Default = {"SSH server",
359+
"Enter password for \""++User++"\"",
360+
"password: ",
361+
false},
362+
363+
{Name, Instruction, Prompt, Echo} =
364+
case ?GET_OPT(auth_method_kb_interactive_data, Opts) of
365+
undefined ->
366+
Default;
367+
{_,_,_,_}=V ->
368+
V;
369+
F when is_function(F, 4) ->
370+
{_,PeerName} = Ssh#ssh.peer,
371+
F(PeerName, User, "ssh-connection", Ssh#ssh.pwdfun_user_state);
372+
F when is_function(F) ->
373+
{_,PeerName} = Ssh#ssh.peer,
374+
F(PeerName, User, "ssh-connection")
375+
end,
376+
EchoEnc = case Echo of
377+
true -> <<?TRUE>>;
378+
false -> <<?FALSE>>
379+
end,
380+
Msg = #ssh_msg_userauth_info_request{name = unicode:characters_to_list(Name),
381+
instruction = unicode:characters_to_list(Instruction),
382+
language_tag = "",
383+
num_prompts = 1,
384+
data = <<?STRING(unicode:characters_to_binary(Prompt)),
385+
EchoEnc/binary
386+
>>
387+
},
388+
{not_authorized, {User, undefined},
389+
{Msg, Ssh#ssh{user = User}}
390+
};
414391

415392
handle_userauth_request(#ssh_msg_userauth_request{user = User,
416393
service = "ssh-connection",
417-
method = Other}, _,
418-
#ssh{userauth_supported_methods = Methods} = Ssh) ->
419-
{not_authorized, {User, {authmethod, Other}},
420-
{#ssh_msg_userauth_failure{authentications = Methods,
421-
partial_success = false}, Ssh}
422-
}.
394+
method = Other}, _, Ssh) ->
395+
userauth_failure(User, Other, {authmethod, Other}, Ssh).
423396

424397

425398
%%%----------------------------------------------------------------
@@ -445,9 +418,7 @@ handle_userauth_info_request(#ssh_msg_userauth_info_request{name = Name,
445418
handle_userauth_info_response(#ssh_msg_userauth_info_response{num_responses = 1,
446419
data = <<?UINT32(Sz), Password:Sz/binary>>},
447420
#ssh{opts = Opts,
448-
kb_tries_left = KbTriesLeft,
449-
user = User,
450-
userauth_supported_methods = Methods} = Ssh) ->
421+
user = User} = Ssh) ->
451422
SendOneEmpty =
452423
(?GET_OPT(tstflg,Opts) == one_empty)
453424
orelse
@@ -469,10 +440,8 @@ handle_userauth_info_response(#ssh_msg_userauth_info_response{num_responses = 1,
469440
{#ssh_msg_userauth_success{}, Ssh1}};
470441

471442
{false,Ssh1} ->
472-
{not_authorized, {User, {error,"Bad user or password"}},
473-
{#ssh_msg_userauth_failure{authentications = Methods,
474-
partial_success = false},
475-
Ssh1#ssh{kb_tries_left = max(KbTriesLeft-1, 0)}}}
443+
Method = "keyboard-interactive",
444+
userauth_failure(User, Method, {error,"Bad user or password"}, Ssh1)
476445
end;
477446

478447
handle_userauth_info_response({extra,#ssh_msg_userauth_info_response{}},
@@ -668,6 +637,20 @@ write_if_nonempty(_, "") -> ok;
668637
write_if_nonempty(_, <<>>) -> ok;
669638
write_if_nonempty(IoCb, Text) -> IoCb:format("~s~n",[Text]).
670639

640+
userauth_failure(User, _Method, Error, #ssh{auth_tries_left = Tries} = Ssh)
641+
when Tries =/= infinity, Tries =< 1 ->
642+
{auth_tries_exceeded, {User, Error}, Ssh};
643+
userauth_failure(User, _Method, Error,
644+
#ssh{auth_tries_left = Tries, userauth_supported_methods = Methods} = Ssh) ->
645+
AuthTriesLeft = reduce_tries_count(Tries),
646+
{not_authorized, {User, Error},
647+
{#ssh_msg_userauth_failure{authentications = Methods,
648+
partial_success = false},
649+
Ssh#ssh{auth_tries_left = AuthTriesLeft}}}.
650+
651+
reduce_tries_count(infinity) -> infinity;
652+
reduce_tries_count(N) -> N - 1.
653+
671654
%%%----------------------------------------------------------------
672655
%%% Called just for the tracer ssh_dbg
673656
ssh_msg_userauth_result(_R) -> ok.
@@ -832,7 +815,7 @@ fmt_req(#ssh_msg_userauth_request{user = User,
832815
service = "ssh-connection",
833816
method = Method,
834817
data = Data},
835-
#ssh{kb_tries_left = KbTriesLeft,
818+
#ssh{auth_tries_left = AuthTriesLeft,
836819
userauth_supported_methods = Methods}) ->
837820
[io_lib:format("req user = ~p~n"
838821
"req method = ~p~n"
@@ -841,18 +824,21 @@ fmt_req(#ssh_msg_userauth_request{user = User,
841824
case Method of
842825
"none" -> "";
843826
"password" -> fmt_bool(Data);
844-
"keyboard-interactive" -> fmt_kb_tries_left(KbTriesLeft);
845-
"publickey" -> [case Data of
846-
<<?BYTE(_), ?UINT32(ALen), Alg:ALen/binary, _/binary>> ->
847-
io_lib:format("~nkey-type = ~p", [Alg]);
848-
_ ->
849-
""
850-
end];
827+
"keyboard-interactive" -> "";
828+
"publickey" -> case Data of
829+
<<?BYTE(_), ?UINT32(ALen), Alg:ALen/binary, _/binary>> ->
830+
io_lib:format("~nkey-type = ~p", [Alg]);
831+
_ ->
832+
""
833+
end;
851834
_ -> ""
852-
end].
835+
end,
836+
fmt_auth_tries_left(AuthTriesLeft)].
853837

854838

855-
fmt_kb_tries_left(N) when is_integer(N)->
839+
fmt_auth_tries_left(infinity) ->
840+
"";
841+
fmt_auth_tries_left(N) when is_integer(N) ->
856842
io_lib:format("~ntries left = ~p", [N-1]).
857843

858844

@@ -867,4 +853,3 @@ fmt_bool(<<>>) ->
867853
"".
868854

869855

870-

0 commit comments

Comments
 (0)