Caddy 2 使用 Caddyfile 部署博客网站

2020年04月01日 • 更新于 2020年09月29日

分享我的轻量级博客搭建方案,使用了 Caddy2 + Hugo + Remark42 架构,部署在阿里云的服务器上,通过 Cloudflare 管理 DNS 记录。

一切先从介绍 Caddy2 开始,我的 Caddy2 运行在 Debian 10 环境中。

Caddy

在传统的反向代理服务中,已经有了广受欢迎的 Nginx。为什么会涌现崭露头角的 Caddy 呢?

Caddy 的优势是帮我们自动维护 Let’s Encrypt 证书,轻松配置网站的 TLS。在最新的 Caddy 2 中支持 HTTP/3 轻而易举。 相比于 Nginx,这两点就足够打动人心了。目前 Caddy 2 的玩法也趋向于多样化,使用 Golang 编程可以胜任多种复杂需求。 Caddy 有志于成为新一代的 Web 服务。

我当前使用的 Caddy 版本为v2.0.0-beta.20,配置可能会随更新发生重大变化,不保证一直有效。 这里是我的 完整配置示例

安装 Caddy2 插件

插件集成在二进制文件 caddy 中,如果需要安装额外的 Caddy 插件的话,直接安装 Caddy2 是不够的。目前 Caddy2 没有 v1 版本时的快速安装脚本。 暂时可以用官方推荐工具 xcaddy

1
go get github.com/caddyserver/builder/cmd/xcaddy

获取 xcaddy 后,就可以用 xcaddy 在本地编译 caddy 了。

1
xcaddy build v2.0.0-beta.20 --with github.com/caddyserver/tls.dns/providers/cloudflare@master

xcaddy 命令需要指定编译的 Caddy 版本,截止目前的最新版本是v2.0.0-beta.20, 通过选项--with指定插件。v1 版本的 DNS Provider 插件在 v2 版本中无效,v2 使用新仓库github.com/caddyserver/tls.dns

但是,新版本的 Cloudflare DNS 插件无法从环境变量中获取 API Key 和 API Token 了,在 Caddyfile 中也没有指令可以设置 API Token,只能在 JSON 配置 中设置。但我不想使用 JSON 配置,所以我对插件源码做了一点小修改,可以使用我的 Fork 仓库 (dev 分支), 让新版本的 Cloudflare DNS 插件也能从环境变量中获取 Cloudflare API Token,这样就能继续愉快地使用 Caddyfile 了。

1
xcaddy build v2.0.0-beta.20 --with github.com/rydesun/tls.dns/providers/cloudflare@dev

该命令取代上面的命令,编译出的 caddy 能从环境变量中获取 Cloudflare API Token。

此处安装的 Cloudflare DNS Provider 插件会在下文中用到。不安装此插件也可以,但是使用 Cloudflare DNS 的话强烈建议安装。

运行后在本地生成了./caddy,上传至服务器,路径可以选择在/usr/local/bin/caddy2,下文我会使用此路径。

Caddy 服务

官方文档 已经提供了 Systemd 范例。 同时我需要作出一些修改,我选择让 Caddy 服务以 www-data 用户和用户组的身份运行,而不是新建一个用户名为 caddy 的用户。

Caddy2 有别于 v1 版本,v1 版本的证书路径使用变量CADDYPATH指定,约定为/etc/ssl/caddy;从 v2 版本起遵守 XDG 目录规范, 证书在$XDG_DATA_HOME/caddy目录。对于 www-data 用户来说,家目录是/var/www,所以证书在/var/www/.local/share/caddy; 如果以 caddy 用户运行,那就在/home/caddy/.local/share/caddy下面了。

不过,还是强烈建议通过新建用户 caddy 来使用 caddy。其一,可以和其他 HTTP 服务的权限分离,提升安全性;其二,证书在/var/www/中 不安全,容易误操作,比如将整个目录对外提供 HTTP 服务,从而导致泄漏。

而且,最好是添加额外的环境变量文件,在文件中存放 Cloudflare API Token 等环境变量。

1
EnvironmentFile=/etc/caddy/caddy.env

另外我的 Caddy2 配置文件使用 Caddyfile 而不是 JSON 配置。

我的 Systemd 服务完整范例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
[Unit]
Description=Caddy Web Server
Documentation=https://caddyserver.com/docs/
After=network.target

