Mythsman


I wonder how the world builds software.


纯Docker部署Https服务——以Nextcloud为例

背景

近期阿里云的廉价学生机小水管到期了,打算把一些服务打包迁移到微软员工不要钱的azure云上。

原先各种服务的部署方式都是直接部署的,换主机需要重新搭建各种环境非常麻烦,也容易遗漏。于是就想跟随潮流把这些服务都上docker,能够做到部署一把梭。

难点

一般的开源组件都会有官方docker镜像,部署起来其实都很方便。大部分对着官方文档改改配置再用docker-compose组合一下参数就能跑起来。比较麻烦的点就是网站要部署成 https 的话需要 SSL 证书。对于个人使用的小网站来说,云厂商的 SSL 证书又实在是比较贵(阿里云上通配符域名大概2000¥/年,单域名也要400¥/年)。

比较经济实惠的做法是使用 Let’s Encrypt 的免费证书,不过代价就是他需要定时check你对这个域名的所有权。显然,我们肯定不能手动更新,那样简直要疯。一般我们会用 certbot 来定时进行站点所有权的确认。经常对接站长后台工具的人应该都知道,认证的方式一般有两种:一种是将给定的验证字符串写进 DNS 的 TXT 记录,从而确认你对这个域名的所有权;第二种是将给定的验证字符串写在网站的给定目录下,从而确认你对这个网站的所有权。

理论上最方便的做法是通过一个定时任务,定时调用 DNS 服务商的接口来改 TXT 记录。这样可以做到将 SSL 证书的校验和网站本身的部署分离开,使得校验所有权的逻辑不干扰正常的网站配置。不过尴尬的是 certbot 提供的 DNS插件 基本都不包含国内的运营商。而考虑到域名在国内解析的性能,我还是不太想把域名切到国外的服务商去。

因此事实上我只能采用 webroot 的方式来认证。这个方式比较万金油,不过缺点就是不支持通配符域名,如果需要同时验证多个子域名,则需要手动添加。同时,传统的 certbot 使用方式中一定还需要在系统中配置一个 crontab 任务来做更新,而我现在则希望将这个 crontab 任务也集成在 docker 中,尽量不要对除 docker 外的逻辑做任何感知。

步骤

逻辑上讲,步骤大致应当如下:

  1. 去 DNS 服务商提供的配置后台,将给定域名配置好A记录指向目的主机。
  2. 安装 docker  docker-compose ,并设置好配置文件。
  3. 配置并启动 nginx 只开放 http 端口 ,准备 certbot 的认证环境。
  4. 启动 certbot 初始化,配合nginx,生成首个私钥和证书链。
  5. 利用 certbot 提供的私钥和证书,配置 nginx 的 https 端口。
  6. 配置 certbot 的自动 renew ,进行自动验证。

样例

下面以在 https://pan.mythsman.com 下配置 nextcloud 为例。 DNS 配置、docker 和 docker-compose 安装等步骤略过。

初始化docker配置

./docker-compose.yml

version: "3.8"
services:
    nginx:
        container_name: nginx
        image: nginx:latest
        restart: always
        volumes:
            - ./nginx/logs:/var/log/nginx
            - ./nginx/conf.d:/etc/nginx/conf.d
            - ./certbot/conf:/etc/nginx/ssl
            - ./certbot/data:/var/www/certbot
        ports:
            - "80:80"
            - "443:443"
        command: ["/bin/sh", "-c", "while :;do sleep 24h & wait $${!}; nginx -s reload; done & nginx -g \"daemon off;\""]

    certbot:
        container_name: certbot
        image: certbot/certbot:latest
        command: certonly --webroot --webroot-path=/var/www/certbot --agree-tos --email mythsman@foxmail.com -d  pan.mythsman.com
        # entrypoint: ["/bin/sh", "-c", "trap exit TERM;while :; do certbot renew --webroot -w /var/www/certbot; sleep 24h & wait $${!}; done;"]
        volumes:
            - ./certbot/conf:/etc/letsencrypt
            - ./certbot/logs:/var/log/letsencrypt
            - ./certbot/data:/var/www/certbot

说明:

  1. ./certbot/conf 目录存放的即是 certbot 生成的证书等一大堆文件,因此需要挂载给 nginx 。
  2. ./certbot/logs 目录下存放的是证书创建相关的日志。
  3. ./certbot/data 目录是 certbot 用来存放校验字符串的,需要通过nginx对外网暴露,因此也需要挂在给 nginx。
  4. ./nginx/logs 目录存放的即是 nginx 的相关日志。
  5. ./nginx/conf.d 目录存放 nginx 的配置,下面会介绍。
  6. nginx 的 command 指令写了一个小脚本,用来让 nginx 定时自动 reload,方便 certbot 刷新证书后 nginx 能及时更新。
  7. certbot 的 command 指令中需要配置接受邮箱,用来接受一些通知消息,以及需要配置的域名 pan.mythsman.com
  8. certbot 的 entrypoint 指令是用来后续 renew 的,暂时注释掉。

