站点搬家记:如何在干净机器上重建网站

warning: 这篇文章距离上次修改已过158天,其中的内容可能已经有所变动。

这篇文章不是正经的教程,更像是自己安装过程中的一个记录,目标是按照这个步骤,可以在一台干净的Debian 12机器上重建枫林灯语(及各个附属站点)。

搬迁实乃无奈之举,事情的起因已经在上一篇文章中有所提及,简单来说就是,原来的供应商Cloudcone的某个Virtualizor(控制面板)下的那群母鸡进木马了,一群小鸡被通过带外通道下载了勒索脚本,数据自然是全炸了,给钱也不一定能拿回来。所以干脆换个供应商,重新来过吧。

1. 前言

笔者其实是CC的老用户了,他家主打一个便宜,比如之前托管本站的VPS,4H2G带30G硬盘一年只要20来刀,上哪找这么便宜的去?玩灵车就要有灵车的觉悟,本来想着靠提升自己技术来硬扛灵车的,但根据官方说法,这次勒索,黑客是在 Virtualizor 面板上干进去的,说白了就是降维打击,内部折腾个啥咧?

其实这个故事也告诉我们,玩灵车VPS一定要注意:

  • 不要放重要数据,经常备份
  • 不要跑重要业务,确保留有后手
  • 避免当作主入口,免得城门失火殃及池鱼

于是还是花钱买平安算了,在可承受范围内,笔者还是挑了最为知名的搬瓦工,最便宜的一年49.99刀套餐,洛杉矶机房,2C1G带20G硬盘,装上了Debian 12。虽然贵是贵了点,但也不贵太多,一个月4刀多一点,少喝两杯奶茶就赚回来了,而且至少他没类似这样的运维事故爆出来过,多少能放心一点。

2. VPS初始化

这里假设读者已经有过在使用VPS的经验了,因此购买和注册和查IP过程在此略过,连接工具的选择也在此略过,提升到root权限的方式也略过。

至于笔者的习惯,目前还是Debian 12系统,PuTTY登录。用Debian 13理论上也不是不行,主要是如果有涉及到sysctl的操作,原来写/etc/sysctl.conf改成/etc/sysctl.d/99-custom.conf,生效命令也需要把sysctl -p改为sysctl --system,理论上就可以了。

2.1 安全登录

SSH上去之后首要的三件事:改端口,上公钥,禁密码。

首先生成密钥对,笔者用PuTTYGen,当然你也可以用别的。生成的私钥保存好,公钥放入服务器的~/.ssh/authorized_keys里面,有多条的话就一行一条。然后打开SSH配置文件,改配置:

# nano /etc/ssh/sshd_config

# 具体配置按需修改
# 注意改完之后拉到最下面,看看有没有覆盖的配置文件
# 再检查下 /etc/ssh/ssh_config.d/ 有没有额外加载的
PermitRootLogin prohibit-password
PasswordAuthentication no
PermitEmptyPasswords no
UseDNS no
Port 11451

改完重启sshd,不要马上关终端,新开一个终端看看是否正常工作:

  • 加载私钥后可以免密登录
  • 不加载则直接提示无可用鉴权方式

就可以了。

2.2 配置基础环境

# 基本工具集,要什么装什么
apt update
apt install curl wget lsof psutils htop unzip rsync psmisc

# 设置时区
timedatectl set-timezone Asia/Shanghai

# 启用ls的颜色,以及ll等命令
nano ~/.bashrc
# 在里面取消注释掉对应选项

# 把rc.local弄回来
cat <<EOF >/etc/rc.local
#!/bin/sh -e
#
# rc.local
# By default this script does nothing.

exit 0
EOF

chmod +x /etc/rc.local
systemctl enable --now rc-local

2.3 配置运行环境

Typecho 运行在LNMP环境下,直接开装

# Debian下配LNMP是很舒服的
apt install nginx php8.2-fpm mariadb-server redis-server

# 根据需要装各种PHP扩展
apt install php8.2-mbstring php8.2-redis php8.2-mysqli php8.2-curl

# 别忘了运行安全设置
# 尤其是不要把数据库端口暴露到公网
mariadb-secure-installation
# 最好进mariadb控制台运行一下
# ALTER USER 'root'@'localhost' IDENTIFIED BY '你的数据库root密码';
# 关掉控制台直接登录

# PHP要调一下时区和文件上传限制
nano /etc/php/8.2/fpm/php.ini

