1313
1414import os
1515import subprocess
16+ import sys
1617import tempfile
1718
1819from awscli .customizations .emr import constants , emrutils , sshutils
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
2890class 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