初始化nginx配置

./nginx/conf.d/default.conf

server {
     listen [::]:80 default_server;
     listen 80 default_server;

     server_name _;

     location ~ /.well-known/acme-challenge {
         allow all;
         root /var/www/certbot;
     }

     location / {
         if ( $host = "pan.mythsman.com" ){
             return 301 https://pan.mythsman.com$request_uri;
         }
         return 444;
     }

}

server {
       listen 443 default_server;
       listen [::]:443 default_server;
       server_name _;
       ssl_reject_handshake on;
       ssl_session_tickets off;
}

说明:

  1. 这里最关键的是 .well-known 行,用来关联 certbot 的校验文件。
  2. return 444 是用来拒绝掉一些未知域名的http访问。
  3. ssl_reject_handshake 是用来拒绝掉一些未知域名的https的访问。

生成证书

执行 docker-compose up -d ,证书生成成功后会在 ./certbot/logs/letsencrypt.log 打印相关日志:

2022-03-09 03:41:39,879:DEBUG:certbot._internal.storage:Creating directory /etc/letsencrypt/archive.
2022-03-09 03:41:39,879:DEBUG:certbot._internal.storage:Creating directory /etc/letsencrypt/live.
2022-03-09 03:41:39,879:DEBUG:certbot._internal.storage:Writing README to /etc/letsencrypt/live/README.
2022-03-09 03:41:39,879:DEBUG:certbot._internal.storage:Creating directory /etc/letsencrypt/archive/pan.mythsman.com.
2022-03-09 03:41:39,879:DEBUG:certbot._internal.storage:Creating directory /etc/letsencrypt/live/pan.mythsman.com.
2022-03-09 03:41:39,880:DEBUG:certbot._internal.storage:Writing certificate to /etc/letsencrypt/live/pan.mythsman.com/cert.pem.
2022-03-09 03:41:39,880:DEBUG:certbot._internal.storage:Writing private key to /etc/letsencrypt/live/pan.mythsman.com/privkey.pem.
2022-03-09 03:41:39,880:DEBUG:certbot._internal.storage:Writing chain to /etc/letsencrypt/live/pan.mythsman.com/chain.pem.
2022-03-09 03:41:39,880:DEBUG:certbot._internal.storage:Writing full chain to /etc/letsencrypt/live/pan.mythsman.com/fullchain.pem.
2022-03-09 03:41:39,880:DEBUG:certbot._internal.storage:Writing README to /etc/letsencrypt/live/pan.mythsman.com/README.
2022-03-09 03:41:39,909:DEBUG:certbot._internal.plugins.selection:Requested authenticator webroot and installer <certbot._internal.cli.cli_utils._Default object at 0x7f94027a1370>
2022-03-09 03:41:39,909:DEBUG:certbot._internal.cli:Var authenticator=webroot (set by user).
2022-03-09 03:41:39,909:DEBUG:certbot._internal.cli:Var webroot_path=/var/www/certbot (set by user).
2022-03-09 03:41:39,909:DEBUG:certbot._internal.cli:Var webroot_path=/var/www/certbot (set by user).
2022-03-09 03:41:39,909:DEBUG:certbot._internal.cli:Var webroot_map={'webroot_path'} (set by user).
2022-03-09 03:41:39,909:DEBUG:certbot._internal.storage:Writing new config /etc/letsencrypt/renewal/pan.mythsman.com.conf.
2022-03-09 03:41:39,911:DEBUG:certbot._internal.display.obj:Notifying user:
Successfully received certificate.
Certificate is saved at: /etc/letsencrypt/live/pan.mythsman.com/fullchain.pem
Key is saved at:         /etc/letsencrypt/live/pan.mythsman.com/privkey.pem
This certificate expires on 2022-06-07.
These files will be updated when the certificate renews.
2022-03-09 03:41:39,911:DEBUG:certbot._internal.display.obj:Notifying user: NEXT STEPS:
2022-03-09 03:41:39,912:DEBUG:certbot._internal.display.obj:Notifying user: - The certificate will need to be renewed before it expires. Certbot can automatically renew the certificate in the background, but you may need to take steps to enable that functionality. See https://certbot.org/renewal-setup for instructions.
2022-03-09 03:41:39,913:DEBUG:certbot._internal.display.obj:Notifying user: If you like Certbot, please consider supporting our work by:
 * Donating to ISRG / Let's Encrypt:   https://letsencrypt.org/donate
 * Donating to EFF:                    https://eff.org/donate-le

