Skip to content

Commit ae077a9

Browse files
committed
Merge branch 'kuba/ssh/services_disabled_by_default_hardening_guide_update/OTP-19969/OTP-20078/OTP-20079'
* kuba/ssh/services_disabled_by_default_hardening_guide_update/OTP-19969/OTP-20078/OTP-20079: ssh: extend hardening guide ssh: Disable SFTP subsystem by default ssh: Change shell and exec defaults to disabled
2 parents fdf0394 + f6b844c commit ae077a9

28 files changed

Lines changed: 601 additions & 312 deletions

lib/kernel/test/shell_test_lib.erl

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -184,9 +184,10 @@ setup_tty(Config) ->
184184
PrivDir = filename:join(proplists:get_value(priv_dir, Config), "nopubkey"),
185185
file:make_dir(PrivDir),
186186
SysDir = proplists:get_value(data_dir, Config),
187-
{ok, _Sshd} = ssh:daemon(8989, [{system_dir, SysDir},
188-
{user_dir, PrivDir},
189-
{password, "bar"}])
187+
{ok, _Sshd} = ssh:daemon(8989, [{shell, {shell, start, []}},
188+
{system_dir, SysDir},
189+
{user_dir, PrivDir},
190+
{password, "bar"}])
190191
end),
191192
os:cmd(os:find_executable("tmux") ++ " new-window -n " ++ ClientName ++ " -d -- "++
192193
"ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null localhost -p 8989 -l foo"),
-39.3 KB
Binary file not shown.

lib/ssh/doc/assets/ssh_timeouts.jpg.license

Lines changed: 0 additions & 21 deletions
This file was deleted.

lib/ssh/doc/guides/hardening.md

Lines changed: 130 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ limitations under the License.
2626
The Erlang/OTP SSH application is intended to be used in other applications as a
2727
library.
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+
2933
Different applications using this library may have very different requirements.
3034
One application could be running on a high performance server, while another is
3135
running 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

@@ -122,7 +136,7 @@ excessive memory consumption.
122136

123137
### SFTP Server Resource Limits
124138

125-
When running an SFTP server via `ssh_sftpd:subsystem_spec/1`, additional
139+
When enabling the SFTP subsystem via `ssh_sftpd:subsystem_spec/1`, additional
126140
resource limits can be configured to protect against resource exhaustion attacks:
127141

128142
#### max_handles
@@ -220,20 +234,18 @@ connect to and not an evil one impersonating the expected one using its own
220234
valid key-pair? There are two alternatives available with the default key
221235
handling 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

@@ -303,12 +315,29 @@ _exec_ server-side service takes a string provided by the client, evaluates it
303315
and returns the result. The _shell_ function enables the client to open a shell
304316
in the shell host.
305317

306-
Those service could - and should - be disabled when they are not needed. The
307-
options [exec](`t:ssh:exec_daemon_option/0`) and
308-
[shell](`t:ssh:shell_daemon_option/0`) are enabled per default but could be set
309-
to `disabled` if not needed. The same options could also install handlers for
318+
The options [exec](`t:ssh:exec_daemon_option/0`) and
319+
[shell](`t:ssh:shell_daemon_option/0`) are disabled per default.
320+
The same options could also install handlers for
310321
the string(s) passed from the client to the server.
311322

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+
312341
### The id string
313342

314343
One way to reduce the risk of intrusion is to not convey which software and
@@ -333,7 +362,7 @@ This brand and version may be changed with the option
333362
option:
334363

335364
```erlang
336-
ssh:daemon(1234, [{id_string,"hi there"}, ... ]).
365+
ssh:daemon({192, 168, 1, 10}, 1234, [{id_string,"hi there"}, ... ]).
337366
```
338367

339368
and the daemon will present itself as:
@@ -358,6 +387,10 @@ The negotiation (session setup time) time can be limited with the _parameter_
358387

359388
## SFTP Security
360389

390+
Note that the SFTP server runs with the file access rights of the OS
391+
process running the Erlang emulator, regardless of the authenticated
392+
SSH user. See the [Terminology](terminology.md) section for details.
393+
361394
### Root Directory Isolation
362395

363396
The `root` option (see `m:ssh_sftpd`) restricts SFTP users to a
@@ -380,3 +413,82 @@ using OS-level mechanisms (PAM chroot, containers, file permissions).
380413
**Defense-in-depth:** For high-security deployments, combine the `root` option
381414
with OS-level isolation mechanisms such as chroot jails, containers, or
382415
mandatory 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+

lib/ssh/doc/guides/introduction.md

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,9 @@ The `ssh` application is an implementation of the SSH Transport, Connection and
3434
Authentication Layer Protocols in Erlang. It provides the following:
3535

3636
- API functions to write customized SSH clients and servers applications
37-
- The Erlang shell available over SSH
38-
- An SFTP client (`m:ssh_sftp`) and server (`m:ssh_sftpd`)
37+
- The Erlang shell available over SSH, enabled via the `shell` daemon option
38+
- An SFTP client (`m:ssh_sftp`) and server (`m:ssh_sftpd`), enabled via the
39+
`subsystems` daemon option
3940

4041
## Prerequisites
4142

@@ -114,15 +115,16 @@ the application logic.
114115
Channels come in the following three flavors:
115116

116117
- _Subsystem_ \- Named services that can be run as part of an SSH server, such
117-
as SFTP [(ssh_sftpd)](`m:ssh_sftpd`), that is built into the SSH daemon
118-
(server) by default, but it can be disabled. The Erlang `ssh` daemon can be
119-
configured to run any Erlang- implemented SSH subsystem.
120-
- _Shell_ \- Interactive shell. By default the Erlang daemon runs the Erlang
121-
shell. The shell can be customized by providing your own read-eval-print loop.
118+
as SFTP [(ssh_sftpd)](`m:ssh_sftpd`). No subsystems are enabled by default.
119+
The Erlang `ssh` daemon can be configured to run any Erlang-implemented SSH
120+
subsystem.
121+
- _Shell_ \- Interactive shell. By default the Erlang daemon does not expose the Erlang
122+
shell. It can be enabled with option `{shell, {shell, start, []}}`.
123+
The shell can be customized by providing your own read-eval-print loop.
122124
You can also provide your own Command-Line Interface (CLI) implementation, but
123125
that is much more work.
124-
- _Exec_ \- One-time remote execution of commands. See function
125-
`ssh_connection:exec/4` for more information.
126+
- _Exec_ \- By default one-time remote execution of commands is disabled.
127+
See function `ssh_connection:exec/4` for more information.
126128

127129
## Where to Find More Information
128130

lib/ssh/doc/guides/terminology.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -157,8 +157,9 @@ the server's emulator. The rights in that shell is independent of the just
157157
authenticated user.
158158

159159
In case of an sftp request, an sftp server is started with the rights of the
160-
user of the Erlang emulator's OS process. So with sftp the authenticated user
161-
does not influence the rights.
160+
user of the Erlang emulator's OS process, provided the SFTP subsystem is
161+
enabled. So with sftp, the file access rights are those of the OS process
162+
running the Erlang emulator, regardless of the authenticated SSH user.
162163

163164
So after an authentication, the user name is not used anymore and has no
164165
influence.

0 commit comments

Comments
 (0)