我把这个博客当成一个安全项目来做
起点:一个博客有什么可"安全"的?
大多数个人博客的安全讨论到"上个 HTTPS"就结束了。但如果你打算把简历链接交给面试官,这个站就同时承担了三件事:
- 它是我的门面——被别人看到的那一刻,它就在替我表达"这个人懂不懂安全"
- 它上面挂着在线 Demo 的入口——一旦被篡改,跳转目标就可能被替换
- 它与我的私有服务共用同一套基础设施思路——边界划错了,问题会扩散
所以这个站从第一天起,选型的标准不是"哪个框架流行",而是哪条路留下的攻击面最小。
决策一:纯静态,不要 CMS、不要数据库
选择:Hugo 生成静态 HTML,托管在 GitHub Pages。
换来了什么:
| 常见攻击面 | 静态站是否还有 |
|---|---|
| SQL 注入 | ❌ 没有数据库 |
| 后台弱口令 | ❌ 没有后台 |
| 任意文件上传 getshell | ❌ 没有上传接口 |
| 反序列化 / RCE | ❌ 没有服务端运行时 |
| 依赖组件漏洞 | ⚠️ 只剩构建期依赖,不进入线上 |
代价:动态功能(评论、搜索、访问统计)都不能直接做。
怎么补:
- 搜索 → 用 Pagefind,在构建时生成索引,浏览器本地检索,不需要后端
- 评论 → 用 Giscus,数据存在 GitHub Discussions,我不维护任何存储
- 统计 → 用 Cloudflare Web Analytics,不引入 Cookie、不追踪个人
共同点:这三样都把"数据处理"外包给了别人已经维护好的基础设施,我这边零服务端。
决策二:不引入任何第三方脚本
打开很多博客,<head> 里躺着五六个外部域名的脚本:统计、字体、CDN、客服、广告。
对我这个站来说,每一个都是把访问者的浏览器控制权分一份给别人:
- 第三方脚本被投毒 → 你的站跟着中招(供应链攻击最常见的入口)
- 第三方域名被劫持 → 你的访问者被重定向
- 第三方能看到访问者的 Referer 和 IP
所以本站的原则是:
<!-- 一个都不引 -->
<script src="https://third-party.example.com/analytics.js"></script> ❌
字体用系统字体栈,图标用内联 SVG,代码高亮在构建时完成。
唯一的例外是评论组件——它本来就依赖 GitHub 登录,属于功能必要,且我会明确标注出来。
决策三:源站 IP 不暴露
后面我会把 SecAgent 的在线 Demo 部署到云服务器上。如果直接把服务器 IP 写进前端代码,那么所有 CDN、WAF、限流都形同虚设——攻击者绕过它们直连源站就行。
做法:
访问者 → 域名 → Cloudflare(CDN / WAF / 限流)
↓ 加密隧道回源
源站(不对外暴露端口)
源站的业务端口只绑定回环地址(127.0.0.1),由隧道进程转发进来。外网扫不到,也连不上。
附带好处:换服务器时,只要重建隧道,域名和 DNS 记录一个字都不用改。
决策四:把信任边界画清楚
这是我认为最重要的一条。个人项目最常见的错误是:把公开的东西和私密的东西放在同一个地方。
本站的分层:
| 层 | 内容 | 谁能访问 |
|---|---|---|
| 公开层 | 这个博客、在线 Demo | 任何人 |
| 私有层 | 知识库、笔记服务 | 只有我,需登录,走内网 |
| 本地层 | 含敏感信息的实验记录、靶机 | 不出本机 |
红线:任何包含真实凭据、客户信息、未脱敏日志的内容,永远不进公开仓库——哪怕它现在看起来"删掉了"。Git 历史是不会忘的,一旦推上去就永远在里面。
(这一点我踩过:曾经把内部准备的文档和公开仓库放在同一个目录树下。现在内部资料统一放在 docs/internal/,并且从第一次提交起就被 .gitignore 排除——它从来没有进入过 git 历史,这是关键区别。)
决策五:默认凭据一律 fail-closed
这条是从部署 Demo 的过程中逼出来的。
后端服务最初对默认口令只做了"启动时打印一条警告"。这在本地无妨,但一旦暴露到公网,等于把门开着挂了个"请随手关门"的牌子。
改成:
检测到默认/弱口令
├─ 本地开发(开关 = false)→ 打印告警,照常启动
└─ 对外部署(开关 = true)→ 抛异常,拒绝启动
宁可服务起不来,也不带着公开的口令对外提供服务。
这就是 fail-closed(失败关闭) 与 fail-open(失败放行) 的区别。安全组件的默认行为应该是前者:
出问题的时候,系统应该拒绝服务,而不是降级到不安全的状态继续跑。
还没解决的
诚实记录一下当前的问题:
- 评论组件是唯一的第三方依赖——虽然只加载在文章页,但仍是一个外部请求
- GitHub Pages 在国内访问不稳定——后续考虑加一层 CDN,或做国内镜像
- 没有做子资源完整性(SRI)校验——因为目前没有外部脚本,等真引入了再补
- 没有正式的威胁建模——上面的决策是工程直觉,不是系统化的 STRIDE 分析
小结
把一个"写文章的地方"当成"一个有攻击面的系统"来看,选型标准会完全不一样:
| 通常的选型标准 | 这里的选型标准 |
|---|---|
| 功能多不多 | 攻击面小不小 |
| 好不好看 | 依赖少不少 |
| 社区大不大 | 能不能自己说清楚它怎么工作 |
这个习惯带到工作里,就是在做任何一个功能之前,先问一句:这块新增了什么可以被攻击的东西?
这个站的全部源码在 GitHub 上。