# 按需调节选项即可,比如
# date.timezone = "Asia/Shanghai"
# post_max_size = 50M
# upload_max_filesize = 50M
# 此外还可以检查下错误输出有没有关闭,防止被人抓口子
# display_errors = Off

# 最后重启服务
systemctl restart php8.2-fpm.service

上面的 Redis 是因为本站部分功能要用到,顺便装了。装好之后,我们需要给他配个鉴权,毕竟安全手段永远不要少。

nano /etc/redis/redis.conf

# 搜索requirepass,应该很快能找到设置密码的地方,设置一个足够安全的密码
requirepass a-veRy-lOng-aNd-FuZHaDeMIMA-1145141919810

为了运行Umami,我们还需要准备Docker:

curl -fSL get.docker.com | bash

然后就是Umami的安装步骤,这个也很简单,按照官方文档来就行了。注意官方给的docker-compose文件里面端口映射默认监听0.0.0.0,且加盐密钥也是没有修改的,下载Compose文件后记得修改,避免安全问题。

2.4 Nginx 配置文件

因为这一台服务器跑三四个站点,大致可以分为:

  • 需要PHP(Typecho)
  • 需要反代到其他服务(Umami)
  • 纯静态

所以Nginx配置文件也有所不同,主要是要保持权限最小原则,比如纯静态站点就不应该配置PHP文件的解析。不过总体来说,架构都是一致的,可以直接按官方示例配置改动,以及上网找一份Nginx反代配置就行(反代时注意部分服务可能需要Websocket的配置),因此具体的配置不再给出。当然,博客站的配置还需要伪静态,这个看文档配置就好。

至于SSL,笔者配置的是Cloudflare的源站证书(仅在CDN到源站这一段生效),所以到域名后台生成并下载源站证书和私钥,放入固定的文件夹中,方便各个配置文件引用。

有个很常用的小技巧:阻止可上传目录(不论是后台还是前台)内脚本文件的运行:

location ~* ^/usr/uploads/.*\.(php|php5|php7|phtml)$ {
    deny all;
}

这个也不妨加上。

2.5 源站安全防护

给站点套CDN的目的,除了就近缓存静态资源之外,还有一个重要目的是保护源站IP。

这里有两方面的意思,一个是复活被封的IP,一个是不让人摸到源站。前者好理解,搬瓦工旗下VPS经常被用于搭建代理,导致大量IP被封,如果不做复活处理,很难提供服务;后者主要是站在攻击者的角度来说的,如果攻击者可以直接摸到源站,那就直接打源站得了,前面的CDN扛打性能再强,也阻止不了他。

防止源站IP泄露的方法有很多,笔者采用的是:

  • 回源白名单
  • mTLS
  • 禁止裸IP访问

三板斧下来,即使某一环节不小心失效(例如重启后添加封堵IP规则失败),也有其他两个方式挡着,可以最大限度防止源站暴露而带来的不必要的麻烦。当然,你也可以根据需要,配合Cloudflare Argo Tunnel等工具,实现真正意义上的不暴露任何端口。

需要注意的是,上面提到的几种方法,侧重于在Web服务方面防止暴露,但暴露IP的途径并不只这一个,还有其他的,比如说,你用同一台服务器搭建了SMTP来发信息,那么源站IP地址就会在SMTP消息头的Received字段中暴露。如果你有部署其他服务,应注意这方面的风险。

需要注意的是,并没有哪种做法是完全能防止泄漏的。我们的目的也不是这个,而是把获取源站IP的成本大幅提高,以及就是把某IP与某站点相关联的线索大幅减少,这就足够了。

2.5.1 回源白名单

我们对接的是Cloudflare,因此配置防火墙只允许Cloudflare的CDN服务来访问就行了,这样在外部看来,你的机器就是没有开放80/443端口,降低被扫站(包括网络空间测绘)和被关联的概率。

# 开一个目录放各种杂七杂八脚本
mkdir /var/scripts
cd /var/scripts

nano get.cloudflare.ip.sh
# 脚本的功能是:用curl下载Cloudflare官方的CDN回源IPv4和IPv6段
# 并通过iptables添加到防火墙,阻止非此IP范围内的设备访问本机80和443端口,防止网站被扫。
# 把上面这段话粘贴到任何一个现代一些的LLM,都能得到功能类似的脚本,因此这里不再给出代码。

chmod +x get.cloudflare.ip.sh
./get.cloudflare.ip.sh

# 此时执行 iptables -nvL 和 ip6tables -nvL
# 应该能看到相关规则
# 别忘记把上面的脚本的绝对路径加入开机启动项中

