前言

想了老半天终于还是把博客搬到国内服务器上了,简单的记录一下搬迁过程。主要记录了证书申请&管理,nginx部署,顺便附带一些服务器管理和防爆破的小技巧。

部署实践

环境准备

服务器

从阿里云购买云服务器一台,配置如下:

  • 实例规格: ecs.e-c1m1.large
  • CPU: 2vCpu
  • 内存: 2GiB
  • 带宽: 3Mbps
  • 存储: 40GiB
  • 操作系统: Alibaba Cloud Linux 3.2104 LTS 64位 UEFI版

我在安装操作系统时选了禁止root登录,所以需要知道root和ecs-user密码,其中root密码用于执行sudo命令。

因为是需要作为网站的入口使用,所以需要开放80和443端口。具体步骤为 实例详情页-进入安全组tab-管理规则,在规则管理页面,添加入方向的80和443规则,开放给所有IP。可以点击快速添加,勾选80和443。

开放安全组

修改主机名(可选)
执行命令hostnamectl set-hostname ${HOST_NAME},其中HOST_NAME更换为需要更改的主机名 修改后可通过 hostname 验证,并重启验证是否永久生效

Nginx

使用nginx作为站点的HTTP容器。

登录服务器后,执行 sudo yum install nginx 进行安装,遇到提示后按y表示同意安装,如果需要输入root密码则输入root密码。

输入命令 nginx -v 来验证nginx是否安装完成,输出版本即为安装完成,比如我安装的是 1.20.1 ,则输出为 nginx version: nginx/1.20.1

启动测试(可选)
输入命令sudo nginx,来启动nginx。可以通过 ps -ef|grep nginx 命令来验证进程是否启动。如正常启动,可以看到一个root用户启动的master进程和若干个nginx用户启动的worker进程。(数量取决于核心数,由配置项 worker_processes auto; 控制) 如果需要在浏览器中验证页面是否可以访问,需要在阿里云的安全组中放行80端口。可以访问的话可以在浏览器中看到一个经典的Nginx小白页面。

Hugo部署

首先,需要规划一下我们网站部署的目录,以便修改nginx的配置。这里简单介绍一下nginx如何配置静态网站。我们的网站都被打包成静态文件放在服务器的某个目录里,而nginx可以通过指定root到这个目录来实现访问,网站的入口就是这个目录的index.html 。在Hugo中,这个目录就是运行hugo server后生成的 public/ 目录。所以,部署只需要把 public/ 目录放到服务器上,然后修改nginx的配置文件就好,网站的更新也只需要更新这个目录,而不需要修改nginx配置。 但是考虑到增量部署的场景,不断在目录中更新是也可以解决文件的新增和修改,无法删除文件。所以,在我的规划中,我每一次的部署都会放一个将打包出来的内容放到一个新的目录中。为了不修改nginx的配置,并保证目录切换的时间足够短(提供高的可访问性,虽然必要性不大),我对比了三个方案,最终选择在自动部署方案中采用了软链接的方式。

  • 先删除后上传:上传时间较长,会有比较长时间的一段无法提供服务的时间
  • 先上传后删除:一个相当比较好的方案,具体步骤为 上传新版本-删除老版本-重命名新版本,但是上一个版本删除后无法回滚,不过一般也用不到,再部署修复即可。(这里也可以把删除老版本改为重命名,但是依然需要做历史版本的管理)
  • 软链接:具体步骤为 上传新版本-修改软链接-删除老版本。在更新了软连接好就可用了。
    使用Git的版本控制来实现更新
    其实使用git更新也是一个非常好的方案,维护成本也不高。只是长期使用git的历史堆积会使得文件夹变得比较大,需要定期清理历史版本信息来控制文件大小。
    最初方案下图所示:
    部署方案
    文件权限说明
    /website/website/data/website/data/blog_xxx 的属主为ecs-user,权限为755,需要保证nginx的读和部署用户ecs-user的读写。

nginx配置修改

按照规划,更改nginx的配置,在 /etc/nginx/default.d/ 下新建文件 blog.conf 输入以下内容:

[ conf ]
server_name ${YOUR_DOMAIN} # 替换为自己的域名
location / { 
	root /website/blog/;
	index index.html;
}

