Skip to content

Commit

Permalink
Merge branch 'maint'
Browse files Browse the repository at this point in the history
* maint:
  ssh: Documentation
  ssh: Implement max_initial_idle_time
  • Loading branch information
HansN committed Aug 25, 2022
2 parents 3131421 + b873668 commit c7e5c1c
Show file tree
Hide file tree
Showing 10 changed files with 148 additions and 44 deletions.
2 changes: 1 addition & 1 deletion lib/ssh/doc/src/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ BOOK_FILES = book.xml
XML_FILES = $(BOOK_FILES) $(XML_APPLICATION_FILES) $(XML_REF3_FILES) $(XML_REF6_FILES)\
$(XML_PART_FILES) $(XML_CHAPTER_FILES)

IMAGE_FILES = SSH_protocols.png
IMAGE_FILES = SSH_protocols.png ssh_timeouts.jpg

TOP_SPECS_FILE = specs.xml

Expand Down
95 changes: 58 additions & 37 deletions lib/ssh/doc/src/hardening.xml
Original file line number Diff line number Diff line change
Expand Up @@ -59,43 +59,64 @@
However, some measures could be taken in the configuration of the SSH server to increase the resilence.
The options to use
are:</p>
<taglist>
<tag><seetype marker="ssh#hello_timeout_daemon_option">hello_timeout</seetype></tag>
<item>
If the client fails to send the first ssh message after a tcp connection setup
within this time (in milliseconds), the connection is closed.
The default value is 30 seconds. This is actually a generous time, so it can lowered
to make the daemon less prone to DoS attacks.
</item>
<tag><seetype marker="ssh#negotiation_timeout_daemon_option">negotiation_timeout</seetype></tag>
<item>
Maximum time in milliseconds for the authentication negotiation.
If the client fails to log in within this time, the connection is closed.
The default value is 2 minutes. It is quite a long time, but can lowered if the client is
supposed to be fast like if it is a program logging in.
</item>
<tag><seeerl marker="ssh#hardening_daemon_options--max_sessions">max_sessions</seeerl></tag>
<item>
The maximum number of simultaneous sessions that are accepted at any time for this daemon.
This includes sessions that are being authorized. The default is that an unlimited number of
simultaneous sessions are allowed. It is a good candidate to set if the capacity of the server
is low or a capacity margin is needed.
</item>
<tag><seeerl marker="ssh#hardening_daemon_options--max_channels">max_channels</seeerl></tag>
<item>
The maximum number of channels that are accepted for each connection. The default is unlimited.
</item>
<tag><seeerl marker="ssh#hardening_daemon_options--parallel_login">parallel_login</seeerl></tag>
<item>
If set to false (the default value), only one login is handled at a time.
If set to true, the number of simultaneous login attempts are limited by the value of
<seeerl marker="ssh#hardening_daemon_options--max_sessions">max_sessions</seeerl> option.
</item>
<tag><seetype marker="ssh#max_idle_time_common_option">idle_time<!--sic!--></seetype></tag>
<item>
Sets a time-out on a connection when no channels are open. Defaults to infinity.
</item>
</taglist>
<section>
<title>Counters and parallelism</title>
<taglist>
<tag><seeerl marker="ssh#hardening_daemon_options--max_sessions">max_sessions</seeerl></tag>
<item>
The maximum number of simultaneous sessions that are accepted at any time for this daemon.
This includes sessions that are being authorized. The default is that an unlimited number of
simultaneous sessions are allowed. It is a good candidate to set if the capacity of the server
is low or a capacity margin is needed.
</item>
<tag><seeerl marker="ssh#hardening_daemon_options--max_channels">max_channels</seeerl></tag>
<item>
The maximum number of channels that are accepted for each connection. The default is unlimited.
</item>
<tag><seeerl marker="ssh#hardening_daemon_options--parallel_login">parallel_login</seeerl></tag>
<item>
If set to false (the default value), only one login is handled at a time.
If set to true, the number of simultaneous login attempts are limited by the value of the
<seeerl marker="ssh#hardening_daemon_options--max_sessions">max_sessions</seeerl> option.
</item>
</taglist>
</section>

