Skip to content

ssh: Change shell and exec defaults to disabled - #10733

Closed
u3s wants to merge 2 commits into
erlang:masterfrom
u3s:kuba/ssh/shell_disabled_by_default/OTP-19969
Closed

ssh: Change shell and exec defaults to disabled#10733
u3s wants to merge 2 commits into
erlang:masterfrom
u3s:kuba/ssh/shell_disabled_by_default/OTP-19969

Conversation

@u3s

@u3s u3s commented Feb 18, 2026

Copy link
Copy Markdown
Contributor

The shell and exec daemon options now default to disabled instead of enabling the Erlang shell and Erlang term evaluation respectively. This improves security by following the "secure by default" principle.

Previously, all authenticated SSH users could execute arbitrary Erlang code via the default shell or exec services. This violated the principle of least privilege and created unnecessary security exposure.

Changes:

  • Remove ?DEFAULT_SHELL macro from ssh.hrl
  • Change shell default from {shell, start, []} to disabled
  • Change exec default from undefined to disabled
  • Allow undefined as explicit value for exec (backward compatibility)
  • Update ssh_cli.erl exec logic for new defaults
  • Update test suites to explicitly enable shell/exec where needed
  • Add shell_disabled/1 test case to verify default behavior
  • Update documentation (hardening.md, introduction.md, using_ssh.md)

Applications requiring shell or exec functionality must now explicitly enable them:

%% Enable Erlang shell
ssh:daemon(Port, [{shell, {shell, start, []}} | Options])

%% Enable Erlang term evaluation via exec
ssh:daemon(Port, [{exec, undefined} | Options])

SFTP and other subsystems are unaffected by this change.

@u3s u3s self-assigned this Feb 18, 2026
@u3s u3s added the team:PS Assigned to OTP team PS label Feb 18, 2026
@github-actions

github-actions Bot commented Feb 18, 2026

Copy link
Copy Markdown
Contributor

CT Test Results

    3 files     92 suites   1h 36m 28s ⏱️
2 190 tests 1 768 ✅ 419 💤 3 ❌
3 641 runs  3 137 ✅ 501 💤 3 ❌

For more details on these failures, see this check.

Results for commit 3dbfc21.

♻️ This comment has been updated with latest results.

To speed up review, make sure that you have read Contributing to Erlang/OTP and that all checks pass.

See the TESTING and DEVELOPMENT HowTo guides for details about how to run test locally.

Artifacts

// Erlang/OTP Github Action Bot

@u3s
u3s requested a review from IngelaAndin February 18, 2026 18:38
@u3s u3s added the testing currently being tested, tag is used by OTP internal CI label Feb 18, 2026
@u3s
u3s force-pushed the kuba/ssh/shell_disabled_by_default/OTP-19969 branch from 8a3ec45 to 51d5259 Compare February 19, 2026 11:55
@u3s
u3s requested review from Mikaka27 and removed request for Mikaka27 February 20, 2026 19:31

@Mikaka27 Mikaka27 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor formatting problems and one question.

Comment thread lib/ssh/test/ssh_basic_SUITE.erl Outdated
Comment thread lib/ssh/test/ssh_basic_SUITE.erl Outdated
Comment thread lib/ssh/test/ssh_basic_SUITE.erl Outdated
Comment thread lib/ssh/test/ssh_basic_SUITE.erl Outdated
Comment thread lib/ssh/test/ssh_basic_SUITE.erl Outdated
Comment thread lib/ssh/test/ssh_connection_SUITE.erl Outdated
Comment thread lib/ssh/test/ssh_connection_SUITE.erl
Comment thread lib/ssh/test/ssh_connection_SUITE.erl Outdated
Comment thread lib/ssh/test/ssh_options_SUITE.erl
Comment thread lib/ssh/test/ssh_basic_SUITE.erl Outdated
@u3s
u3s force-pushed the kuba/ssh/shell_disabled_by_default/OTP-19969 branch from 51d5259 to 954d2cf Compare February 23, 2026 16:06
@u3s
u3s requested a review from Mikaka27 February 23, 2026 16:06
Mikaka27
Mikaka27 previously approved these changes Feb 24, 2026
@u3s u3s removed the testing currently being tested, tag is used by OTP internal CI label Mar 20, 2026
Change the shell and exec daemon options to default to disabled
instead of enabling the Erlang shell and Erlang term evaluation
respectively. This implements the "secure by default" principle,
preventing authenticated users from executing arbitrary Erlang code
unless explicitly configured.

The new erlang_eval exec option enables Erlang term evaluation via
exec requests, replacing the previous implicit behavior.

Applications requiring these services must now explicitly enable them:

  %% Enable Erlang shell
  ssh:daemon(Port, [{shell, {shell, start, []}} | Options])

  %% Enable Erlang term evaluation via exec
  ssh:daemon(Port, [{exec, erlang_eval} | Options])

Also fix a client-side crash in ssh:shell/1 where the missing exec
key in options caused channel_cb_init_args/1 to append undefined to
the callback init args. Both erlang_eval and undefined are now
filtered out, as neither is a callback init argument.
@u3s
u3s force-pushed the kuba/ssh/shell_disabled_by_default/OTP-19969 branch from 954d2cf to dda3f20 Compare April 3, 2026 14:22
@u3s u3s added the testing currently being tested, tag is used by OTP internal CI label Apr 3, 2026
@u3s
u3s force-pushed the kuba/ssh/shell_disabled_by_default/OTP-19969 branch 2 times, most recently from ff8b09f to b7ed446 Compare April 3, 2026 15:34
@u3s
u3s requested a review from Mikaka27 April 3, 2026 15:35
The subsystems daemon option now defaults to an empty list instead
of enabling the SFTP subsystem via ssh_sftpd:subsystem_spec([]).
This extends the "secure by default" principle to also cover SFTP,
complementing the shell and exec default changes.

