Hugo 部署 Plan B 之小鸡部署 (nginx/github action/dns/ssl)

前几天黑五在大佬推荐下入了台服务器,年付 40 刀,4 core 4G 内存,每月 2T 的流量,性价比要高过一大批国内主机商。闲来无事打算给自己的博客换个新家。
之前博客部署一直用的 vercel 平台托管,虽然免费但是会有容量限制不是长久之计。

DNS 负载均衡方案探讨

但是 vercel 的服务器也不想直接扔了,一开始的思路采用循环 DNS 的方式部署站点,即在 DNS 服务器中配置多个 A 记录,一个到 vercel 的 ip,另一个到 crowncloud 的 ip。
后来发现 vercel 配置 www 域的方式用的 CNAME 记录,而 CNAME 记录不能和 A 记录同时启用,而我希望我博客的最终域名是带 www 三级域名的。又了解到循环 DNS 的缺陷,故放弃了循环 DNS 方案,索性直接将 vercel 作为镜像站保留。
最终站点如下:
主站点:nikunokoya.com
镜像站:vercel.nikunokoya.com

循环 DNS

Round-robin DNS 是基于 DNS 的负载均衡方案,它将多个 IP 地址映射到一个域名,每次请求时,DNS 服务器会将域名对应的 IP 地址返回给客户端,客户端会选择其中一个 IP 地址进行连接。
优势

  • 配置简单
  • 交给 DNS 提供商处理,不需要自己搭建负载均衡服务器

缺陷

  • 无法做到热备,如果有两台服务,如果其中一台服务挂了可能就有一半分用户无法访问
  • 不能提供均匀的负载均衡服务

服务器配置

debian 12

服务器系统选择了 Debian 12,还是熟悉的 apt 包管理器。需要注意的一点是 vps 的 locale 配置,crowncloud 提供的系统会配置中文 locale,建议修改为英文 en_US.UTF-8

debian locale wiki
arch locale wiki

创建用户

创建自己常用的用户,可以使用 adduser 比较方便,也可以使用 useradd 命令自主性更强。

区别
  • adduser 命令是 useradd 的前端会使用 perl 脚本中使用 useradd 命令创建用户,提供了较为丰富的交互功能,删除用户对应 deluser 命令,该命令并非所有发行版都可用。
  • useradd 如果不加参数仅仅是创建用户,不会创建用户的家目录,删除用户对应 userdel 命令。

Debian 系统默认没有 sudo 命令,需要手动安装 apt install sudo,通过 visudo 修改配置,visudo 默认使用 nano 编辑器,如果不喜欢 nano 可运行 apt purge nano 将 nano 包和配置删除。

sudoers 配置中 all 的含义
1
2
3
4
5
# User privilege specification
root	ALL=(ALL:ALL) ALL
niku    ALL=(ALL) NOPASSWD: ALL
# Allow members of group sudo to execute any command
%sudo	ALL=(ALL:ALL) ALL
  • %sudo 表示 sudo 组
  • 第一个 ALL 表示所有主机,如果有多个主机的同样一份 sudo 配置上为给用户授权
  • 括号中 (ALL:ALL) 第一个表示作为任何目标用户,第二个表示允许切换的组列表
  • 最后 ALL 表示无密码运行任何命令

更详细说明参见 man sudoers

ssh 配置

  1. 修改 ssh 端口
    sshd 的配置文件位于 etc/ssh/sshd_config,将 Port 22 修改为其他端口。
  2. 禁止 root 登录
    禁止 root 密码登入 PermitRootLogin no(以上操作在创建好自己的个人用户,并配置好 ssh 秘钥登录后再修改)。
  3. 单独禁止博客部署用户密码登入
    为了安全考虑我为博客服务单独创建了一个低权限的普通用户,通过该用户部署我的 hugo 编译好的 public 文件。
    1
    2
    
    Match User miku
        PasswordAuthentication no
    修改完 sshd 配置后记得重启 ssh 服务 sudo systemctl restart ssh
  4. 在本机生成 ssh 密钥对,将公钥上传到服务器
    1
    2
    3
    
    ssh-keygen -t ed25519 -f niku_cc_vps -C "ubuntu PC to niku@vps.nikunokoya.com"  
    # 公钥会添加到 `~/.ssh/authorized_keys` 文件中,如果没有该文件则创建。
    ssh-copy-id -i {{path/to/certificate}} -p {{port}} {{username}}@{{remote_host}}
  5. 修改 .ssh/authorized_keys 文件权限,仅所有者可读写 chmod 600 ~/.ssh/authorized_keys

安装 ufw 防火墙

为了防止自己疏忽启动了一些服务,导致服务器被攻击,打算还是安装一下 ufw 防火墙。

1
2
3
4
5
6
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow your_ssh_port/tcp comment 'SSH'
sudo ufw allow http
sudo ufw allow https
sudo ufw enable

fail2ban 配置

