永远不要使用二级域名作为 Linux Hostname

我们常常使用 Linux 的 Hostname 来标识一台服务器。Phil KarIton 曾说过:

计算机科学只存在两个难题:命名和缓存失效。

如何想出一个有意义、便于运维,甚至有个人风格(对于个人服务器来说)的 Hostname 也是一件难事。曾有一个有趣的 RFC —— RFC 1178: Choosing a Name for Your Computer 给出了一些关于命名 Hostname 的建议。

在这份 RFC 中,曾经提到了「不要使用域名或类似域名的东西作为主机名」:

Avoid domain names.
For technical reasons, domain names should be avoided. In particular, name resolution of non-absolute hostnames is problematic. Resolvers will check names against domains before checking them against hostnames. But we have seen instances of mailers that refuse to treat single token names as domains. For example, assume that you mail to “libes@rutgers” from yale.edu. Depending upon the implementation, the mail may go to rutgers.edu or rutgers.yale.edu (assuming both exist).

Avoid domain-like names.
Domain names are either organizational (e.g., cia.gov) or geographical (e.g., dallas.tx.us). Using anything like these tends to imply some connection. For example, the name “tahiti” sounds like it means you are located there. This is confusing if it is really somewhere else (e.g., “tahiti.cia.gov is located in Langley, Virginia? I thought it was the CIA’s Tahiti office!”). If it really is located there, the name implies that it is the only computer here. If this isn’t wrong now, it inevitably will be There are some organizational and geographical names that work fine. These are exactly the ones that do not function well as domain names. For example, amorphous names such as rivers, mythological places and other impossibilities are very suitable. (“earth” is not yet a domain name.)

使用域名作为 Hostname 不仅是一个技术错误,还会带来潜在的信息安全问题,尤其是对于使用二级域名(例如 example.com)来说。就在刚刚,我碰到了一个生动的例子。

问题描述