用外部服务扫描,看不到80/443开放,就可以了。

2.5.2 mTLS

还可以做mTLS(双向TLS),当Cloudflare前来拉取的时候,必须提供有效的客户端证书,否则拒绝连接。这种方法会让回源延迟略微提升,但总体来说影响并不是很大。

在Cloudflare的域名页面,找到SSL/TLS,进入『源服务器』,打开『经过身份验证的源服务器拉取』:

然后,下载Cloudflare的回源CA证书(如果此链接失效,请到CF帮助页下载),保存到你服务器上的方便路径。这个证书是Cloudflare所签发的客户端证书的CA证书,不需要保密,因为私钥在CF手里,而nginx就得靠这个CA证书来确定来者就是Cloudflare。

最后别忘了改Nginx配置:

ssl_client_certificate /path/to/cert/origin_pull_ca.pem;
ssl_verify_client on; 

重读配置后,用curl或浏览器检查,应该能看到要求证书的提示。

2.5.3 禁止裸IP访问

对于直接访问IP的情况,我们可以让nginx直接切断连接,防止证书暴露。

新建或修改nginx的default配置:

server {
    listen 80 default_server;
    server_name _;
    return 444;
}
server {
    listen 443 default_server;
    listen [::]:443 default_server;
    server_name _;
    ssl_reject_handshake on;
}

重读配置,此时用curl直接访问IP,应该能看到连接被切断的提示,这样就配置好了。

3. 迁移主站点

准备好基础环境,就可以着手开始迁移了。

3.1 文件复制

各个站点的网站根目录还是放在/var/www下,再加上Typecho迁移过程也很简单,打包之后拷过来就行。

# 原来的临时服务器
cd /var/www

# 用你喜欢的方式打包
zip -r site.zip *

# 然后再找个你喜欢的方式传输到正式服务器
# 比如SFTP就是个相对不错的选择
# 笔者这边,因为正式服务器锁了密码登录,又懒得传别的公钥
# 因此是用已经配好的FileZilla拉下来再传上去的
cd /var/www
unzip site.zip

# 确认无误后可以删除源文件
rm site.zip

# 我们上面用的是zip,而部分版本的zip不会保留权限信息
# 因此需要手动给上传目录,主题目录(如果你需要Web编辑)加写入权限
# 当然,如果你打包用的是tar之类的,那么可以跳过这一步
chmod -R a+x your.site.com/usr/uploads
chmod -R a+x your.site.com/usr/themes

拷完后建议看一下是否齐全。

3.2 数据库导入

文件复制过来后,站点还不能马上访问,因为数据库还是空白的,这时候就需要之前备份好的数据库文件出马了。

3.2.1 安全连接数据库

导入数据库,虽然可以在命令行上进行,但笔者继续偷懒,选择在自己电脑用HeidiSQL上操作。不过前面提到,数据库端口不能暴露到公网,而且执行安全安装命令的时候,也锁掉了数据库root用户的公网访问权限,该怎么办呢?

秘诀就在于,可以通过SSH临时开一条加密通道。大多数终端软件都会提供此类选项,如果你和我一样用的是PuTTY,在调入连接信息之后,点击左侧的Connection - SSH - Tunnel里,就可以找到添加隧道的选项:

其他软件的选项也类似其他软件的选项也类似

Source port填一个本机未被占用的端口,Destination填127.0.0.1:3306,确保类型为Local,最后点击Add,就可以看到上面多出来一条类型为L的转发条目,此时再点击Open打开终端,PuTTY就会自动帮你起一条SSH隧道转发,就可以安全操作远端的数据库了。而且此时连接是远端的SSH服务器发起,连到本机的,因此源地址也是127.0.0.1,不会受到我们前面的安全策略的影响。

当然,如果你用的是系统自带的OpenSSH,那么操作方法也差不多,只需执行:

ssh -L 3306:127.0.0.1:3306 [email protected]

就同样可以一边在Shell操作,一边使用这个转发好的端口了。

3.2.2 恢复备份

这一步其实操作也不算麻烦,只需要根据站点配置文件里面的连接信息,重建对应的数据库用户,然后加载sql文件,依次执行即可。

不过关于备份数据库,这里需要多说一句,包含加密/随机数据的备份,永远不应该被GUI当作可编辑文本。我举个例子你就明白了:笔者的Vaultwarden也装在这台受影响的VPS上,虽然也有备份,但在迁移过程中出现了一个问题:如果把备份好的sql文件用HeidiSQL打开,会报错:

如果此时你尝试用文本编辑器打开sql文件,复制粘贴到查询框执行。恭喜你,你会发现你登不上Vaultwarden了。

究其原因在于,部分软件(如本例中的Vaultwarden)写入数据库的虽然说是TEXT或者VARCHAR字段,但其中的内容并不一定是可读文本,可能是加密后的二进制数据什么的,虽然满足数据库层面的UTF-8存储要求,但并不保证是合法Unicode码点,也不保证能被本地代码页表示。如果使用带GUI的数据库编辑软件打开,有不小的概率会在映射字符的时候出错,轻则无法导入,重则跟上面的复制粘贴一样,数据完全损坏。

碰到此类文件,就不能跟恢复Typecho一样直接粘贴操作了,而是得改用命令行导入:

mariadb -u[user] -p[pass] < backup.sql

恢复程序看到的是未经转换的原始字节流,因此数据不会有事。

更安全的做法,应该是在创建备份的时候,在mariadb-dump里主动加上--hex-blob参数,这样当他碰到无法被解释为可读文本的二进制数据的时候,会自动转换为0xdeadbeef....这样的纯ASCII十六进制表示,文件就安全了。不过需要注意,这样操作并不能避免某些GUI数据库管理软件的加载预转换问题,因此恢复备份最合适的办法,依然是通过命令行来操作,不要有任何涉及到『GUI显示文本』的地方,避免被误转换。

3.3 功能检查

此时,站点应该就可以恢复运行了,到CF后台修改解析到新服务器的地址,用浏览器访问,应该能看到页面。

剩下的都是标准操作:出现报错信息,就根据报错信息来修;如果光溜溜一个HTTP 500,就临时打开错误显示,看到后就关上。一般来说这类错误都是缺依赖什么的,补装对应的依赖就是了。

当然,本站魔改过侧边栏的每日推荐功能,因此需要多开一个crontab,定期执行更新推荐的脚本,不过这个也属于依赖问题,不影响核心功能,因此优先级可以适当降低。

4. 备份系统

备份,备份,一定要备份,备份救我狗命。

目前的备份方案还是比较简陋的,只有简单的快照机制,并非完全没有缺点,但也足够用了:这个简陋的方案,让笔者的站点在这次勒索事故中生存了下来,至少核心内容全部都在。

这个简单的备份系统分为两部分:一个备份脚本,和一个拉取机制。

4.1 备份脚本

站点数据主要是两大块,网站目录和数据库。目录还好说,文件就在那,至于数据库,笔者写了个简单的备份脚本:

#!/bin/bash

# 主博客数据库信息
blog_db_file="blog"
blog_db_user=""
blog_db_pass=""

# 插件数据库(如文章阅读量)信息
diy_plugins_db_file="diy_plugins"
diy_plugins_db_user=""
diy_plugins_db_pass=""

# 通过脚本参数,确定备份目标文件名
filename=""
if [ -z "$1" ]; then
    filename=""
else
    safe_param=$(echo "$1" | sed 's/[\/\\:*?"<>|]/_/g')
    filename=".${safe_param}"
fi

echo "备份中"
/usr/bin/mariadb-dump -u${blog_db_user} -p${blog_db_pass} --all-databases --hex-blob --quick --single-transaction > "/path/to/backup/${blog_db_file}${filename}.sql"
/usr/bin/mariadb-dump -u${diy_plugins_db_user} -p${diy_plugins_db_pass} --all-databases  --hex-blob --quick --single-transaction > "/path/to/backup/${diy_plugins_db_file}${filename}.sql"

给脚本加上可执行权限之后,在crontab里面设定好定时任务:

# 不知道当时设置任务的时候是怎么想的
# 1day应该是daily,1month应该是monthly才对
# 算了,懒得改了
0 * * * * /path/to/backup.sh
0 0 * * * /path/to/backup.sh 1day
0 0 */3 * * /path/to/backup.sh 3day
0 0 * * 1 /path/to/backup.sh 1week
0 0 1 * * /path/to/backup.sh 1month

这样就可以在指定目录下,看到有特定后缀的备份数据库文件了。至于这个指定目录,你可以任选,笔者放在了所有网站根目录的上一层,这样可以一起打包带走。

另外一个需要备份的是Umami,虽然说统计数据不太重要,但是丢了也有些心疼:

docker exec umami-db-1 pg_dump -U umami umami > /path/to/backup.umami.sql