[Service]
User=www-data
Group=www-data
EnvironmentFile=/etc/caddy/caddy.env

ExecStart=/usr/local/bin/caddy2 run --config /etc/caddy/Caddyfile
ExecReload=/usr/local/bin/caddy2 reload --config /etc/caddy/Caddyfile

TimeoutStopSec=5s
LimitNOFILE=1048576
LimitNPROC=512
PrivateTmp=true
ProtectSystem=full
AmbientCapabilities=CAP_NET_BIND_SERVICE

[Install]
WantedBy=multi-user.target

该服务将从/etc/caddy中读取配置,所以需要提前准备/etc/caddy目录文件。

Caddyfile 配置

目前 Caddy2 官方推荐配置文件使用 JSON 格式,或者,不使用配置文件,直接调用 admin API。但我推荐使用 Caddyfile。

为什么要用 Caddyfile

在我看来,Caddy2 大力推广 JSON 配置和 admin API 的目的是为了让 Caddy 的配置更适应编程,面向 JSON 和 API 接口是 为了程序化动态管理 Caddy 配置。而且 JSON 格式能实现十分复杂的配置。可见 Caddy2 决心打下更大市场。

但是这一切不意味着 Caddy2 放弃了 Caddyfile,毕竟 Caddyfile 更适合人类编写和维护。我曾经尝试用 YAML 适配器 编写 YAML 格式的 Caddy 配置文件,虽然从结果上看能正常使用,但是维护起来相当麻烦,我甚至不想看第二遍… JSON 和 YAML 释放的强大能力我根本用不上。因为对于静态博客这种小网站来说,配置相对静止,不会有反复变动的需求, 所以使用简洁明快的 Caddyfile 更利于维护。 而且在掌握 Caddyfile 的编写后,完全可以从 Caddyfile 生成 JSON 格式的配置,就能满足更复杂的定制了。

从 Caddyfile 生成 JSON 配置

1
caddy adapt --config ./Caddyfile --pretty

验证 Caddyfile 有效性

1
caddy adapt --config ./Caddyfile --validate

Caddyfile 从 v1 升级至 v2

尽管 Caddy 官方反复强调 Caddyfile 升级到 v2 十分简单,但是仍需注意很多基础指令的用法发生了很多改变。 现在 Caddyfile 引入了全局选项global options的概念,并且如果全局选项存在,必须位于 Caddyfile 的开头。

你需要从 此处 参考 Caddyfile 的具体变动。 比如,现在 Caddy 提供静态文件,只使用root指令是不够的,你需要再加上file_server指令。

Let’s Encrypt 测试环境

第一个需要注意的是设置 Let’s Encrypt 的测试环境,可以在 Caddyfile 的全局选项中设置,

1
2
3
{
  acme_ca https://acme-staging-v02.api.letsencrypt.org/directory
}

不再像 v1 版本那样在命令行中指定了。

申请 Let’s Encrypt 证书有速率限制,而 Caddy 却是在失败后会反复申请的,所以很快就会达到速率上限。测试环境 相较宽松,容许 Caddy 获取证书时多次失败。

因为 Caddy 默认处在生产环境中,所以在使用任何新配置前务必加上测试环境

支持 HTTP/3

HTTP/3 工作在 UDP 协议上,所以阿里云服务器需要在安全组里放行 443 端口上的 UDP 数据。其他的服务器可以在 iptables 里确认是否放行。

Caddyfile 的全局选项里加上experimental_http3,HTTP/3 就有了。

1
2
3
{
  experimental_http3
}

很简单,对吧?现在 HTTP/3 特性尚处于实验性阶段,所以这条指令以后可能会发生变化。

HTTP 重定向

新网站通常需要考虑是否使用 www 前缀。

在 DNS 记录中,裸域名 (不带 www 前缀) 的 CNAME 记录是会和 MX 记录冲突的,但 A 记录可以和 MX 记录共存。我已经添加了裸域的 MX 记录; 虽然网上有关的讨论提到了一些办法可以绕过此限制,而且 Cloudflare 支持 CNAME Flattening, 但我不喜欢这些方法,何况现在的人们也不认为上网必须加上 www 了,对于个人网站来说没有必要考虑裸域带来的影响, 所以我的博客直接使用没有 www 前缀的裸域名没什么不妥。 那么,只需把带 www 的 URL 重定向到没有 www 前缀的裸域名就可以了。