fail2ban 是一个防止暴力破解的工具,可以监控 ssh 登录失败的次数,超过一定次数后会将该 ip 加入到防火墙黑名单中。
配置 sshd 的 jail 规则,/etc/fail2ban/jail.d/sshd.conf。ssh 链接失败 5 次会 ban ip 5 分钟。

1
2
3
4
5
6
7
[sshd]
backend = systemd
enabled = true
port = your_port
filter = sshd
maxretry = 5
bantime = 300

注意这里的 backend 需要选择 systemd,否则日志配置会有冲突,详情见issue#3292

1
2
sudo fail2ban-client status #查看状态
sudo fail2ban-client status sshd #查看sshd的详细状态

nginx 配置

nginx 安装见文档 nginx: Linux packages
nignx 本身使用 root 启动 master 进程,默认使用 nginx 用户启动 worker 进程,并且 nginx 用户权限很低,没必要使用非 root 用户启动或者 chroot 隔离。

http 与链接配置

参考 archwiki 的建议。

 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
user  nginx;
worker_processes  auto;
worker_cpu_affinity auto;

error_log  /var/log/nginx/error.log notice;
pid        /var/run/nginx.pid;


events {
    multi_accept on;
    worker_connections  1024;
}


http {
    charset utf-8;
    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;
    server_tokens off;
    log_not_found off;
    types_hash_max_size 4096;
    client_max_body_size 16M;
    
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;

    keepalive_timeout  65;

    include /etc/nginx/conf.d/*.conf;
    include /etc/nginx/sites-enabled/*;
}

server 配置

创建两个目录方便管理 sites-available 中存放不同模块的配置文件,sites-enabled 中存放链接到 sites-available 的配置文件。

1
2
3
4
5
6
7
mkdir /etc/nginx/sites-available
mkdir /etc/nginx/sites-enabled

# 启动一个站点的配置文件
ln -s /etc/nginx/sites-available/example.conf /etc/nginx/sites-enabled/example.conf
# 停止一个站点的配置文件
unlink /etc/nginx/sites-enabled/example.conf

我的博客配置文件如下:
我打算不管 http 还是 https 协议,访问域名 nikunokoya.com 还是 www.nikunokoya.com 都能访问到我的博客,并且 https://www.nikunokoya.com 是最终的目的地址(因为我的不蒜子一直统计的带 www 的站点)。
因此将 80 端口与 443 端口做了 308 重定向到 https://www.nikunokoya.com,为何使用 308 而不是 301 见下面注释。

 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
server {
    listen 80; 
    server_name nikunokoya.com www.nikunokoya.com;
    return 308 https://www.nikunokoya.com$request_uri;
}

server { 
    server_name nikunokoya.com;
    return 308 https://www.nikunokoya.com$request_uri;

    listen 443 ssl; # managed by Certbot
    ssl_certificate /etc/letsencrypt/live/nikunokoya.com/fullchain.pem; # managed by Certbot
    ssl_certificate_key /etc/letsencrypt/live/nikunokoya.com/privkey.pem; # managed by Certbot
    include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
}

server {
    server_name www.nikunokoya.com;
    set $base ***;                                                              
    root $base/public;

    index index.html; # Hugo generates HTML

    location / {
        try_files $uri $uri/ =404;
    }
    error_page 404 /404.html;

    listen 443 ssl; # managed by Certbot
    ssl_certificate /etc/letsencrypt/live/nikunokoya.com/fullchain.pem; # managed by Certbot
    ssl_certificate_key /etc/letsencrypt/live/nikunokoya.com/privkey.pem; # managed by Certbot
    include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
}
308 重定向
浏览器会进行重定向,同时搜索引擎也会更新其链接(用 SEO 的行话来说,意思是“链接汁”(link juice)被传递到了新的 URL)有利于 SEO。 在重定向过程中,请求方法和消息主体不会发生改变,然而在返回 301 状态码的情况下,请求方法有时候会被客户端错误地修改为 GET 方法。

gzip 配置

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
gzip on;
gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript;

gzip_static on;
gzip_proxied any;
gzip_vary on;
gzip_comp_level 4;
gzip_buffers 16 8k;
gzip_min_length 1k;
gzip_http_version 1.1;
  • gzip_types:要采用 gzip 压缩的 MIME 文件类型,其中 text/html 被系统强制启用;
  • gzip_static:默认 off,该模块启用后,Nginx 首先检查是否存在请求静态文件的 gz 结尾的文件,如果有则直接返回该 .gz 文件内容;
  • gzip_proxied:默认 off,nginx做为反向代理时启用,用于设置启用或禁用从代理服务器上收到相应内容 gzip 压缩;
  • gzip_vary:用于在响应消息头中添加 Vary:Accept-Encoding,使代理服务器根据请求头中的 Accept-Encoding 识别是否启用 gzip 压缩;
  • gzip_comp_level:gzip 压缩比,压缩级别是 1-9,1 压缩级别最低,9 最高,级别越高压缩率越大,压缩时间越长,建议 4-6;
  • gzip_buffers:获取多少内存用于缓存压缩结果,16 8k 表示以 8k*16 为单位获得;
  • gzip_min_length:允许压缩的页面最小字节数,页面字节数从header头中的 Content-Length 中进行获取。默认值是 0,不管页面多大都压缩。建议设置成大于 1k 的字节数,小于 1k 可能会越压越大;
  • gzip_http_version:默认 1.1,启用 gzip 所需的 HTTP 最低版本;

禁止直接 ip 访问站点

自签一个 ssl 证书

1
2
mkdir /etc/nginx/ssl/
openssl req -x509 -nodes -days 3650 -newkey rsa:2048 -keyout /etc/nginx/self-signed-certs/default.key -out /etc/nginx/self-signed-certs/default.crt
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
server {
    listen 80 default_server;
    listen [::]:80 default_server;
    listen 443 ssl default_server;
    listen [::]:443 ssl default_server;
    
    server_name _;
    
    # 为 443 端口配置自签名证书(或任意证书),不要使用实际域名证书
    ssl_certificate /etc/nginx/self-signed-certs/default.crt;
    ssl_certificate_key /etc/nginx/self-signed-certs/default.key;

    # 返回 444 关闭连接,或者 403 禁止访问
    return 444;
}

遇到的问题,历史的 HSTS 缓存干扰

由于之前使用 vercel 部署的博客,而 vercel 中配置 HSTS,因此浏览器中缓存了 307 重定向,导致我在 nginx 中配置 308 重定向后,浏览器中依然是 307 重定向。
如何删除 chrome HSTS 缓存:
chrome://net-internals/#hsts

HSTS
HSTS的作用是强制客户端(如浏览器)使用HTTPS与服务器建立连接。服务器开启HSTS的方法是,当客户端通过HTTPS发出请求时,在服务器返回的超文本传输协议(HTTP)响应头中包含Strict-Transport-Security字段。非加密传输时设置的HSTS字段无效。
比如,https://example.com/ 的响应头含有Strict-Transport-Security: max-age=31536000; includeSubDomains。

certbot 申请 let’s encrypt 证书

certbot.eff.org
certbot 文档

1
sudo certbot --nginx -d your_domain -d www.your_domain

certbot 会自动配置 nginx 安装证书,并同时 your_domain 的证书包括了 www 域。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
sudo certbot certificates

Found the following certs:
  Certificate Name: nikunokoya.com
    Serial Number: ***
    Key Type: ECDSA
    Domains: nikunokoya.com www.nikunokoya.com
    Expiry Date: 2024-03-09 16:43:23+00:00 (VALID: 88 days)
    Certificate Path: ***
    Private Key Path: ***

certbot 会自动续签证书,sudo certbot renew --dry-run 测试无错误即可。

github action

github action 可以很方便的为仓库自定义一些 CI/CD 工作流。 当仓库接收到 push 动作时,会触发 github action 工作流。
github action 会启动一个 ubuntu 虚拟机,并检出仓库的全部代码,使用 peaceiris/actions-hugo@v2 action 配置完 hugo 环境后运行 hugo --minify 编译仓库代码,编译成功后使用 burnett01/rsync-deployments@6.0.0 执行 rsync 将编译好的 public 文件传到服务器。
我的博客仓库工作流如下:

 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
name: Deploy Hugo Site

on: push

jobs:
  build:
    runs-on: ubuntu-22.04
    concurrency:
      group: ${{ github.workflow }}-${{ github.ref }}
    steps:
      - uses: actions/checkout@v4
        with:
          submodules: true
          fetch-depth: 0

      - name: Setup Hugo
        uses: peaceiris/actions-hugo@v2
        with:
          hugo-version: '0.118.2'
          extended: true

      - name: Build
        run: hugo --minify

      - name: Deploy
        uses: burnett01/rsync-deployments@6.0.0
        with:
          switches: -avzr --delete
          path: public/
          remote_path: ${{ secrets.DEPLOY_PATH }}
          remote_host: ${{ secrets.DEPLOY_HOST }}
          remote_port: ${{ secrets.DEPLOY_PORT }}
          remote_user: ${{ secrets.DEPLOY_USER }}
          remote_key: ${{ secrets.DEPLOY_KEY }}
注意
  • concurrency 用于防止多次 push 时触发多次工作流,如果不设置则每次 push 都会触发一次工作流。
  • burnett01/rsync-deployments 需要配置 DEPLOY_KEY,该 key 为服务器的私钥,需要在服务器上配置对应的公钥,rsync 默认使用 sftp 的方式传输文件。

引用

GitHub Actions 文档
小鸡配置
How I automated the deployment of my blog using GitHub Actions
使用 Certbot 为 Nginx 自动配置 SSL 证书
Nginx 禁止 IP 访问并防止泄漏 SSL 证书
什么是循环 DNS?

0%