这个就没有放入上面的备份脚本,毕竟一天一次就差不多了,当然有需要的话你也可以自行扩充,添加更多的备份频次和后缀。

4.2 拉取机制

正经情况下,笔者推荐使用restic这样支持快照的备份工具,而且笔者最近也在计划适配这个工具,不过类似restic的工具,笔者会把他分类为推送式,也就是在需要备份的设备上安装一套软件,把对备份内容进行处理后推送到其他存储设备上,例如另一块磁盘或者对象存储。而正如标题所见,笔者这里进行的是拉取式,因为备份设备运行在隔壁杂物间,服务器不可能主动推送过来,只能自己去拉。

因此,笔者选用了rsync作为拉取手段。需要注意的是,rsync严格意义上来说是同步软件(sync),而同步软件≠备份软件,这是很多第一次使用同步软件的朋友很容易碰到的误区,常见表现为,删了本地的文件,结果远端也被删了。明白这一点之后,这里选择使用rsync的原因,也就很简单了:

  • 是拉取式的工具
  • 可以只拉取有变动的文件
  • 默认不会出现『远端删了,也删本端』的情况

不过rsync在传输过程中是没有加密的,他给出的解决方案是SSH,毕竟这是几乎每台服务器都有的加密传输软件。不过笔者用不着这个,不是有杏泉网络吗,这个时候的好处就出来了。

打开/etc/rsyncd.conf,写入如下配置:

# 仅监听杏泉网络
# 以及非标端口
address = 198.18.3.1
port = 51463
pid file = /var/run/rsyncd.pid
uid = root
gid = root

#禁止列出可用模块
list = no

[backup]
path = /path/to/backup/
# 开只读,禁删除
read only = yes
delete = no
# 只有授权用户才能访问
auth users = bkfldy
secrets file = /etc/rsyncd.secrets

完事之后,打开/etc/rsyncd.secrets,写入授权信息,也就是用户名和密码,用冒号隔开:

bkfldy:a-very-long-and-complex-password-is-good-for-security

起个service文件,扔到/etc/systemd/system/下:

[Unit]
Description=Rsync Daemon
After=network.target

[Service]
Type=simple
# --daemon 以守护进程模式运行
ExecStart=/usr/bin/rsync --daemon --no-detach --config=/etc/rsyncd.conf
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target

最后把服务开起来:

systemctl enable rsyncd --now

服务端就齐活了。

至于拉取端,也不是难事,只需要预先把上面定义的密码(只需要密码)放入某个文件,例如/etc/rsync/rsync.secret,然后通过crontab -e定期执行命令:

# 单向接入杏泉网络,需要配置入口HTTP代理
export RSYNC_PROXY="10.0.0.159:7890"
rsync -avzP --password-file /etc/rsync/rsync.secret rsync://[email protected]:51463/backup /your/backup/dir

就可以定期拉取远端dump和打包好的数据到本地了。

由此可见,这套备份系统真的很简陋,但毕竟能用,而且近期也计划加入其他备份方案,所以也能接受。

5. 写在最后

原本打算一边重建一边写的,最后还是发现,一口气重建完再一口气写完,效率要高些。

其实这就是为什么说建站一时爽维护火葬场,要建站,网上大把的教程,大把的一键脚本,甚至去某宝和咸鱼都能买到代为建站的服务,然后呢?如果不对站点有基本了解,不对工具有基本了解,建好站点虽然能用,恐怕也会在不知道多久之后的一场事故中损毁。

说到事故,笔者去看了下那台CC VPS,发现他给我安排的不是DC2机房,而是DC1机房,之前一直以为是DC2机房。 虽然目前后台面板还处于能看不能动的状态,但可能也有一线生机,站点数据都还在?

如果真的是这样,那就可以恢复之前的统计数据了。

2026年2月3日 19:46:55更新:所有VPS都要强制重装系统,数据无了

(完)


木头箱子脆脆,但是这样正好

如无特殊声明,本站内容遵循 CC BY-NC-SA 4.0 协议

转载请注明出处并保留作者信息,谢谢!

本站由 搬瓦工VPS 强力驱动

none
最后修改于:2026年02月03日 19:47

已有 4 条评论

  1. 写的很详细,收藏一下

  2. 面板是不是更加方便一些。

    1. 理论上会,但毕竟引入了新的攻击面,以及又多开一个程序,也占用资源,所以我很久不用面板了

添加新评论

提醒:『评论回复邮件提醒』功能正在测试中
评论后,如果站长有回复,会有邮件通知