Skip to content

Commit 458acc4

Browse files
authored
feat(emr): Improve SSH host key verification with accept-new (#10391)
EMR SSH/SCP helper commands (ssh, socks, put, get) now default to StrictHostKeyChecking=accept-new for improved host key verification. Uses ssh -G probe to detect support; falls back to =no with warning on OpenSSH < 7.6. New --ssh-options parameter allows passing arbitrary SSH options (space-separated) to override defaults. This provides an escape hatch for legacy clients and addresses #1799.
1 parent 64531d1 commit 458acc4

3 files changed

Lines changed: 297 additions & 56 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"type": "enhancement",
3+
"category": "emr",
4+
"description": "EMR SSH/SCP helper commands (``aws emr ssh``, ``socks``, ``put``, ``get``) now default to ``StrictHostKeyChecking=accept-new`` for improved host key verification security. A new ``--ssh-options`` parameter allows passing arbitrary SSH options to override defaults. On systems with OpenSSH < 7.6, the CLI automatically falls back to the previous behavior with a warning."
5+
}

awscli/customizations/emr/ssh.py

Lines changed: 112 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
import os
1515
import subprocess
16+
import sys
1617
import tempfile
1718

1819
from awscli.customizations.emr import constants, emrutils, sshutils
@@ -24,6 +25,67 @@
2425
'"aws configure set emr.key_pair_file <value>" command.\n'
2526
)
2627

28+
SSH_OPTIONS_HELP_TEXT = (
29+
'Additional SSH options passed directly to the ssh/scp command. '
30+
'Multiple options can be specified space-separated. Example: '
31+
'--ssh-options StrictHostKeyChecking=no ConnectTimeout=30'
32+
)
33+
34+
DEFAULT_STRICT_HOST_KEY_CHECKING = 'StrictHostKeyChecking=accept-new'
35+
36+
UNSUPPORTED_OPTION_MSG = (
37+
'WARNING: Your OpenSSH version does not support '
38+
'StrictHostKeyChecking=accept-new (requires OpenSSH 7.6+). '
39+
'Falling back to StrictHostKeyChecking=no. '
40+
'Upgrade to OpenSSH 7.6+ for improved security.\n'
41+
)
42+
43+
PUTTY_SSH_OPTIONS_MSG = (
44+
'WARNING: --ssh-options is only supported with OpenSSH. '
45+
'Options are ignored when using PuTTY/pscp.\n'
46+
)
47+
48+
49+
def _supports_accept_new():
50+
"""Check if OpenSSH supports StrictHostKeyChecking=accept-new."""
51+
try:
52+
result = subprocess.run(
53+
['ssh', '-G', '-o', 'StrictHostKeyChecking=accept-new',
54+
'localhost'],
55+
capture_output=True, text=True,
56+
)
57+
return result.returncode == 0
58+
except (OSError, subprocess.SubprocessError):
59+
return False
60+
61+
62+
def _has_strict_host_key_override(extra_options):
63+
"""Check if user provided a StrictHostKeyChecking override."""
64+
if not extra_options:
65+
return False
66+
for opt in extra_options:
67+
if opt.lower().startswith('stricthostkeychecking='):
68+
return True
69+
return False
70+
71+
72+
def _build_ssh_options(extra_options):
73+
"""Build the -o flags list for ssh/scp commands."""
74+
options = []
75+
if _has_strict_host_key_override(extra_options):
76+
for opt in extra_options:
77+
options.extend(['-o', opt])
78+
else:
79+
if _supports_accept_new():
80+
options.extend(['-o', DEFAULT_STRICT_HOST_KEY_CHECKING])
81+
else:
82+
sys.stderr.write(UNSUPPORTED_OPTION_MSG)
83+
options.extend(['-o', 'StrictHostKeyChecking=no'])
84+
if extra_options:
85+
for opt in extra_options:
86+
options.extend(['-o', opt])
87+
return options
88+
2789

2890
class Socks(Command):
2991
NAME = 'socks'
@@ -42,6 +104,11 @@ class Socks(Command):
42104
'required': True,
43105
'help_text': 'Private key file to use for login',
44106
},
107+
{
108+
'name': 'ssh-options',
109+
'nargs': '+',
110+
'help_text': SSH_OPTIONS_HELP_TEXT,
111+
},
45112
]
46113

47114
def _run_main_command(self, parsed_args, parsed_globals):
@@ -56,28 +123,20 @@ def _run_main_command(self, parsed_args, parsed_globals):
56123
sshutils.validate_ssh_with_key_file(key_file)
57124
f = tempfile.NamedTemporaryFile(delete=False)
58125
if emrutils.which('ssh') or emrutils.which('ssh.exe'):
59-
command = [
60-
'ssh',
61-
'-o',
62-
'StrictHostKeyChecking=no',
63-
'-o',
64-
'ServerAliveInterval=10',
65-
'-ND',
66-
'8157',
67-
'-i',
68-
parsed_args.key_pair_file,
126+
ssh_options = _build_ssh_options(parsed_args.ssh_options)
127+
command = ['ssh'] + ssh_options + [
128+
'-o', 'ServerAliveInterval=10',
129+
'-ND', '8157',
130+
'-i', parsed_args.key_pair_file,
69131
constants.SSH_USER + '@' + master_dns,
70132
]
71133
else:
134+
if parsed_args.ssh_options:
135+
sys.stderr.write(PUTTY_SSH_OPTIONS_MSG)
72136
command = [
73-
'putty',
74-
'-ssh',
75-
'-i',
76-
parsed_args.key_pair_file,
137+
'putty', '-ssh', '-i', parsed_args.key_pair_file,
77138
constants.SSH_USER + '@' + master_dns,
78-
'-N',
79-
'-D',
80-
'8157',
139+
'-N', '-D', '8157',
81140
]
82141

83142
print(' '.join(command))
@@ -105,6 +164,11 @@ class SSH(Command):
105164
'help_text': 'Private key file to use for login',
106165
},
107166
{'name': 'command', 'help_text': 'Command to execute on Master Node'},
167+
{
168+
'name': 'ssh-options',
169+
'nargs': '+',
170+
'help_text': SSH_OPTIONS_HELP_TEXT,
171+
},
108172
]
109173