<section>
<title>Timeouts</title>
<taglist>
<tag><seetype marker="ssh#hello_timeout_daemon_option">hello_timeout</seetype></tag>
<item>
If the client fails to send the first ssh message after a tcp connection setup
within this time (in milliseconds), the connection is closed.
The default value is 30 seconds. This is actually a generous time, so it can lowered
to make the daemon less prone to DoS attacks.
</item>
<tag><seetype marker="ssh#negotiation_timeout_daemon_option">negotiation_timeout</seetype></tag>
<item>
Maximum time in milliseconds for the authentication negotiation counted from the TCP connection establishment.
If the client fails to log in within this time the connection is closed.
The default value is 2 minutes. It is quite a long time, but can lowered if the client is
supposed to be fast like if it is a program logging in.
</item>
<tag><seetype marker="ssh#max_idle_time_common_option">idle_time<!--sic!--></seetype></tag>
<item>
Sets a time-out on a connection when no channels are left after closing the final one.
It defaults to infinity.
</item>
<tag><seetype marker="ssh#max_initial_idle_time_daemon_option">max_initial_idle_time</seetype></tag>
<item>
Sets a time-out on a connection that will expire if no channel is opened on the connection.
The timeout is started when the authentication phase is completed.
It defaults to infinity.
</item>
</taglist>
<p>A figure clarifies when a timeout is started and when it triggers:
</p>
<image file="ssh_timeouts.jpg">
<icaption>SSH server timeouts</icaption>
</image>
</section>
</section>


Expand Down
29 changes: 29 additions & 0 deletions lib/ssh/doc/src/ssh.xml
Original file line number Diff line number Diff line change
Expand Up @@ -725,6 +725,10 @@
Defaults to 30000 ms (30 seconds). If the client fails to send the first message within this time,
the connection is closed.
</p>
<p>For more information about timeouts, see the
<seeguide marker="hardening#timeouts">Timeouts section </seeguide>
in the User's Guide <seeguide marker="hardening">Hardening</seeguide> chapter.
</p>
</desc>
</datatype>

Expand All @@ -735,12 +739,33 @@
Defaults to 120000 ms (2 minutes). If the client fails to log in within this time,
the connection is closed.
</p>
<p>For more information about timeouts, see the
<seeguide marker="hardening#timeouts">Timeouts section </seeguide>
in the User's Guide <seeguide marker="hardening">Hardening</seeguide> chapter.
</p>
</desc>
</datatype>

<datatype>
<name name="max_initial_idle_time_daemon_option"/>
<desc>
<p>Maximum time in milliseconds for the first channel start after
completion of the authentication negotiation.
Defaults to <c>infinity</c>.
</p>
<p>For more information about timeouts, see the
<seeguide marker="hardening#timeouts">Timeouts section </seeguide>
in the User's Guide <seeguide marker="hardening">Hardening</seeguide> chapter.
</p>
</desc>
</datatype>

<datatype>
<name name="hardening_daemon_options"/>
<desc>
<p>For more information about hardening, see the
<seeguide marker="hardening">Hardening</seeguide> section in the User's Guide chapter.
</p>
<taglist>
<tag>
<marker id="hardening_daemon_options--max_sessions"/>
Expand Down Expand Up @@ -893,6 +918,10 @@
<p>The timeout is not active until channels are started, so it does
not limit the time from the connection creation to the first channel opening.
</p>
<p>For more information about timeouts, see the
<seeguide marker="hardening#timeouts">Timeouts section </seeguide>
in the User's Guide <seeguide marker="hardening">Hardening</seeguide> chapter.
</p>
</desc>
</datatype>

Expand Down
Binary file added lib/ssh/doc/src/ssh_timeouts.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added lib/ssh/doc/src/ssh_timeouts.odp
Binary file not shown.
2 changes: 2 additions & 0 deletions lib/ssh/src/ssh.hrl
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,7 @@
| tcpip_tunnel_in_daemon_option()
| authentication_daemon_options()
| diffie_hellman_group_exchange_daemon_option()
| max_initial_idle_time_daemon_option()
| negotiation_timeout_daemon_option()
| hello_timeout_daemon_option()
| hardening_daemon_options()
Expand Down Expand Up @@ -392,6 +393,7 @@
-type explicit_group_file() :: {file,string()} .
-type ssh_moduli_file() :: {ssh_moduli_file,string()}.

