这编写安全的shell脚本
13、设置权限
你希望以安全的方式设置权限。
如果出于安全原因需要设置确切的权限(或者说你确定不在乎先前的权限设定,就是要对其进行改动),可以使用带有 4 位八进制数模式的 chmod。
bash
chmod 0755 some_script
如果只是想添加或删除权限,同时还要保留原先设置的权限,可以在符号模式中使用 + 或 - 操作。
bash
chmod +x some_script
如果试图使用类似于 chmod -R 0644 some_directory 的命令递归设置目录中所有文件的权限,那你会后悔的,因为你将所有的子目录都设置成了不可执行,这意味着无法访问目录中的内容,无法使用 cd 进入,也无法遍历其中。可以使用 find 和 xargs 配合 chmod 来分别设置文件和目录的权限。对于文件:
bash
find some_directory -type f -print0 | xargs -0 chmod 0644
对于目录:
bash
find some_directory -type d -print0 | xargs -0 chmod 0755
当然了,如果只是想设置单个目录中所有文件的权限(非递归设置),切换到该目录并设置即可。
创建目录时使用 mkdir -m mode new_directory,这样不仅可以用一个命令完成两项任务,还能够避免创建目录和设置权限之间可能存在的竞争条件。
很多人习惯使用 3 位八进制数模式,但我们喜欢将 4 位八进制数全部写出来,以明确所有的权限。在可能的情况下,我们也偏好使用八进制模式,因为它能够清晰地表达出要设置的权限。你也可以使用符号模式中的绝对赋权操作(=),但作为传统主义者,我们还是最爱老派的八进制模式。
使用符号模式的 + 或 - 操作时,确保最终的权限比较麻烦,因为这种设置方式是相对的,而非绝对的。问题是,在很多情况下,你无法使用八进制模式将现有权限替换了之。此时你只能指靠符号模式,在不影响现有权限的同时用 + 添加权限。具体细节请查询所在系统的 chmod,并核实结果是否符合预期。以下是几个例子。
bash
$ ls -l
-rw-r--r--1 jp users 0 Dec 1 02:09 script.sh
$
# 使用八进制写法令文件对属主可读、可写、可执行
$ chmod 0700 script.sh
$ ls -l
-rwx------1 jp users 0 Dec 1 02:09 script.sh
# 使用符号写法令文件对所有人可读、可执行
$ chmod ugo+rx *.sh
$ ls -l
-rwxr-xr-x 1 jp users 0 Dec 1 02:09 script.sh
注意,在最后的示例中,虽然我们为所有人(ugo)添加了(+)rx 权限,但属主仍旧保留了写入(w)权限。这正是我们想要的效果,也是经常要面对的场景。你有没有发现,这种方式很容易出错,一不小心就会造成不当的权限设置,招惹麻烦。这就是为什么我们喜欢尽可能使用绝对赋权形式的八进制模式,当然,我们也从不会忘记检查命令结果。
14、密码被泄露到进程列表
ps 可能会将你在命令行上输入的密码一清二楚地显示出来。例如:
bash
$ ./cheesy_app -u user -p password &
[1] 13301
$ ps
PID TT STAT TIME COMMAND
5280 p0 S 0:00.08 -bash
9784 p0 R+ 0:00.00 ps
13301 p0 S 0:00.01 /bin/sh ./cheesy_app -u user -p password
尽可能不要在命令行上使用密码。
说真的,别这么干。
如果没有根据要求输入密码,很多提供了 -p 或类似选项的应用程序会提示用户。对于交互式用法,这当然是好事,但在脚本中就未必如此了。也许你想编写一个普通的"包装器"脚本或别名,尝试在命令行上封装密码。遗憾的是,这种做法无济于事,因为命令最终还是要运行并出现在进程列表中。如果命令能从 STDIN 接受密码,则可以像下面这样做。
bash
./bad_app < ~/.hidden/bad_apps_password
虽然还会产生其他问题,但至少不会在进程列表中显示出密码了。
如果还不行,要么换一个程序或给现有的程序打补丁,要么就凑合着用吧。
15、编写setuid或setgid脚本
你认为可以通过设置 shell 脚本的 setuid 或 setgid 位来解决碰到的问题。
使用 Unix 组、文件权限或 sudo 为适合的用户授予完成任务所需要的最小权限。
启用 shell 脚本的 setuid 或 setguid 位会产生更多的问题,尤其安全方面会得不偿失。有些系统(如 Linux)甚至就没把 shell 脚本的 setuid 位当回事,因此,除了安全风险外,创建 setuid shell 脚本反倒会产生不必要的可移植性问题。
setuid root 脚本尤其危险,压根就别考虑。使用 sudo吧。
setuid 和 setgid 应用于目录的含义与应用于可执行文件的含义不同。如果在目录上设置,则会使得在其中新创建的文件或子目录归属于该目录的属主或属组。
注意,可以分别用 test -u 和 test -g 测试是否设置了setuid 和 setgid。
bash
$ mkdir suid_dir sgid_dir
$ touch suid_file sgid_file
$ ls -l
total 4
drwxr-xr-x 2 jp users 512 Dec 9 03:45 sgid_dir
-rw-r--r-- 1 jp users 0 Dec 9 03:45 sgid_file
drwxr-xr-x 2 jp users 512 Dec 9 03:45 suid_dir
-rw-r--r-- 1 jp users 0 Dec 9 03:45 suid_file
$ chmod 4755 suid_dir suid_file
$ chmod 2755 sgid_dir sgid_file
$ ls -l
total 4
drwxr-sr-x 2 jp users 512 Dec 9 03:45 sgid_dir
-rwxr-sr-x 1 jp users 0 Dec 9 03:45 sgid_file
drwsr-xr-x 2 jp users 512 Dec 9 03:45 suid_dir
-rwsr-xr-x 1 jp users 0 Dec 9 03:45 suid_file
$ [ -u suid_dir ] && echo 'Yup, suid' || echo 'Nope, not suid'
Yup, suid
$ [ -u sgid_dir ] && echo 'Yup, suid' || echo 'Nope, not suid'
Nope, not suid
$ [ -g sgid_file ] && echo 'Yup, sgid' || echo 'Nope, not sgid'
Yup, sgid
$ [ -g suid_file ] && echo 'Yup, sgid' || echo 'Nope, not sgid'
Nope, not sgid
16、限制访客
你需要在系统中启用一些访客用户,同时限制这些用户的行为。
尽可能避免使用共享账户,因为如果用户一走了之,你将无从查起,而且会给组织惹来麻烦,还得修改密码并通知其他用户。可以创建独立账户,为其赋予完成任务所需要的最小权限。考虑使用下列方法:
- chroot 囚牢(chroot jail
- SSH,允许非交互式访问命令或资源
受限 shell 的作用是将用户置于移动能力和文件写入能力都受到极大限制的环境之中。通常应用于访客账户。通过将 rbash(如果编译 bash 时加入了该选项)放入用户在/etc/passwd 文件中对应的条目,你可以将该用户的登录shell 设置为受限 shell。
受限 shell 施加的特定限制不允许用户执行以下操作。
- 修改工作目录。cd 命令不可用。如果尝试使用,你会得到错误消息 cd:restricted from bash。
- 将输出重定向到文件。重定向操作符 >、>|、<>、>>均不可用。
- 为环境变量
$ENV、$BASH_ENV、$SHELL、$PATH赋值。 - 指定含有斜线(/)的命令。任何位于当前目录之外的文件都会被 shell 视为"未找到(not found)"。
- 使用内建命令 exec。
- 指定含有 / 的文件名作为内建命令 .(source)的参数。
- 启动时从 shell 环境中导入函数定义。
- 使用内建命令 enable 的 -f 和 -d 选项来添加或删除内建命令。
- 使用内建命令 command 的 -p 选项。
- 使用 set +r 关闭受限模式。
这些限制在用户的 .bash_profile 和环境文件运行之后生效。除此之外,最好将用户的 .bash_profile 和 .bashrc 文件属主更改为 root,同时将这些文件设置为只读。用户的主目录也应该是只读的。
这意味着受限 shell 用户的整个环境都是在 /etc/profile 和.bash_profile 中设置的。因为用户既无法访问/etc/profile,也覆盖不了 .bash_profile,所以只能让系统管理员根据需要配置环境。可以将启动文件中的最后一个命令设置为其他目录(通常是用户主目录下的某个子目录)的 cd 命令,以此多一层保护,这种做法也不错。
设置此类环境的常见方式有两种:建立一个包含安全命令的目录并使其成为 $PATH 中唯一的目录;设置一个命令选单,用户只能从中选择,除非退出 shell。
受限 shell 也抵挡不住铁了心的攻击者。你很难一厢情愿地将用户锁定在特定环境中,因为很多常用软件(如 vi 和 Emacs)具备 shell escape 功能 3,有可能完全绕过受限 shell。
如果善加利用,受限 shell 会成为重要的附加安全层,但不应该将其作为唯一的安全层。
注意,最初的 Bourne shell 有一个叫作 rsh 的受限版本,这可能会和所谓的 r-tools(rsh、rcp、rlogin 等)中的远程 shell 程序(也叫作 rsh)搞混。rsh 极不安全,基本上已经被 SSH(Secure Shell,安全 shell)替代了(我们诚挚地希望如此)。
17、使用chroot囚牢
你不得不使用一个并不信任的脚本或程序。
可以考虑将其放入 chroot 囚牢。chroot 命令能够将当前进程的根目录更改为你所指定的目录,然后返回一个 shell或调用 exec 来执行特定的命令。这就产生了将进程(程序)投入囚牢的效果,从理论上来说,该进程是无法从中逃脱到父目录的。因此,如果程序遭到损坏或者从事恶意 活动,那它也只能影响到所局限于的那一小部分文件系统。如果再以权限极为有限的用户身份运行,这可以成为非常有用的额外安全层。
遗憾的是,涵盖 chroot 方方面面的细节超出了本实例的范围,可能需要单独一本书。这里我们只是想要提高你对相关功能的认识。
那干吗不把一切都放进 chroot 囚牢中运行呢?因为很多程序还得跟文件系统中的其他程序、文件、目录或套接字打交道。这正是使用 chroot 囚牢过程中棘手的地方。由于程序接触不到囚牢以外的世界,所需要的一切都必须存在于囚牢之内。程序越复杂,就越难以在囚牢中运行。
有些应用程序生就属于 Internet,比如 DNS(如BIND)、Web、邮件服务器(如 Postfix),这类应用程序有可能通过难度不一的配置运行于 chroot 囚牢之中。具体细节参见发行版和具体应用程序的文档。
另一种值得留意的 chroot 应用是在系统恢复过程中。从LiveCD 引导并挂载好根文件后,你可能需要运行如 LILO或 GRUB 等工具,根据配置,这类工具需要认为自己的确运行在受损系统上。如果 LiveCD 和安装的系统没有太大不同,通常可以使用 chroot 将受损系统的挂载点设置为根目录并进行修复。该方法是可行的,因为所有的工具、库、配置文件以及设备文件都已经存在于囚牢之内,这已经是一个完整的(如果不算很正常的话)系统了。执行chroot 之后,你可能得检查一下 $PATH,以便查找需要使用的命令(这就是为什么先前要提醒"如果 LiveCD 和安装的系统没有太大不同")。
另外,不妨留意一下美国国家安全局在其安全增强型Linux(security enhanced Linux,SELinux)中所实现的强制性访问控制(mandatory access control,MAC)。MAC 提供了一种非常细化的方式,可以在系统级别指定允许 / 禁止的操作以及系统的各个组件之间如何交互。这种细化的定义称为安全策略,其效果类似于囚牢:特定程序或进程只能执行策略允许的操作。
Red Hat Linux 在其企业版产品中引入了 SELinux。Novell SUSE 中的 MAC 实现称作 AppArmor,Solaris、BSD、macOS 也有类似实现。
18、以非root用户身份运行
你想以非 root 用户身份运行脚本,但又担心有些操作无法完成。
在非 root 用户 ID(要么是你本人,要么是专门的用户)下运行脚本,并以非 root 身份交互式运行,但配置 sudo来处理各种需要提升权限的任务。
无论是交互式用法还是在脚本中使用,sudo 用起来都不难。尤其注意一下 sudoers 的 NOPASSWD 选项。
19、更安全地使用sudo
你想使用 sudo,但又担心给过多的用户授予太多权限。
很好!关注安全是应有之举。虽然用 sudo 要比不用安全太多,但 sudo 的默认设置还有很大的改进空间。
可以花点时间学习一下 sudo 本身和 /etc/sudoers 文件。尤其可以了解一下为什么大多数情况下不应该使用 ALL=(ALL) ALL !不是说不管用,但是远谈不上安全。相较于将 root 密码告诉所有人,唯一的区别是,这里大家并不知道 root 密码,但仍然可以做 root 能做的一切。sudo 会记录下所执行的命令,但这使用 sudo bash 就能轻而易举地避开。
其次,认真思考你的需求。正如不应该使用 ALL=(ALL)ALL,你或许也不应该逐个管理用户。sudoers 允许非常精细的管理,我们强烈建议使用它。man sudoers 提供了大量材料和示例,尤其是有关防止 shell escape 的那部分内容。
第三,sudoers 文件有一个 NOPASSWD 选项,该选项允许用户执行特权操作,无须先输入用户密码,这可能正是你所期望的。这种方式能够实现需要 root 访问特权的自动化操作,同时还不会将明文密码弄得到处都是,不过显然也是有利有弊。
sudoers 允许 4 种别名:user、runas、host、command。审慎地将其作为角色或组可以大大减轻维护负担。例如,你可以为 BUILD_USERS 设置一个User_Alias,然后用 Host_Alias 和 Cmnd_Alias 分别定义用户需要在哪些机器上执行命令以及执行哪些命令。如果设置策略,仅在一台机器上编辑 /etc/sudoers 并定期使用带有公钥认证的 scp 将其复制到所有相关机器,就可以形成一个非常安全且可用的最低特权系统。
当 sudo 要求输入密码时,其实索要的是你的用户账户密码,而不是 root 的。出于某些原因,人们一开始常常对此感到困惑。
遗憾的是,并非所有系统都默认安装了 sudo。Linux、macOS 和 OpenBSD 系统中基本都有,其他系统就不一定了。你可以查询系统文档,如果没有,自行安装即可。
你应该坚持用 visudo 编辑 /etc/sudoers 文件。和 vipw 一样,visudo 会锁定文件,一次只允许一个用户编辑,另外还会在替换正式文件前执行语法检查,以免用户不小心把自己锁在系统之外。
20、在脚本中使用密码
你需要将密码硬编码在脚本中。
这种做法显然不是什么好主意,应该尽可能避免。可惜有时在所难免。
第一种规避的方法是,看看能否使用包含 NOPASSWD 的sudo,这样就不用到处硬编码密码了。不过这本身也有风险,但值得一试。
另一种方法是将 SSH 和公钥及受限命令配合使用。
如果没有其他解决方法,那么最好的做法就是将用户 ID 和密码放进单独的文件,该文件只能由需要的用户读取,然后必要时用 source 命令读入该文件。当然了,这个文件不用进行版本控制。
使用 SSH 安全地访问远程计算机上的数据相对比较容易。甚至还可以使用这种方法访问同一主机上的其他数据,不过使用 sudo 的效率也许会高得多。但是,如何用 SQL 命令访问远程数据库中的数据呢?在这种情况下,你基本上做不了什么。
没错,你可能会说,用 crypt 或者其他密码散列如何?问题是,存储密码的安全方法都涉及使用单向散列(one-way hash)。密码管进不管出。也就是说,对于特定的散列,理论上是无法还原出明文密码的。但明文密码是关键,我们得用它访问数据库或别的地方。因此,安全存储就算了吧。
那剩下的就只有非安全存储了,但这可能比明文还糟糕,因为它带来了一种虚假的安全感。如果你就是喜欢,也确保不会迷信这种所谓的安全,可以动手使用 ROT13 或其他算法来混淆密码。
bash
ROT13=$(echo password | tr 'A-Za-z' 'N-ZA-Mn-za-m')
ROT13 只能处理 ASCII 字符,你也可以使用 ROT47 来处理某些标点符号。
bash
ROT47=$(echo password | tr '!-~' 'P-~!-O')
再强调一次也不为过,ROT13 和 ROT47 就是一种"隐晦式安全",根本谈不上什么安全。当且仅当你(或者你的管理层)没有误认为自己"万无一失"的时候再使用这种方法,毕竟有胜于无嘛。注意,这可是有风险的。话虽如此,有时在现实中还是利大于弊。
21、使用无密码的SSH
你需要在脚本中使用 SSH 或 scp,而且不想使用密码。或者你想将其用于不能有密码的 cron 作业。
无密码的 SSH 有两种使用方式:错误的和正确的。前者是使用未经口令加密的公钥。后者是将受口令保护的公钥与ssh-agent 或 keychain 一起使用。
假设你使用的是 OpenSSH。如果不是,请查询文档(命令和文件类似)。
首先需要创建密钥对(如果还没有的话)。无论配置了多少台机器,只用一对密钥就可以完成认证,但出于个人和工作原因,也许你会使用多对密钥。密钥对由私钥和公钥(*.pub)组成,前者应该不惜一切代价保护好,后者可以随处张贴,只要你喜欢。两者之间以复杂的数学形式相互关联,双方能够识别彼此,但无法从一个推算出另一个。
使用 ssh-keygen(如果使用的不是 OpenSSH,这可能是ssh-keygen2)来创建密钥对。-t 用于指定类型,可取值请查询系统手册页。-b 是可选的,它指定了新密钥的位数(撰写本书之时,RSA 密钥的默认长度是 2048 位)。-C可用于指定注释,如果忽略,则默认为user@hostname。我们推荐使用 -t rsa -b 4096 -Cmeaningful comment,同时强烈反对不使用口令。ssh-keygen 还允许修改密钥文件的口令或注释。
bash
$ ssh-keygen --help
unknown option -- -
usage: ssh-keygen [options]
Options:
-A Generate non-existent host keys for all key types.
-a number Number of KDF rounds for new key format or moduli primality tests.
-B Show bubblebabble digest of key file.
-b bits Number of bits in the key to create.
-C comment Provide new comment.
-c Change comment in private and public key files.
-D pkcs11 Download public key from pkcs11 token.
-e Export OpenSSH to foreign format key file.
-F hostname Find hostname in known hosts file.
-f filename Filename of the key file.
-G file Generate candidates for DH-GEX moduli.
-g Use generic DNS resource record format.
-H Hash names in known_hosts file.
-h Generate host certificate instead of a user certificate.
-I key_id Key identifier to include in certificate.
-i Import foreign format to OpenSSH key file.
-J number Screen this number of moduli lines.
-j number Start screening moduli at specified line.
-K checkpt Write checkpoints to this file.
-k Generate a KRL file.
-L Print the contents of a certificate.
-l Show fingerprint of key file.
-M memory Amount of memory (MB) to use for generating DH-GEX moduli.
-m key_fmt Conversion format for -e/-i (PEM|PKCS8|RFC4716).
-N phrase Provide new passphrase.
-n name,... User/host principal names to include in certificate
-O option Specify a certificate option.
-o Enforce new private key format.
-P phrase Provide old passphrase.
-p Change passphrase of private key file.
-Q Test whether key(s) are revoked in KRL.
-q Quiet.
-R hostname Remove host from known_hosts file.
-r hostname Print DNS resource record.
-S start Start point (hex) for generating DH-GEX moduli.
-s ca_key Certify keys with CA key.
-T file Screen candidates for DH-GEX moduli.
-t type Specify type of key to create.
-u Update KRL rather than creating a new one.
-V from:to Specify certificate validity interval.
-v Verbose.
-W gen Generator to use for generating DH-GEX moduli.
-y Read private key file and print public key.
-Z cipher Specify a cipher for new private key format.
-z serial Specify a serial number.
$ ssh-keygen -v -t rsa -b 4096 -C 'This is my new key'
Generating public/private rsa key pair.
Enter file in which to save the key (/home/jp/.ssh/id_rsa):
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /home/jp/.ssh/id_rsa.
Your public key has been saved in /home/jp/.ssh/id_rsa.pub.
The key fingerprint is:
eb:b3:0b:3a:d8:9f:d0:02:5d:99:ce:69:98:ef:f0:0c This is my new key
The key's randomart image is:
+--[ RSA 4096]----+
| |
| o |
| + |
| . * . |
| . + = S |
| . + . |
| oE + . |
| . oX +. |
| .o* ++ |
+-----------------+
$ $ ls -l ~/.ssh/id_rsa*
-rw------- 1 jp jp 3.3K Aug 27 15:10 /home/jp/.ssh/id_rsa
-rw-r--r-- 1 jp jp 744 Aug 27 15:10 /home/jp/.ssh/id_rsa.pub
$ fold -w75 ~/.ssh/id_rsa.pub
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQCrxvIjPrLxx9VgkE0uBfdiGGZ5KC38OyTB477
MFyw4W7JMDnN5p7Yx8dvl91Fuc13U+RsuBBqWjNvB6hHesdWr/6D2EgoTGJDbegNNla+qb8jJtX
ZK1s+B9sk9SoIlT4AF5wEAMag0K4Jmv0v/xFHwVRm1BfuEQQIVP7Z8v56e7HWz/pZMb0tM89WMg
ITyJh6cuTG1XHRmYxpOoaPBEKeDXTM0mfyAQwO2yQt6fl29RW1DH5J+jVYarsWScGe6SKSYGQPZ
L7a3KRkbpGPRdVK2CY2P1tXQlnh9hPYqvHtAzXUMYJpSwBkNzRN3A571FBtNUxLGtP+xHNEN7Kz
WpUsT1wv6DQw//UDSHJZShVUHMKp414y6dwmKgXTtqVWXYbB/t2EU+CuWk8OkLA2Tv7dKUnn8tA
87D1LU3hAhr58jDEzXbIfl9yYhV2xHBxVUDf80Lv9p9ZKngRx8hkj8MoDr0J6Eql3JhWKRqRdJy
GwKAyjCk5UQ9EH/sQ3NjhJE1Qb31o0dgE3ZKXfm8VXBZS0XTH4OHjd9RA4VCQWjEpdR2QUgeSXW
aM94v3p6O6njKT6fFXV36S33/F/ROc1vZlcJDTpRCbpCXRNkgPtDAImBNmmweaYB0Ym3wqHRB2I
bnw5vftDpptndB774sV2FcRxptkM8Pd/vRS35q56FSgcT6Q== This is my new key
获得密钥对之后,将公钥添加到你想使用该密钥对进行连接的其他机器主目录下的~/.ssh/authorized_keys 文件中。使用 scp、cp(借助软盘或 U 盘),或者干脆在终端会话中复制粘贴,这些方法都可以。重要的是公钥必须是一整行,中间不能出现换行。虽然一个命令(如 scpid_dsa.pub remote_host:.ssh/authorized_keys)全部都能搞定,但我们不推荐这么做,哪怕你"百分之百确 定"authorized_keys 并不存在。你可以使用另一个略微复杂却安全得多的命令。
bash
$ ssh remote_host "echo $(cat ~/.ssh/id_rsa.pub) >> ~/.ssh/authorized_keys"
jp@remote_host's password:
$ ssh remote_host
Last login: Thu Dec 14 00:02:52 2006 from openbsd.jpsdomain.org
NetBSD 2.0.2 (GENERIC) #0: Wed Mar 23 08:53:42 UTC 2005
Welcome to NetBSD!
$ exit
logout
Connection to remote_host closed.
如你所见,也就是一开始的 ssh 需要输入密码,随后就不用了。这里并没有展示 ssh-agent 的使用,该命令用于缓存密钥口令,这样就无须手动输入了。
此处的命令还假设 ~/.ssh 存在于本地主机和远程主机上。如果情况并非如此,可以使用 mkdir -m 0700 -p ~/.ssh 创建。~/.ssh 目录的权限必须是 0700,否则 OpenSSH 会报错。使用 chmod 0600 ~/.ssh/authorized_keys 也不错。
另外值得注意的是,我们建立的只是一种单向关系。也就是说,无须密码就可以通过 SSH 从本地主机连接到远程主机,但反方向就不行了,因为缺少私钥和远程主机上的代理。你可以将私钥复制到各处,形成一个"纵横交错的无密码 SSH 网络",但想修改口令时可就麻烦了,这增加了私钥保护工作的难度。如有可能,最好是配备一台安全措施良好且信得过的主机,在此之上根据需要用 ssh 连接远程主机。
SSH 代理聪明过人,用法微妙,我们甚至都觉得它机智过头了。它原本的实践用法是通过 eval 和命令替换:eval'ssh-agent'。结果会产生两个环境变量,以便 ssh 或 scp能找到代理并询问用户身份。这种方式很巧妙,很多地方都可以找到详细的描述。唯一的问题是和其他程序的常见用法不一样(除了 less 的某些特性,参见 8.15 节),新手或不知情的用户可能会不明所以。
如果只是运行代理,它会输出一些细节信息,看起来似乎已经开始工作了。没错,的确是在运行。但其实什么都做不了,因为必要的环境变量尚未设置。顺便提一下,-k 选项可以结束当前代理。
下面展示了一些 SSH 代理的错误用法和正确用法。
bash
# 错误的代理用法
# 没有相关环境变量
$ set | grep SSH
$ ssh-agent
SSH_AUTH_SOCK=/tmp/ssh-bACKp27592/agent.27592; export SSH_AUTH_SOCK;
SSH_AGENT_PID=24809; export SSH_AGENT_PID;
echo Agent pid 24809;
# 仍没有相关环境变量
$ set | grep SSH
# 甚至无法结束代理,因为-k需要$SSH_AGENT_PID
$ ssh-agent -k
SSH_AGENT_PID not set, cannot kill agent
# 代理是否正在运行?是的
$ ps x
PID TT STAT TIME COMMAND
24809 ?? Is 0:00.01 ssh-agent
22903 p0 I 0:03.05 -bash (bash)
11303 p0 R+ 0:00.00 ps -x
$ kill 24809
$ ps x
PID TT STAT TIME COMMAND
22903 p0 I 0:03.06 -bash (bash)
30542 p0 R+ 0:00.00 ps -x
# 正确用法
$ eval `ssh-agent`
Agent pid 21642
# 搞定,正常工作了!
$ set | grep SSH
SSH_AGENT_PID=21642
SSH_AUTH_SOCK=/tmp/ssh-ZfEsa28724/agent.28724
# 结束代理------错误的方式
$ ssh-agent -k
unset SSH_AUTH_SOCK;
unset SSH_AGENT_PID;
echo Agent pid 21642 killed;
# 进程已经结束,但没有完成清理工作
$ set | grep SSH
SSH_AGENT_PID=21642
SSH_AUTH_SOCK=/tmp/ssh-ZfEsa28724/agent.28724
# 正确的代理用法
$ eval `ssh-agent`
Agent pid 19330
$ set | grep SSH
SSH_AGENT_PID=19330
SSH_AUTH_SOCK=/tmp/ssh-fwxMfj4987/agent.4987
$ eval `ssh-agent -k`
Agent pid 19330 killed
$ set | grep SSH
$
挺直观的,不是吗?不。非常巧妙,非常有效,也非常微妙。但对用户就不怎么友好了。
一旦代理按预期运行,就必须用 ssh-add 命令加载身份信息。这非常简单:直接运行,根据需要选择要加载的密钥文件列表即可。ssh-add 会提示输入所需要的全部密码。这个示例没有列出任何密钥,因此它只使用主 SSH 配置文件中设置的默认值。
bash
$ ssh-add
Enter passphrase for /home/jp/.ssh/id_rsa:
Identity added: /home/jp/.ssh/id_rsa (/home/jp/.ssh/id_rsa)
$
现在我们就可以在该 shell 会话中以交互方式使用 SSH,不需要输入密码或口令就能登录先前配置过的任何主机。那其他会话、脚本或者 cron 呢?
使用 Daniel Robbins 的 keychain 脚本,该脚本的作用如下。
可以充当 ssh-agent 和 ssh-add 的前端,但允许你在每个系统中轻松拥有一个能够长期运行的 ssh-极大地减少了输入口令的次数。有了 keychain,在本地主机每次重启时输入口令即可。
极大地减少了输入口令的次数。有了 keychain,在本地主机每次重启时输入口令即可。
keychain 还使得远程 cron 作业能够轻松安全地"挂接"(hook in)在长期运行的 ssh-agent 进程上,这样就允许脚本利用基于密钥的登录(key-based login)。
shell 脚本 keychain 不仅精巧、质量上乘,而且注释详尽,先前我们讨论过的那些将环境变量导到其他会话的烦琐过程,都可以通过 keychain 实现自动化并进行管理。而且还能使其为脚本和 cron 所用。但你可能会自问:先等一下,你想让我把自己所有的密钥交给 keychain,直到重启主机为止?没错,不过这并没有听起来那么糟糕。
首先,你随时都能终止 keychain,不过这也使得脚本或cron 无法再使用它。其次,--clear 选项可以在你登录时冲洗掉已缓存的密钥。听起来怎么又倒退了?其实这么做是有道理的。keychain 的作者给出了详细的解释(首发于IBM developerWorks)。
要想在脚本或 cron 中使用 keychain 包装过的 ssh-agent,对脚本中创建的 keychain 文件使用 source 即可。keychain 也能处理 GPG 密钥。
bash
[ -r ~/.ssh-agent ] && source ~/.ssh-agent \
|| { echo "keychain not runnin" >&2 ; exit 1; }
在脚本中使用 SSH 时,你不想被提示进行认证或显示过多的警告信息。-q 选项可以开启安静模式并禁止警告,而 -o'BatchMode yes' 会阻止用户提示。显然,如果不能完成SSH 身份验证,肯定会失败,因为根本就无法回退,提示你再输入密码。不过既然你已经读到这里了,这应该不是问题。
SSH 是一款令人称奇的工具,有太多值得一说的地方,足以再写一本和本书同等厚度的专著。我们强烈推荐由Daniel J. Barrett、Richard Silverman、Robert G.Byrnes 合著的 SSH, The Secure Shell: The Defnitive Guide, 2nd Edition(O'Reilly 出版),你想知道的有关SSH 的一切(甚至更多),尽在其中。
22、限制SSH命令
你想限制接入的 SSH 用户或脚本能够执行的操作。
编辑 ~/.ssh/authorized_keys 文件,使用 SSH 的强制命令(forced command),同时有选择地禁用不必要的SSH 特性。例如,假设你希望允许 rsync 进程,但不允许交互式用法。
首先,你需要决定究竟在远端运行什么命令。创建密钥并添加强制命令。编辑~/.ssh/authorized_keys 并在密钥前加入:
bash
command="/bin/echo Command was: $SSH_ORIGINAL_COMMAND"
如下所示,所有内容全部出现在一行中。
bash
command="/bin/echo Command was: $SSH_ORIGINAL_COMMAND" ssh-dss
AAAAB3NzaC1kc3MAAAEBANpgvvTslst2m0ZJA0ayhh1Mqa3aWwU3kfv0m9+myFZ9veFsxM7
IVxIjWfAlQh3jplY+Q78fMzCTiG+ZrGZYn8adZ9yg5wAC03KXm2vKt8LfTx6I+qkMR7v15N
I7tZyhxGah5qHNehReFWLuk7JXCtRrzRvWMdsHcL2SA1Y4fJ9Y9FfVlBdE1Er+ZIuc5xIlO
6D1HFjKjt3wjbAal+oJxwZJaupZ0Q7N47uwMslmc5ELQBRNDsaoqFRKlerZASPQ5P+AH/+C
xa/fCGYwsogXSJJ0H5S7+QJJHFze35YZI+A1D3BIa4JBf1KvtoaFr5bMdhVAkChdAdMjo96
xhbdEAAAAVAJSKzCEsrUo3KAvyUO8KVD6e0B/NAAAA/3uAx2TIB/M9MmPqjeH67Mh5Y5NaV
WuMqwebDIXuvKQQDMUU4EPjRGmS89Hl8UKAN0Cq/C1T+OGzn4zrbE06COSm3SRMP24HyIbE
lhlWV49sfLR05Qmh9fRl1s7ZdcUrxkDkr2J6on5cMVB9M2nIl90IhRVLd5RxP01u81yqvhv
E61ORdA6IMjzXcQ8ebuD2R733O37oGFD7e2O7DaabKKkHZIduL/zFbQkzMDK6uAMP8ylRJN
0fUsqIhHhtc/16OT2H6nMU09MccxZTFUfqF8xIOndElP6um4jXYk5Q30i/CtU3TZyvNeWVw
yGwDi4wg2jeVe0YHU2RhZcZpwAAAQEAv2O86701U9sIuRijp8sO4h13eZrsE5rdn6aul/mk
m+xAlO+WQeDXRONm9BwVSrNEmIJB74tEJL3qQTMEFoCoN9Kp00Ya7Qt8n4gZ0vcZlI5u+cg
yd1mKaggS2SnoorsRlb2LhHpe6mXus8pUTf5QT8apgXM3TgFsLDT+3rCt40IdGCZLaP+UDB
uNUSKfFwCru6uGoXEwxaL08Nv1wZOc19qrc0Yzp7i33m6i3a0Z9Pu+TPHqYC74QmBbWq8U9
DAo+7yhRIhqfdJzk3vIKSLbCxg4PbMwx2Qfh4dLk+L7wOasKnl5//W+RWBUrOlaZ1ZP1/az
sK0Ncygno/0F1ew== This is my new key
现在执行命令,看看结果如何。
bash
$ ssh remote_host 'ls -l /etc'
Command was: ls -l /etc
$
这种方法的问题是,这会破坏像 rsync 这样的程序,该程序依赖于将 STDOUT/STDIN 全部连接到自身。
bash
$ ssh remote_host 'ls -l /etc'
Command was: ls -l /etc
$
这种方法的问题是,这会破坏像 rsync 这样的程序,该程序依赖于将 STDOUT/STDIN 全部连接到自身。
bash
$ rsync -avzL -e ssh remote_host:/etc .
protocol version mismatch -- is your shell clean?
(see the rsync manpage for an explanation)
rsync error: protocol incompatibility (code 2) at compat.c(64)
$
但可以通过修改强制命令来解决这个问题。
bash
command="/bin/echo Command was: $SSH_ORIGINAL_COMMAND >> ~/ssh_command"
在客户端再试一次:
bash
$ rsync -avzL -e ssh 192.168.99.56:/etc .
rsync: connection unexpectedly closed (0 bytes received so far) [receiver]
rsync error: error in rsync protocol data stream (code 12) at io.c(420)
$
在远端主机上:
bash
$ cat ../ssh_command
Command was: rsync --server --sender -vlLogDtprz . /etc
$
因此,我们可以根据需要更新强制命令。
另外两件能做的事情分别是设置源主机限制和禁用 SSH 命令。主机限制指定了源主机的主机名或 IP 地址。禁用命令的写法也相当直观。
bash
no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty
将所有这些合并在一起,如下所示(仍旧全部出现在很长的一行中)。
bash
no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty,from="local_
client",command="rsync --server --sender -vlLogDtprz . /etc" ssh-dss
AAAAB3NzaC1kc3MAAAEBANpgvvTslst2m0ZJA0ayhh1Mqa3aWwU3kfv0m9+myFZ9veFsxM7
IVxIjWfAlQh3jplY+Q78fMzCTiG+ZrGZYn8adZ9yg5wAC03KXm2vKt8LfTx6I+qkMR7v15N
I7tZyhxGah5qHNehReFWLuk7JXCtRrzRvWMdsHcL2SA1Y4fJ9Y9FfVlBdE1Er+ZIuc5xIlO
6D1HFjKjt3wjbAal+oJxwZJaupZ0Q7N47uwMslmc5ELQBRNDsaoqFRKlerZASPQ5P+AH/+C
xa/fCGYwsogXSJJ0H5S7+QJJHFze35YZI+A1D3BIa4JBf1KvtoaFr5bMdhVAkChdAdMjo96
xhbdEAAAAVAJSKzCEsrUo3KAvyUO8KVD6e0B/NAAAA/3uAx2TIB/M9MmPqjeH67Mh5Y5NaV
WuMqwebDIXuvKQQDMUU4EPjRGmS89Hl8UKAN0Cq/C1T+OGzn4zrbE06COSm3SRMP24HyIbE
lhlWV49sfLR05Qmh9fRl1s7ZdcUrxkDkr2J6on5cMVB9M2nIl90IhRVLd5RxP01u81yqvhv
E61ORdA6IMjzXcQ8ebuD2R733O37oGFD7e2O7DaabKKkHZIduL/zFbQkzMDK6uAMP8ylRJN
0fUsqIhHhtc/16OT2H6nMU09MccxZTFUfqF8xIOndElP6um4jXYk5Q30i/CtU3TZyvNeWVw
yGwDi4wg2jeVe0YHU2RhZcZpwAAAQEAv2O86701U9sIuRijp8sO4h13eZrsE5rdn6aul/mk
m+xAlO+WQeDXRONm9BwVSrNEmIJB74tEJL3qQTMEFoCoN9Kp00Ya7Qt8n4gZ0vcZlI5u+cg
yd1mKaggS2SnoorsRlb2LhHpe6mXus8pUTf5QT8apgXM3TgFsLDT+3rCt40IdGCZLaP+UDB
uNUSKfFwCru6uGoXEwxaL08Nv1wZOc19qrc0Yzp7i33m6i3a0Z9Pu+TPHqYC74QmBbWq8U9
DAo+7yhRIhqfdJzk3vIKSLbCxg4PbMwx2Qfh4dLk+L7wOasKnl5//W+RWBUrOlaZ1ZP1/az
sK0Ncygno/0F1ew== This is my new key
如果在运用 ssh 的过程中碰到问题,-v 选项能帮上大忙。ssh -v 或 ssh -v -v 起码在大部分情况下能给出一些错误线索。使用时不妨尝试一下这些选项,了解输出结果。
如果对密钥的功能感兴趣,可以深入了解 OpenSSH 的受限 shell:rssh,它支持 scp、sftp、rdist、rsync 和cvs。
你会觉得这种限制很容易实现,但事实证明并非如此。该问题与 SSH(以及在其之前的 r 系列命令)的实际运行方式有关。这是个绝妙的想法,效果也很好,但不易实现。为简化起见,你可以将 SSH 看作是将本地的 STDOUT 连接到远端的 STDIN,同时将远端的 STDOUT 连接到本地的 STDIN,因此,诸如 scp 或 rsync 的所有操作都是将本地主机的一个个字节送往远程主机,就好像二者之间有条管道一样。但这种高度的灵活性使得 SSH 无法在允许 scp的同时限制交互式访问。两者之间并没有什么不同。这也是你不能在 bash 配置文件中放置大量 echo 和调试语句的原因(参见 16.21 节),其输出会与字节流混杂在一起,造成灾难性后果。
那么 rssh 是如何工作的?它提供了一个包装器,用于替代/etc/passwd 中指定的默认登录 shell(如 bash)。该包装器决定了哪些是允许的,哪些是不允许的,但又比普通的旧式 SSH 受限命令灵活得多。
23、断开非活跃会话
你希望能够自动注销那些不活跃的用户,尤其是 root。
将 /etc/bashrc 或 ~/bashrc 中的环境变量 TMOUT 设置为结束会话之前的非活跃秒数。在交互模式中,只要出现了命令行提示符,如果用户没有在 TMOUT 秒内输入命令,那么 bash 就会退出。
$TMOUT 也可用于脚本中的 read 内建命令和 select 命令。
如果不想有人改动 $TMOUT,记得在用户没有写入权限的系统级文件(如 /etc/profile 或 /etc/bashrc)将其设置为只读变量。
bash
declare -r TMOUT=3600
# 或者:
readonly TMOUT=3600
由于用户能够控制自己的环境变量,即便你已经将 $TMOUT 设为只读,也别完全依靠它:用户只需要运行其他 shell,甚至是 bash 的不同实例便可轻松化解!可以将此视作对协作用户的善意提示,尤其是那些知识丰富而且随时待命的系统管理员,他们可能会(不断地)分神。