acme.sh与nginx
acme.sh可以自动从letsencrypt申请免费的ssl证书,方便我们搭建自己的小网站练手。本质上acme.sh使用acme协议请求letsencrypt的服务器下发证书。
acme协议要求证书申请方,即acme客户端完成验证(challenge),目的是证明申请者对域名的所有权。一般有两种方法,DNS challenge与HTTP challenge。DNS方法要求临时更改域名的 DNS 解析结果到指定的地址,相对难操作且影响较大。更加常用的验证方式是HTTP验证,原理是acme服务端(letsencrypt)接到申请后发送一个token和key给客户端,要求客户端将token通过一个特定的uri提供出来,服务端访问这个uri来验证客户端是否拥有域名(指向的服务器)的控制权。
因此在HTTP验证中,我们需要一个网页服务器,通过80端口,将对应的内容临时呈现给外部,让letsencrypt访问对应内容完成验证。acme.sh自动化了整个申请流程,包括HTTP验证,如果服务器上没有运行任何网页服务器,我们可以使用acme.sh的standalone模式临时运行一个网页服务器并监听80端口,完成验证流程。但如果你需要或者已经运行了网页服务器,比如nginx,在80端口被nginx占用的情况下我们没法使用standalone模式。acme.sh提供了nginx(以及apache)模式,通过临时修改nginx的配置文件,实现HTTP验证,并在验证通过后还原nginx配置。
理想很丰满,但我在实际操作中nginx模式总是报错。nginx本身的配置没有问题,外部通过80端口可以访问网页,但acme.sh在nginx下无法完成HTTP验证。nginx配置如下(与上一篇中的配置相同):
server {
# 两个端口分别监听http1.1和h2c
listen localhost:8080 default_server;
listen localhost:8081 default_server http2;
# HSTS设置
add_header Strict-Transport-Security "max-age=63072000" always;
server_name 博客域名;
gzip on;
location / {
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-Host $http_host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# 传递协议头https
proxy_set_header X-Forwarded-Proto https;
proxy_pass http://127.0.0.1:2368;
}
location ~ /.well-known {
allow all;
}
client_max_body_size 50m;
}
# 80端口接收请求后重定向到https
server {
listen 80;
return 301 https://$host$request_uri;
}
大概浏览acme.sh代码(我也不懂bash脚本怎么写的)以及多次测试后,我大致了解了acme.sh的nginx模式的运作方式,上面的配置文件存在几个问题(虽然在nginx看来并不是问题)导致acme.sh的nginx模式报错。
首先acme.sh会搜索nginx的配置文件,寻找需要注册证书的那个域名对应的配置,具体是通过搜索 server_name
字段实现的。如果你刚安装了nginx,还没有修改配置文件,默认配置中不会指定 server_name
。这时用浏览器访问域名能打开测试网页,但是acme.sh找不到对应域名的nginx配置,从而报错,无法完成验证。
其次,acme.sh在搜索配置时似乎只看 server_name
字段,而不管这一配置监听的是不是80端口。如果你在一个配置文件中写了两个不一样的 server
,分别监听不同端口,但是 server_name
设置相同(类似上面的配置,当然上例中第二个 server
未设置 server_name
),那acme.sh会选择第一个符合条件的 server
(即在前面的那一个)。即你的80端口的 server
不在第一个的情况下,acme.sh修改的 server
并不是监听80端口的那个,实际上没有达到效果,所以同样无法完成验证。
最后一个问题是监听80的 server
中301重定向的写法不合理,实际上nginx官方将这种写法列为常见的配置陷阱之一(最后一个)。上面配置文件中的301重定向目的是将对80端口的访问(即明文http请求)重定向到https开头的uri,实际上起到了强制tls加密的作用。在 server
中直接 return
是可以达到目的,但这并不是推荐的写法。acme.sh的nginx模式,会找到对应配置文件中的 server
,然后插入一段 location
,如下:
location ~ \"^/\.well-known/acme-challenge/([-_a-zA-Z0-9]+)\$\" {
default_type text/plain;
return 200 \"\$1.$_thumbpt\";
}
这种情况下,直接写在 server
中的 return
会覆盖 location
的效果,所以acme.sh无法完成验证从而报错。这里比较奇怪的是,我测试的时候用 --test
参数调用acme.sh是能正常下发证书的。但去掉 --test
就不行。修改办法就是将 return
写在一个 location
里面,限制了作用范围,就不会影响acme.sh的运行了。
修正了上面三个问题后,能够和acme.sh兼容的nginx配置文件如下:
server {
listen 80 default_server;
server_name 你的域名;
location / {
return 301 https://$host$request_uri;
}
}
server {
listen localhost:8008 default_server;
listen localhost:8009 default_server http2;
add_header Strict-Transport-Security "max-age=63072000" always;
server_name 你的域名;
gzip on;
root /usr/share/nginx/html;
location / {
}
error_page 404 /404.html;
location = /40x.html {
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
}
}
可见修改了上述的三点:监听80端口的 server
写在最前,里面添加 server_name
字段并设置为需要申请证书的域名,并且将 return
写在 location
中。这样的配置后,acme.sh终于可以正常使用nginx模式申请证书了,理论上证书的更新也不会受到影响。实际是否能自动更新还有待观察。