Previously, all authenticated SSH users had access to the entire
file system visible to the Erlang VM process through the default
SFTP subsystem, with no path restrictions.

Changes:
- Change subsystems default from [ssh_sftpd:subsystem_spec([])] to []
- Update subsystem_spec doc string in ssh.hrl
- Remove dead code in ssh_connection:check_subsystem/2
- Update test suites to explicitly enable SFTP where needed
- Update documentation (hardening.md, introduction.md, using_ssh.md,
  ssh_app.md, terminology.md)

Applications requiring SFTP must now explicitly enable it:

  ssh:daemon(Port, [{subsystems, [ssh_sftpd:subsystem_spec([])]}
                    | Options])
@u3s
u3s force-pushed the kuba/ssh/shell_disabled_by_default/OTP-19969 branch from b7ed446 to 3dbfc21 Compare April 3, 2026 16:04
@u3s
u3s removed request for IngelaAndin and Mikaka27 April 3, 2026 18:49

@Mikaka27 Mikaka27 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Besides the added comments change looks ok me. It would be great if the comment in the PR description was updated to reflect the current state of changes before/after this is merged.

Comment thread lib/ssh/src/ssh_cli.erl
deduced_encoding,
group,
shell,
exec = erlang_eval,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why have this value as default if exec is disabled by default?

Comment on lines +2098 to +2142
replace_options_enable_services(Config) when is_list(Config) ->
PrivDir = proplists:get_value(priv_dir, Config),
UserDir = filename:join(PrivDir, nopubkey),
file:make_dir(UserDir),
SysDir = proplists:get_value(data_dir, Config),
{Pid, Host, Port} = ssh_test_lib:daemon([{system_dir, SysDir},
{user_dir, UserDir},
{password, "morot"}]),
ConnOpts = [{silently_accept_hosts, true},
{user, "foo"},
{password, "morot"},
{user_dir, UserDir}],

%% Verify all services disabled before replace
C1 = ssh_test_lib:connect(Host, Port, ConnOpts),
?CT_LOG("Checking shell is disabled before replace", []),
test_shell_is_disabled(C1),
?CT_LOG("Checking exec is disabled before replace", []),
test_exec_is_disabled(C1),
?CT_LOG("Checking SFTP is unavailable before replace", []),
{error, _} = ssh_sftp:start_channel(C1),
?CT_LOG("All services confirmed disabled", []),
ssh:close(C1),

%% Enable shell, exec and SFTP
?CT_LOG("Replacing options: enabling shell, exec and SFTP", []),
{ok, Pid} = ssh:daemon_replace_options(Pid,
[{shell, {shell, start, []}},
{exec, erlang_eval},
{subsystems, [ssh_sftpd:subsystem_spec([])]}]),

%% Verify all services work after replace
C2 = ssh_test_lib:connect(Host, Port, ConnOpts),
?CT_LOG("Checking shell is enabled after replace", []),
test_shell_is_enabled(C2),
?CT_LOG("Checking exec is enabled after replace", []),
test_exec_is_enabled(C2),
?CT_LOG("Checking SFTP is available after replace", []),
{ok, SftpPid} = ssh_sftp:start_channel(C2),
ssh_sftp:stop_channel(SftpPid),
?CT_LOG("All services confirmed enabled after replace", []),

ssh:close(C2),
ssh:stop_daemon(Pid).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test fails on my machine (line 2114) with following error:

ssh_connection_SUITE:2075 Got unexpected {#Ref<0.1007350121.354418689.14699>,
                                          #{meta =>
                                             #{error_logger =>
                                                #{tag => info_report,
                                                  type => progress,
                                                  report_cb =>
                                                   fun supervisor:format_log/1},
                                               line => 2323,
                                               pid => <0.18567.0>,
                                               time => 1775469388246684,
                                               file => "supervisor.erl",
                                               gl => <0.18564.0>,
                                               domain => [otp,sasl],
                                               logger_formatter =>
                                                #{title => "PROGRESS REPORT"},
                                               mfa =>
                                                {supervisor,report_progress,3},
                                               report_cb =>
                                                fun supervisor:format_log/2},
                                            msg =>
                                             {report,
                                              #{label => {supervisor,progress},
                                                report =>
                                                 [{supervisor,
                                                   {local,sshd_sup}},
                                                  {started,
                                                   [{pid,<0.18568.0>},
                                                    {id,ssh_lsocket_sup},
                                                    {mfargs,
                                                     {ssh_lsocket_sup,
                                                      start_link,[]}},
                                                    {restart_type,permanent},
                                                    {significant,false},
                                                    {shutdown,infinity},
                                                    {child_type,
                                                     supervisor}]}]}},
                                            level => info}}
Expect: {ssh_cm,<0.18575.0>,{data,0,'0|1',<<"Prohibited.">>}}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should be fixed in other PR, once I push

@u3s u3s removed the testing currently being tested, tag is used by OTP internal CI label Apr 7, 2026
@u3s

u3s commented Apr 7, 2026

Copy link
Copy Markdown
Contributor Author

close in favor of PR-10970

@u3s u3s closed this Apr 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

team:PS Assigned to OTP team PS

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants