起因

  • 原先一直有一个腾讯云的1核和1G的服务器在闲着,只是偶尔用下frp来映射开发演示,这次想再利用下,于是试下搭建bitwarden,转而找到更加小巧的vaultwarden,但实际浏览器插件测试必须是要https才行,所以必须要有域名及证书,顺便就想试下certbot来自动续签。

  • 域名随便在阿里云上弄了个top的,10年¥189,要想解析到服务器是要备案的,所以又在腾讯云上备案,大约有7、8个工作日吧,备案才通过,此时可以正常访问了,然后还要30日内进行公安备案。

  • 由于服务器上的nginxfrpvaultwarden都是docker来部署的,那certbot也顺便就用docker,官方虽然提供了docker镜像,但实际上并不是很推荐使用,因为容器内比较难触发外部的一些操作,会导致一些自动配置的功能很难使用,参考官方文档:Running with Docker。没实际安装前个人也觉得用docker更方便,但实际操作完成后会发现还是官网说的对,没特殊情况不要用docker来做这些。

certbot

发放证书前需要验证域名的合法性,确保你申请证书的域名是你的,验证方式主要分两种:

  • 通过http请求验证,会向http://你的域名/.well-known/acme-challenge/发送一条请求。

  • 通过dns解析,需要在域名解析中增加一条_acme-challenge.你的域名的TXT记录用于验证。

certbot的命令有很多种不同的参数,这些参数也主要是为了自动生成http验证文件或自动添加dns解析等功能,主要的大体说下:

  • --manual,手动配置,你可以自己选择http验证还是dns验证都行,需要自己纯手工去配置http验证文件或是dns解析,个人没有做测试。

  • --nginx ,走http验证,如果不用docker且用nginx感觉是最方便的,会自动修改nginx的配置文件,增加http请求的访问配置及后续的ssl配置,但由于我们certbot是在docker容器内,所以要修改另一个nginx容器的配置文件及控制启停不太好实现,所以不采用这种方式。查资料时发现有人会将nginxcertbot通过dockerfile打包到一个镜像内,这样此命令就比较方便的执行了,感觉是个好办法,但个人没有测试了解。

  • --dns-xxx,走dns解析验证,这个也很方便,配置好后会自动在域名解析中增加一条记录进行域名验证,但官方目前只支持国外的一些dns服务商,国内的阿里云、dnspod等貌似也有第三方的插件,可以自己去github上找下。如果想申请通配符泛域名的,则只能通过此方式,目前个人没有测试。

  • --standalone,走http验证,这种方式你就算是没有nginx等服务,仍然可以验证域名并获取证书,它会虚拟一个服务来进行验证,但需要保证你的80端口别被占用,由于我这边有nginx了,所以便没有用此类方法测试。

  • --webroot,走http验证,已有类似nginx等服务的情况下可以用此方法,会自动在你配置的目录下生成http验证文件,与standalone相比好处是不需要预留80端口,也就是原有的网站不需要停止就可以申请证书,而缺点就是你还需要在nginx等服务中手动配置/.well-known/acme-challenge/使其对应到自动生成的验证文件上。后续我们用的就是这种。

通过webroot的方式运行后,会在两个地方产生文件:

  • 一个是在命令行配置的--webroot-path指定目录下产生临时文件,用于http验证。

  • 一个是验证成功会产生存放证书的文件,默认是在/etc/letsencrypt/live/你的域名目录下,需要注意的是这些证书是个软链接,对应着../archive下,所以我们在做volume映射时不要只映射到live这个目录,而是要映射/etc/letsencrypt这个目录,否则无法找到相关的证书文件。

以上两个文件虽然是certbot容器生成的,但我们nginx容器都会用到,所以我们需要用宿主机做为中转,在certbotnginx容器中都配置映射到宿主机的同一个目录,从而实现certbot容器和nginx容器共用文件。我的服务器/data/https-certbot下的目录可参照:

├── conf
│   ├── live //存放证书
│   └── archive //存放http验证的临时文件
├── nginx
│   ├── conf 
│   │   └── conf.d //配置文件
│   │       └── default.conf //配置文件
├── www // 存放网站web前端服务目录
└── docker-compose.yml //docker-compse文件

准备

开始

  • 主要是把volume映射弄好,顺便配置好nginx的相关http验证:

    • docker-compose.yml:

      version: "3.8"
      services:
        nginx:
          image: nginx:alpine
          container_name: nginx
          restart: unless-stopped
          network_mode: host
          #ports:
          #  - "80:80"
          #  - "443:443"
          volumes:
            - ./nginx/conf.d:/etc/nginx/conf.d
            - ./www:/usr/share/certbot/www
            - ./conf:/etc/letsencrypt
          depends_on:
            - certbot
        certbot:
          image: certbot/certbot
          container_name: certbot
          volumes:
            - ./www:/usr/share/certbot/www
            - ./conf:/etc/letsencrypt
          entrypoint: >
            sh -c "
            trap exit TERM;
            while :; do
              certbot renew --webroot -w /usr/share/certbot/www;
              sleep 12h;
            done
            "
    • nginx配置default.conf:

      server {
          listen 80;
          server_name www.xxkk.top;
      ​
          location /.well-known/acme-challenge/ {
              root /usr/share/certbot/www;
              default_type "text/plain";
              try_files $uri =404;
          }
      ​
          location / {
              return 200 "Nginx is running.\n";
          }
          
      }
      server {
          listen 443 ssl;
          server_name www.xxkk.top;
          ssl_certificate /etc/letsencrypt/live/www.xxkk.top/fullchain.pem;
          ssl_certificate_key /etc/letsencrypt/live/www.xxkk.top/privkey.pem;
          ssl_protocols       TLSv1.2 TLSv1.3;
          ssl_ciphers         'HIGH:!aNULL:!MD5';
          location /.well-known/acme-challenge/ {
              root /usr/share/certbot/www;
              default_type "text/plain";
              try_files $uri =404;
          }
          root /usr/share/certbot/www;
          index index.html;
          
          location / {
              try_files $uri $uri/ /index.html;
          }
      ​
          # 支付宝 API 代理
          location /aliPay/ {
              proxy_pass https://2dshuziren2.xxkk.top/aliPay/;
              proxy_set_header Host 2dshuziren2.xxkk.top;
              proxy_set_header X-Real-IP $remote_addr;
              proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
              proxy_set_header X-Forwarded-Proto $scheme;
              proxy_connect_timeout 60s;
              proxy_send_timeout 60s;
              proxy_read_timeout 60s;
          }
      ​
          # 微信支付 API 代理
          location /wxPay/ {
              proxy_pass https://2dshuziren2.xxkk.top/wxPay/;
              proxy_set_header Host 2dshuziren2.xxkk.top;
              proxy_set_header X-Real-IP $remote_addr;
              proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
              proxy_set_header X-Forwarded-Proto $scheme;
              proxy_connect_timeout 60s;
              proxy_send_timeout 60s;
              proxy_read_timeout 60s;
          }
      ​
          # 静态资源缓存
          location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
              expires 1y;
              add_header Cache-Control "public, immutable";
          }
      ​
          # Gzip 压缩
          gzip on;
          gzip_vary on;
          gzip_min_length 1024;
          gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml+rss application/javascript application/json;
      }
      server {
          listen 80;
          server_name doc.xxkk.top;
          location /.well-known/acme-challenge/ {
              root /usr/share/certbot/www;
              default_type "text/plain";
              try_files $uri =404;
          }
         location / {
              return 301 https://$server_name$request_uri;
          }
      }
      upstream doc {
      server 127.0.0.1:8090;
      }
      server {
        listen 443 ssl http2;
        ssl_certificate /etc/letsencrypt/live/doc.xxkk.top/fullchain.pem;
        ssl_certificate_key /etc/letsencrypt/live/doc.xxkk.top/privkey.pem;
        ssl_protocols TLSv1.2 TLSv1.3;
        ssl_ciphers HIGH:!aNULL:!MD5;
        ssl_prefer_server_ciphers on;       
        server_name doc.xxkk.top;
        client_max_body_size 1024m;
        location / {
          proxy_pass http://doc;
          proxy_set_header HOST $host;
          proxy_read_timeout 600;
          proxy_send_timeout 600;
          proxy_set_header X-Forwarded-Proto $scheme;
          proxy_set_header X-Real-IP $remote_addr;
          proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        } 
      }
      server {
          listen 80;
          server_name 2dshuziren2.xxkk.top;
          location /.well-known/acme-challenge/ {
              root /usr/share/certbot/www;
              default_type "text/plain";
              try_files $uri =404;
          }
         location / {
              return 301 https://$server_name$request_uri;
          }
      }
      upstream 2dshuziren2 {
      server 127.0.0.1:8088;
      }
      server {
        listen 443 ssl http2;
        ssl_certificate /etc/letsencrypt/live/2dshuziren2.xxkk.top/fullchain.pem;
        ssl_certificate_key /etc/letsencrypt/live/2dshuziren2.xxkk.top/privkey.pem;
        ssl_protocols TLSv1.2 TLSv1.3;
        ssl_ciphers HIGH:!aNULL:!MD5;
        ssl_prefer_server_ciphers on;       
        server_name 2dshuziren2.xxkk.top;
        client_max_body_size 1024m;
        location / {
          proxy_pass http://2dshuziren2;
          proxy_set_header HOST $host;
          proxy_read_timeout 600;
          proxy_send_timeout 600;
          proxy_set_header X-Forwarded-Proto $scheme;
          proxy_set_header X-Real-IP $remote_addr;
          proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        } 
      }
      upstream gptweb {
      server 127.0.0.1:8000;
      }
      server {
        listen 443 ssl http2;
        ssl_certificate /etc/letsencrypt/live/gptweb.xxkk.top/fullchain.pem;
        ssl_certificate_key /etc/letsencrypt/live/gptweb.xxkk.top/privkey.pem;
        ssl_protocols TLSv1.2 TLSv1.3;
        ssl_ciphers HIGH:!aNULL:!MD5;
        ssl_prefer_server_ciphers on;
        server_name gptweb.xxkk.top;
        client_max_body_size 1024m;
        location / {
          proxy_pass http://gptweb;
          proxy_set_header HOST $host;
          proxy_read_timeout 600;
          proxy_send_timeout 600;
          proxy_set_header X-Forwarded-Proto $scheme;
          proxy_set_header X-Real-IP $remote_addr;
          proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        }
      }
      upstream buy {
      server 127.0.0.1:8001;
      }
      server {
        listen 443 ssl http2;
        ssl_certificate /etc/letsencrypt/live/buy.xxkk.top/fullchain.pem;
        ssl_certificate_key /etc/letsencrypt/live/buy.xxkk.top/privkey.pem;
        ssl_protocols TLSv1.2 TLSv1.3;
        ssl_ciphers HIGH:!aNULL:!MD5;
        ssl_prefer_server_ciphers on;       
        server_name buy.xxkk.top;
        client_max_body_size 1024m;
        location / {
          proxy_pass http://buy;
          proxy_set_header HOST $host;
          proxy_read_timeout 600;
          proxy_send_timeout 600;
          proxy_set_header X-Forwarded-Proto $scheme;
          proxy_set_header X-Real-IP $remote_addr;
          proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        } 
      }
      ​
  • 上方的volumes映射串联关系如下,通过宿主机来实现certbotnginx中相关的目录实际上是同一个目录,除了certbot中的/etc/letsencrypt 是固定的外,其余的目录都是可以随便找目录放,只要对应起来就行。

    目录

    certbot

    宿主机

    nginx

    http验证目录

    /usr/share/certbot/www

    /data/https-certbot/www

    /usr/share/certbot/www

    证书目录

    /etc/letsencrypt

    /data/https-certbot/conf

    /usr/share/certbot/ssl

    mkdir -p /data/https-certbot/nginx/conf.d \
       /data/https-certbot/www/.well-known/acme-challenge \
       /data/https-certbot/conf
    cd /data/https-certbot/
    docker-compose down
    docker-compose up -d
  • 先把nginx开启,然后测试证书发放是否可以:

    # --dry-run是只测试不实际生成; --webroot-path对应着certbot内的http验证目录;-d后面是域名;--rm是运行后接着删除,certbot容器不需要一直开启,只是启动下生成证书即可
    echo test > /data/https-certbot/www/.well-known/acme-challenge/test.txt
    curl http://www.xxkk.top/.well-known/acme-challenge/test.txt
  • 输入邮箱等信息,如果提示The dry run was successful则说明成功,可以去掉--dry-run参数来进行实际的获取证书

    docker run --rm \
      -v /data/https-certbot/www:/usr/share/certbot/www \
      -v /data/https-certbot/conf:/etc/letsencrypt \
      certbot/certbot certonly \
      --webroot \
      --webroot-path /usr/share/certbot/www \
      -d www.xxkk.top \
      --non-interactive \
      --agree-tos \
      -m admin@xxkk.top
  • 如果上方正常,则会在certbot容器下的/etc/letsencrypt下也就是nginx容器下的/usr/share/certbot/ssl下产生证书,此时,只需要配置nginx配置ssl即可,还是在文件default.conf中:

    server {
        listen 80;
        server_name www.xxkk.top;
    ​
        location /.well-known/acme-challenge/ {
            root /usr/share/certbot/www;
            default_type "text/plain";
            try_files $uri =404;
        }
    ​
        location / {
            return 200 "Nginx is running.\n";
        }
        
    }
    server {
        listen 443 ssl;
        server_name www.xxkk.top;
        ssl_certificate /etc/letsencrypt/live/www.xxkk.top/fullchain.pem;
        ssl_certificate_key /etc/letsencrypt/live/www.xxkk.top/privkey.pem;
        ssl_protocols       TLSv1.2 TLSv1.3;
        ssl_ciphers         'HIGH:!aNULL:!MD5';
        location /.well-known/acme-challenge/ {
            root /usr/share/certbot/www;
            default_type "text/plain";
            try_files $uri =404;
        }
        root /usr/share/certbot/www;
        index index.html;
        
    ​
        location / {
            try_files $uri $uri/ /index.html;
        }
    ​
        # 支付宝 API 代理
        location /aliPay/ {
            proxy_pass https://2dshuziren2.xxkk.top/aliPay/;
            proxy_set_header Host 2dshuziren2.xxkk.top;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
            proxy_connect_timeout 60s;
            proxy_send_timeout 60s;
            proxy_read_timeout 60s;
        }
    ​
        # 微信支付 API 代理
        location /wxPay/ {
            proxy_pass https://2dshuziren2.xxkk.top/wxPay/;
            proxy_set_header Host 2dshuziren2.xxkk.top;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
            proxy_connect_timeout 60s;
            proxy_send_timeout 60s;
            proxy_read_timeout 60s;
        }
    ​
        # 静态资源缓存
        location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
            expires 1y;
            add_header Cache-Control "public, immutable";
        }
    ​
        # Gzip 压缩
        gzip on;
        gzip_vary on;
        gzip_min_length 1024;
        gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml+rss application/javascript application/json;
    }
    ​
  • 以上docker compose exec nginx nginx -t检测配置nginx文件是否有问题,docker compose exec nginx nginx -s reload或者是直接docker compose restart nginx来重启nginx

  • 正常启动的话应该就没问题了,我们接着需要做定期更新证书,主要就是为了这个,否则直接腾讯云、阿里云申请一年的免费证书好了。增加计划任务, sudo crontab -e打开定时任务编辑,每月1号和15号运行一次

    #1、切换到docker-compose.yml所在目录;2、然后更新证书,只有距离过期时间30天内才会真正成功;3、然后重载nginx使新证书生效
    0 0 1,15 * * cd /data/https-certbot && docker-compose run --rm certbot renew  && docker-compose exec nginx nginx -s reload
    #上方命令测试可以获取新证书,但是reload后实际测试新证书没有生效,可以换成下方的直接重启nginx试试,网上有人说reload可以,有人说必须restart,我这边的情况是第一次没自动生效,手动执行了reload后刷新浏览器可以了,但第二次同样没自动生效,手动reload也不行,restart后可以了,所以就很怪。而且都没自动生效,怀疑nginx的操作并没有等待renew执行完成后,可能在renew时间长在nginx重启之后才执行完成了。总之目前还没结论,所以这里可能还是不应该用docker,而应该用原生的钩子来触发比较好?
    0 0 1,15 * * cd /data/https-certbot && docker-compose run --rm certbot renew  && docker-compose restart nginx

    以上是计划任务相关。certbot容器中renew虽然有--deploy-hook等实际更新后触发钩子的功能,但由于和nginx不在同一个容器,因此无法触发nginx重启,所以上述命令不管是否证书真正更新成功nginx是一定会重启的,最好放到深夜执行。搜索了下从一个容器中控制另一个容器的操作,一种是本容器内装docker,然后映射宿主机内的docker.sock,这样容器内操作docker就和宿主机内操作一致了;还有一种就是ssh来连接另一个容器来操作了,这两种都挺麻烦的,这里就没必要也不做尝试了。

其它

  • certbot申请证书是有次数限制的,重复申请5次以上,再次请求会报too many certificates already issued...等错误,会停用一周后才能再继续,此时只能再换个域名或者是等一周了,参考速率限制

  • 除官网外的参考文档:HTTPS using Nginx and Let's encrypt in Docker,写的很详细。