使用 Let's Encrypt 和 Nginx 从同一服务器托管多个 HTTPS 域名

2017-02-16 19:32

现在网站越来越需要 HTTPS,而这正是顺应了发展趋势。Chrome 现已将带有密码或信用卡字段的 HTTP 网站明确标记为“不安全的”。在过去的一年里,我一直在将我的客户端网站切换到 HTTPS 上。事实证明,作为系统管理员的工作总是这样做,也存在与此相关的隐藏的挑战。

HTTPS 简述

在我们深入下面的细节之前,我们先弄清写这篇文章的目的。

从同一服务器上托管多个 HTTPS 网站的问题与 HTTPS 连接的建立方式有关。安全的(读取:不是pwned)HTTPS 是在 TLS 上运行。当 TLS 连接建立时,我们倾向于认为它是与网站构建的。在技术上这是不正确的。 TLS 连接是连接到特定的 IP 地址。一旦你连接到那个 IP,服务器会假定这就是你想访问的网站。这意味着您不能在同一 IP 地址的主机上托管的多个 HTTPS 网站。

进入(和退出)SNI

SNI(服务器名称标识)是一项在 21 世纪初期就被引入的协议,引入的目的就是为了解决这个问题。它是一种用于在浏览器设置一个 TLS 连接时向网页服务器发送一个域名的方法。通过在初次握手阶段传入域名,网页服务器就可以知道你想要访问的是哪一个域,从而让其为正确的那一个提供服务。

不过尽管其已经得到了广泛的支持,但也有不被支持的情况。我自己就曾在处理对 SNI 的依赖时掉进了移动端浏览器的坑。从另外一方面来看,IP 地址是廉价的,好像是 1 $/月,或者还要更便宜。因此你索性可以为你的 HTTPS 站点单独弄一个 IP,这样做所避免掉的一些在终端/浏览器组合上遇到的头疼事儿,花费的代价可能百倍不止。

添加 IP 地址

对于初学者而言,首先需要给每一个你准备从同一台服务器上托管在 HTTPS 上的域名获取一个 IP。大多数像 Linode 以及 AWS 这样的托管平台都提供了为任何东西添加 IP 的能力。

一旦你有那些 IP,你就需要确认你的 web 服务器识别输入他们。如果你开始运行 ping,你就可能看到下面的输出:

$ ping you.new.ip.addr
PING you.new.ip.addr (you.new.ip.addr): 56 data bytes  
Request timeout for icmp_seq 0  Request timeout for icmp_seq 1  
...

这是因为服务器忽略了没有配置的 IP 地址。要让服务器看到那些输入的 IP,我们需要更新网络接口。这听起来很可怕,但实际上并没有那么坏。我们开始编辑 /etc/network/interfaces 这个文件。 你大概会有一个庞大的清单是关于你的服务器所需要存储的 IP 地址的。

# Something like this probably already exists
auto eth0  
iface eth0 inet static  
    address X.X.X.X/24
    gateway X.X.X.1
    dns-nameservers 8.8.8.8 8.8.4.4
    dns-search your.dns.info
    dns-options rotate

这里,我们将会在 eth0 块下,添加新 IP 地址。

# Add support for our new IP addresses
iface eth0:1 inet static  
    address Y.Y.Y.Y/24
    dns-nameservers 8.8.8.8 8.8.4.4

iface eth0:2 inet static  
    address Z.Z.Z.Z/24
    dns-nameservers 8.8.8.8 8.8.4.4

如何获得证书

尽管我在之前的帖子中已经说的很详细了,但是在这里我还是想再提一点。使用 Let's Encrypt,我们有几种选择来取得证书。

# First we need the Let's Encrypt bin
sudo git clone https://github.com/letsencrypt/letsencrypt /opt/letsencrypt

有了 Let's EncryptOnce 软件包之后,我们可以用以下几种方法注册我们的域名。

# Get a cert for a domain (include all subdomains that apply to this file path, including www)
/opt/letsencrypt/letsencrypt-auto certonly --webroot \
  -d 'mysite.com,www.mysite.com' -w /var/www/mysite/public 

# Get a cert for a domain and subdomain with different filesystem paths 
/opt/letsencrypt/letsencrypt-auto certonly --webroot \
  -d 'mysite.com,www.mysite.com' -w /var/www/mysite/public \
  -d 'blog.mysite.com' -w /var/www/mysite_blog/ 

# Get a cert for an entirely new domain name
/opt/letsencrypt/letsencrypt-auto certonly --webroot \
  -d 'newsite.com,www.newsite.com' -w /var/www/newsite/public

更新 nginx 服务器

接下来,我们需要做一点 HTTPS 配置的修改,目的是使 nginx 从 域名+端口 认证方法切换到一种 ip+端口 的认证系统。这是在我们 HTTPS 服务器模块中改变监听(listen)指令最简单的配置方式 (并且在我的使用中一直没有问题) 。

server {  
    # Old method:
    # listen          443 ssl; 

    # Ip-based:
    listen          X.X.X.X:443 ssl;
    server_name:    mysite.com www.mysite.com;

    # ...
}

自动续期证书

最后,当我们在服务器上维护多个站点时,我们真的应该依赖某种方法自动更新我们的 Let's Encrypt 证书。没人愿意每三个月去手动更新一次。很幸运,Let's Encrypt 官方提供了一段代码来处理这个问题 ,而我们只需要创建一个计划任务,采用 cron 定时任务命令定期自动运行(这段代码) 。现在,我们创建一个文件  /etc/letsencrypt/auto_renew.sh。把下面的代码添加进来,你也可以自己做些修改,实现你想要的功能。

#!/bin/sh
# This script renews all the Let's Encrypt certificates with a validity < 30 days

if ! /opt/letsencrypt/letsencrypt-auto renew > /var/log/letsencrypt/renew.log 2>&1 ; then  
    echo Automated renewal failed:
    cat /var/log/letsencrypt/renew.log
    exit 1
fi  
nginx -t && nginx -s reload

现在将以上的脚本添加到计划任务, 执行 sudo crontab -e 添加下面一行代码:

@daily  /etc/letsencrypt/auto_renew.sh

隐私和安全问题

技术革新领域正在飞速发展,尽管这种发展在用户的隐私和安全方面并无助益。虽然没有明确言明,但是用户变成客户的过程在交互中已经隐含了双方的信任。不管是作为开发人员、工程师还是系统管理员,我们都有责任保障用户权益。

保障用户用 app 发出的任何连接都安全可靠是最佳方案。显然,没有一定程度上安全的连接难以轻松实现。Let's Encrypt 巧妙降低了进入开发领域的壁垒,成为开发人员亟需掌握的一步。由此可知,所有以避免为客户网站提供安全支持为目的的借口都是疏忽和无知。

希望本文对解决问题有所助益。

总结:如果你对从一个服务器托管多个 HTTPS 域名一无所知,使用 Let's Encrypt 和 Nginx 吧!轻松解决问题。