• 首页
  • 博客
  • 关于
  • 归档
  • 分类

舞乐漪的小站

一个折腾服务器、Live2D 看板娘和 Typecho 的个人站点
建站笔记

把站点从「能看」推到「能扛」:一次安全、性能与可发现性的系统加固

2026-10-18 浏览量 0 暂无评论

一、起因:我以为「能打开」就是没事

博客推到公网之后,我的验收标准一直只有一条:浏览器里敲进去,页面出来了,图能看见,字能读。这条标准支撑了我很久的安心感——站点确实是活的,18 篇文章、6 个分类、26 个标签都在那儿,Typecho 1.3 加 sagiri 主题,跑在一台 2 核 2G 的云服务器上(Ubuntu 22.04),同一个机器上还养着一只 QQ 机器人。没挂过,我甚至管这叫「稳定」。

回头看,问题不在我不懂这些知识,而在我从来没给「没事」下过一个可检验的定义。

这轮加固是从一次很随意的检查开始的:我想量一下首页到底传了多少字节。量完顺手往下翻了几层目录,然后发现,「能打开」和「能扛」之间隔的不是一条沟,是好几个我从来没想过要去捅的地方。下面按被我捅破的顺序写。

二、第一个洞:数据库文件就躺在站点目录里

这个洞毫无技术含量。我在站点目录里翻,看见一个 .db 文件,188KB,位置在 /usr/xxx.db 这种「只要猜得出路径就能直接请求」的地方。用一个没登录的会话请求了一次,HTTP 200,整个 SQLite 文件就被下载下来了。188KB 里装的是整站数据:文章、评论、用户表——用户表里有账号和口令哈希。

那种荒谬感很强。我为了防注入、防爆破折腾了半天,结果门根本没锁,是我自己把保险柜放在了门口。

修法没什么好说的:文件移出 webroot,顺手加一条 deny。真正值得写的是下一条,因为那才是我这轮里最丢人的地方。

三、我自己写的 deny 正则,只匹配「以 .bak 结尾」

加完 deny 我做了「验证」:请求一下 xxx.db.bak,403;请求一下 config.inc.php.bak,403。绿灯,收工。

这个验证的毛病在于,我验证的是「我以为的备份文件名」,不是「服务器上真实存在的备份文件名」。

后面我用 find 把 webroot 下所有非预期文件列了一遍,出来 4 个形如 header.php.bak.1792302906 的东西——时间戳后缀,某次改模板时留下的。每个 2.8KB 到 10KB,全是 PHP 源码明文。随便请求一个,HTTP 200,源码原样吐出来:连接参数、内部路径、模板逻辑,一页一页全在里面。

根因就在我写的那条正则上。我当时的规则大意是「匹配以 .bak 结尾的请求」。以 .bak 结尾,意味着 header.php.bak 会被拦,header.php.bak.1792302906 不会——尾巴上多一个后缀,整条规则就失效了。我写的不是一条防护规则,是一条针对我自己想象中文件名的特判。攻击者不需要猜我的正则,他只需要猜「人会怎么备份」。

修法两条,缺一不可。第一条是把「后缀黑名单」改成「包含式匹配」,文件名里出现下面这些片段就拦:

location ~* \.(db|sqlite3?|sql|bak|orig|old|save|swp|log|ini|tar|gz|zip)([.\-_~]|$) {
    deny all;
}

关键是最后那个 ([.\-_~]|$):它要求这些片段后面要么紧跟一个分隔符(.、-、_、~),要么就是文件名结尾。header.php.bak.1792302906 里的 .bak 后面跟的是 .,命中;backup.zip 命中;而正常文件不会因为名字里恰好带一段 log 就被误伤。

回头看,这条正则之所以写成那样,是因为我脑子里有一个很具体的敌人形象:某个脚本拿着字典,去撞一堆 index.php.bak、config.php.bak 这种教科书式文件名。但真实世界里留在服务器上的备份,名字是我自己随手打的——带时间戳、带日期、带个 copy。防护规则一旦写成对自己「备份命名习惯」的复刻,就等于只防住了我记得的那一次。

第二条才是真正保底的:把备份、源码、数据库文件全部搬出 webroot。nginx 的 deny 是第二道闸,不该是唯一一道——我这一轮的两次事故,都是因为把「第二道闸」当成了「唯一的墙」。

四、403 不是 404:验证时别被自己骗

这条单独成节,因为我在验证阶段结结实实骗了自己一次。

加完 deny 我去复查那 4 个文件,返回的是 403。403 说明「被规则拦了」,看起来很好。但它同时说明不了另一件更要紧的事:文件还在不在。因为 nginx 的匹配顺序里,location 级别的 deny 在「文件是否存在」的检查之前生效——路径先命中规则,直接 deny,压根走不到文件系统去看这个文件。只要规则还在,文件在不在都是 403。

