blog-demo · 规划
Studio 外网编辑与内容快速发布规划
单作者远程编辑、GitHub OAuth、通行密钥与受限内容发布的安全架构。
- 状态
- 已完成
- 最后核对
- 源码修订
e0d3baa2
Studio 外网编辑与内容快速发布规划
日期:2026-09-12
状态:已于 2026-09-13 启用生产入口、认证网关和受限部署器;首次浏览器登录与通行密钥登记仍需站点作者验收
工作分支:codex/feature/studio-remote-auth
这份文档记录 Studio 从本机编辑器演进为单作者远程编辑后台的方案。公开博客仍是 Astro 静态站,Studio 作为独立受保护服务运行;普通推送不会更新线上环境。
已确认的输入
- Studio 永远只有一个作者账号,不提供注册、邀请、角色或多人协作。
- 作者需要从任意现代浏览器访问,不要求预装 VPN 客户端。
- 公网入口使用
studio.doyouhang.live,DNS 继续由阿里云管理。 - 现有 ECS 同时承载公开博客入口与 Studio,是 Studio 的固定在线节点。
- 可信设备需要长期免登录;删除内容和正式发布需要更强的二次确认。
- SSH Tunnel 是登录服务故障时的紧急恢复入口。
- 内容可以在线上传、编辑和预览;上线必须由作者明确触发,不能在每次保存时自动发生。
- 内容快速发布优先使用阿里云镜像仓库 ACR,目标耗时为 2 至 5 分钟。该数字是设计目标,完成真实测量前不写成承诺。
方案结论
公开网站不迁移到 SSR,也不引入 Next.js、React 或整套第三方 CMS。Studio 保留现有原生 HTML、CSS、JavaScript 和 Node HTTP 服务,在 ECS 上独立运行。
公网登录使用 oauth2-proxy 接入 GitHub OAuth。网关只配置 authenticated_emails_file,按作者在 GitHub 验证过的邮箱精确放行;不能同时配置 email_domains=["*"],因为 7.15.1 源码会把两者按并集处理并放行任意邮箱。Studio 再核对 GitHub 用户名,只接受两项都匹配的身份。删除内容和正式发布使用 SimpleWebAuthn 完成通行密钥确认。Nginx 继续负责 TLS 和公网入口,Studio 服务不直接发布主机端口。
阿里云 DNS 已经够用。Cloudflare Access 和 Cloudflare Tunnel 可以提供另一套身份网关与源站隐藏能力,但这套方案不依赖它们。当前没有必要为了一个后台再引入第二家 DNS、证书和访问控制平面。
系统边界
任意浏览器
|
| HTTPS: studio.doyouhang.live
v
生产 Nginx
|
| 只转发到认证网关
v
oauth2-proxy ---- GitHub OAuth
|
| 已验证的单一身份
v
Studio Node 服务
|
+---- 私有运行状态:通行密钥、审计、任务状态
|
+---- 可写 Git 工作副本
|
+---- 保存 / 提交 / 推送
|
+---- 内容发布请求
|
v
GitHub Actions -> ACR -> ECS 受限部署器
|
v
www.doyouhang.live
公开站和编辑器仍是两个产品面:
- 公开站继续由只读 Nginx 容器提供 Astro 静态产物。
- Studio 有文件写入、删除、Git 提交和推送能力,使用独立进程、用户、工作目录和运行配置。
- Studio 不编辑
/opt/blog-demo/current,也不挂载 Docker Socket。 - 部署器与 Studio 进程分开。即使 Studio 被攻破,进程本身也不能直接取得 Docker 或 root 权限。
运行边界
Studio 的在线主链路只依赖 ECS、GitHub Actions 与 ACR,不依赖家庭网络中的非持续在线设备。维护电脑只承担日常管理和 SSH 应急,不能成为公网登录或实时发布的必经节点。
| 节点 | 本方案中的职责 | 不承担的职责 |
|---|---|---|
| ECS | TLS、认证网关、Studio、工作副本、部署控制 | 不运行模型推理,不把 Studio 端口直接暴露公网 |
| GitHub Actions | 内容校验、Astro 构建、测试、生成不可变镜像 | 不保存 Studio 会话和作者草稿 |
| 阿里云 ACR | 保存按提交 SHA 标记的镜像 | 不作为内容源,Git 仍是内容记录 |
| 维护电脑 | 日常维护、SSH 应急、离线完整仓库 | 不要求常开,不进入在线主链路 |
实施前需要重新做一次只读盘点,核对 ECS 的 CPU、内存、磁盘、Docker 网络、当前 Nginx 容器、证书目录和端口。ai-infra 中的历史实测是设计依据,不代替上线前检查。
登录与会话
GitHub OAuth
访问 Studio 时,oauth2-proxy 直接发起 GitHub OAuth,不显示自建用户名和密码表单。网关先用 authenticated_emails_file 形式的单行白名单匹配作者在 GitHub 验证过的邮箱;Studio 再从受信任的认证头中读取用户名,按规范化后的精确值校验。任一项不匹配都返回拒绝页。仓库不提供注册接口,也没有可以误开的管理员创建入口。
不能把 --github-user 单独当作排他白名单。oauth2-proxy 7.15.x 的文档和实现把它定义为绕过组织、团队或仓库限制的额外放行项;没有其他限制时,未列入其中的 GitHub 用户仍可能通过认证。实施测试必须包含“另一个真实 GitHub 账号得到 403”,不能只验证作者账号能够登录。
使用 GitHub 登录有一个明确前提:作者的 GitHub 账号必须启用通行密钥或两步验证。GitHub 账号被接管时,Studio 的第一层身份也随之失效。SSH 恢复通道不能绕过对 GitHub 账号本身的保护要求。
oauth2-proxy 已有可复用的 GitHub 身份信息、邮箱白名单、Nginx 集成、安全 Cookie 和 API 401 行为。实施时固定经过核对的版本与镜像摘要,不自行复制其 OAuth 协议实现。
长期可信设备
目标是常用设备无感续期,而不是在界面上声称永不过期。初始策略如下:
- 会话固定最长 180 天。GitHub provider 没有可供 oauth2-proxy 使用的 refresh token,因此
cookie_refresh=0;界面和文档不把固定过期写成无感续期。 - Cookie 设置
Secure、HttpOnly和SameSite=Lax,不设置父域Domain,只作用于 Studio 主机。Lax用于保证从 GitHub 回到 OAuth callback 时携带状态 Cookie;写接口仍由 Origin、CSRF token 和请求类型共同保护。 - OAuth 或 Cookie 密钥轮换可以让全部会话失效。
- 作者主动退出后清除本机 Cookie。
- 会话过期时保留浏览器里的本机恢复副本,重新登录后返回原编辑位置。
需要让所有可信设备立即退出时,通过 SSH 轮换 Cookie secret 并重启认证网关。Cookie 会话存储没有单设备服务端撤销能力;若以后确实需要逐台撤销,再评估 Redis 会话存储,不为当前单作者场景预装。
通行密钥二次确认
GitHub 登录允许日常浏览、编辑、上传、保存、预览、提交和普通推送。下面两类操作需要最近 5 分钟内完成一次 WebAuthn 验证:
- 删除内容或关联图片;
- 发布到生产站。
二次确认只使用通行密钥,不要求再输一遍 GitHub 密码。首次登记和恢复都先通过 SSH 生成单次令牌,再在 https://studio.doyouhang.live 完成浏览器登记;WebAuthn 必须绑定正式 HTTPS 源站,不能冒充可在 localhost 为生产域名登记。通行密钥公钥、计数器、设备名称和已消费恢复令牌的不可逆摘要保存在服务器私有运行目录,不进入 Git。
挑战必须一次性使用,并绑定当前身份、操作类型、目标提交或删除计划。验证通过后,服务端签发短时 step-up 状态。前端传来的“已经验证”布尔值没有任何权限意义。
SSH 紧急恢复
应急入口只绑定 ECS 回环地址,通过现有 SSH 密钥进入:
作者设备 -> SSH Tunnel -> ECS 回环应急端口 -> Studio
这个入口用于检查或轮换 OAuth 配置、让全部 Cookie 失效、生成新的通行密钥登记令牌和修复认证网关。它不加入阿里云安全组,不接受公网连接,也不提供可伪造身份头的长期旁路。恢复完成后回到正式 HTTPS 域名登记通行密钥,并检查公网入口仍不能绕过 oauth2-proxy。
公网和容器网络
- 阿里云 DNS 为
studio.doyouhang.live增加指向现有 ECS 的 A 记录。当前没有生产 AAAA 基线时,不为 Studio 单独添加未经验证的 IPv6 入口。 - 证书增加
studio.doyouhang.live,继续通过现有 Certbot HTTP-01、只读证书挂载和 deploy hook 续期。 - 公网安全组仍只需要 80 和 443;80 仅处理 ACME 与 HTTPS 跳转。
- Nginx 的 Studio server block 只代理 oauth2-proxy,不能直接代理 Studio。
- oauth2-proxy 与 Studio 通过独立后端 Docker 网络连接。Studio 不加入公网边缘网络,优先用网络隔离防止伪造身份头。
- 若最终沿用
host.docker.internal:host-gateway,Studio 上游端口仍只绑定宿主机回环地址,并增加来源和共享网关凭据校验。内部 Docker 网络是首选方案。 - 登录页、编辑器、API、媒体和预览均使用
Cache-Control: no-store;拒绝 framing,设置严格 CSP、Referrer Policy 和 MIME sniffing 防护。 - 图片上传继续允许较大请求,但 Nginx 和 Node 两层使用一致且明确的大小上限。认证接口使用更小的限制。
威胁模型
Studio 不是普通内容页。它可以写文件、删图片、生成 Git 提交并推送,失陷后足以篡改公开网站。
| 风险 | 处理 |
|---|---|
| 未授权账号登录 | oauth2-proxy 精确邮箱白名单,Studio 再校验 GitHub 用户名;无注册入口 |
| Cookie 被窃取 | HTTPS、安全 Cookie、CSP、长期会话可整体吊销;发布和删除仍需通行密钥 |
| CSRF 与 DNS rebinding | 保留并扩展现有 Host/Origin 检查;状态变更只接受 JSON 或明确的上传格式;增加 CSRF token |
| 伪造认证头绕过网关 | Nginx 覆盖外部身份头;Studio 只在隔离后端网络接受 oauth2-proxy 请求 |
| 暴力尝试和扫描 | Nginx 与 oauth2-proxy 限速;登录失败不进入 Studio 日志正文 |
| 路径穿越 | 继续使用安全片段和目录边界断言,新增认证不能削弱现有检查 |
| SSRF | 保留封面来源白名单、协议限制、响应大小与超时;不得添加通用取图接口 |
| 恶意 Markdown 或图片 | 构建期内容校验、预览脚本禁用、生产 CSP;图片重新编码并限制像素、格式和体积 |
| Git 凭据泄露 | Studio 专用部署密钥或细粒度凭据,运行配置不进 Git,文件权限只给服务用户 |
| Studio 获得部署主机权限 | Studio 无 sudo、无 Docker Socket;生产部署由独立受限程序完成 |
| 供应链被替换 | 第三方镜像固定摘要,npm 使用 lockfile,发布清单记录源提交和镜像摘要 |
安全日志记录登录结果、会话退出、通行密钥登记、删除计划、提交、推送、发布请求、部署结果和恢复操作。日志不得包含 OAuth token、Cookie、图片内容、文章正文或密钥。
编辑、上传和发布状态
Studio 继续区分以下状态:
本机恢复副本
-> 文件已保存
-> 本地已提交
-> 已推送
-> 构建中
-> 候选版本已验证
-> 部署中
-> 公网已确认上线
构建或部署失败
-> 线上继续运行旧版本
上传图片只会把压缩后的文件写入内容目录。保存、提交和推送都不会自动上线。作者点击“发布到线上”,核对清单并完成通行密钥确认后,才进入内容快速发布流程。
内容快速发布
快速通道只接受下列源码:
src/content/**中的 Markdown 与 Studio 重编码后的 JPEG;AVIF、HEIF、SVG 和其他未审文件类型不走快速通道src/data/editorial.jsonsrc/data/now.json
待发布范围中只要出现组件、样式、脚本、依赖、工作流、Studio 或基础设施文件,快速发布立即停止,并提示改走稳定 SemVer Release。它不尝试猜测某次代码改动“看起来应该没事”。
一次内容发布按以下顺序执行:
- Studio 检查工作副本、分支、远端、未保存修改和待推送提交。
- 作者查看真实文件清单;必要时先保存、提交并推送到
main。普通推送仍不会自动上线。 - 作者点击“发布到线上”,使用通行密钥确认目标提交 SHA。
- Studio 用专用 GitHub App 或细粒度 token 触发固定的
workflow_dispatch,只提交目标 SHA 和发布请求 ID;凭据不能修改工作流。 - GitHub Actions 再次计算已部署 SHA 到目标 SHA 的完整差异,独立执行路径白名单检查。
- 使用固定 Node 版本和 lockfile 运行 Astro 检查、单元测试、生产构建及内容相关 smoke test。
- 构建
content-<commit SHA>的linux/amd64不可变镜像,记录源码 SHA、镜像摘要和校验值,并推送 ACR。 - Studio 只轮询该 workflow run。成功后,它通过本机 Unix Socket 请求 ECS 受限部署器处理目标 SHA,不向浏览器暴露部署器接口。
- 部署器自己从 GitHub API 核对固定 workflow、受信任的 workflow 文件哈希、目标 SHA、run conclusion 和发布范围,再从 ACR 解析并拉取对应镜像摘要;不能信任 Studio 传来的“构建已通过”布尔值。
- 部署器取得部署锁,在回环候选端口启动并检查首页、目标文章、静态资源和健康页。候选通过后替换生产 Web 容器;失败则保留旧容器。
- 从公网读取版本标记并确认目标 SHA。只有这一步通过,Studio 才显示“已上线”。
生产构建应生成不含凭据的版本标记,例如 /version.json,只包含提交 SHA、构建时间和发布类型。它用于部署核对,不用旧页面内容或 GitHub Actions 的成功状态冒充线上版本。
发布权限隔离
GitHub Actions 可以构建并推送镜像,但不应获得 ECS 的普通交互式 shell。部署端使用独立凭据和固定命令,只接受格式正确的镜像摘要及发布清单。部署程序自行检查允许的镜像仓库、部署锁、磁盘空间、候选健康和回滚目标。
Studio 只提交发布请求并查询状态,不能直接调用 Docker。浏览器会话被盗时,攻击者还需要通过通行密钥确认。Studio 进程被攻破时,网络和 Unix Socket 权限仍应阻止它直接控制 Docker 或取得宿主机权限;但它可能修改、删除或推送内容,通行密钥也无法约束已经取得服务端执行能力的攻击者。这里的剩余控制是最小化 Git 凭据、部署器独立复核、脱敏审计、凭据快速吊销以及 Git 和备份恢复,不能把二次确认写成对服务端入侵的保护。
“实时”的定义
本方案追求的是显式发布后的近实时上线,目标为 2 至 5 分钟。实际耗时要按构建、镜像上传、ECS 拉取和健康检查分别测量。
如果要求十几秒内上线,就要改成可写静态目录、对象存储同步或长期运行的构建服务。这会削弱当前不可变镜像、版本目录和回滚记录,不纳入本方案。
Studio 代码调整
不做大迁移。先把 studio/server.mjs 的修改边界拆开,再接入认证和发布:
studio/
server.mjs HTTP 入口与依赖装配
config.mjs 运行目录、公网源站、可信代理
security.mjs Host、Origin、CSRF、响应头
identity.mjs oauth2-proxy 身份校验
step-up.mjs WebAuthn 通行密钥与短时授权
audit.mjs 脱敏审计事件
publish.mjs 内容发布状态机
routes/ 内容、媒体、预览、Git、发布接口
现有 API 和 Markdown 格式尽量保持不变。拆分本身不能混入登录行为变化;认证、发布和部署分别提交,方便回退与审阅。
界面沿用 Studio 的暖纸、墨色、藏青和小面积朱红。登录失败、会话过期、发布等待与部署失败使用直接中文,不显示 OAuth、OIDC、WebAuthn assertion 等协议术语。手机端仍要保留保存、提交和发布状态;高风险确认不能依赖 hover。
私有状态、备份和恢复
服务器目录建议分为三类:
- 可重建:应用镜像、依赖、临时预览和构建缓存。
- 必须备份:Studio Git 工作副本中尚未推送的内容、通行密钥公钥状态、发布记录和必要审计日志。
- 只能由配置恢复:OAuth client secret、Cookie secret、Git 推送凭据、ACR 凭据和部署器凭据。
真实配置保存在仓库外,例如 /etc/blog-studio/,权限只给服务用户或 root。Git 只保存 .example、Compose 模板、测试、文档和无敏感信息的部署脚本。
GitHub 已保存推送后的内容,但不能代替未推送草稿和认证状态备份。可让 ECS 每日生成加密快照,并在受控备份节点可达时传送第二份。备份方案完成的判断标准不是“文件存在”,而是能在隔离目录恢复并登录、预览一条测试内容。
实施阶段
阶段 0:现场盘点与决策记录
- 只读核对 ECS 资源、Nginx、Docker、证书、磁盘、Git 和现有端口。
- 确认 GitHub OAuth App、唯一用户名、白名单邮箱、回调地址和 GitHub 两步验证状态。
- 确认 ACR namespace、认证方式和 ECS 拉取速度。
- 形成不含真实凭据的部署清单与回滚清单。
验收:没有修改生产状态;文档中的实际端口、路径和运行方式与服务器一致。
阶段 1:本地重构与安全测试
- 拆分 Studio 服务端模块,保持本机行为不变。
- 补未登录、错误 Origin、错误 Host、代理头伪造、CSRF、媒体和预览保护测试。
- 所有写入测试继续使用临时仓库,不触碰作者内容。
验收:原 Studio 测试、站点检查、单元测试和视觉回归通过;本机 Studio 使用方式不变。
阶段 2:ECS 私有运行与 OAuth
- 建立专用用户、工作目录、私有配置和容器网络。
- 先通过 SSH Tunnel 验证 Studio,再接入 oauth2-proxy。
- 增加
studio.doyouhang.live的 DNS、证书和 Nginx 配置。 - 从外网验证未登录、错误账号、正确账号、退出和会话过期。
验收:公网只能通过 GitHub 白名单访问;Studio 端口和应急入口公网不可达;没有改变公开博客内容。
阶段 3:通行密钥与高风险操作
- 通过 SSH 一次性引导登记首个通行密钥。
- 删除和正式发布增加绑定目标的 WebAuthn 验证。
- 增加恢复、密钥轮换和审计命令。
验收:普通会话不能绕过二次确认;过期挑战、重放和目标替换均失败;SSH 可以恢复但不能从公网调用恢复接口。
阶段 4:内容快速发布
- 增加内容路径检查、发布工作流、ACR 镜像和受限部署器。
- 增加部署锁、候选健康检查、版本标记、公网确认和自动回滚。
- Studio 显示各阶段真实状态,网络中断后可以继续查询同一任务。
验收:内容提交可上线;代码混入时被拒绝;构建失败、镜像拉取失败和健康检查失败都保持旧站;成功状态对应公网目标 SHA。
阶段 5:备份、恢复和运维收口
- 配置加密备份与保留周期。
- 演练 OAuth 故障、Cookie 全部失效、通行密钥丢失、Studio 工作副本损坏和生产回滚。
- 将真实命令、日志位置和告警写入运维手册;不把临时端口与秘密写进
AGENTS.md。
验收:恢复演练留下时间、输入、结果和校验记录;至少保留当前生产版本及一个实测可用的回滚版本。
测试清单
- 单元测试:身份头、Origin/Host、CSRF、挑战过期、重放、发布范围和状态机。
- Studio 浏览器测试:登录返回、会话过期不丢字、窄屏上传、删除确认、发布状态恢复。
- 容器测试:没有 Studio 公网端口、无 Docker Socket、非 root、只挂载必要目录。
- Nginx 测试:未知 Host 拒绝、HTTP 跳转、证书域名、未登录 API 401、上传上限和安全响应头。
- 发布测试:内容成功、代码混入、并发发布、构建失败、ACR 不可达、候选失败和回滚。
- 人工验证:按项目约定优先使用 Microsoft Edge;桌面和手机宽度都要检查。其他浏览器只做兼容性补充。
- 恢复测试:SSH Tunnel、OAuth 配置回滚、通行密钥重新登记、加密备份还原。
开源项目参考
| 项目 | 本方案借鉴的部分 | 是否引入 |
|---|---|---|
| oauth2-proxy/oauth2-proxy | GitHub OAuth、精确邮箱白名单、Nginx 前置认证、安全 Cookie、API 401;github_users 只作额外放行项,不能独自承担排他白名单 |
引入独立认证网关,固定版本和镜像摘要 |
| MasterKale/SimpleWebAuthn | WebAuthn 注册与验证、浏览器能力处理、服务端响应校验 | 只用于发布和删除的二次确认 |
| decaporg/decap-cms | 草稿、就绪、发布分层以及发布前确认 | 只借鉴状态模型,不引入 CMS 本体 |
| authelia/authelia | 反向代理认证、长期会话和二次认证的运维思路 | 不引入;一个账号、一个应用时服务与持久化偏重 |
| pocket-id/pocket-id | 自托管 OIDC 与 passkey-only 登录 | 不引入;会增加一套身份服务、备份和恢复面 |
| tinacms/tinacms | Git 内容管理、媒体上传与跨源写请求防护 | 不引入;会改变现有 schema、编辑器和构建边界 |
| hunvreus/pagescms | 轻量 Git-based CMS 的内容与媒体管理思路 | 不引入;现有 Studio 已覆盖主要编辑能力 |
第三方项目只提供设计和实现参考。实施前需要重新核对维护状态、许可证、目标版本文档、已知安全问题、镜像来源与摘要;本规划不锁定未来版本号。
当前实现记录
截至 2026-09-12,分支已经实现但尚未部署:本机/远程双模式、oauth2-proxy 入口模板、身份头二次核对、Host/Origin/CSRF/请求类型与体积限制、安全响应头、脱敏 JSONL 审计、通行密钥登记和目标绑定的一次性授权、内容快速发布工作流、不可变镜像清单、Unix Socket 受限部署器、候选检查、公网版本核对与自动回滚、systemd 和环境文件模板。
恢复令牌成功登记后,其 SHA-256 摘要与通行密钥状态原子保存;同一令牌在服务重启后仍不能再次使用。Studio 和 oauth2-proxy 都没有主机端口、Docker Socket、sudo 或 root 权限;部署器是唯一能调用 Docker 的受信任 root 服务,只通过 systemd 创建的 root-owned Unix Socket 接受固定格式请求。
本轮新增的 SimpleWebAuthn 依赖没有出现在 npm audit 告警中。仓库当前仍有来自既有 Astro 5.18.2 依赖树的 1 low、4 high、1 critical 公告,其中整体自动修复会要求迁移到 Astro 7。该架构升级不混入本功能分支;发布前需要单独评估,不能宣称审计已清零。
当前不做
- 多账号、邀请、角色和权限编辑器。
- 评论、审核队列和多人 editorial workflow。
- 保存即发布或上传即发布。
- 将公开博客迁移为动态 SSR 服务。
- 把 Studio、认证状态、密钥或工作副本打进公开网站镜像。
- 让 Studio 直接获得 Docker、root 或任意 shell 能力。
- 为了十几秒上线改成可写生产目录。
开始实施时仍需要作者提供或现场确认:唯一 GitHub 用户名、该账号在 GitHub 验证过且愿意用于白名单的邮箱、GitHub OAuth App 的创建结果、ACR namespace,以及首次通行密钥登记。client secret、Cookie secret、Git 和 ACR 凭据不得写入聊天、Git 或普通命令日志。