也可以通过 nginx 的访问日志 /nginx/logs/access.log  查看他在校验的过程中访问了哪些页面:

3.120.130.29 - - [09/Mar/2022:03:41:36 +0000] "GET /.well-known/acme-challenge/fNVMV-CtDCdq_eAdiaV0rF_J7I-ZW38M7Bo3UqEtOY4 HTTP/1.1" 200 87 "-" "Mozilla/5.0 (compatible; Let's Encrypt validation server; +https://www.letsencrypt.org)" "-"
3.19.56.43 - - [09/Mar/2022:03:41:36 +0000] "GET /.well-known/acme-challenge/fNVMV-CtDCdq_eAdiaV0rF_J7I-ZW38M7Bo3UqEtOY4 HTTP/1.1" 200 87 "-" "Mozilla/5.0 (compatible; Let's Encrypt validation server; +https://www.letsencrypt.org)" "-"
34.221.255.206 - - [09/Mar/2022:03:41:36 +0000] "GET /.well-known/acme-challenge/fNVMV-CtDCdq_eAdiaV0rF_J7I-ZW38M7Bo3UqEtOY4 HTTP/1.1" 200 87 "-" "Mozilla/5.0 (compatible; Let's Encrypt validation server; +https://www.letsencrypt.org)" "-"
64.78.149.164 - - [09/Mar/2022:03:41:36 +0000] "GET /.well-known/acme-challenge/fNVMV-CtDCdq_eAdiaV0rF_J7I-ZW38M7Bo3UqEtOY4 HTTP/1.1" 200 87 "-" "Mozilla/5.0 (compatible; Let's Encrypt validation server; +https://www.letsencrypt.org)" "-"

配置站点容器和证书更新

./docker-compose.yml

version: "3.8"
services:
    nginx:
        container_name: nginx
        image: nginx:latest
        restart: always
        volumes:
            - ./nginx/logs:/var/log/nginx
            - ./nginx/conf.d:/etc/nginx/conf.d
            - ./certbot/conf:/etc/nginx/ssl
            - ./certbot/data:/var/www/certbot
        ports:
            - "80:80"
            - "443:443"
        command: ["/bin/sh", "-c", "while :;do sleep 24h & wait $${!}; nginx -s reload; done & nginx -g \"daemon off;\""]

    certbot:
        container_name: certbot
        image: certbot/certbot:latest
        # command: certonly --webroot --webroot-path=/var/www/certbot --agree-tos --email mythsman@foxmail.com -d  pan.mythsman.com
        entrypoint: ["/bin/sh", "-c", "trap exit TERM;while :; do certbot renew --webroot -w /var/www/certbot; sleep 24h & wait $${!}; done;"]
        volumes:
            - ./certbot/conf:/etc/letsencrypt
            - ./certbot/logs:/var/log/letsencrypt
            - ./certbot/data:/var/www/certbot
    nextcloud:
        container_name: nextcloud
        image: nextcloud:latest
        volumes:
            - ./nextcloud:/var/www/html
        ports:
            - "8080:80"

说明:

  1. 这里将 cerbot 的 command 注释掉,更换了 entrypoint 的启动脚本,用来每天自动更新 cerbot 证书。
  2. 新增了 nextcloud 的镜像。

配置站点的Nginx

./nginx/conf.d/pan.mythsman.com-ssl.conf

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;

    server_name pan.mythsman.com;

    ssl_certificate /etc/nginx/ssl/live/pan.mythsman.com/fullchain.pem;
    ssl_certificate_key /etc/nginx/ssl/live/pan.mythsman.com/privkey.pem;

    ssl_stapling on;
    ssl_stapling_verify on;

    location / {
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header Host $http_host;
        proxy_pass http://172.17.0.1:8080;
    }

    client_max_body_size 10G;

}

说明:

  1. proxy_pass 中的 172.17.0.1 为 host 主机在 docker 中访问的 ip,8080端口暴露的即为前文设置的 nextcloud 的服务。
  2. ssl_certificate 和 ssl_certificate_key 配置为前文 certbot 生成的文件。
  3. client_max_body_size 10G; 配置是增大上传文件的大小限制。

重新部署