所以正确的验证是两条独立观测。一条从 HTTP 侧看规则生效(403),一条从文件系统侧看文件真没了:

[ -e /path/to/webroot/header.php.bak.1792302906 ] && echo still-there || echo gone

后来我把这一步写成脚本,两条断言各占一行,跑完直接输出 rule=blocked file=gone 才算过。人肉验证的毛病是它会累,累了就只跑一半——而我恰好就是在只跑了一半的那一次上骗过了自己。

这件事的教训比 403 和 404 的区别更大:一个测试如果无论「修好了」还是「没修」都通过,它就不是测试,是安慰剂。后来我给这轮加固列验收项时,每一项都要求两个现象能分别被观测到——改之前的坏现象,改之后的好现象。

五、防火墙:从全开收到只留四个洞,以及那个我以为开着的密码登录

ufw status 一开始是 inactive。也就是说这台机器上所有监听端口,只要进程绑在对外地址上,公网就能敲。

我数了一下,除了 22 和博客入口,还有几个我以为「反正没人知道端口号」的服务:一个高位端口的面板、一个调试服务、一个本地开发用的端口。「没人知道端口号」这个假设在公网上活不过一天,端口扫描是背景噪音,不是攻击行为。

修法是把默认策略反过来,再白名单放行:

ufw default deny incoming
ufw default allow outgoing
ufw allow 22/tcp
ufw enable

22 单独先放行,是为了不在启用规则那一瞬间把自己锁在外面;剩下三个业务入口随后逐条加,每加一条都从外部验一次连通性。最终只放行 22/8080/8443/9443。验证是从外部对 8081、3210、3000 各打一次,结果从「连得上」变成「超时」——超时(drop)比拒绝(reject)好,少泄露一点「这个端口上其实有服务」的信息。

同一批里我还准备改 sshd,因为主配置里明明是 PasswordAuthentication yes。手快按下之前多看了一眼实际生效值:

sshd -T | grep -i passwordauthentication