1
2
3
www.example.com {
  redir https://example.com{uri}
}

该方式按照 Caddy 配置的逻辑,访问http://www.example.com会先触发 Caddy 的自动 HTTPS 规则,也就是 HTTP 重定向, 即先跳转至https://www.example.com;然后触发redir指令,跳转至https://example.com。也许有人会觉得为何 不直接配置成http://www.example.com跳转至https://example.com呢?

1
2
3
http://www.example.com, https://www.example.com {
  redir https://example.com{uri}
}

此时,即使www.example.comexample.com都启用了 HSTS,但是对http://www.example.com任何资源的请求都直接跳转到了example.com, 所以浏览器一直没有www.example.com的 HSTS 设置(HSTS 响应头是在 HTTPS 下才有效的)。虽然可以让example.com的 HSTS 响应头 加上includeSubDomains,从而让 www 域名也用上 HSTS,但该方法需要清楚自己是在做什么,会带来怎样的后果。 所以不该省的跳转最好不省。

启用 HSTS

在未使用 CDN 的情况下,要让 Caddy 启用 HSTS 很简单,在配置文件里加上

1
2
3
header {
    Strict-Transport-Security "max-age=31536000;"
}

就足够了。

要不要加上includeSubdomains呢?如果有加入 HSTS preload list 的需要,那就务必加上。 因为 Caddy 获取证书是如此的简单,所以我可以确定我所有的子域名都只用 HTTPS 协议。

最终的样子是

1
2
3
header {
    Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
}

使用 Cloudflare CDN 的情况

在 Cloudflare DNS 添加记录的时候,为了测试的便利,一般是先设置成 DNS only。需要启用 Cloudflare 的 CDN 服务时, 在 DNS 记录那里设置成 Proxied 就可以了,但需要注意, 在服务器上的 Caddy 获取证书后,Cloudflare 的加密模式应该使用Full SSL (Strict),也就是「完全(严格)」。其他几种 模式,Full SSL是给自签名证书使用的,由于 Caddy 使用 Let’s Encrypt 证书所以用不上。OFFFlexible SSL就更不用上了, OFF全程未加密,Flexible SSL在回源时未加密,都有被劫持的风险。

另外,上述的 HTTP/3,HTTP 重定向和 HSTS 设置是影响不了客户端和 CDN 的连接的,所以在启用 CDN 后会无效。 这些都需要在 CDN 的设置面板里额外设置。至于为什么要在 Caddy 中设置,是为了确保不使用 CDN 的情况下也能正常提供服务。

推荐使用 DNS challenge

Let’s Encrypt 支持三种验证方式:HTTP-01,TLS-ALPN-01,DNS-01。所谓验证,是为了确定是否对域名有控制权。 前两种分别需要开放主机端口 80 和 443,并在.well-known/acme-challenge/<TOKEN>提供特定的验证用文件; DNS-01 则需要在对应的 DNS 提供商那里添加临时的 TXT 记录。如果做不到,Let’s Encrypt 会认为对方没有实际控制权,所以不予颁发证书。

实际上,最好是让所有域名都使用 DNS challenge 的方式进行验证。首先,Caddy 能做到自动化添加 TXT 记录和获取证书。并且, 要考虑到并不是所有情况下都能开放 80 和 443 端口,因为国内的服务器上想要提供任何 HTTP 服务,是需要备案的。域名未备案的情况下, 这两个端口是不被允许使用的。另外,在启用 CDN 服务之后,客户端只会和 CDN 连接,Let’s Encrypt 的续期也会因为 CDN 受到影响。 这需要设置 CDN,让.well-known/acme-challenge/<TOKEN>不走 CDN 的 SSL,同时关闭 HTTP 重定向和 HSTS。(关于这一段我不是很清楚, 我在 Cloudflare 上的设置未能成功。)

所以综合来看,用 DNS challenge 的方式获取证书最轻松。更重要的是,之后要提到的通配符证书必须用 DNS challenge。 想让 Caddy 使用 DNS challenge,需要在 tls 指令中,指定 DNS 提供商。特定的 DNS 提供商需要特定的 Caddy 插件才能实现。 所以这就是我为什么在一开始在编译 Caddy 的时候选用了 Cloudflare DNS 插件。

同时,在 Cloudflare 网站生成 API Token 必不可少,Caddy 正是通过此 API Token 生成 TXT 记录的。 这里有 详细步骤,比较简单我就不重复了, 总之保证 API Token 有两个权限:

  • Zone / Zone / Read
  • Zone / DNS / Edit