在我的服务器上,我使用 josephcz.xyz 作为主机名。我刚刚调整了我的 Nginx 设置,以增加新的 Web 服务。但是,似乎我的 DNS 解析并没有生效。当我使用 curl 访问我的新设置的主机名时,却发现解析到了我从未见过的地址:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
$ curl -v api-v2023.josephcz.xyz
* Trying 52.9.36.254:80...
* TCP_NODELAY set
* Connected to api-v2023.josephcz.xyz (52.9.36.254) port 80 (#0)
> GET / HTTP/1.1
> Host: api-v2023.josephcz.xyz
> User-Agent: curl/7.68.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 301 Moved Permanently
< Server: awselb/2.0
< Date: Thu, 15 Jun 2023 01:00:25 GMT
< Content-Type: text/html
< Content-Length: 134
< Connection: keep-alive
< Location: https://api-v2023.josephcz.xyz:443/
<
<html>
<head><title>301 Moved Permanently</title></head>
<body>
<center><h1>301 Moved Permanently</h1></center>
</body>
</html>
* Connection #0 to host api-v2023.josephcz.xyz left intact

当我看到我的域名被解析到未预期的 IP 地址时,我的第一反应是 DNS 劫持。然而我的服务器位于香港,使用了 Google DNS 和 Cloudflare DNS 作为 DNS 服务器。当我使用 nslookupdig 命令查询域名时,得到的结果却是正确的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$ dig api-v2023.josephcz.xyz

; <<>> DiG 9.16.1-Ubuntu <<>> api-v2023.josephcz.xyz
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 61092
;; flags: qr rd ra ad; QUERY: 1, ANSWER: 0, AUTHORITY: 1, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1232
;; QUESTION SECTION:
;api-v2023.josephcz.xyz. IN A

;; AUTHORITY SECTION:
josephcz.xyz. 180 IN SOA ns3.dnsv2.com. level3dnsadmin.dnspod.com. 1686782943 3600 180 1209600 180

;; Query time: 211 msec
;; SERVER: 1.1.1.1#53(1.1.1.1)
;; WHEN: Thu Jun 15 09:02:51 HKT 2023
;; MSG SIZE rcvd: 122

事实上,问题的根源在于 Hostname 的设置。

Hostname 和 Search Domain

在安装 Linux 的时候,发行版会让我们输入 Hostname。如果输入的是一个带点号的域名(如 webserver.example.com),安装程序会自动将其拆分为 Hostname 和 Search Domain。Hostname 的部分为 webserver,Search Domain 的部分为 example.com

Search Domain 是怎样工作的

假如您有两台将 Search Domain 配置为 example.com,服务器,假设它们的 Hostname 为:

  • webserver-1
  • webserver-2

并且,在 DNS 服务器上,配置了如下的解析:

  • webserver-1.example.com: 192.168.1.1
  • webserver-2.example.com: 192.168.1.2

当你在 webserver-1 上输入域名 webserver-2 访问各种服务时,由于配置了 example.com 为 Search Domain,Linux 会自动解析为 webserver-2.example.com。具体地说,这个过程是这样的:

  1. 检查 /etc/hosts 文件是否存在 webserver-2 的记录,如果存在,直接返回对应的 IP 地址;
  2. 系统向 DNS 服务器查询 webserver-2 的 IP 地址;
  3. DNS 服务器会将 webserver-2 理解为一个类似 .com.net 的根域名。由于不存在这个根域名,DNS 服务器返回 NXDOMAIN 错误;
  4. 由于 NXDOMAIN 错误,系统将 webserver-2 加上 Search Domain 拼接为 webserver-2.example.com,再次向 DNS 服务器查询;
  5. DNS 服务器返回 webserver-2.example.com 的 IP 地址 192.168.1.2

通过使用 Search Domain,我们可以减少输入、记忆域名的工作量,对于运维工作提供了很大的便利。

当 Hostname 是一个 FQDN 时

当 Hostname 是一个 FQDN 时,其解析过程也是类似的。同样以在 webserver-1 上解析 webserver-2 为例:

  1. 检查 /etc/hosts 文件是否存在 webserver-2 的记录,如果存在,直接返回对应的 IP 地址;
  2. 系统向 DNS 服务器查询 webserver-2 的 IP 地址;
  3. DNS 服务器会将 webserver-2 理解为一个类似 .com.net 的根域名。由于不存在这个根域名,DNS 服务器返回 NXDOMAIN 错误;
  4. 由于 NXDOMAIN 错误,系统移除 Hostname 的第一个分段,并将其替换为需要查询的域名。即将 webserver-1.example.com 移除 webserver-1 变成 example.com,再拼接上需要查询的域名,向 DNS 服务器查询 webserver-2.example.com 的 IP 地址;
  5. DNS 服务器返回 webserver-2.example.com 的 IP 地址 192.168.1.2

如果我们使用二级域名作为 Hostname

在以上的例子中,服务器的 Hostname 都是一个三级域名。倘若我们将服务器的 Hostname 切换为二级域名,例如 example.com 时,会发生什么呢?在这里我们假设一个没有被任何人注册的域名,例如 some-not-registered-domain.com;并假设 com.com 这一域名被恶意人士注册。

接下来,我们使用 ping 命令,加上需要解析的域名,看看实际访问的域名时哪一个:

ping 命令结果对比图

是的,在使用二级域名作为 Hostname 的情况下,当 ping 一个尚未注册的域名 some-not-registered-domain.com 时,由于 Hostname 和 Search Domain 的配置,会自动 fallback 到 some-not-registered-domain.com.com 上!遗憾的是,不管是否使用 Search Domain,这个问题都存在——而将 Hostanme 设置为 FQDN 让这个问题更加隐蔽了。

这个问题不仅影响了 ping、curl,也影响了几乎所有使用 glibc 的程序:OpenSSL、MySQL、Nginx、Apache……等等。如果它们的某个功能需要解析域名,则都会有同样的问题。

未被注册的域名值得警惕,但是这一问题如果发生在你自己的域名上呢?

如果我们使用已经被注册的域名作为 Hostname

回到我遇到的问题上来。我刚刚设置了 api-v2023.josephcz.xyz 这一域名的解析。而 DNS 解析的生效需要时间,在递归 DNS 未获取到生效的记录的情况下,会返回一个空记录。而空记录和 NXDOMAIN 错误一样,会触发 Search Domain 的 fallback 机制。因此,服务器上对 api-v2023.josephcz.xyz 全部被发送到了 api-v2023.josephcz.xyz.xyz

使用 tcpdump -i ens3 -nt -s 500 port domain 命令可以清楚地看到这一过程:

1
2
3
4
5
6
7
IP 10.0.0.12.57943 > 1.1.1.1.53: 27244+ A? api-v2023.josephcz.xyz. (35)
IP 10.0.0.12.57943 > 1.1.1.1.53: 19043+ AAAA? api-v2023.josephcz.xyz. (35)
IP 1.1.1.1.53 > 10.0.0.12.57943: 19043 0/1/0 (106)
IP 1.1.1.1.53 > 10.0.0.12.57943: 27244 0/1/0 (106)
IP 10.0.0.12.46092 > 1.1.1.1.53: 51629+ A? api-v2023.josephcz.xyz.xyz. (39)
IP 10.0.0.12.46092 > 1.1.1.1.53: 61104+ AAAA? api-v2023.josephcz.xyz.xyz. (39)
IP 1.1.1.1.53 > 10.0.0.12.46092: 51629 3/0/0 CNAME xyz.xyz., A 52.9.36.254, A 54.241.183.58 (85)

好在 XYZ 域名的注册局保留了 XYZ.XYZ。但是如果换成解析其他域名,例如 some-api-host.aws 或者 pki.goog,而相关域名的注册局又忘记保留了 awsgoog 之类的域名呢?

结论

在设置 Linux 的 Hostname 时,永远不要使用一个二级域名——无论是将 Hostname 设置为 FQDN 还是配置 Search Domain。永远使用一个三级或以上的、受你自己控制的域名作为 Hostname。

此外,启用强制性的 TLS 并正确部署证书信任也可以在一定程度上缓解这一问题——由于对方无法获得正确的 TLS 证书,即使解析被 fallback 到恶意攻击者的域名上,也难以窃取你的机密信息。