110174
def _run_main_command(self, parsed_args, parsed_globals):
@@ -118,27 +182,21 @@ def _run_main_command(self, parsed_args, parsed_globals):
118182
sshutils.validate_ssh_with_key_file(key_file)
119183
f = tempfile.NamedTemporaryFile(delete=False)
120184
if emrutils.which('ssh') or emrutils.which('ssh.exe'):
121-
command = [
122-
'ssh',
123-
'-o',
124-
'StrictHostKeyChecking=no',
125-
'-o',
126-
'ServerAliveInterval=10',
127-
'-i',
128-
parsed_args.key_pair_file,
185+
ssh_options = _build_ssh_options(parsed_args.ssh_options)
186+
command = ['ssh'] + ssh_options + [
187+
'-o', 'ServerAliveInterval=10',
188+
'-i', parsed_args.key_pair_file,
129189
constants.SSH_USER + '@' + master_dns,
130190
'-t',
131191
]
132192
if parsed_args.command:
133193
command.append(parsed_args.command)
134194
else:
195+
if parsed_args.ssh_options:
196+
sys.stderr.write(PUTTY_SSH_OPTIONS_MSG)
135197
command = [
136-
'putty',
137-
'-ssh',
138-
'-i',
139-
parsed_args.key_pair_file,
140-
constants.SSH_USER + '@' + master_dns,
141-
'-t',
198+
'putty', '-ssh', '-i', parsed_args.key_pair_file,
199+
constants.SSH_USER + '@' + master_dns, '-t',
142200
]
143201
if parsed_args.command:
144202
f.write(parsed_args.command)
@@ -175,6 +233,11 @@ class Put(Command):
175233
'help_text': 'Source file path on local machine',
176234
},
177235
{'name': 'dest', 'help_text': 'Destination file path on remote host'},
236+
{
237+
'name': 'ssh-options',
238+
'nargs': '+',
239+
'help_text': SSH_OPTIONS_HELP_TEXT,
240+
},
178241
]
179242

180243
def _run_main_command(self, parsed_args, parsed_globals):
@@ -187,27 +250,20 @@ def _run_main_command(self, parsed_args, parsed_globals):
187250
key_file = parsed_args.key_pair_file
188251
sshutils.validate_scp_with_key_file(key_file)
189252
if emrutils.which('scp') or emrutils.which('scp.exe'):
190-
command = [
191-
'scp',
192-
'-r',
193-
'-o StrictHostKeyChecking=no',
194-
'-i',
195-
parsed_args.key_pair_file,
253+
ssh_options = _build_ssh_options(parsed_args.ssh_options)
254+
command = ['scp', '-r'] + ssh_options + [
255+
'-i', parsed_args.key_pair_file,
196256
parsed_args.src,
197257
constants.SSH_USER + '@' + master_dns,
198258
]
199259
else:
260+
if parsed_args.ssh_options:
261+
sys.stderr.write(PUTTY_SSH_OPTIONS_MSG)
200262
command = [
201-
'pscp',
202-
'-scp',
203-
'-r',
204-
'-i',
205-
parsed_args.key_pair_file,
206-
parsed_args.src,
207-
constants.SSH_USER + '@' + master_dns,
263+
'pscp', '-scp', '-r', '-i', parsed_args.key_pair_file,
264+
parsed_args.src, constants.SSH_USER + '@' + master_dns,
208265
]
209266

210-
# if the instance is not terminated
211267
if parsed_args.dest:
212268
command[-1] = command[-1] + ":" + parsed_args.dest
213269
else:
@@ -237,6 +293,11 @@ class Get(Command):
237293
'help_text': 'Source file path on remote host',
238294
},
239295
{'name': 'dest', 'help_text': 'Destination file path on your machine'},
296+
{
297+
'name': 'ssh-options',
298+
'nargs': '+',
299+
'help_text': SSH_OPTIONS_HELP_TEXT,
300+
},
240301
]
241302

242303
def _run_main_command(self, parsed_args, parsed_globals):
@@ -249,21 +310,16 @@ def _run_main_command(self, parsed_args, parsed_globals):
249310
key_file = parsed_args.key_pair_file
250311
sshutils.validate_scp_with_key_file(key_file)
251312
if emrutils.which('scp') or emrutils.which('scp.exe'):
252-
command = [
253-
'scp',
254-
'-r',
255-
'-o StrictHostKeyChecking=no',
256-
'-i',
257-
parsed_args.key_pair_file,
313+
ssh_options = _build_ssh_options(parsed_args.ssh_options)
314+
command = ['scp', '-r'] + ssh_options + [
315+
'-i', parsed_args.key_pair_file,
258316
constants.SSH_USER + '@' + master_dns + ':' + parsed_args.src,
259317
]
260318
else:
319+
if parsed_args.ssh_options:
320+
sys.stderr.write(PUTTY_SSH_OPTIONS_MSG)
261321
command = [
262-
'pscp',
263-
'-scp',
264-
'-r',
265-
'-i',
266-
parsed_args.key_pair_file,
322+
'pscp', '-scp', '-r', '-i', parsed_args.key_pair_file,
267323
constants.SSH_USER + '@' + master_dns + ':' + parsed_args.src,
268324
]
269325

0 commit comments

Comments
 (0)