6. 安全性和 NFS

此安全提示和解释列表不会使您的站点完全安全。没有任何东西能使您的站点完全安全。阅读本节可能有助于您了解 NFS 的安全问题。这不是一个全面的指南,并且会不断更新。如果您有任何提示或建议要提供给我们,请发送给 HOWTO 维护者。

如果您在一个无法访问外部世界(甚至没有调制解调器)的网络上,并且您信任所有内部机器和所有用户,那么本节对您没有用处。但是,我们认为,处于这种情况的网络相对较少,因此我们建议任何设置 NFS 的人都应彻底阅读本节。

使用 NFS,客户端需要两个步骤才能访问服务器上远程目录中包含的文件。 第一步是挂载访问。挂载访问是通过客户端计算机尝试连接到服务器来实现的。 此安全由/etc/exports文件提供。 该文件列出了允许访问共享点的计算机的名称或 IP 地址。 如果客户端的 IP 地址与访问列表中的一个条目匹配,则允许它挂载。 这不是很安全。 如果有人能够欺骗或接管受信任的地址,那么他们就可以访问您的挂载点。 为了给出一个真实的“身份验证”示例:这相当于有人向您介绍自己,并且您相信他们是他们所声称的人,因为他们佩戴了一个写着“你好,我的名字是....”的贴纸。一旦机器挂载了一个卷,它的操作系统将可以访问卷上的所有文件(可能除了 root 拥有的文件;见下文),并且如果卷是使用rw选项导出的,也可以写入这些文件。

第二步是文件访问。 这是客户端上正常文件系统访问控制的功能,而不是 NFS 的专门功能。 一旦驱动器被挂载,文件上的用户和组权限就决定了访问控制。

一个例子:服务器上的 bob 映射到 UserID 9999。 Bob 在服务器上创建一个只有该用户可以访问的文件(相当于输入chmod 600 文件名)。 允许客户端挂载存储该文件的驱动器。 在客户端上,mary 映射到 UserID 9999。 这意味着客户端用户 mary 可以访问 bob 的文件,该文件被标记为只有他才能访问。 更糟糕的是:如果有人成为客户端机器上的超级用户,他们可以使用 su - 用户名 并成为任何用户。 NFS 不会知道。

并非一切都很糟糕。 您可以在服务器上采取一些措施来抵消客户端的危险。 我们将在稍后介绍这些。

如果您认为安全措施不适用于您,那么您可能错了。 在第 6.1 节,我们将介绍保护端口映射器,分别在 第 6.2 节第 6.3 节介绍服务器和客户端安全。 最后,在 第 6.4 节中,我们将简要讨论 NFS 服务器的正确防火墙设置。

最后,至关重要的是,您的所有 NFS 守护程序和客户端程序都是最新的。 如果您认为某个缺陷的公布时间太短,不会对您造成问题,那么您可能已经被入侵了。

了解安全警报的一个好方法是订阅 bugtraq 邮件列表。 您可以在此处阅读有关如何订阅以及有关 bugtraq 的各种其他信息:http://www.securityfocus.com/forums/bugtraq/faq.html

此外,在 securityfocus.com的搜索引擎上搜索NFS将显示所有与 NFS 相关的安全报告。

您还应该定期查看 CERT 咨询报告。 请参阅 CERT 网页:www.cert.org

6.1. 端口映射器

端口映射器维护一个列表,其中列出了哪些服务在哪些端口上运行。 连接机器使用此列表来查看它要通过哪些端口与某些服务通信。

端口映射器不像几年前那么糟糕了,但它仍然是许多系统管理员担心的一点。 端口映射器(如 NFS 和 NIS)实际上不应该在受信任的局域网之外建立连接。 如果您必须将它们暴露给外部世界 - 请小心并保持对这些系统的勤奋监控。

并非所有 Linux 发行版都是相同的。 一些看似最新的发行版不包括可保护的端口映射器。 检查您的端口映射器是否好用的简单方法是运行strings(1),看看它是否读取相关文件,/etc/hosts.deny/etc/hosts.allow。 假设您的端口映射器是/sbin/portmap您可以使用此命令检查它
     strings /sbin/portmap | grep hosts.  
     