执行 docker-compose down 和  docker-compose up -d ,重新部署各个服务。部署完成后,可以查看 certbot 的日志:

2022-03-09 06:27:54,847:DEBUG:certbot._internal.main:certbot version: 1.24.0
2022-03-09 06:27:54,848:DEBUG:certbot._internal.main:Location of certbot entry point: /usr/local/bin/certbot
2022-03-09 06:27:54,848:DEBUG:certbot._internal.main:Arguments: ['--webroot', '-w', '/var/www/certbot']
2022-03-09 06:27:54,848:DEBUG:certbot._internal.main:Discovered plugins: PluginsRegistry(PluginEntryPoint#manual,PluginEntryPoint#null,PluginEntryPoint#standalone,PluginEntryPoint#webroot)
2022-03-09 06:27:54,863:DEBUG:certbot._internal.log:Root logging level set at 30
2022-03-09 06:27:54,865:DEBUG:certbot._internal.display.obj:Notifying user: Processing /etc/letsencrypt/renewal/pan.mythsman.com.conf
2022-03-09 06:27:54,883:DEBUG:certbot._internal.plugins.selection:Requested authenticator webroot and installer <certbot._internal.cli.cli_utils._Default object at 0x7f64a6694f10>
2022-03-09 06:27:54,883:DEBUG:certbot._internal.cli:Var authenticator=webroot (set by user).
2022-03-09 06:27:54,883:DEBUG:certbot._internal.cli:Var webroot_path=/var/www/certbot (set by user).
2022-03-09 06:27:54,883:DEBUG:certbot._internal.cli:Var webroot_map={'webroot_path'} (set by user).
2022-03-09 06:27:54,883:DEBUG:certbot._internal.cli:Var webroot_path=/var/www/certbot (set by user).
2022-03-09 06:27:54,907:DEBUG:urllib3.connectionpool:Starting new HTTP connection (1): r3.o.lencr.org:80
2022-03-09 06:27:55,136:DEBUG:urllib3.connectionpool:http://r3.o.lencr.org:80 "POST / HTTP/1.1" 200 503
2022-03-09 06:27:55,137:DEBUG:certbot.ocsp:OCSP response for certificate /etc/letsencrypt/archive/pan.mythsman.com/cert1.pem is signed by the certificate's issuer.
2022-03-09 06:27:55,138:DEBUG:certbot.ocsp:OCSP certificate status for /etc/letsencrypt/archive/pan.mythsman.com/cert1.pem is: OCSPCertStatus.GOOD
2022-03-09 06:27:55,141:DEBUG:certbot._internal.display.obj:Notifying user: Certificate not yet due for renewal
2022-03-09 06:27:55,142:DEBUG:certbot._internal.plugins.selection:Requested authenticator webroot and installer None
2022-03-09 06:27:55,142:DEBUG:certbot._internal.display.obj:Notifying user:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
2022-03-09 06:27:55,142:DEBUG:certbot._internal.display.obj:Notifying user: The following certificates are not due for renewal yet:
2022-03-09 06:27:55,142:DEBUG:certbot._internal.display.obj:Notifying user:   /etc/letsencrypt/live/pan.mythsman.com/fullchain.pem expires on 2022-06-07 (skipped)
2022-03-09 06:27:55,142:DEBUG:certbot._internal.display.obj:Notifying user: No renewals were attempted.
2022-03-09 06:27:55,143:DEBUG:certbot._internal.display.obj:Notifying user: - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
2022-03-09 06:27:55,143:DEBUG:certbot._internal.renewal:no renewal failures

没有报错说明流程就是通的,不过由于这个证书是刚申请的,离过期还很远,因此在renew的时候显示不需要更新。如果是想强制更新的话,可以在 certbot 的参数中加 --renew-by-default 选项。如果想确认证书的过期时间,可以通过 openssl 命令查看:

$ openssl x509 -in cert.pem -noout -dates
notBefore=Jan 30 03:00:32 2022 GMT
notAfter=Apr 30 03:00:31 2022 GMT

这时候访问 pan.mythsman.com 即可发现 https 证书已经OK了。

新增子域名

有时候,我们可能想在 nginx 里新增个子域名,这时候就需要把整个 certbot/ 文件夹删除,然后全部重新来一遍。

Nextcloud的其他配置

配置好 https 后,nextcloud有时还不认得自己的scheme已经是https了,这里最好需要修改一下配置。

./nextcloud/config/config.php 新增一行:

'overwriteprotocol' => 'https',

参考资料

Nginx & Certbot (Letsencrypt) via Docker

Certbot User Guide

Can’t use webroot authenticator needed for wildcard domain