-type max_initial_idle_time_daemon_option() :: {max_initial_idle_time, timeout()} .
-type negotiation_timeout_daemon_option() :: {negotiation_timeout, timeout()} .
-type hello_timeout_daemon_option() :: {hello_timeout, timeout()} .

Expand Down
8 changes: 7 additions & 1 deletion lib/ssh/src/ssh_connection_handler.erl
Original file line number Diff line number Diff line change
Expand Up @@ -1118,7 +1118,10 @@ handle_event(info, {Proto, Sock, NewData}, StateName, D0 = #data{socket = Sock,
#ssh_msg_global_request{} = Msg -> {keep_state, D1, ?CONNECTION_MSG(Msg)};
#ssh_msg_request_success{} = Msg -> {keep_state, D1, ?CONNECTION_MSG(Msg)};
#ssh_msg_request_failure{} = Msg -> {keep_state, D1, ?CONNECTION_MSG(Msg)};
#ssh_msg_channel_open{} = Msg -> {keep_state, D1, ?CONNECTION_MSG(Msg)};
#ssh_msg_channel_open{} = Msg -> {keep_state, D1,
[{{timeout, max_initial_idle_time}, cancel} |
?CONNECTION_MSG(Msg)
]};
#ssh_msg_channel_open_confirmation{} = Msg -> {keep_state, D1, ?CONNECTION_MSG(Msg)};
#ssh_msg_channel_open_failure{} = Msg -> {keep_state, D1, ?CONNECTION_MSG(Msg)};
#ssh_msg_channel_window_adjust{} = Msg -> {keep_state, D1, ?CONNECTION_MSG(Msg)};
Expand Down Expand Up @@ -1232,6 +1235,9 @@ handle_event({timeout,idle_time}, _Data, _StateName, D) ->
keep_state_and_data
end;

handle_event({timeout,max_initial_idle_time}, _Data, _StateName, _D) ->
{stop, {shutdown, "Timeout"}};