至于个人网站,如果对安全不十分敏感的话,就没必要用 Zone API Token 了。

上文 中提到的 Systemd 服务配置已指定了环境变量文件, 所以要在该文件中设置CLOUDFLARE_DNS_API_TOKEN或者别名CF_DNS_API_TOKEN。 如果在启动服务后获取证书时出现超时错误,则需要另外设置一个环境变量CLOUDFLARE_PROPAGATION_TIMEOUT, 单位为秒。由于我的阿里云服务器在此方法下申请证书时总是出现超时错误,于是我索性设置成了 720,

/etc/caddy/caddy.env

1
2
CLOUDFLARE_DNS_API_TOKEN=HereYourToken
CLOUDFLARE_PROPAGATION_TIMEOUT=720

这样在 Caddyfile 中的设置就比较简单了

1
2
3
4
5
example.com {
  tls {
    dns cloudflare
  }
}

完整的 Caddyfile 示例

至此,我的 Caddyfile 的完整示例如下。acme_ca选项不应该出现在最终的配置文件中。

 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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
{
  # 开启实验性 HTTP/3
  experimental_http3
  # 测试通过的生产环境中去除该项
  acme_ca https://acme-staging-v02.api.letsencrypt.org/directory
}

(tls) {
  tls {
    dns cloudflare
  }
}
(common_headers) {
  encode gzip
}
(secure_headers) {
  header {
    Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
    X-Frame-Options SAMEORIGIN
    X-Content-Type-Options nosniff
  }
}

www.2cat.cc {
  import tls
  import common_headers
  import secure_headers
  redir https://2cat.cc{uri}
}

2cat.cc {
  import tls
  import common_headers
  import secure_headers
  root * /var/www/blog
  @immutable {
    path_regexp (^/css/|^/js/|^/images/)
  }
  header @immutable {
    Cache-Control "public, max-age=31536000"
  }
  file_server
}

通配符证书

我的评论服务部署在comments.srv.2cat.cc域名下, 考虑以后有可能为博客提供其他服务,所以服务都统一安排在srv.2cat.cc子域名下。 但我不想每次增加新的子域名就需要向 Let’s Encrypt 申请新证书,不然有太多证书需要维护了,有没有更简单的办法呢?答案是通配符证书 (泛域名证书),可以申请*.srv.2cat.cc证书,这样以后添加的新服务不用反复申请证书了,直接使用已经申请的通配符证书。但是需要 注意,Let’s Encrypt 提供的通配符证书只支持同级域名,也就是说,比如app.2cat.cccaptcha.test.srv.2cat.ccsrv.2cat.cc都是不被允许的。

通配符证书必须使用 DNS challenge 方式获取。按照 上文 配置好 DNS challenge 后,和往常一样, 在申请证书前记得先在 DNS 提供商添加 DNS 解析记录。 值得注意的是,Cloudflare 也支持*.srv.2cat.cc这样通配符形式的域名解析记录哦!

Caddy1 有wildcard指令,但在 Caddy2 Beta 里没有(不知道以后是否会加入)。可以用通配符域名取代普通域名,然后 用 named matchers 捕捉通配符域名的请求。

Caddyfile 里用通配符指定域名

1
2
3
4
5
*.srv.2cat.cc {
  tls {
    dns cloudflare
  }
}

这样 Caddy 会申请*.srv.2cat.cc通配符证书。接着使用 named matcher 捕获具体的域名,reverse_proxy 等指令也该加上 named matcher,比如

1
2
3
4
5
6
7
8
9
*.srv.2cat.cc {
  tls {
    dns cloudflare
  }
  @comments {
    host comments.srv.2cat.cc
  }
  reverse_proxy @comments localhost:8080
}

总结

以上 Caddy 的所有用法,都是目前 Caddy 2 beta 版本中采用的用法,不保证今后有效,需要自行甄别。

在完成了 Caddy 的介绍后,我会在本系列接下来的文章中说明使用 Hugo 的技巧。 似乎 Hugo 没什么内容是我想写的,所以就没 Hugo 的文章了。至于 本站已经在 Github 开源 啦! 想了解本站和 Hugo 的话,就直接前往源码吧 ~

CC BY-NC-SA 4.0

© 2020-2024 rydesun