结果是 no。原因很简单:Ubuntu 22.04 的 sshd 配置是分层的,主文件里那一行只是「默认值的样子」,真正覆盖它的是 sshd_config.d/*.conf 里预置的条目。sshd -T 看的是合并后的最终配置,grep 主文件看的是我以为的配置。

这里没有洞要补,但它是这轮里最有价值的一次「不改成」。如果我没查 sshd -T 就直接在末尾追加一行 no,结果一模一样,但我不会知道它本来就是关的——而「不知道」意味着换一台机器时我还得重新猜一遍。加固的过程里,把「我以为」变成「我验证过」,比多写一条规则值钱得多。

六、首屏 703KB → 48.8KB:把最大的那个库请出首屏

性能这块本来没打算动,直到量出 /blog/ 的首屏传输量是 703KB。一台 2 核 2G 的机器,一个内容站首页,703KB,不合理。

拆开就清楚了:最大的一块是看板娘库 l2d-widget.min.js,646KB。703 减 646,剩下的才是真正的页面。也就是说,为了右下角那个小人,每个访客都得先下 646KB 才轮到看正文——这个交易在任何流量下都不划算。

修法不是删掉它,是挪加载时机:页面 load 之后,在空闲时再注入 script。首屏关键路径上不再有它,看板娘该出现还是会出现,只是晚几百毫秒。结果首屏 703KB → 48.8KB。

顺手做了两件配套的:静态资源开 gzip,加 Cache-Control: max-age=2592000。前者省带宽,后者省往返——回访用户第二次进来,图片和样式基本不出网。

然后把版式也重排了一遍:容器 1110px → 1240px,内容区 772px → 937px;卡片固定三列,每列 299px,内边距从 40px 收到 20px;侧栏 280px 加 position: sticky。侧栏里词云 22 个词,字号按文章数从 12px 线性映射到 26px,8 色循环;下面分类 6 个、标签 16 个胶囊。

这里有个取舍值得说:容器从 1110 加到 1240、内容区从 772 涨到 937,多出来的宽度没拿去塞更多东西,而是让三列各自松一口气——每列 299px、内边距 20px,正文行长回到一个能连续读下去的长度。首屏变小靠的是把 646KB 挪出关键路径,版式反而更宽,这两件事不矛盾。

看板娘跟着改。它原来 300px 宽、锚在视口右下,实测压在侧栏上——盖掉一张 150×79 的数据卡和一整块 150×152 的词云区。改成桌面 240px、锚定内容区右边缘(跟着内容走,不跟视口走),手机 200px。改完侧栏终于是能看的。

七、移动端、无限滚动,以及「验证工具自己会骗人」

侧栏改完我拿手机看了一眼——侧栏是空的。

查下去,根因在主题的 isPc():它用一条普通 User-Agent 正则判断是不是桌面端,是就输出侧栏 HTML,不是就整块不输出。所以之前手机上「没有侧栏」从来不是样式问题,是 HTML 压根没生成。我把这块改成移动端也输出之后,问题从「没有」变成了「空的」:模板默认激活的侧栏模块是「文章目录」,而主题的 JS 在移动端不构建 TOC。空侧栏比没侧栏更难看,这一改等于把一个隐藏问题换成了一个显性问题。

无限滚动踩了三下,都是同一类「状态没人维护」:

  • IntersectionObserver 的哨兵如果一直留在视口里,回调不会重复触发——加载完一页就停住,看起来像「没有更多了」。得在每次触发后重建哨兵,或者先移除再插回,强制重新观测。
  • 注入的脚本被主题里前一句吞掉,报错是 setInterval(...) is not a function——上一条语句末尾少了分号,下一行被当成接着调用。加个前导分号就解决。
  • 后插入的图片,主题的懒加载不接管,因为它的初始化只在首屏那次跑过。新节点得手动补一次初始化。

最后是验证工具本身。我一开始用 page.mouse 模拟双击,测出来是通的,真机不通。换成 Playwright 的真触屏 page.touchscreen.tap 才复现出来。原因在 CDP:单次 touch 事件往返 300–600ms,而我那个双击的两次点击间隔小于 320ms,走 CDP 根本来不及,只能在页内直接派发真实的 TouchEvent。

还有个更气人的:position: sticky 的元素在 fullPage 截图里整块不绘制,截图看着像元素凭空消失了,实际是那层没被拍进去。这类「工具骗人」在验证前端时比真 bug 更耗时间。

这几条连起来看其实是同一件事:「状态」散落在主题 JS、浏览器和测试工具三处,任何一处没人维护,问题都会以「看起来正常」的形式留下来。

八、让搜索引擎终于看得见:robots、sitemap、canonical、h1

安全和性能弄完,我才想起来查可发现性。/robots.txt 404,/sitemap.xml 404。两个都 404 意味着:爬虫来了没有任何指引,我也没主动告诉它站点里有什么。

补了两件事:写 robots.txt 指向 sitemap,生成 sitemap.xml 覆盖 29 条 URL——18 篇文章、5 个页面、6 个分类。顺手补了 canonical,免得列表页和详情页在索引里互相打架。

sitemap 只收 29 条,没把 26 个标签页都塞进去:标签是聚合视图,内容重复度高,塞进去只会稀释抓取预算。分类 6 个进、标签不进,是我能说清理由的一次取舍。

然后是一个纯语义的错误:列表页上有 5 个 <h1>。Typecho 的默认模板把每篇文章标题都渲染成 h1——在详情页这是对的,在列表页就是 5 个同级最高标题,爬虫没法判断这一页的主题到底是什么。改成 h2 之后,列表页的 h1 数量是 0。这个改动对肉眼零影响,对索引结构影响很大,属于典型的「不查不知道、知道了五分钟改完」。

九、收尾:三句话

这轮下来我最想留住的是三条做法。

观测优先。数据库那个洞、703KB 那个数字、5 个 h1——它们都不是我「想」出来的,是我量出来的。加固的第一步不是改配置,是先把现状变成一个能比较的数字或状态。

改动可回滚。文件搬走而不是删掉,配置先备份再改,规则加在独立文件里而不是揉进主配置。加固本身也会引入故障,回滚能力是它的一部分。

验证要能证伪。403 那件事教我的就一句:一个测试如果「修好了」和「没修」都能过,它就没在测任何东西。每条改动都配一对观测——坏现象在改之前看得到,好现象在改之后看得到。

从「能看」到「能扛」,中间隔的不是技术难度,是「我以为」和「我验证过」之间的那段距离。

相关阅读

  • 别再「看起来对」:用真浏览器验证前端
  • 一屏能塞多少信息:三列卡片、侧栏重做与移动端
踩坑复盘建站安全加固
国内访问 GitHub 的四种方式,和它们的边界
一屏能塞多少信息:三列卡片、侧栏重做与移动端

评论/ 取消评论

  • 文章目录
  • 站点概览
    舞乐漪

    舞乐漪

    个人站长

    Funny创造世界

    河北省廊坊市广阳区 2026-10-17
    14 文章 6 分类
    10 标签

    分类与标签

    踩坑复盘运维建站故障排查前端工具TypechoLive2DAI安全加固

    分类

    默认分类2运维实录1建站笔记5前端折腾2工具箱3杂谈1

    标签

    踩坑复盘4运维4建站4故障排查3前端3工具3Typecho3Live2D1AI1安全加固1

    浏览量 : 0

    冀ICP备2026047696号-1

    © 2026 舞乐漪 · Powered by Typecho · Theme Sagiri

    This is just a placeholder img.