<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>折腾记录 on BINGBlog</title><link>https://xbzhang.xyz/categories/%E6%8A%98%E8%85%BE%E8%AE%B0%E5%BD%95/</link><description>Recent content in 折腾记录 on BINGBlog</description><generator>Hugo -- gohugo.io</generator><language>zh-cn</language><lastBuildDate>Thu, 17 Sep 2026 17:00:00 +0800</lastBuildDate><atom:link href="https://xbzhang.xyz/categories/%E6%8A%98%E8%85%BE%E8%AE%B0%E5%BD%95/index.xml" rel="self" type="application/rss+xml"/><item><title>把 dsh 挂上公网：两道闸与自动登录</title><link>https://xbzhang.xyz/posts/2026-09-17-dsh-public-web/</link><pubDate>Thu, 17 Sep 2026 17:00:00 +0800</pubDate><guid>https://xbzhang.xyz/posts/2026-09-17-dsh-public-web/</guid><description>&lt;p&gt;dsh（DeepSeek Harness）是 DeepSeek 开源的 agent harness，有 CLI 也有 Web UI，默认只监听 &lt;code&gt;127.0.0.1:3080&lt;/code&gt;，CLI 明确拒绝 &lt;code&gt;--host 0.0.0.0&lt;/code&gt;。它自带启动 token 与签名 cookie 两道鉴权，但没有 TLS、没有账号体系，官方也把「网络部署」列为不支持。想在服务器上用，就得自己在前面架一层。下面是完整过程，机器是 Azure 上那台 892MB 的小 VPS。&lt;/p&gt;&#10;&lt;p&gt;&lt;img alt="整体链路" data-filename="architecture.svg" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://xbzhang.xyz/images/posts/dsh-public-web/architecture.svg"&gt;&#10;&lt;/p&gt;&#10;&lt;h2 id="一为什么不能直接开出去"&gt;&lt;a href="#%e4%b8%80%e4%b8%ba%e4%bb%80%e4%b9%88%e4%b8%8d%e8%83%bd%e7%9b%b4%e6%8e%a5%e5%bc%80%e5%87%ba%e5%8e%bb" class="header-anchor"&gt;&lt;/a&gt;一、为什么不能直接开出去&#10;&lt;/h2&gt;&lt;p&gt;dsh 的 Web 宿主在 &lt;code&gt;/api&lt;/code&gt; 前缀上有一道「浏览器信任栅栏」，代码注释写得很直白：它挡的是 DNS rebinding 和跨站请求，不是身份认证。&lt;/p&gt;&#10;&lt;table&gt;&#10;&#9;&lt;thead&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;检查&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;规则&lt;/th&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/thead&gt;&#10;&#9;&lt;tbody&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Host&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;必须是回环地址，或与 &lt;code&gt;--trusted-host&lt;/code&gt; 声明的 authority 匹配&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Origin（带了才查）&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;必须与 Host 完全一致&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;sec-fetch-site&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;cross-site&lt;/code&gt; 一律拒&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;POST 媒体类型&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;必须是 &lt;code&gt;application/json&lt;/code&gt;，否则 415&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;p&gt;认证是另一层：每个进程随机生成一个启动 token，只有 &lt;code&gt;GET /?token=...&lt;/code&gt; 能把它换成签名 cookie（HttpOnly、SameSite=Strict、绑定 &lt;code&gt;host:port&lt;/code&gt;、默认 30 天）。token 不落盘、每次启动都变；签名密钥存在 &lt;code&gt;$DSH_HOME/.credentials.yaml&lt;/code&gt;，所以 cookie 能跨重启继续用。&lt;/p&gt;&#10;&lt;p&gt;两条结论直接决定反代怎么写：&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;&lt;strong&gt;Host 必须原样转发&lt;/strong&gt;。写成 &lt;code&gt;$host:443&lt;/code&gt; 会当场 403 —— 浏览器发来的 Origin 是 &lt;code&gt;https://域名&lt;/code&gt;，归一化后不带端口，和 &lt;code&gt;host:443&lt;/code&gt; 对不上。&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;必须传 &lt;code&gt;--trusted-host &amp;lt;域名&amp;gt;&lt;/code&gt;&lt;/strong&gt;，否则 &lt;code&gt;/api&lt;/code&gt; 全部被 Host 栅栏拒掉。&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;h2 id="二安装与首启"&gt;&lt;a href="#%e4%ba%8c%e5%ae%89%e8%a3%85%e4%b8%8e%e9%a6%96%e5%90%af" class="header-anchor"&gt;&lt;/a&gt;二、安装与首启&#10;&lt;/h2&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo useradd -m -s /bin/bash dsh &lt;span class="c1"&gt;# 独立账号，不进 sudo 组&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo npm i -g @deepseek-ai/dsh &lt;span class="c1"&gt;# 本次装到 0.1.5-rc.1&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;首次启动会从内置模板生成 profile（&lt;code&gt;$DSH_HOME/profiles&lt;/code&gt;，要装一批插件依赖），所以第一遍慢一些：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo -u dsh env &lt;span class="nv"&gt;HOME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/home/dsh &lt;span class="nv"&gt;DSH_HOME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/home/dsh/.dsh &lt;span class="se"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; dsh web --port &lt;span class="m"&gt;3080&lt;/span&gt; --no-open --trusted-host dsh-xxxx.你的域名&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# dsh web: http://127.0.0.1:3080/?token=...&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;--no-open&lt;/code&gt; 不弹浏览器，&lt;code&gt;--trusted-host&lt;/code&gt; 只扩信任栅栏，不给身份。&lt;/p&gt;&#10;&lt;h2 id="三systemd-托管"&gt;&lt;a href="#%e4%b8%89systemd-%e6%89%98%e7%ae%a1" class="header-anchor"&gt;&lt;/a&gt;三、systemd 托管&#10;&lt;/h2&gt;&lt;p&gt;机器只有 892MB 内存，必须给上限；再按 RCE 服务的标准一并做隔离：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-ini" data-lang="ini"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# /etc/systemd/system/dsh-web.service&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;[Unit]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;Description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;DeepSeek Harness (dsh) Web UI&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;After&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;network-online.target&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;Wants&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;network-online.target&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;[Service]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;simple&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;User&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;dsh&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;Group&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;dsh&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;WorkingDirectory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/home/dsh/workspace # 工作区就是启动目录&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;Environment&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;HOME=/home/dsh&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;Environment&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;DSH_HOME=/home/dsh/.dsh&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;EnvironmentFile&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/etc/dsh/dsh.env # DEEPSEEK_API_KEY，600 root&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;ExecStart&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/usr/bin/dsh web --port 3080 --no-open --trusted-host dsh-xxxx.你的域名&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;Restart&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;always&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;RestartSec&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;5&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;MemoryMax&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;800M&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;TasksMax&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;512&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;NoNewPrivileges&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;PrivateTmp&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;PrivateDevices&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;ProtectSystem&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;strict&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;ProtectHome&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;read-only&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;ReadWritePaths&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/home/dsh&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;ProtectKernelTunables&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;ProtectKernelModules&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;ProtectControlGroups&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;RestrictSUIDSGID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;LockPersonality&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;CapabilityBoundingSet&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;AmbientCapabilities&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;RestrictAddressFamilies&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;AF_UNIX AF_INET AF_INET6 AF_NETLINK&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;UMask&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;0077&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;[Install]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;WantedBy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;multi-user.target&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;ProtectSystem=strict&lt;/code&gt; 把整个根文件系统挂成只读，唯一能写的是 &lt;code&gt;ReadWritePaths&lt;/code&gt; 指定的 &lt;code&gt;/home/dsh&lt;/code&gt;。&lt;code&gt;systemd-analyze security dsh-web&lt;/code&gt; 实测 4.1 OK。&lt;/p&gt;&#10;&lt;h2 id="四key-走环境变量"&gt;&lt;a href="#%e5%9b%9bkey-%e8%b5%b0%e7%8e%af%e5%a2%83%e5%8f%98%e9%87%8f" class="header-anchor"&gt;&lt;/a&gt;四、Key 走环境变量&#10;&lt;/h2&gt;&lt;p&gt;内置的 &lt;code&gt;llm-deepseek&lt;/code&gt; 适配器默认凭据引用就是 &lt;code&gt;DEEPSEEK_API_KEY&lt;/code&gt;，用 systemd &lt;code&gt;EnvironmentFile&lt;/code&gt; 注入：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# /etc/dsh/dsh.env（600 root:root）&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;DEEPSEEK_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;sk-xxxxxxxx&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;由环境变量供值的引用在 Web 设置里是只读的（&lt;code&gt;writable: false&lt;/code&gt;），key 不进应用可写状态，轮换只要改这个文件加一次 restart。&lt;/p&gt;&#10;&lt;h2 id="五公网入口"&gt;&lt;a href="#%e4%ba%94%e5%85%ac%e7%bd%91%e5%85%a5%e5%8f%a3" class="header-anchor"&gt;&lt;/a&gt;五、公网入口&#10;&lt;/h2&gt;&lt;h3 id="为什么用子域名"&gt;&lt;a href="#%e4%b8%ba%e4%bb%80%e4%b9%88%e7%94%a8%e5%ad%90%e5%9f%9f%e5%90%8d" class="header-anchor"&gt;&lt;/a&gt;为什么用子域名&#10;&lt;/h3&gt;&lt;p&gt;dsh 前端是标准 SPA，静态资源与 &lt;code&gt;/api&lt;/code&gt; 都走绝对路径，用路径前缀反代要动一堆地方，cookie 的 authority 绑定也更绕。子域名最省事；没有域名就用 sslip.io：&lt;code&gt;dsh-xxxx.&amp;lt;IP&amp;gt;.sslip.io&lt;/code&gt; 会解析到那个 IP，Let&amp;rsquo;s Encrypt 照签。&lt;/p&gt;&#10;&lt;p&gt;随机子域名本身当第一层&amp;quot;隐藏&amp;quot;：串不可猜。证书用 webroot 签，80 端口的 server 块只放行 ACME 校验：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-nginx" data-lang="nginx"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;location&lt;/span&gt; &lt;span class="s"&gt;^~&lt;/span&gt; &lt;span class="s"&gt;/.well-known/acme-challenge/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="kn"&gt;root&lt;/span&gt; &lt;span class="s"&gt;/var/www/html&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="kn"&gt;auth_basic&lt;/span&gt; &lt;span class="no"&gt;off&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo certbot certonly --webroot -w /var/www/html -d dsh-xxxx.你的域名&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Basic Auth 账号，密码用 openssl 生成 apr1 哈希（服务器上没装 htpasswd 也能用）&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;dsh:%s\n&amp;#39;&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;openssl passwd -apr1 &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$PASS&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="se"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;|&lt;/span&gt; sudo install -m &lt;span class="m"&gt;640&lt;/span&gt; -o root -g www-data /dev/stdin /etc/nginx/.htpasswd-dsh&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="反代与限速"&gt;&lt;a href="#%e5%8f%8d%e4%bb%a3%e4%b8%8e%e9%99%90%e9%80%9f" class="header-anchor"&gt;&lt;/a&gt;反代与限速&#10;&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-nginx" data-lang="nginx"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;server&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;listen&lt;/span&gt; &lt;span class="mi"&gt;443&lt;/span&gt; &lt;span class="s"&gt;ssl&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;server_name&lt;/span&gt; &lt;span class="s"&gt;dsh-xxxx.你的域名&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;# 只有这个名字能进&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;auth_basic&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;DeepSeek&lt;/span&gt; &lt;span class="s"&gt;Harness&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;# 第一道闸&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;auth_basic_user_file&lt;/span&gt; &lt;span class="s"&gt;/etc/nginx/.htpasswd-dsh&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;limit_req&lt;/span&gt; &lt;span class="s"&gt;zone=dsh_zone&lt;/span&gt; &lt;span class="s"&gt;burst=120&lt;/span&gt; &lt;span class="s"&gt;nodelay&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;client_max_body_size&lt;/span&gt; &lt;span class="mi"&gt;100m&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="s"&gt;/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;proxy_pass&lt;/span&gt; &lt;span class="s"&gt;http://127.0.0.1:3080&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;proxy_http_version&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="s"&gt;.1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;Host&lt;/span&gt; &lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;# 不带端口，理由见第一节&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;Upgrade&lt;/span&gt; &lt;span class="nv"&gt;$http_upgrade&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;Connection&lt;/span&gt; &lt;span class="nv"&gt;$connection_upgrade&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;proxy_buffering&lt;/span&gt; &lt;span class="no"&gt;off&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;# SSE / 流式必需&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;proxy_read_timeout&lt;/span&gt; &lt;span class="s"&gt;3600s&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;# 长任务别被 60s 掐断&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;proxy_request_buffering&lt;/span&gt; &lt;span class="no"&gt;off&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="六第二道闸的坑"&gt;&lt;a href="#%e5%85%ad%e7%ac%ac%e4%ba%8c%e9%81%93%e9%97%b8%e7%9a%84%e5%9d%91" class="header-anchor"&gt;&lt;/a&gt;六、第二道闸的坑&#10;&lt;/h2&gt;&lt;p&gt;只过 Basic Auth 会被 dsh 自己挡住：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;dsh web authentication required; reopen the URL printed by dsh web.&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这是启动 token 那道闸。手动过的话，从服务日志里捞当前进程的 URL：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;journalctl -u dsh-web -o cat &lt;span class="p"&gt;|&lt;/span&gt; grep -o &lt;span class="s1"&gt;&amp;#39;token=[A-Za-z0-9_-]*&amp;#39;&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; tail -1&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;两个容易踩的点：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;上游返回的 401 &lt;strong&gt;不会&lt;/strong&gt;触发 &lt;code&gt;error_page&lt;/code&gt;：nginx 默认 &lt;code&gt;proxy_intercept_errors off&lt;/code&gt;，只拦自己产生的错误码，要在这个 location 里显式打开&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;map $http_upgrade $connection_upgrade&lt;/code&gt; 得定义在 http 块；漏了 WebSocket 升级会失败，页面上表现为一直转圈&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="七让闸门无感"&gt;&lt;a href="#%e4%b8%83%e8%ae%a9%e9%97%b8%e9%97%a8%e6%97%a0%e6%84%9f" class="header-anchor"&gt;&lt;/a&gt;七、让闸门无感&#10;&lt;/h2&gt;&lt;p&gt;token 每进程都在 journal 里，可以让 nginx 自己拿它换 cookie：首页拿到 401 时内部重试一次带 token 的请求，把 &lt;code&gt;303 + Set-Cookie&lt;/code&gt; 原样透给浏览器。浏览器地址栏全程不带 token，比手动粘贴 URL 更干净。&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-nginx" data-lang="nginx"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;location&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;error_page&lt;/span&gt; &lt;span class="mi"&gt;401&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;@dsh_login&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;proxy_intercept_errors&lt;/span&gt; &lt;span class="no"&gt;on&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;# 关键：否则不触发 error_page&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;proxy_pass&lt;/span&gt; &lt;span class="s"&gt;http://127.0.0.1:3080&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;location&lt;/span&gt; &lt;span class="s"&gt;@dsh_login&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;proxy_pass&lt;/span&gt; &lt;span class="s"&gt;http://127.0.0.1:3080/?token=&lt;/span&gt;&lt;span class="nv"&gt;$dsh_launch_token&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;$dsh_launch_token&lt;/code&gt; 由一个小脚本同步：从 journal 取当前进程 token，写进 &lt;code&gt;/etc/nginx/conf.d/dsh-token.conf&lt;/code&gt; 里的 &lt;code&gt;map&lt;/code&gt;，再 reload nginx。挂到服务启动钩子上，重启后 token 自动跟上：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-ini" data-lang="ini"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# /etc/systemd/system/dsh-web.service.d/token-sync.conf&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;[Service]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;ExecStartPost&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;+/usr/local/bin/dsh-token-sync # + 前缀 = 以 root 跑，才能读 journal、reload nginx&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;img alt="两道闸的时序" data-filename="auth-flow.svg" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://xbzhang.xyz/images/posts/dsh-public-web/auth-flow.svg"&gt;&#10;&lt;/p&gt;&#10;&lt;p&gt;代价要说清楚：这样一来对外实际只剩 Basic Auth 一道闸，加上它前面的随机子域名。想恢复两道闸，屏蔽掉内部重试即可：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo touch /etc/dsh/auto-login.off &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; sudo dsh-token-sync --now &lt;span class="c1"&gt;# 关&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo rm /etc/dsh/auto-login.off &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; sudo dsh-token-sync --now &lt;span class="c1"&gt;# 开&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="八验证"&gt;&lt;a href="#%e5%85%ab%e9%aa%8c%e8%af%81" class="header-anchor"&gt;&lt;/a&gt;八、验证&#10;&lt;/h2&gt;&lt;p&gt;鉴权矩阵，全部从公网入口测：&lt;/p&gt;&#10;&lt;table&gt;&#10;&#9;&lt;thead&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;请求&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;结果&lt;/th&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/thead&gt;&#10;&#9;&lt;tbody&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;无 Basic Auth&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;401&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;密码错误&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;401&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Basic Auth、无 cookie&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;303 + Set-Cookie（自动登录）&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Basic Auth、带 cookie&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;200&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;直连 &lt;code&gt;127.0.0.1:3080&lt;/code&gt; 并伪造 &lt;code&gt;Host: localhost&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;401（信任栅栏与 cookie 双查）&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;p&gt;最后用无头浏览器跑一轮真实任务，确认 &lt;code&gt;/api&lt;/code&gt;、工具调用、模型都通：&lt;/p&gt;&#10;&lt;p&gt;&lt;img alt="dsh Web UI 里跑一轮任务" class="gallery-image" data-filename="ui.png" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://xbzhang.xyz/images/posts/dsh-public-web/ui.png"&gt;&#10;&lt;/p&gt;&#10;&lt;h2 id="九隔离与限制"&gt;&lt;a href="#%e4%b9%9d%e9%9a%94%e7%a6%bb%e4%b8%8e%e9%99%90%e5%88%b6" class="header-anchor"&gt;&lt;/a&gt;九、隔离与限制&#10;&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;同机隔离&lt;/strong&gt;：dsh 能执行任意命令，这台机器上还跑着网关和博客，它的进程不该读到后者的凭据。除 &lt;code&gt;ProtectHome=read-only&lt;/code&gt; 外再补一条 ACL，把整个家目录对它关掉：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo setfacl -m u:dsh:--- /home/你的用户 &lt;span class="c1"&gt;# 撤销：sudo setfacl -x u:dsh /home/你的用户&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;同机其他服务的家目录里若有 world-readable 的凭据文件（&lt;code&gt;~/.cli-proxy-api/config.yaml&lt;/code&gt;、&lt;code&gt;~/.pi&lt;/code&gt;、&lt;code&gt;~/.codex&lt;/code&gt;），一并收成 700。&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;设置页在非回环访问下不可用&lt;/strong&gt;：客户端会判断页面自身是不是 loopback，不是就把设置持久化降级成内存（源码里是 &lt;code&gt;isLoopback ? 'host' : 'memory'&lt;/code&gt;），于是「设置 → 模型」直接报 &lt;code&gt;settings are unavailable in this browser&lt;/code&gt;。模型与凭据都改服务端文件：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# /home/dsh/.dsh/settings.yaml，改完下一次请求生效，不用重启&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;llm-deepseek&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;reasoningEffort&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;high&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;公网暴露 RCE 的现实&lt;/strong&gt;：过了 Basic Auth 就是一个能跑命令的 agent。密码用 24 位随机串、限定单域名、开限速，能做的都做了；要更稳就关掉自动登录，只用一次性 token URL 进。&lt;/p&gt;&#10;&lt;h2 id="小结"&gt;&lt;a href="#%e5%b0%8f%e7%bb%93" class="header-anchor"&gt;&lt;/a&gt;小结&#10;&lt;/h2&gt;&lt;ol&gt;&#10;&lt;li&gt;dsh 默认只回环，反代要原样转发 Host 并传 &lt;code&gt;--trusted-host&lt;/code&gt;，否则 &lt;code&gt;/api&lt;/code&gt; 被信任栅栏拒掉&lt;/li&gt;&#10;&lt;li&gt;上游 401 不触发 &lt;code&gt;error_page&lt;/code&gt;，&lt;code&gt;proxy_intercept_errors on&lt;/code&gt; 是自动换 cookie 的前提&lt;/li&gt;&#10;&lt;li&gt;token 每进程轮换、cookie 签名密钥持久化；用 &lt;code&gt;ExecStartPost=+脚本&lt;/code&gt; 把 token 同步给 nginx，重启后进入无感&lt;/li&gt;&#10;&lt;li&gt;按 RCE 服务对待：独立用户、只读根、只写自己 home、清空 capability，再用 ACL 挡住同机其他家目录&lt;/li&gt;&#10;&lt;li&gt;非回环页面的设置页是禁用的，模型与凭据统一走 &lt;code&gt;settings.yaml&lt;/code&gt; 和环境变量&lt;/li&gt;&#10;&lt;/ol&gt;&#10;</description></item><item><title>把云主机挂成本地盘：WSL + Windows 双端实战</title><link>https://xbzhang.xyz/posts/2026-09-16-mount-azure-vm/</link><pubDate>Wed, 16 Sep 2026 22:40:00 +0800</pubDate><guid>https://xbzhang.xyz/posts/2026-09-16-mount-azure-vm/</guid><description>&lt;p&gt;博客和代理跑在同一台 Azure 小机器上，改文件只能 &lt;code&gt;ssh&lt;/code&gt; + &lt;code&gt;vim&lt;/code&gt;。于是把它的家目录挂成本地盘：WSL 里是普通路径，Windows 里是盘符。&lt;/p&gt;&#10;&lt;p&gt;&lt;img alt="最终效果：资源管理器里的 Z: 盘" class="gallery-image" data-filename="z-drive.png" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://xbzhang.xyz/images/posts/mount-azure-vm/z-drive.png"&gt;&#10;&lt;/p&gt;&#10;&lt;table&gt;&#10;&#9;&lt;thead&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;场景&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;方案&lt;/th&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/thead&gt;&#10;&#9;&lt;tbody&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;终端 / VS Code&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;WSL：&lt;code&gt;~/remote/vm&lt;/code&gt;（sshfs + systemd，断线自愈）&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;资源管理器&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Windows：&lt;strong&gt;Z: 盘&lt;/strong&gt;（rclone + WinFsp）&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;p&gt;前提：这台机器&lt;strong&gt;直连不通&lt;/strong&gt;——TCP 握手能通，SSH 立刻被 reset，所有流量必须走本机 clash 代理（&lt;code&gt;127.0.0.1:7890&lt;/code&gt;）。&lt;/p&gt;&#10;&lt;p&gt;&lt;img alt="架构" data-filename="architecture.svg" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://xbzhang.xyz/images/posts/mount-azure-vm/architecture.svg"&gt;&#10;&lt;/p&gt;&#10;&lt;h2 id="wsl-侧sshfs--systemd-自愈"&gt;&lt;a href="#wsl-%e4%be%a7sshfs--systemd-%e8%87%aa%e6%84%88" class="header-anchor"&gt;&lt;/a&gt;WSL 侧：sshfs + systemd 自愈&#10;&lt;/h2&gt;&lt;p&gt;sshfs 协议成熟、支持断线重连，用起来和本地路径没差别。先让 SSH 走代理：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Host proxy-vm&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; HostName 20.48.50.55&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; User azureuser&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; IdentityFile ~/.ssh/id_rsa&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ProxyCommand nc -X connect -x 127.0.0.1:7890 %h %p&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;再交给 systemd user 服务托管，断线自动重挂：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-ini" data-lang="ini"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# ~/.config/systemd/user/azure-proxy-vm-mount.service&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;[Unit]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;Description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;Mount Azure proxy-vm home via SSHFS&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;StartLimitIntervalSec&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;[Service]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;simple&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;ExecStartPre&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;-/usr/bin/fusermount3 -uz %h/remote/vm # 清掉残留挂载&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;ExecStart&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/usr/bin/sshfs proxy-vm:/home/azureuser %h/remote/vm -f \&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; -o reconnect,ServerAliveInterval=15,ServerAliveCountMax=3&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;ExecStop&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/usr/bin/fusermount3 -u %h/remote/vm&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;Restart&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;always&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;RestartSec&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;10&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;[Install]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;WantedBy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;default.target&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;systemctl --user &lt;span class="nb"&gt;enable&lt;/span&gt; --now azure-proxy-vm-mount&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo loginctl enable-linger &lt;span class="nv"&gt;$USER&lt;/span&gt; &lt;span class="c1"&gt;# 不登录也保活&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;实测：&lt;code&gt;kill -9&lt;/code&gt; 后 10 秒内自动恢复。&lt;/p&gt;&#10;&lt;h2 id="windows-侧为什么最后选了-rclone"&gt;&lt;a href="#windows-%e4%be%a7%e4%b8%ba%e4%bb%80%e4%b9%88%e6%9c%80%e5%90%8e%e9%80%89%e4%ba%86-rclone" class="header-anchor"&gt;&lt;/a&gt;Windows 侧：为什么最后选了 rclone&#10;&lt;/h2&gt;&lt;p&gt;目标：资源管理器里出现一个盘符。&lt;/p&gt;&#10;&lt;p&gt;先试了 &lt;strong&gt;SSHFS-Win&lt;/strong&gt;（WinFsp 上的 sshfs 移植），挂载直接报 &lt;code&gt;read: Connection reset by peer&lt;/code&gt;：它不支持 SOCKS 代理，Windows 上也没有 &lt;code&gt;connect&lt;/code&gt; / &lt;code&gt;ncat&lt;/code&gt; 这类跳板工具，直连又会被 reset。&lt;/p&gt;&#10;&lt;p&gt;换成 &lt;strong&gt;rclone&lt;/strong&gt;：自带 SSH 客户端，一行 &lt;code&gt;socks_proxy&lt;/code&gt; 就能走 clash，私钥、known_hosts 校验都支持：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-ini" data-lang="ini"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# %APPDATA%\rclone\rclone.conf&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;[azure]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;sftp&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;host&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;20.48.50.55&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;azureuser&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;key_file&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;C:/Users/&amp;lt;用户名&amp;gt;/.ssh/id_rsa&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;socks_proxy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;127.0.0.1:7890&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;known_hosts_file&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;C:/Users/&amp;lt;用户名&amp;gt;/.ssh/known_hosts&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-powershell" data-lang="powershell"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;rclone&lt;/span&gt; &lt;span class="nb"&gt;mount &lt;/span&gt;&lt;span class="n"&gt;azure&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="p"&gt;/&lt;/span&gt;&lt;span class="n"&gt;home&lt;/span&gt;&lt;span class="p"&gt;/&lt;/span&gt;&lt;span class="n"&gt;azureuser&lt;/span&gt; &lt;span class="n"&gt;Z:&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="n"&gt;-vfs-cache-mode&lt;/span&gt; &lt;span class="n"&gt;full&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;盘符出现，&lt;code&gt;Z:\&lt;/code&gt; 也能列目录，但&lt;strong&gt;资源管理器里没有&lt;/strong&gt;。&lt;/p&gt;&#10;&lt;h2 id="最大的坑盘符按登录会话隔离"&gt;&lt;a href="#%e6%9c%80%e5%a4%a7%e7%9a%84%e5%9d%91%e7%9b%98%e7%ac%a6%e6%8c%89%e7%99%bb%e5%bd%95%e4%bc%9a%e8%af%9d%e9%9a%94%e7%a6%bb" class="header-anchor"&gt;&lt;/a&gt;最大的坑：盘符按登录会话隔离&#10;&lt;/h2&gt;&lt;p&gt;结论：&lt;strong&gt;Windows 的盘符按登录会话隔离。&lt;/strong&gt;&lt;/p&gt;&#10;&lt;p&gt;量一下进程令牌里的 &lt;code&gt;AuthenticationId&lt;/code&gt;（登录会话 ID）：&lt;/p&gt;&#10;&lt;p&gt;&lt;img alt="会话隔离验证" data-filename="session-check.svg" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://xbzhang.xyz/images/posts/mount-azure-vm/session-check.svg"&gt;&#10;&lt;/p&gt;&#10;&lt;p&gt;WSL 的 interop 进程也在同一个 Windows 会话里，但令牌属于&lt;strong&gt;另一个登录会话&lt;/strong&gt;——从那儿挂载的 Z:，Explorer 看不见。&lt;/p&gt;&#10;&lt;p&gt;（小坑：&lt;code&gt;GetTokenInformation&lt;/code&gt; 的 &lt;code&gt;TokenStatistics&lt;/code&gt; 类号是 &lt;strong&gt;10&lt;/strong&gt;，不是 12——12 是 &lt;code&gt;TokenSessionId&lt;/code&gt;；&lt;code&gt;AuthenticationId&lt;/code&gt; 在结构体偏移 8 字节。）&lt;/p&gt;&#10;&lt;p&gt;解法：挂载必须从&lt;strong&gt;交互会话&lt;/strong&gt;启动。两个入口：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;计划任务&lt;/strong&gt;：登录时自动跑（隐藏窗口）&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;&lt;code&gt;azure-vm-remount.cmd&lt;/code&gt;&lt;/strong&gt;：需要手动重挂时双击一下&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;至此，Z: 可见、可用、可自启。&lt;/p&gt;&#10;&lt;h2 id="另一个问题wsllocalhost-看不到-fuse-挂载"&gt;&lt;a href="#%e5%8f%a6%e4%b8%80%e4%b8%aa%e9%97%ae%e9%a2%98wsllocalhost-%e7%9c%8b%e4%b8%8d%e5%88%b0-fuse-%e6%8c%82%e8%bd%bd" class="header-anchor"&gt;&lt;/a&gt;另一个问题：&lt;code&gt;\\wsl.localhost&lt;/code&gt; 看不到 FUSE 挂载&#10;&lt;/h2&gt;&lt;p&gt;WSL 里挂好了，但从 &lt;code&gt;\\wsl.localhost\Ubuntu\...&lt;/code&gt; 访问是空的。&lt;/p&gt;&#10;&lt;p&gt;原因：这个入口由 WSL 的 &lt;strong&gt;9p 文件服务器&lt;/strong&gt;提供，只导出 ext4 根文件系统与 &lt;code&gt;/mnt/c&lt;/code&gt; 这类 drvfs；&lt;strong&gt;FUSE 挂载（sshfs / rclone / NFS / CIFS）一概不导出&lt;/strong&gt;。这是 WSL 的架构限制，没有开关。&lt;/p&gt;&#10;&lt;p&gt;所以 Windows 端要自己挂一份，不能借 WSL 的挂载。&lt;/p&gt;&#10;&lt;h2 id="日常使用"&gt;&lt;a href="#%e6%97%a5%e5%b8%b8%e4%bd%bf%e7%94%a8" class="header-anchor"&gt;&lt;/a&gt;日常使用&#10;&lt;/h2&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;systemctl --user restart azure-proxy-vm-mount &lt;span class="c1"&gt;# WSL 挂载卡住时&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Z:\ # Windows 直接访问&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;C:\Users\&amp;lt;用户名&amp;gt;\bin\azure-vm-remount.cmd # 重挂&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;C:\Users\&amp;lt;用户名&amp;gt;\bin\azure-vm-umount.cmd # 卸载&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="小结"&gt;&lt;a href="#%e5%b0%8f%e7%bb%93" class="header-anchor"&gt;&lt;/a&gt;小结&#10;&lt;/h2&gt;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;直连不通就全线走代理&lt;/strong&gt;：sshfs 用 &lt;code&gt;ProxyCommand&lt;/code&gt;，rclone 用 &lt;code&gt;socks_proxy&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;挂载要有自愈&lt;/strong&gt;：systemd 的 &lt;code&gt;Restart=always&lt;/code&gt;、rclone 的 &lt;code&gt;--vfs-cache-mode&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;盘符是登录会话级的&lt;/strong&gt;：务必从交互会话启动挂载，否则 Explorer 看不见&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;&lt;code&gt;\\wsl.localhost&lt;/code&gt; 不导出 FUSE 挂载&lt;/strong&gt;：Windows 端直接另挂一份&lt;/li&gt;&#10;&lt;/ul&gt;&#10;</description></item><item><title>我是怎么搭这个博客的</title><link>https://xbzhang.xyz/posts/how-i-built-this-blog/</link><pubDate>Wed, 16 Sep 2026 21:10:00 +0800</pubDate><guid>https://xbzhang.xyz/posts/how-i-built-this-blog/</guid><description>&lt;img src="https://xbzhang.xyz/images/uploads/Default.jpg" alt="Featured image of post 我是怎么搭这个博客的" /&gt;&lt;p&gt;这台博客跑在一台只有 &lt;strong&gt;892MB 内存&lt;/strong&gt;的 Azure 小机器上，同时兼着代理服务。整套方案的第一原则是：&lt;strong&gt;轻&lt;/strong&gt;。以下只讲思路和取舍，代码只放关键几段。&lt;/p&gt;&#10;&lt;h2 id="全貌写作--存储--展示"&gt;&lt;a href="#%e5%85%a8%e8%b2%8c%e5%86%99%e4%bd%9c--%e5%ad%98%e5%82%a8--%e5%b1%95%e7%a4%ba" class="header-anchor"&gt;&lt;/a&gt;全貌：写作 → 存储 → 展示&#10;&lt;/h2&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-plain" data-lang="plain"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 你在浏览器里写作&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; │&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ▼&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ┌─────────────────┐&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; │ Sveltia CMS │ 写作后台（/admin/）&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; │ 文章 + 图片 │&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; └────────┬────────┘&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; │ 保存时直接提交&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ▼&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ┌─────────────────┐&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; │ GitHub 私有仓库 │ ← 唯一的“真相来源”，也是备份&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; └────────┬────────┘&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; │ 每 1 分钟自动拉取&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ▼&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ┌─────────────────┐&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; │ 小机器 (VM) │ git pull → Hugo 构建 → nginx 托管&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; └────────┬────────┘&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; │&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ▼&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 访客访问博客&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;一句话：&lt;strong&gt;GitHub 是内容的家，小机器只负责渲染和展示。&lt;/strong&gt; 好处很直接：内容永远有一份私有仓库备份；机器只负责构建，压力小；换设备也能写。&lt;/p&gt;&#10;&lt;h2 id="选型"&gt;&lt;a href="#%e9%80%89%e5%9e%8b" class="header-anchor"&gt;&lt;/a&gt;选型&#10;&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;Hugo&lt;/strong&gt;：单文件二进制，整站构建几十毫秒，内存占用几乎可以忽略——在 1GB 小机器上是决定性优势。对比过 Hexo（依赖多）、WordPress（太重）、Ghost（官方建议 1GB 起步）。&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;Stack 主题&lt;/strong&gt;：卡片式、自带暗色模式、搜索、归档、目录，社区里最成熟的 Hugo 主题之一。&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;Sveltia CMS&lt;/strong&gt;：最初自己写了个网页后台，能用但很土：图片手动传、预览自己写、格式容易出错。换成 Sveltia 后：富文本 + 实时预览、图片拖拽上传、用表单生成 front matter（不会再因格式写错导致文章不显示）、支持草稿和修订历史。它是纯前端（一个 JS 文件从 CDN 加载），不占服务器内存；支持&lt;strong&gt;用 GitHub Token 登录&lt;/strong&gt;，省掉了自建 OAuth 的麻烦。&lt;/p&gt;&#10;&lt;h2 id="为什么不直接用-github-pages"&gt;&lt;a href="#%e4%b8%ba%e4%bb%80%e4%b9%88%e4%b8%8d%e7%9b%b4%e6%8e%a5%e7%94%a8-github-pages" class="header-anchor"&gt;&lt;/a&gt;为什么不直接用 GitHub Pages？&#10;&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;内容都在 GitHub 了，为什么不直接用 GitHub Pages 托管？&lt;/strong&gt; 有三个现实约束：&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;1. 仓库是私有的，Pages 用不了。&lt;/strong&gt; 免费账号只能从&lt;strong&gt;公开&lt;/strong&gt;仓库发布 Pages，私有仓库发布需要 Pro/Team 付费计划。想省掉这台机器，就得把整个仓库转公开。&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;2. github.io 在国内基本不可达。&lt;/strong&gt; 实测：直连 &lt;code&gt;https://xxxx.github.io/&lt;/code&gt; 超时，而自己的 Azure IP 是通的。GitHub Pages 走 Fastly CDN，国内访问不可靠；换一个「更省事」的方案，结果自己和读者都打不开，没有意义。&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;3. 那台小机器本来 24 小时开着。&lt;/strong&gt; 它同时兼着代理服务，博客只是「蹭」在上面：nginx + 构建，平时内存占用不到 20MB，边际成本约等于零。&lt;/p&gt;&#10;&lt;p&gt;这个架构也没有绑死：内容全在 Git 里，换机器 &lt;code&gt;git clone&lt;/code&gt; + 构建就能恢复。哪天仓库转公开、或者 GitHub 访问不再是问题，迁到 Pages 也就十分钟。&lt;/p&gt;&#10;&lt;h2 id="没有域名怎么上-https"&gt;&lt;a href="#%e6%b2%a1%e6%9c%89%e5%9f%9f%e5%90%8d%e6%80%8e%e4%b9%88%e4%b8%8a-https" class="header-anchor"&gt;&lt;/a&gt;没有域名，怎么上 HTTPS？&#10;&lt;/h2&gt;&lt;p&gt;没有域名就签不了证书，而写作后台的密码不能明文传输。取巧的办法是 &lt;strong&gt;sslip.io&lt;/strong&gt;——一个公共通配 DNS 服务，把 IP 写进域名就能用：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-plain" data-lang="plain"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;20.48.50.55.sslip.io → 自动解析到 20.48.50.55&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;有了它，就能走标准 HTTP 验证，用 certbot 签一张浏览器信任的真证书，并自动续期。代价是依赖 sslip.io 这个第三方服务——对个人博客够用。&lt;/p&gt;&#10;&lt;h2 id="核心github-私有仓库--双向同步"&gt;&lt;a href="#%e6%a0%b8%e5%bf%83github-%e7%a7%81%e6%9c%89%e4%bb%93%e5%ba%93--%e5%8f%8c%e5%90%91%e5%90%8c%e6%ad%a5" class="header-anchor"&gt;&lt;/a&gt;核心：GitHub 私有仓库 + 双向同步&#10;&lt;/h2&gt;&lt;p&gt;写作后台直接往 GitHub 提交，小机器上也可能有本地改动，所以需要&lt;strong&gt;双向同步&lt;/strong&gt;：GitHub → 机器（拉新文章），机器 → GitHub（推本地改动），两个方向处理不好容易冲突。&lt;/p&gt;&#10;&lt;p&gt;做法是一个每分钟跑一次的脚本，逻辑四步：&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;&lt;strong&gt;先拉取&lt;/strong&gt;：&lt;code&gt;git pull --rebase --autostash&lt;/code&gt;，把远端新文章合并进来&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;判断要不要重建&lt;/strong&gt;：只有「有新提交」或「本地有改动」才构建，避免空转&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;重新构建&lt;/strong&gt;：Hugo 生成静态页&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;再推送&lt;/strong&gt;：本地有改动就提交并推回 GitHub&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;两个细节：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;加锁&lt;/strong&gt;：定时任务和手动触发可能撞车，用 &lt;code&gt;flock&lt;/code&gt; 保证同一时间只有一个同步在跑&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;记住上次构建的版本&lt;/strong&gt;：用标记文件记录上次构建的 commit，精确判断站点是否最新&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;这正是备份：&lt;strong&gt;所有会变化的东西——文章、图片、站点配置——都在 Git 里&lt;/strong&gt;。换机器 &lt;code&gt;git clone&lt;/code&gt; 几分钟复原整站；误删文章 &lt;code&gt;git revert&lt;/code&gt; 就能回来；每次改动有完整历史。&lt;strong&gt;版本控制本身就是最好的备份。&lt;/strong&gt;&lt;/p&gt;&#10;&lt;h2 id="顺带搭的几件小事"&gt;&lt;a href="#%e9%a1%ba%e5%b8%a6%e6%90%ad%e7%9a%84%e5%87%a0%e4%bb%b6%e5%b0%8f%e4%ba%8b" class="header-anchor"&gt;&lt;/a&gt;顺带搭的几件小事&#10;&lt;/h2&gt;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;HTTPS 自动续期&lt;/strong&gt;：certbot 定时器自动检查、自动续&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;缓存策略&lt;/strong&gt;：HTML 每次校验（刷新即最新），带哈希指纹的 CSS/JS 长期缓存&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;安全加固&lt;/strong&gt;：后台路径随机化、Basic Auth、登录限速、响应头加固&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="踩过的坑"&gt;&lt;a href="#%e8%b8%a9%e8%bf%87%e7%9a%84%e5%9d%91" class="header-anchor"&gt;&lt;/a&gt;踩过的坑&#10;&lt;/h2&gt;&lt;table&gt;&#10;&#9;&lt;thead&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;坑&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;现象&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;解法&lt;/th&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/thead&gt;&#10;&#9;&lt;tbody&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;未来日期&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;文章发了但不显示&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Hugo 默认不构建未来文章，开 &lt;code&gt;buildFuture&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;缺少 front matter&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;随手记的笔记泄漏到公开页面&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Hugo 会把它当页面处理，后加了自动告警&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;并发推送&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;git 报 &lt;code&gt;remote rejected&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;同步脚本加 &lt;code&gt;flock&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;中文文件名&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;后台打不开&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;校验规则放宽到支持中文&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;h2 id="成本"&gt;&lt;a href="#%e6%88%90%e6%9c%ac" class="header-anchor"&gt;&lt;/a&gt;成本&#10;&lt;/h2&gt;&lt;p&gt;机器是 Azure 最低配（2 vCPU / 892MB），本来就在跑代理，博客算是「蹭」的；域名 0 元（sslip.io）；证书 0 元（Let&amp;rsquo;s Encrypt）；仓库 0 元（GitHub 私有仓库免费）。平时内存占用不到 20MB。&lt;/p&gt;&#10;&lt;h2 id="小结"&gt;&lt;a href="#%e5%b0%8f%e7%bb%93" class="header-anchor"&gt;&lt;/a&gt;小结&#10;&lt;/h2&gt;&#10; &lt;blockquote&gt;&#10; &lt;p&gt;用静态站点保证「轻」，用 Git 仓库保证「稳」，用成熟的 CMS 保证「好写」，用自动化把三者串起来。&lt;/p&gt;&#10;&#10; &lt;/blockquote&gt;&#10;&lt;p&gt;它不是最省事的方案（想省事直接用 Ghost 或 WordPress），但在&lt;strong&gt;一台很小的机器上、不花钱、有完整掌控权&lt;/strong&gt;的前提下，这是我能找到的最好平衡。&lt;/p&gt;&#10;</description></item></channel></rss>