@@ -26,6 +26,10 @@ limitations under the License.
2626The Erlang/OTP SSH application is intended to be used in other applications as a
2727library.
2828
29+ Ensure the Erlang VM runs as a non-root OS user. All SSH services (shell,
30+ exec, SFTP) inherit the OS-level rights of the VM process. See
31+ [ Terminology] ( terminology.md ) for details on the rights model.
32+
2933Different applications using this library may have very different requirements.
3034One application could be running on a high performance server, while another is
3135running on a small device with very limited cpu capacity. For example, the first
@@ -53,7 +57,7 @@ increase the resilence. The options to use are:
5357
5458- ** [ max_sessions] ( `m:ssh#hardening_daemon_options-max_sessions` ) ** - The
5559 maximum number of simultaneous sessions that are accepted at any time for this
56- daemon. This includes sessions that are being authorized . The default is that
60+ daemon. This includes sessions that are being authenticated . The default is that
5761 an unlimited number of simultaneous sessions are allowed. It is a good
5862 candidate to set if the capacity of the server is low or a capacity margin is
5963 needed.
@@ -96,9 +100,19 @@ increase the resilence. The options to use are:
96100 receiving any message back. Alive messages are typically used to detect that a connection
97101 became unresponsive.
98102
99- A figure clarifies when a timeout is started and when it triggers:
103+ The following table clarifies when a timeout is started and when it triggers:
100104
101- ![ SSH server timeouts] ( assets/ssh_timeouts.jpg " SSH server timeouts ")
105+ | # | Event | Timeout started | Timeout ended |
106+ | ---| -------| -----------------| ---------------|
107+ | 1 | TCP connected | ` hello_timeout ` , ` negotiation_timeout ` | |
108+ | 2 | First SSH message received | | ` hello_timeout ` |
109+ | 3 | Key Exchange finished | | |
110+ | 4 | Authenticated | ` max_initial_idle_time ` | ` negotiation_timeout ` |
111+ | 5 | Channel 1 opened | | ` max_initial_idle_time ` |
112+ | 6 | Channel * n* opened | | |
113+ | 7 | Channel * x_1* closed | | |
114+ | 8 | Channel * x_n* closed (all channels closed) | ` idle_time ` | |
115+ | 9 | Connection closed | | ` idle_time ` |
102116
103117### Resilience to compression-based attacks
104118
@@ -220,20 +234,18 @@ connect to and not an evil one impersonating the expected one using its own
220234valid key-pair? There are two alternatives available with the default key
221235handling plugin ` m:ssh_file ` . The alternatives are:
222236
223- - ** Pre-store the host key** - \* For the default handler ssh_file, store the
237+ - ** Pre-store the host key** - For the default handler ssh_file, store the
224238 valid host keys in the file [ ` known_hosts ` ] ( `m:ssh_file#FILE-known_hosts` ) and
225239 set the option
226240 [ silently_accept_hosts] ( `m:ssh#hardening_client_options-silently_accept_hosts` )
227- to ` false ` .
228-
229- - or, write a specialized key handler using the
230- [ SSH client key API] ( `m:ssh_client_key_api` ) that accesses the pre-shared
231- key in some other way.
241+ to ` false ` . Alternatively, write a specialized key handler using the
242+ [ SSH client key API] ( `m:ssh_client_key_api` ) that accesses the pre-shared
243+ key in some other way.
232244
233- - ** Pre-store the "fingerprint" (checksum) of the host key** - \*
245+ - ** Pre-store the "fingerprint" (checksum) of the host key** - Use
234246 [ silently_accept_hosts] ( `m:ssh#hardening_client_options-silently_accept_hosts` )
235- - [ ` accept_callback() ` ] ( `t:ssh:accept_callback/0` )
236- - [ ` {HashAlgoSpec, accept_callback()} ` ] ( `t:ssh:accept_hosts/0` )
247+ with a callback: [ ` accept_callback() ` ] ( `t:ssh:accept_callback/0` )
248+ or [ ` {HashAlgoSpec, accept_callback()} ` ] ( `t:ssh:accept_hosts/0` ) .
237249
238250## Verifying the remote client in a daemon (server)
239251
@@ -308,6 +320,24 @@ The options [exec](`t:ssh:exec_daemon_option/0`) and
308320The same options could also install handlers for
309321the string(s) passed from the client to the server.
310322
323+ ### Enabling the SFTP subsystem
324+
325+ The SFTP subsystem is not enabled by default. When enabled, SFTP provides
326+ access to the file system with the rights of the OS process running the
327+ Erlang emulator, regardless of the authenticated SSH user. See the
328+ [ Terminology] ( terminology.md ) section for details.
329+
330+ The [ subsystems] ( `t:ssh:subsystem_daemon_option/0` ) option controls which
331+ subsystems are available. To enable SFTP:
332+
333+ ``` erlang
334+ ssh :daemon ({192 , 168 , 1 , 10 }, Port ,
335+ [{subsystems , [ssh_sftpd :subsystem_spec ([])]} | Options ]).
336+ ```
337+
338+ Use the ` root ` option to restrict SFTP users to a specific directory
339+ tree (see [ Root Directory Isolation] ( #root-directory-isolation ) below).
340+
311341### The id string
312342
313343One way to reduce the risk of intrusion is to not convey which software and
@@ -332,7 +362,7 @@ This brand and version may be changed with the option
332362option:
333363
334364``` erlang
335- ssh :daemon (1234 , [{id_string ," hi there" }, ... ]).
365+ ssh :daemon ({ 192 , 168 , 1 , 10 }, 1234 , [{id_string ," hi there" }, ... ]).
336366```
337367
338368and the daemon will present itself as:
@@ -383,3 +413,82 @@ using OS-level mechanisms (PAM chroot, containers, file permissions).
383413** Defense-in-depth:** For high-security deployments, combine the ` root ` option
384414with OS-level isolation mechanisms such as chroot jails, containers, or
385415mandatory access control (SELinux, AppArmor).
416+
417+ ## Network-Level Security
418+
419+ ### IP Binding Restrictions
420+
421+ ` ssh:daemon/1 ` and ` ssh:daemon/2 ` bind to ** all network interfaces** by
422+ default. For hardened deployments, use ` ssh:daemon/3 ` with an explicit
423+ IP address or ` loopback ` :
424+
425+ ``` erlang
426+ ssh :daemon ({192 , 168 , 1 , 10 }, 2222 , Options ). % Specific interface
427+ ssh :daemon (loopback , 2222 , Options ). % Localhost only
428+ ```
429+
430+ ** Note** : In the examples above, ` HostAddress ` (1st argument) takes precedence
431+ over a potentially provided ` {ip, Address} ` in ` Options ` (3rd argument).
432+
433+ ## Advanced Authentication
434+
435+ The following techniques provide enhanced authentication controls using
436+ custom callbacks. These require implementation specific to your environment.
437+
438+ ### Public Key Validation
439+
440+ Use a custom [ ` key_cb ` ] ( `t:ssh:key_cb_common_option/0` ) module implementing
441+ the ` m:ssh_server_key_api ` behaviour. The ` is_auth_key ` callback can enforce
442+ client key strength requirements (e.g. reject RSA keys shorter than 2048 bits)
443+ and log key usage for auditing. Enable
444+ [ ` pk_check_user ` ] ( `m:ssh#option-pk_check_user` ) to verify that the username
445+ is known before accepting public key authentication.
446+
447+ ``` erlang
448+ ssh :daemon ({192 , 168 , 1 , 10 }, Port , [
449+ {key_cb , {my_key_handler , []}},
450+ {pk_check_user , true }
451+ ]).
452+ ```
453+
454+ ### Account Lockout Policies
455+
456+ Use [ ` pwdfun ` ] ( `t:ssh:pwdfun_4/0` ) with an ETS table to track failed
457+ attempts across connections and lock accounts after repeated failures.
458+ Return ` disconnect ` when the account is locked — this immediately
459+ terminates the connection with ` SSH_DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE ` :
460+
461+ > #### Note {: .info }
462+ >
463+ > The lockout example below is conceptual. With
464+ > [ ` parallel_login ` ] ( `m:ssh#hardening_daemon_options-parallel_login` ) enabled,
465+ > race conditions may reduce lockout accuracy.
466+
467+ ``` erlang
468+ lockout_pwdfun (User , Password , _PeerAddr , State ) ->
469+ case ets :lookup (ssh_lockouts , {locked , User }) of
470+ [_ ] ->
471+ disconnect ;
472+ [] ->
473+ case validate_password (User , Password ) of
474+ true ->
475+ ets :delete (ssh_lockouts , {attempts , User }),
476+ {true , State };
477+ false ->
478+ N = ets :update_counter (ssh_lockouts ,
479+ {attempts , User }, 1 ,
480+ {{attempts , User }, 0 }),
481+ case N >= ? LOCKOUT_THRESHOLD of
482+ true ->
483+ ets :insert (ssh_lockouts ,
484+ {{locked , User }, true }),
485+ ets :delete (ssh_lockouts ,
486+ {attempts , User });
487+ false ->
488+ ok
489+ end ,
490+ {false , State }
491+ end
492+ end .
493+ ```
494+
0 commit comments