%%% So that terminate will be run when supervisor is shutdown
handle_event(info, {'EXIT', _Sup, Reason}, StateName, _D) ->
Role = ?role(StateName),
Expand Down
26 changes: 22 additions & 4 deletions lib/ssh/src/ssh_fsm_userauth_server.erl
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,12 @@ handle_event(internal,
{keep_state, D};
{authorized, User, {Reply, Ssh1}} ->
D = connected_state(Reply, Ssh1, User, Method, D0),
{next_state, {connected,server}, D, {change_callback_module,ssh_connection_handler}}
{next_state, {connected,server}, D,
[set_max_initial_idle_timeout(D),
{change_callback_module,ssh_connection_handler}
]
}

end;

{"ssh-connection", "ssh-connection", Method} ->
Expand All @@ -82,7 +87,10 @@ handle_event(internal,
case ssh_auth:handle_userauth_request(Msg, Ssh0#ssh.session_id, Ssh0) of
{authorized, User, {Reply, Ssh1}} ->
D = connected_state(Reply, Ssh1, User, Method, D0),
{next_state, {connected,server}, D, {change_callback_module,ssh_connection_handler}};
{next_state, {connected,server}, D,
[set_max_initial_idle_timeout(D),
{change_callback_module,ssh_connection_handler}
]};
{not_authorized, {User, Reason}, {Reply, Ssh}} when Method == "keyboard-interactive" ->
retry_fun(User, Reason, D0),
D = ssh_connection_handler:send_msg(Reply, D0#data{ssh_params = Ssh}),
Expand Down Expand Up @@ -115,7 +123,10 @@ handle_event(internal, #ssh_msg_userauth_info_response{} = Msg, {userauth_keyboa
case ssh_auth:handle_userauth_info_response(Msg, D0#data.ssh_params) of
{authorized, User, {Reply, Ssh1}} ->
D = connected_state(Reply, Ssh1, User, "keyboard-interactive", D0),
{next_state, {connected,server}, D, {change_callback_module,ssh_connection_handler}};
{next_state, {connected,server}, D,
[set_max_initial_idle_timeout(D),
{change_callback_module,ssh_connection_handler}
]};
{not_authorized, {User, Reason}, {Reply, Ssh}} ->
retry_fun(User, Reason, D0),
D = ssh_connection_handler:send_msg(Reply, D0#data{ssh_params = Ssh}),
Expand All @@ -130,7 +141,11 @@ handle_event(internal, #ssh_msg_userauth_info_response{} = Msg, {userauth_keyboa
{authorized, User, {Reply, Ssh1}} =
ssh_auth:handle_userauth_info_response({extra,Msg}, D0#data.ssh_params),
D = connected_state(Reply, Ssh1, User, "keyboard-interactive", D0),
{next_state, {connected,server}, D, {change_callback_module,ssh_connection_handler}};
{next_state, {connected,server}, D,
[set_max_initial_idle_timeout(D),
{change_callback_module,ssh_connection_handler}
]
};


%%% ######## UNHANDLED EVENT!
Expand Down Expand Up @@ -164,6 +179,9 @@ connected_state(Reply, Ssh1, User, Method, D0) ->
ssh_params = Ssh#ssh{authenticated = true}}.


set_max_initial_idle_timeout(#data{ssh_params = #ssh{opts=Opts}}) ->
{{timeout,max_initial_idle_time}, ?GET_OPT(max_initial_idle_time,Opts), none}.

connected_fun(User, Method, #data{ssh_params = #ssh{peer = {_,Peer}}} = D) ->
?CALL_FUN(connectfun,D)(User, Peer, Method).

Expand Down
6 changes: 6 additions & 0 deletions lib/ssh/src/ssh_options.erl
Original file line number Diff line number Diff line change
Expand Up @@ -533,6 +533,12 @@ default(server) ->
class => user_option
},

max_initial_idle_time =>
#{default => infinity, %% To not break compatibility
chk => fun(V) -> check_timeout(V) end,
class => user_option
},

negotiation_timeout =>
#{default => 2*60*1000,
chk => fun(V) -> check_timeout(V) end,
Expand Down
24 changes: 23 additions & 1 deletion lib/ssh/test/ssh_basic_SUITE.erl
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@
login_bad_pwd_no_retry3/1,
login_bad_pwd_no_retry4/1,
login_bad_pwd_no_retry5/1,
max_initial_idle_time/1,
misc_ssh_options/1,
multi_daemon_opt_fd/1,
openssh_zlib_basic_test/1,
Expand Down Expand Up @@ -155,7 +156,9 @@ groups() ->
exec, exec_compressed,
exec_with_io_out, exec_with_io_in,
cli, cli_exit_normal, cli_exit_status,
idle_time_client, idle_time_server, openssh_zlib_basic_test,
idle_time_client, idle_time_server,
max_initial_idle_time,
openssh_zlib_basic_test,
misc_ssh_options, inet_option, inet6_option,
shell, shell_socket, shell_ssh_conn, shell_no_unicode, shell_unicode_string,
close
Expand Down Expand Up @@ -471,6 +474,25 @@ idle_time_common(DaemonExtraOpts, ClientExtraOpts, Config) ->
end,
ssh:stop_daemon(Pid).

%%--------------------------------------------------------------------
max_initial_idle_time(Config) ->
SystemDir = filename:join(proplists:get_value(priv_dir, Config), system),
UserDir = proplists:get_value(priv_dir, Config),

{Pid, Host, Port} = ssh_test_lib:daemon([{system_dir, SystemDir},
{user_dir, UserDir},
{failfun, fun ssh_test_lib:failfun/2},
{max_initial_idle_time, 2000}
]),
ConnectionRef =
ssh_test_lib:connect(Host, Port, [{silently_accept_hosts, true},
{user_dir, UserDir},
{user_interaction, false}
]),
timer:sleep(8000),
{error, closed} = ssh_connection:session_channel(ConnectionRef, 1000),
ssh:stop_daemon(Pid).

%%--------------------------------------------------------------------
%%% Test that ssh:shell/2 works
shell(Config) when is_list(Config) ->
Expand Down

0 comments on commit c7e5c1c

Please sign in to comment.