这段配置用于将root指向 /website/blog 目录,同时,需要删除原本 nginx.conf 中的内容,注释即可,可参考以下代码。

/etc/nginx/nginx.conf
listen       80;
listen       [::]:80;
#server_name  _;
#root         /usr/share/nginx/html;

# Load configuration files for the default server block.
include /etc/nginx/default.d/*.conf;

#error_page 404 /404.html;
#    location = /40x.html {
#}

#error_page 500 502 503 504 /50x.html;
#    location = /50x.html {
#}
}

还需要更改 nginx.conf 中的user为 /website/blog 目录的属主。

手动部署

手动部署不做赘述,将本地的 public/ 拉到服务器的 /website/blog 目录下就可以看到网站了(注意,是要用 hugo serve 重新生成过的,不是开发的直接拿上去)。

自动部署(CI/CD集成)

自动部署要做的事情其实不也复杂,就是在Github Action中添加一步,把 public/ 部署到服务器上,然调用服务器上的脚本就可以了。 先来写一个脚本,用于实现软链接的创建/更新和历史版本数量的管理,脚本位于服务器的某个目录就可以了,我放在了 ~/scripts/ 下。

~/scrpits/deploy_blog.sh
 1#!/bin/bash
 2
 3# 指定目录路径
 4DIRECTORY="/website/data/blog"
 5# 软连接路径
 6SOFT_LINK="/website/blog"
 7
 8# 获取所有符合 blog_xxxx 格式的目录,并按创建时间排序
 9directories=($(ls -d "$DIRECTORY"/blog_* | xargs -I {} stat --format '%Y {}' {} | sort -n | cut -d' ' -f2-))
10
11# 获取目录数量
12num_directories=${#directories[@]}
13
14# 如果存在目录,则更新或新建软连接到最新创建的目录
15if [ $num_directories -gt 0 ]; then
16    latest_directory=${directories[-1]}
17	echo "将使用目录 $latest_directory 作为工作目录"
18    # 如果软连接不存在,则新建软连接
19    if [ ! -L "$SOFT_LINK" ]; then
20        ln -s "$latest_directory" "$SOFT_LINK"
21    else
22        # 如果软连接已存在,则更新软连接
23        ln -sfn "$latest_directory" "$SOFT_LINK"
24    fi
25fi
26
27# 如果目录数量大于2,则删除最早创建的目录,直到只剩下2个
28while [ $num_directories -gt 2 ]; do
29    # 删除最早创建的目录
30	echo "删除目录 ${directories[0]}"
31    rm -rf "${directories[0]}"
32    # 更新目录列表
33    directories=("${directories[@]:1}")
34    num_directories=$((num_directories-1))
35done
36
37# 如果目录数量少于或等于2,不需要进一步操作
38if [ $num_directories -le 2 ]; then
39    echo "目录数量符合要求,无需进一步操作。"
40else
41    echo "操作完成,现在目录数量为 $num_directories。"
42fi
43
44chmod -R 755 "$DIRECTORY"/blog_*

写好部署脚本之后,需要添加执行权限 chmod +x deploy_blog.sh ,然后,更新一下Github Action的流水线定义文件,添加了两个步骤。

.github/workflow/hugo.yaml
 1# Sample workflow for building and deploying a Hugo site to GitHub Pages
 2name: Deploy Hugo site to Pages
 3
 4on:
 5  # Runs on pushes targeting the default branch
 6  push:
 7    branches: ["main"]
 8
 9  # Allows you to run this workflow manually from the Actions tab
10  workflow_dispatch:
11
12# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
13permissions:
14  contents: read
15  pages: write
16  id-token: write
17
18# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
19# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
20concurrency:
21  group: "pages"
22  cancel-in-progress: false
23
24# Default to bash
25defaults:
26  run:
27    shell: bash
28
29jobs:
30  # Build job
31  build:
32    runs-on: ubuntu-latest
33    env:
34      HUGO_VERSION: 0.139.3
35    steps:
36      - name: Get current date
37        id: gen-id
38        run: echo "::set-output name=time::$(date +'%Y%m%d%H%M%s')"
39      - name: Install Hugo CLI
40        run: |
41          wget -O ${{ runner.temp }}/hugo.deb https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_extended_${HUGO_VERSION}_linux-amd64.deb \
42          && sudo dpkg -i ${{ runner.temp }}/hugo.deb          
43      - name: Install Dart Sass
44        run: sudo snap install dart-sass
45      - name: Checkout
46        uses: actions/checkout@v4
47        with:
48          submodules: recursive
49      - name: Install Node.js dependencies
50        run: "[[ -f package-lock.json || -f npm-shrinkwrap.json ]] && npm ci || true"
51      - name: Build with Hugo
52        env:
53          HUGO_CACHEDIR: ${{ runner.temp }}/hugo_cache
54          HUGO_ENVIRONMENT: production
55        run:
56          hugo --baseURL=https://www.zanks.link/
57      - name: Deploy Pages
58        uses: peaceiris/actions-gh-pages@v3
59        with:
60            PERSONAL_TOKEN: ${{ secrets.PERSONAL_TOKEN }}
61            EXTERNAL_REPOSITORY: ZaNksC/ZaNksC.github.io
62            PUBLISH_BRANCH: main
63            PUBLISH_DIR: ./public
64            commit_message: ${{ github.event.head_commit.message }}
65      - name: scp ssh pipelines
66        uses: cross-the-world/ssh-scp-ssh-pipelines@latest
67        with:
68          host: ${{ secrets.HOST }}
69          port: ${{ secrets.PORT }}
70          user: ${{ secrets.USER }}
71          key: ${{ secrets.PWD }}
72          scp: |
73            ./public/* => /website/data/blog/blog_${{ steps.gen-id.outputs.time }}            
74          last_ssh: |
75            /home/ecs-user/scripts/deploy_blog.sh            

${{ secret.HOST }} 等三个变量,则需要在Github代码仓 - settings -Secrets And variables - Actions 中添加,分别表示 IP地址、ssh端口(如果改过)、部署用户和部署用户登陆凭证(密码用 pass: ${{ VAR }} , 密钥用 key: ${{ VAR }}, 推荐用密钥)。

SSL证书(可选)

觉得没有小绿锁没有格调,又觉得有了小绿锁续签太过麻烦?没关系,接下来就教你从申请到配置再到自动续签的全过程。

SSL证书将通过acme.sh客户端向Let’s Encrypt申请(免费!!!)。

acme.sh 是一个优秀的证书管理客户端。可以非常简单的做到申请与续签。

acmesh-official/acme.sh

登录服务器,找一个目录,安装 acme.sh ,我是在 /home/ecs-user/scripts 下,输入以下命令(国内版)进行安装。my@example.com 替换为自己的邮箱(没有git的话,sudo yum install git 来安装)。

[ bash ]
1git clone https://gitee.com/neilpang/acme.sh.git
2cd acme.sh
3./acme.sh --install -m my@example.com

输出OK表示已经安装完成了。当然,也可以输入 ./acme.sh version 来查看版本来验证。

在安装过程,acme完成了三件事情

  • ~/.acme.sh/ 目录下完成了安装
  • ~/.bashrc 中新增了一行,用于配置别名,可以在任意目录输入 acme.sh 命令(需要 source ~/.bashrc 或重新登录生效)
  • 创建了一个crontab的任务,用于检测即将过期的证书来实现自动续签,可以通过命令 crontab -l 查看

安装完成之后,因为acme的默认证书签发机构不是Let’s Encrypt所以需要手动配置一下。输入命令 acme.sh --set-default-ca --server letsencrypt 来完成配置。当然,也可以在申请命令中添加 --server letsencrypt 来指定机构。

接下来就是主题了,开始申请证书。在申请证书的过程中,机构会验证我们是否为域名的主人,在泛域名证书 *.zanks.link (以下命令中替换为自己的域名)的申请中,只能使用DNS方式来验证。输入以下命令来使用dns验证方式申请证书。

[ bash ]
1acme.sh  --issue  --dns -d *.zanks.link \
2 --yes-I-know-dns-manual-mode-enough-go-ahead-please

执行完成之后,会提示需要添加一条TXT的解析记录。添加完成之后,根据DNS解析时间等待一段时间(NameSilo的解析实在是太慢了,改用腾讯之后体验非常好,秒解析),建议等待半小时,然后执行以下命令。

[ bash ]
1acme.sh  --renew   -d *.zanks.link \
2  --yes-I-know-dns-manual-mode-enough-go-ahead-please

申请证书也可以通过域名商API的方式,参考 【官方文档】 (我试了一下没成功,感觉是网络问题) 申请成功后会出现4个文件,分别是 domainname.cerdomainname.keyca.cerfullchain.cer。主要使用的是 fullchain.cer 文件和 domainname.key 文件。fullchain.cer 提供了完整的证书链,而 domainname.key 是用于解密由客户端发送的加密数据的私钥。

证书生成之后,安装到nginx,不要手动去拷贝,使用以下命令。

[ bash ]
1acme.sh --install-cert -d *.zanks.link \
2--key-file       /home/ecs-user/.acme.sh/*.zanks.link/*.zanks.link.key  \
3--fullchain-file /home/ecs-user/.acme.sh/*.zanks.link/fullchain.cer \
4--reloadcmd     "service nginx force-reload"

其中 --key-file 的参数修改为 domainname.key 的文件路径,--fullchain-file 的参数修改为 fullchain.cer 的文件路径。

之后就是修改nginx的配置开启443端口,代码如下:

[ conf ]
    server {
        listen       443 ssl http2;
        listen       [::]:443 ssl http2;
        #server_name  _;
        #root         /usr/share/nginx/html;

        ssl_certificate "/path/to/fullchain.cer";
        ssl_certificate_key "/path/to/domainname.key";
        ssl_session_cache shared:SSL:1m;
        ssl_session_timeout  10m;
        ssl_ciphers PROFILE=SYSTEM;
        ssl_prefer_server_ciphers on;

        # Load configuration files for the default server block.
        include /etc/nginx/default.d/*.conf;

        #error_page 404 /404.html;
        #    location = /40x.html {
        #}

        #error_page 500 502 503 504 /50x.html;
        #    location = /50x.html {
        #}
    }

保持了跟http一样的配置,只是新增了一些ssl相关的配置,只需要将 fullchain.cerdomainname.key 的文件路径替换成之前申请出来的即可。

强制HTTPS(可选)

配置好https之后,如果希望只能通过https访问,那么可以通过重定向配合HSTS来告诉浏览器使用https。需要修改 /etc/nginx/nginx.conf 来实现。

[ conf ]
server {
	listen       80;
	listen       [::]:80;
	server_name  example.com www.example.com;
	return 301 https://$server_name$request_uri;
}


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

	ssl_certificate "/path/to/fullchain.cer";
	ssl_certificate_key "/path/to/example.key";
	ssl_session_cache shared:SSL:1m;
	ssl_session_timeout  10m;
	ssl_ciphers PROFILE=SYSTEM;
	ssl_prefer_server_ciphers on;
	
	add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

	include /etc/nginx/default.d/*.conf;
}

最后,就是在DNS解析中添加一条A记录,用于指向服务器。

DNS解析实践

由于服务器是没有IPv6地址的,为了使得网站支持IPv6,我在域名的解析规则中保留了Github Pages的IPv6站点。 最终,我的DNS解析规则(最后发现提交不了那么多条,腾讯云免费版的可解析条目有限,按需选择了)就是

subdomain domain Type Value
zanks.link A 我的服务器ip
www zanks.link A 我的服务器ip
zanks.link AAAA 2606:50c0:8000::153
www zanks.link AAAA 2606:50c0:8000::153
zanks.link AAAA 2606:50c0:8001::153
www zanks.link AAAA 2606:50c0:8001::153
zanks.link AAAA 2606:50c0:8002::153
www zanks.link AAAA 2606:50c0:8002::153
zanks.link AAAA 2606:50c0:8003::153
www zanks.link AAAA 2606:50c0:8003::153

通过 nslookup www.zanks.link 可以看到返回1个IPv4地址(自己的服务器地址)和多个IPv6地址(Github Pages)。

同时,分别通过 curl www.zanks.linkcurl -6 www.zanks.link 可以验证IPv4和IPv6的访问。

域名备案

当我们的服务器是国内IP时,要求域名进行备案,否则会无法使用。

国外注册商无法通过备案,需要转到国内

域名转移

我是在NameSilo上买的域名,已转入腾讯云。

首先,需要在NameSilo上解锁域名+获取转移码。转移码的获取可以通过点击 Send Email ,而且需要等到60天之后才可以转移。(为此拖延了好几个月才备案)

域名备案

ICP备案

ICP备案需要在服务器所在的云服务商处发起备份,比如我的域名在腾讯云管理,服务器却在阿里云,我是从阿里云发起的ICP备案。发起是比较简单的,按照规范填好订单直接发起即可,发起后会有专员审核,帮你修改掉简单的错误就提交到管理局了(有一个比较麻烦的是需要拍一个读保证书的视频)。如果有无法通过的则会退回来,比如之前是在NameSilo买的域名没办法备案。提交申请快的话一天就可以,提交之后会收到一个管理局的短信去阿里云备案流程里面输入一下,然后等审批就好。

一些弯路
备案第一次失败了,退回原因为,该网站备案在审核通过前网站已开通,把DNS的解析关了之后重新提交。

公安备案

公安备案需要在ICP备案完成后的30天内进行(不知道不备案会有什么后果,查了一下可能会切断dns解析)。按照阿里云给出的文档,在全国互联网安全管理平台申请主体(需要下载一个app做人脸认证),需要准备 身份证正面、身份证反面、手持身份证照片、网站域名证书等材料。

一些弯路
公安的备案相当麻烦,需要关闭评论等功能,提交之后会当地负责网安的民警来协助你,需要你提交材料,如实回答,如实禀报即可。(就是评论功能这块,需要验证能发送的人每个人都实名有点为难个人博客了)

番外:服务器安全加固

fail2ban

用于防止暴力密码破解,在高位端口被扫到后,黑客会进行暴力密码破解,即便是IP代理池进行破解,代理池中的IP也是有限的,可以通过暴力拆解多次失败后将IP加入黑名单来提高黑客的破解成本,毕竟小破服务器也没什么值得人家大张旗鼓破解的必要,别人也只想要个肉鸡。所以这个操作可以有效防止黑客的暴力密码破解。

安装

输入命令进行安装

[ bash ]
1sudo yum install fail2ban

通过以下命令管理fail2ban

[ bash ]
1sudo systemctl status fail2ban # 查看状态
2sudo systemctl start fail2ban # 启动服务
3sudo systemctl stop fail2ban # 停止服务
4sudo systemctl restart fail2ban # 重启服务

配置

fail2ban的配置位于 /etc/fail2ban 目录下,通常自定配置写在 /etc/fail2ban/jail.local 中,使用命令复制 jail.conf 来自定义配置,进行微调。

[ bash ]
1sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local

可以使用命令 sudo fail2ban-client status 来检查fail2ban的配置

编辑 jail.local ,找到 [sshd] 模块进行修改。其中port如果改过,需要设置为配置的端口。

[ text ]
1enabled = true 
2port = 22222 
3filter = sshd 
4logpath = /var/log/secure 
5maxretry = 5 
6findtime = 5m 
7bantime = 4h

修改后重启服务,并查看配置可以看到有一条生效的。

[ text ]
1sudo fail2ban-client status Status 
2|- Number of jail: 1 
3`- Jail list: sshd

登录选项设置

设置ssh登录方式为禁止root登录。由于机器需要使用Github Action进行部署,暂时无法禁用密码登录(实际上是可以的)

修改 /etc/ssh/sshd_config 中的 PermitRootLogin yesPermitRootLogin no 。 重启ssh服务 sudo systemctl restart sshd

高位端口

将ssh端口设置为高位端口,可以预防大部分的常规扫描。

修改 /etc/ssh/sshd_config 中的 #port 22port 22222 或其他端口。 重启ssh服务 sudo systemctl restart sshd

检查登录日志

查看ssh登录失败日志

[ bash ]
1sudo cat /var/log/secure | grep "Failed"

查看failed2ban的日志

[ bash ]
1sudo cat /var/log/fail2ban.log