我想使用ssh
在 上运行的 bash 脚本中的命令srcHost
来远程执行一系列主机(targetHostX
)上的命令。其中一些远程主机只能通过跳转主机(jumpHost
)访问,而其他远程主机可以直接从 访问srcHost
。SSH 密钥身份验证已到位,密钥srcHost:~/id_rsa
被 和 接受usr@jumpHost
,usr@targetHostX
并且整个过程必须以非交互方式进行,即没有密码提示。这意味着,我无法使用-J
(ProxyJump) 选项,因为它不允许为跳转主机指定 SSH 密钥。
确认可以正常工作的基本命令行如下(例如远程命令:主机名):
usr@srcHost:~$ ssh -i ~/id_rsa -o ProxyCommand="ssh -i ~/id_rsa -W %h:%p usr@jumpHost" usr@targetHostX hostname
在我的 bash 脚本中,我有一个关联数组来定义某些 targetHosts 的跳转主机,我想根据是否定义了跳转主机来编写 SSH 命令行。稍微简化一下,它看起来像这样:
user="usr"
targethost="targetHostX"
remotecommand="hostname"
jumphost="${jumphosts[$targethost]}"
param=""
[ -n "$jumphost" ] && param="-o ProxyCommand=\"ssh -W $targethost:22 $jumphost\""
ssh $param -l $user $targethost "$remotecommand"
当我执行这个(看看set -x
发生了什么)时,我得到:
+ '[' -n jumpHost ']'
+ param='-o ProxyCommand="ssh -W targetHostX:22 jumpHost"'
+ ssh -o 'ProxyCommand="ssh' -W targetHostX:22 'jumpHost"' -l usr targetHostX hostname
/bin/bash: -c: line 0: unexpected EOF while looking for matching `"'
/bin/bash: -c: line 1: syntax error: unexpected end of file
write: Broken pipe
我认为这肯定是一些模糊的引用问题,就像 bash 经常出现的情况一样,我尝试了各种不同的引用样式,包括将命令组合成数组 - 但都无济于事。作为一种解决方法,我现在已经在两个 if 分支中输入了两个完整的 ssh 命令行(整个命令行比这里作为最小示例显示的长度要长),虽然这有效,但既不漂亮也不易于维护。另外,我想了解发生了什么。
然后我注意到,错误只发生在命令上ssh
!如果我用一个只回显其命令行的小脚本替换 ssh,我会得到:
+ '[' -n jumpHost ']'
+ param='-o ProxyCommand="ssh -W targetHostX:22 jumpHost"'
+ ./command.sh -o 'ProxyCommand="ssh' -W targetHostX:22 'jumpHost"' -l usr targetHostX ''
Commandline: ./command.sh -o ProxyCommand="ssh -W targetHostX:22 jumpHost" -l usr targetHostX
没有错误,并且./command.sh
按预期执行。
我对此有点困惑,这里有人能解释一下这个问题吗?我猜我是从 那里得到 bash 错误的jumpHost
,所以也许我必须更改里面的引用ProxyCommand
- 但该怎么做呢?
谢谢!
答案1
分析
该问题有点是由于错误的引用造成的,但你不能通过更好的引用来真正解决这个问题。
为了了解发生了什么,让我们首先分析一下起作用的命令:
ssh -i ~/id_rsa -o ProxyCommand="ssh -i ~/id_rsa -W %h:%p usr@jumpHost" usr@targetHostX hostname
shell 将其拆分为单词,扩展未加引号的部分~
并删除引号。最后ssh
得到以下参数:
-i
/home/…/id_rsa
-o
ProxyCommand=ssh -i ~/id_rsa -W %h:%p usr@jumpHost
usr@targetHostX
hostname
ProxyCommand=
您在消失后使用的双引号。ssh
看不到引号。它们的作用是告诉 shell 不要将您想要的代理命令拆分为单独的单词(它们还阻止了第二个波浪号)。这是完全正确的。看到 后-o
,ssh
期望,在下一个参数的开头Option=value
检测并使用该参数的其余部分作为值,即代理命令。ProxyCommand=
但是,在脚本中存在这样的情况:
ssh $param …
(其中…
表示与问题无关的论点)。一般来说你应该用双引号引起变量。我知道你为什么不使用双引号$param
。你想$param
扩展为多个单词。完成此任务后:
param="-o ProxyCommand=\"ssh -W $targethost:22 $jumphost\""
“正确”引用$param
(即"$param"
)将扩展为一个单词:
-o ProxyCommand="ssh -W …:22 …"
(其中…
表示$targethost
和$jumphost
扩展为什么;注意,它们是在分配给 期间扩展的param
,而不是在 的扩展期间"$param"
)。ssh
不会以单一参数的形式理解它。
您未引用的$param
确实扩展为多个参数;第一个是-o
(到目前为止一切都很好),但是......这些是参数:
-o
ProxyCommand="ssh
-W
…:22
从先前的扩展$targethost:22
…"
从先前的扩展$jumphost\"
- 与问题无关的后续论点
事实上set -x
已经给了你同样的列表:
+ ssh -o 'ProxyCommand="ssh' -W targetHostX:22 'jumpHost"' -l usr targetHostX hostname
它在必要时使用单引号来使其输出无歧义,但您需要注意并知道如何在输出中解释这些单引号。这里的-o
意思是ssh
got -o
,但'ProxyCommand="ssh'
意思是ssh
got ProxyCommand="ssh
。
如果先前的扩展$targethost
或$jumphost
将空格,制表符或换行符带入param
,则我使用的上述列表中的一个或两个参数…
实际上将是多个参数。
我猜你在 的值中嵌入了双引号,$param
以保护其中的某些部分不被未加引号的 分割$param
。这不起作用。变量内的引号对 shell 来说并不特殊。你嵌入的引号不仅无法防止分割;它们也没有消失。ssh
看到了-o
,预期了Option=value
,得到了ProxyCommand="ssh
。它得到的结果看起来像Option=value
,所以ssh
它本身没有抱怨。不过,当它试图value
通过将 as shell 代码传递给 来运行它时/bin/bash -c
,实际值实际上是:
"ssh
这不是有效的 shell 代码。因此出现错误:
/bin/bash: -c: line 0: unexpected EOF while looking for matching `"'
解决方案
您可以使用 egIFS=#
使 shell$param
在您想要的位置准确拆分引号;加上set -o noglob
以防万一。或者您可以使用eval "ssh $param …"
并重新评估 的扩展ssh $param …
,因此嵌入其中的引号$param
在某些时候会变得特殊。每种解决方法都要求您仔细设计 的值$param
以及随附的 shell 代码和变量;如果代码和值是静态的并且您知道自己在做什么,则每种方法都可能有效。另一方面,如果一个人足够熟悉 Bash 以知道他或她将在这里使用IFS
或做什么eval
,那么他可能知道正确的方法。
正确的方法是使用数组。即使在sh
那里,"$@"
它的行为也像数组一样。你的问题被标记为狂欢并且您已经在代码中使用了一个数组("${jumphosts[$targethost]}"
,关联数组),因此您当然可以使用另一个数组。名称可能是param
。
(更多内容请点击这里:我们如何运行存储在变量中的命令?)
您的代码已修复:
user="usr"
targethost="targetHostX"
remotecommand="hostname"
jumphost="${jumphosts[$targethost]}"
param=()
[ -n "$jumphost" ] && param=(-o "ProxyCommand=ssh -W $targethost:22 $jumphost")
ssh "${param[@]}" -l "$user" "$targethost" "$remotecommand"
param=()
定义一个空数组。如果数组保持为空,"${param[@]}"
则将扩展为零个单词。整个过程将按预期进行……
…除非$targethost
或$jumphost
不幸或不完全受您控制。当ssh
将值传递给ProxyCommand=
时/bin/bash -c
,已经扩展的$targethost
和$jumphost
将只是解释为 shell 代码的字符串的子字符串。例如,如果攻击者设法扩展"${jumphosts[$targethost]}"
为,; rm -f /important/file;
那么您的代理命令将执行rm -f /important/file
。如果这是一个问题,请考虑:
[ -n "$jumphost" ] && param=(-o "ProxyCommand=ssh -W ${targethost@Q}:22 ${jumphost@Q}")
Bash 解释脚本时会将${targethost@Q}
和替换${jumphost@Q}
为相应的值(如果需要,会加引号或转义)。实际上,当整个字符串被解释为 shell 代码(通过/bin/bash -c
调用ssh
)时,来自变量的任何内容都无法注入代码。
关于什么echo
然后我注意到,错误只发生在
ssh
命令上!如果我用一个只回显其命令行的小脚本替换 ssh,我就会得到 [无错误]。"
echo
并不是一个适合进行此类调试的好工具,因为它可以接受任何内容,但实际上并不会告诉您它获得了哪些确切的参数。它会连接其参数,并在其间插入空格。从输出中您无法分辨哪些空格来自某个参数,哪些来自echo
。例如,所有这些命令都会打印相同的输出:
echo -o ProxyCommand=ssh …
echo -o "ProxyCommand=ssh …"
echo "-o ProxyCommand=ssh …"
但是如果你替换echo
,ssh
那么显然生成的命令将不等效。
我个人使用printf '<%s>\n'
。逐一尝试这些:
printf '<%s>\n' -o ProxyCommand=ssh …
printf '<%s>\n' -o "ProxyCommand=ssh …"
printf '<%s>\n' "-o ProxyCommand=ssh …"
>(zero or more spaces or tabs)(newline)<
包含或回车符(或我的终端将解释的转义序列,或...)的参数仍然可以欺骗我,尽管如此,这种方法比...好得多echo
。