在可保护的机器上,它看起来像这样
   /etc/hosts.allow
   /etc/hosts.deny
   @(#) hosts_ctl.c 1.4 94/12/28 17:42:27
   @(#) hosts_access.c 1.21 97/02/12 02:13:22
  

首先我们编辑/etc/hosts.deny。 它应该包含以下行

   portmap: ALL
  

这将拒绝所有人访问。 在关闭时运行
   rpcinfo -p
  
只是为了检查您的端口映射器是否真的读取并遵守此文件。 Rpcinfo 应该不输出任何内容,或者可能显示错误消息。 文件/etc/hosts.allow/etc/hosts.deny在您保存它们后立即生效。 无需重启任何守护程序。

为所有人关闭端口映射器有点过激,因此我们通过编辑 /etc/hosts.allow 再次打开它。 但首先我们需要弄清楚该放什么。 它基本上应该列出所有应该有权访问您的端口映射器的机器。 在一台普通的 Linux 系统上,很少有机器需要任何理由访问任何内容。 端口映射器管理 nfsdmountdypbind/ypservrquotadlockd(显示为 nlockmgr)、statd(显示为 status)以及“r”服务,如 ruptimerusers。 在这些服务中,只有 nfsdmountdypbind/ypserv 也许还有 rquotadlockdstatd 才有任何意义。 所有需要访问您的机器上的服务的机器都应该被允许这样做。 假设您的机器的地址是192.168.0.254,并且它位于子网192.168.0.0上,并且子网上所有机器都应该有权访问它(有关这些术语的概述,请参见Networking-Overview-HOWTO)。 然后我们写入
   portmap: 192.168.0.0/255.255.255.0
   
/etc/hosts.allow中。 如果您不确定您的网络或网络掩码是什么,您可以使用 ifconfig 命令来确定网络掩码,并使用 netstat 命令来确定网络。 例如,对于上述机器上的设备 eth0,ifconfig 应该显示

   ...
   eth0   Link encap:Ethernet  HWaddr 00:60:8C:96:D5:56
          inet addr:192.168.0.254  Bcast:192.168.0.255 Mask:255.255.255.0
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:360315 errors:0 dropped:0 overruns:0
          TX packets:179274 errors:0 dropped:0 overruns:0
          Interrupt:10 Base address:0x320
   ...
   
并且 netstat -rn 应该显示
   Kernel routing table
   Destination     Gateway         Genmask         Flags Metric Ref Use    Iface
   ...
   192.168.0.0     0.0.0.0         255.255.255.0   U     0      0   174412 eth0
   ...
   
(网络地址在第一列中)。

这些/etc/hosts.deny/etc/hosts.allow文件在同名手册页中描述。

重要提示:不要在这些文件的 portmap 行中放入任何 IP NUMBER 以外的内容。 主机名查找可能会间接导致 portmap 活动,这将触发主机名查找,这可能会间接导致 portmap 活动,这将触发...

nfs-utils 软件包的 0.2.0 及更高版本也使用hosts.allowhosts.deny文件,因此您也应该在这些文件中放入 lockdstatdmountdrquotad 的条目。 有关完整示例,请参见第 3.2.2 节

以上操作应该使您的服务器更安全。 唯一剩下的问题是,如果有人获得对您受信任的客户端机器之一的管理访问权限,并且能够发送伪造的 NFS 请求。 下一节将讨论针对此问题的安全措施。

6.2. 服务器安全性:nfsd 和 mountd

在服务器上,我们可以决定不信任客户端上以 root 身份发出的任何请求。 我们可以通过使用root_squash/etc/exports:
   /home slave1(rw,root_squash)
   

中的选项来实现。 事实上,这是默认设置。 除非您有非常好的理由关闭它,否则应该始终将其打开。 要关闭它,请使用no_root_squash选项导出的,也可以写入这些文件。

现在,如果客户端上具有 UID 0(即 root 的用户 ID 号)的用户尝试访问(读取、写入、删除)文件系统,服务器会替换服务器“nobody”帐户的 UID。 这意味着客户端上的 root 用户无法访问或更改只有服务器上的 root 才能访问或更改的文件。 这很好,您可能应该在您导出的所有文件系统上使用root_squash。 “但是客户端上的 root 用户仍然可以使用 su 成为任何其他用户并访问和更改该用户的文件的!” 你说。 对此的答案是:是的,这就是 Unix 和 NFS 的方式,而且必须如此。 这有一个重要的含义:所有重要的二进制文件和文件都应该由 root 拥有,而不是 bin 或其他非 root 帐户,因为客户端 root 用户无法访问的唯一帐户是服务器的 root 帐户。 在 exports(5) 手册页中,列出了几个其他的 squash 选项,以便您可以决定不信任客户端上的任何人。

TCP 端口 1-1024 保留供 root 使用(因此有时称为“安全端口”)。 非 root 用户无法绑定这些端口。 将secure选项添加到/etc/exports意味着它只会监听来自客户端上端口 1-1024 的请求,因此客户端上的恶意非 root 用户不能在非保留端口上打开欺骗的 NFS 对话。 默认情况下会设置此选项。

6.3. 客户端安全

6.4. NFS 和防火墙(ipchains 和 netfilter)

IPchains(在 2.2.X 内核下)和 netfilter(在 2.4.x 内核下)提供了良好的安全级别 - 无需依赖守护程序(或其 TCP 包装器)来确定哪些计算机可以连接,连接尝试会在更低的级别被允许或禁止。 在这种情况下,你可以更早、更全局地阻止连接,从而保护你免受各种攻击。

描述如何设置 Linux 防火墙超出了本文档的范围。 感兴趣的读者可以阅读 Firewall-HOWTOIPCHAINS-HOWTO。 对于使用 2.4 及更高版本内核的用户,你可能需要访问 netfilter 网页:http://netfilter.filewatcher.org。 如果你已经熟悉 ipchains 或 netfilter 的工作原理,本节将为你提供一些有关如何更好地设置 NFS 守护程序以更轻松地进行防火墙保护的技巧。

防火墙配置的一个好规则是拒绝所有连接,只允许一些连接 - 这有助于防止你意外地允许超出你预期的连接。

为了理解如何对 NFS 守护程序进行防火墙保护,简要回顾一下它们如何绑定到端口会有所帮助。

当守护程序启动时,它会向 portmapper 请求一个空闲端口。 portmapper 获取守护程序的端口并跟踪该守护程序当前使用的端口。 当其他主机或进程需要与守护程序通信时,它们会从 portmapper 请求端口号以查找该守护程序。 因此,端口将永久浮动,因为不同的端口可能在不同的时间空闲,因此 portmapper 每次都会以不同的方式分配它们。 这对于设置防火墙来说是一个麻烦。 如果你永远不知道守护程序将在哪里,那么你就不确切地知道允许访问哪些端口。 对于许多在受保护或隔离的 LAN 上运行的人来说,这可能不是什么大问题。 但是,对于那些在公共网络上的人来说,这太可怕了。

在内核 2.4.13 和更高版本以及 nfs-utils 0.3.3 或更高版本中,你不再需要担心 portmapper 中端口的浮动。 现在,与 nfs 相关的所有守护程序都可以“固定”到某个端口。 大多数守护程序在启动时都接受 -p 选项; 由内核启动的那些守护程序需要一些内核参数或模块选项。 下面将介绍它们。

通过 nfs 共享数据涉及的一些守护程序已经绑定到一个端口。 portmap 始终在端口 111 (tcp 和 udp) 上。 nfsd 始终在端口 2049 (TCP 和 UDP) 上(但是,从内核 2.4.17 开始,基于 TCP 的 NFS 被认为是实验性的,不适用于生产机器)。

其他守护程序,statdmountdlockdrquotad,通常会移动到 portmapper 通知它们的第一个可用端口。

要强制 statd 绑定到特定端口,请使用-p 端口号选项。 要强制 statd 在特定端口上响应,请在启动时额外使用-o 端口号选项。

要强制 mountd 绑定到特定端口,请使用-p 端口号选项。

例如,要使 statd 在端口 32765 上广播并在端口 32766 上侦听,以及使 mountd 在端口 32767 上侦听,你可以键入

# statd -p 32765 -o 32766
# mountd -p 32767

lockd 由内核在需要时启动。 因此,你需要传递模块选项(如果它是作为模块构建的)或内核选项以强制 lockd 仅在某些端口上侦听和响应。

如果你使用的是可加载模块,并且想在你的/etc/modules.conf文件中指定这些选项,请向该文件添加如下一行

options lockd nlm_udpport=32768 nlm_tcpport=32768

上面这行将为 lockd 指定 udp 和 tcp 端口为 32768。

如果你没有使用可加载模块,或者如果你已将 lockd 编译到内核中而不是将其构建为模块,则需要在内核引导行上向其传递一个选项。

它应该看起来像这样

 vmlinuz 3 root=/dev/hda1 lockd.udpport=32768 lockd.tcpport=32768

端口号不必匹配,但如果不匹配,只会增加不必要的混乱。

如果你正在使用配额并使用 rpc.quotad 通过 nfs 使这些配额可查看,那么在设置防火墙时也需要将其考虑在内。 有两个 rpc.rquotad 源代码树。 其中一个维护在 nfs-utils 树中。 另一个在 quota-tools 树中。 它们的操作方式不一样。 nfs-utils 提供的那个支持使用-p指令将守护程序绑定到端口。 quota-tools 中的那个不支持。 请查阅你的发行版的文档以确定你的发行版是否支持。

为了便于讨论,让我们描述一个网络并设置一个防火墙来保护我们的 nfs 服务器。 我们的 nfs 服务器是 192.168.0.42,我们的客户端只有 192.168.0.45。 如上面的示例中所示,已启动 statd,以便它仅绑定到端口 32765 以进行传入请求,并且必须在端口 32766 上响应。 强制 mountd 绑定到端口 32767。 lockd 的模块参数已设置为绑定到 32768。 当然,nfsd 在端口 2049 上,并且 portmapper 在端口 111 上。

我们没有使用配额。

使用 IPCHAINS,一个简单的防火墙可能看起来像这样

ipchains -A input -f -j ACCEPT -s 192.168.0.45
ipchains -A input -s 192.168.0.45 -d 0/0 32765:32768 -p 6 -j ACCEPT
ipchains -A input -s 192.168.0.45 -d 0/0 32765:32768 -p 17 -j ACCEPT
ipchains -A input -s 192.168.0.45 -d 0/0 2049 -p 17 -j ACCEPT
ipchains -A input -s 192.168.0.45 -d 0/0 2049 -p 6 -j ACCEPT
ipchains -A input -s 192.168.0.45 -d 0/0 111 -p 6 -j ACCEPT
ipchains -A input -s 192.168.0.45 -d 0/0 111 -p 17 -j ACCEPT
ipchains -A input -s 0/0 -d 0/0 -p 6 -j DENY -y -l
ipchains -A input -s 0/0 -d 0/0 -p 17 -j DENY -l

netfilter 中的等效命令集是

iptables -A INPUT -f -j ACCEPT -s 192.168.0.45
iptables -A INPUT -s 192.168.0.45 -d 0/0 32765:32768 -p 6 -j ACCEPT
iptables -A INPUT -s 192.168.0.45 -d 0/0 32765:32768 -p 17 -j ACCEPT
iptables -A INPUT -s 192.168.0.45 -d 0/0 2049 -p 17 -j ACCEPT
iptables -A INPUT -s 192.168.0.45 -d 0/0 2049 -p 6 -j ACCEPT
iptables -A INPUT -s 192.168.0.45 -d 0/0 111 -p 6 -j ACCEPT
iptables -A INPUT -s 192.168.0.45 -d 0/0 111 -p 17 -j ACCEPT
iptables -A INPUT -s 0/0 -d 0/0 -p 6 -j DENY --syn --log-level 5
iptables -A INPUT -s 0/0 -d 0/0 -p 17 -j DENY --log-level 5

第一行表示接受所有数据包片段(除了将被视为普通数据包的第一个数据包片段)。 理论上,在重新组装之前,没有任何数据包会通过,并且除非通过第一个数据包片段,否则不会重新组装。 当然,存在通过数据包片段使机器过载而产生的攻击。 但是,除非你允许片段通过,否则 NFS 将无法正常工作。 有关详细信息,请参见第 7.8 节

其他行允许从客户端主机上的任何端口到服务器上已提供的特定端口的特定连接。 这意味着,如果例如 192.158.0.46 尝试联系 NFS 服务器,它将无法挂载或查看有哪些挂载可用。

有了新的端口固定功能,显然更容易控制允许哪些主机挂载你的 NFS 共享。 值得一提的是,NFS 不是加密协议,同一物理网络上的任何人都可能嗅探流量并重新组装来回传递的信息。

6.5. 通过 SSH 隧道传输 NFS

通过网络加密 NFS 流量的一种方法是使用 ssh 的端口转发功能。 但是,正如我们将要看到的,如果你没有完全信任服务器上的本地用户,这样做会带来严重的缺陷。

第一步是将文件导出到 localhost。 例如,要导出/home分区,请在/etc/exports:
/home   127.0.0.1(rw)

中输入以下内容。下一步是使用 ssh 转发端口。 例如,ssh 可以告诉服务器从客户端上的端口转发到任何机器上的任何端口。 让我们假设,如上一节中所示,我们的服务器是 192.168.0.42,并且我们已使用参数-p 32767mountd 固定到端口 32767。 然后,在客户端上,我们将键入
     # ssh root@192.168.0.42 -L 250:localhost:2049  -f sleep 60m
     # ssh root@192.168.0.42 -L 251:localhost:32767 -f sleep 60m

上面的命令导致客户端上的 ssh 接收定向到客户端端口 250 的任何请求,并将其首先通过服务器上的 sshd 转发到服务器的端口 2049。 第二行导致客户端上的端口 251 和服务器上的端口 32767 之间的类似类型的转发。

localhost相对于服务器;也就是说,转发将完成到服务器本身。 否则,该端口可能会被转发到任何其他机器,并且这些请求对于外界来说看起来像是来自服务器。 因此,这些请求对于服务器上的 NFSD 看起来像是来自服务器本身。 请注意,为了绑定到客户端上低于 1024 的端口,我们必须以客户端上的 root 身份运行此命令。 如果我们使用默认

导出了文件系统,则必须这样做。 secure选项导出的,也可以写入这些文件。

最后,我们用最后一个选项玩了一个小花招,-f sleep 60m。通常,当我们使用 ssh 时,即使使用-L选项,我们也会在远程计算机上打开一个 shell。但是,我们只想在后台执行端口转发,以便我们获得客户端上的 shell。因此,我们告诉 ssh 在服务器上的后台执行一个命令以休眠 60 分钟。这会导致端口转发 60 分钟直到获得连接;此时,端口将继续转发直到连接断开或直到 60 分钟结束,以两者中较晚者为准。上述命令可以放在客户端上的启动脚本中,紧接在网络启动之后。

接下来,我们必须在客户端挂载文件系统。为此,我们告诉客户端在 localhost 上挂载文件系统,但使用的端口与通常的 2049 不同。具体来说,在/etc/fstab中的条目看起来会像这样:
  localhost:/home  /mnt/home  nfs  rw,hard,intr,port=250,mountport=251  0 0

完成此操作后,我们可以理解为什么如果任何普通用户都能够本地登录服务器,上述方法将极其不安全。如果他们可以,那么没有什么能阻止他们做我们做过的事情,并使用 ssh 将他们自己客户端机器上的特权端口(他们合法地拥有 root 权限)转发到服务器上的 2049 和 32767 端口。因此,服务器上的任何普通用户都可以使用与客户端上的 root 相同的权限挂载我们的文件系统。

如果您使用的 NFS 服务器不允许普通用户登录,并且您希望使用此方法,则还有两个额外的注意事项:首先,连接通过 sshd 从客户端传到服务器;因此,您必须在防火墙上将端口 22(sshd 监听的端口)对您的客户端开放。但是,您不再需要开放其他端口,例如 2049 和 32767。其次,文件锁定将不再起作用。无法要求 statd 或锁定管理器向特定挂载的特定端口发出请求;因此,任何锁定请求都会导致 statd 连接到 localhost 上的 statd,即它本身,并且会失败并出现错误。任何纠正此问题的尝试都需要对 NFS 进行重大重写。

也可以使用 IPSec 来加密客户端和服务器之间的网络流量,而不会损害服务器上的任何本地安全性;这里不作介绍。有关在 Linux 下使用 IPSec 的详细信息,请参阅 FreeS/WAN 主页。

6.6. 总结

如果您使用 portmapper/NFS 软件中的hosts.allow, hosts.deny, root_squash, nosuid和特权端口功能,您可以避免目前已知的许多 NFS 错误,并且至少可以对此感到安全。但即便如此,经过所有这些努力:当入侵者有权访问您的网络时,TA 可以在您的.forward中显示奇怪的命令,或者在/home/var/mail被 NFS 导出时读取您的邮件。出于同样的原因,您永远不应通过 NFS 访问您的 PGP 私钥。或者至少您应该了解所涉及的风险。现在您已经了解了一些。

NFS 和 portmapper 构成了一个复杂的子系统,因此在基本设计或我们使用的实现中发现新的错误并非完全不可能。甚至现在可能存在已知的漏洞,有人正在利用它们。但这就是生活。