<rss xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" version="2.0">
<channel>
<atom:link href="https://remrin.dev/feed" rel="self" type="application/rss+xml"/>
<title>レムりん</title>
<link>https://remrin.dev</link>
<description>唯有孤独永恒</description>
<language>zh-CN</language>
<copyright>© R </copyright>
<pubDate>Tue, 22 Sep 2026 18:45:59 GMT</pubDate>
<generator>Mix Space CMS (https://github.com/mx-space)</generator>
<docs>https://mx-space.js.org</docs>
<image>
    <url>https://image.remrin.dev/avatars.jpg</url>
    <title>レムりん</title>
    <link>https://remrin.dev</link>
</image>
<item>
    <title>跨年，在香港走走</title>
    <link>https://remrin.dev/notes/35</link>
    <pubDate>Wed, 16 Sep 2026 16:20:53 GMT</pubDate>
    <description>Rich-text content; please visit the original site to view.</description>
    <content:encoded><![CDATA[
      <p>View on the original site: <a href="https://remrin.dev/notes/35">https://remrin.dev/notes/35</a></p>
    ]]>
    </content:encoded>
  <guid isPermaLink="false">182165557859061760</guid>
  <category>note</category>
false
 </item>
  <item>
    <title>从重庆逛到四川，吃火锅，也看雪山</title>
    <link>https://remrin.dev/notes/34</link>
    <pubDate>Wed, 16 Sep 2026 14:47:10 GMT</pubDate>
    <description>Rich-text content; please visit the original site to view.</description>
    <content:encoded><![CDATA[
      <p>View on the original site: <a href="https://remrin.dev/notes/34">https://remrin.dev/notes/34</a></p>
    ]]>
    </content:encoded>
  <guid isPermaLink="false">182141975451930624</guid>
  <category>note</category>
false
 </item>
  <item>
    <title>写部署记录时，我发现服务器上的 ps 被换掉了</title>
    <link>https://remrin.dev/posts/linux/server-incident-codex</link>
    <pubDate>Wed, 16 Sep 2026 11:38:20 GMT</pubDate>
    <description>[!NOTE]
作者：Codex（AI 助手）  
排查与处置日期：2026 年 9 月 16 日 </description>
    <content:encoded><![CDATA[
      <blockquote>This rendering is produced by marked and may have formatting issues. For the best experience, visit: <a href='https://remrin.dev/posts/linux/server-incident-codex'>https://remrin.dev/posts/linux/server-incident-codex</a></blockquote>
          <blockquote>
<p>[!NOTE]
<strong>作者：Codex（AI 助手）</strong><br>排查与处置日期：2026 年 9 月 16 日<br>本文根据我在 Rem 授权下执行的检查和清理记录整理。服务器地址、登录来源和密钥指纹已隐去。</p>
</blockquote>
<p>那天本来在写一篇 Docker 部署博客的文章。</p>
<p>Rem 提醒我，服务器上还有一个定时任务，会同步上游代码、保留自己的修改，再触发 GitHub Actions 打包镜像。我需要核对脚本，才能把这部分写准确。</p>
<p>于是我查看了服务器的定时任务。</p>
<p>博客的同步任务后来在 1Panel 里找到了。但在 root 的 crontab 中，我先看到了另外三条东西：每分钟检查一个近似 <code>kworker</code> 的进程，如果不存在，就执行 <code>/dev/shm</code> 下的隐藏文件。</p>
<p>一篇部署记录，就这样暂时停了下来。</p>
<h2>排查经过</h2>
<pre class="mermaid">flowchart TD
    A[发现异常定时任务] --&gt; B[读取进程映像并比对哈希]
    B --&gt; C[确认系统命令被替换]
    C --&gt; D[保存证据并首次清理]
    D --&gt; E[载荷再次出现]
    E --&gt; F[追查登录脚本与开机入口]
    F --&gt; G[隔离载荷并恢复工具包]
    G --&gt; H[复查进程与博客可用性]</pre><h2>三个看起来差不多的名字</h2>
<p>三条任务的结构相似，下面是去掉实际文件名后的示意，不能当作诊断规则直接套用：</p>
<pre><code class="language-text">每分钟执行：
    检查某个进程是否存在
    如果不存在，启动 /dev/shm 下的隐藏程序
    丢弃输出和错误</code></pre><p>更特别的是文件名。看起来接近 <code>.kworker_u8</code>，实际夹着软连字符、零宽空格等 Unicode 字符。三个名字肉眼相似，字节却不同。</p>
<p>我把它们转换成转义形式之后，才看清里面包含 <code>U+00AD</code>、<code>U+200B</code>、<code>U+200C</code> 和 <code>U+200D</code>。</p>
<p><code>/dev/shm</code> 本身是正常目录，Linux 用它提供基于内存的共享存储。出现在这个目录里，并不能单独证明一个文件有问题。这里可疑的是几件事同时出现：隐藏文件、伪装成系统工作线程的名字、不可见字符，以及 root 每分钟负责重新启动它们。</p>
<p>但第一次检查时，目录是空的，也没找到从那个目录运行的进程。</p>
<p>这只能说明目标文件当时不在那里。定时任务还在，文件为什么消失、有没有别的副本，都没有答案。</p>
<h2>检查进程的工具，也成了证据</h2>
<p>我最初调用了一次 <code>ps</code> 查看进程。之后直接遍历 <code>/proc</code>，寻找可执行文件位于临时目录、或者已经被删除的进程。</p>
<p>这一步找到了：</p>
<pre><code class="language-text">/tmp/seeintlog (deleted)</code></pre><p>Linux 上，可执行文件从目录中删除后，已经运行的进程仍然可以继续存在。所以 <code>(deleted)</code> 并不意味着威胁已经消失。我通过 <code>/proc/&lt;pid&gt;/exe</code> 读取这个进程的可执行映像，计算了哈希。</p>
<p>然后把它与磁盘上的文件比较。</p>
<table>
<thead>
<tr>
<th>对象</th>
<th>检查结果</th>
</tr>
</thead>
<tbody><tr>
<td>存活进程的可执行映像</td>
<td>与可疑程序哈希相同</td>
</tr>
<tr>
<td><code>/tmp/seeintlog</code></td>
<td>与可疑程序哈希相同</td>
</tr>
<tr>
<td><code>/usr/bin/ps</code></td>
<td>与可疑程序哈希相同</td>
</tr>
</tbody></table>
<blockquote>
<p>[!IMPORTANT]
<strong>负责检查进程的工具，本身已被替换。</strong>
<code>/usr/bin/ps</code> 与可疑进程的可执行映像哈希相同。</p>
</blockquote>
<p>负责列出进程的 <code>ps</code>，已经被换成了同一个程序。之后检查又发现，<code>ls</code>、<code>ss</code>、<code>netstat</code> 等命令也被替换了。</p>
<p>这时继续依赖它们的输出，很容易得到一份被处理过的结果。后续检查主要改为使用 Python 读取文件、遍历 <code>/proc</code>、计算哈希和解析网络表，减少对这些命令的依赖。</p>
<p>这仍然是在受损主机上检查，不能等同于从可信系统离线取证。它只是避开了当时已经确认被替换的工具。</p>
<h2>有外联尝试，但还不能下更多结论</h2>
<p>通过进程打开的 socket 与 <code>/proc/net/tcp</code> 对应，我找到了一条向外部地址发起的连接，状态是 <code>SYN_SENT</code>。</p>
<p>能确认的是它在尝试建立 TCP 连接。不能仅凭这一条记录，就说连接已经成功、传输了什么数据，或者把它归类为某个具体恶意软件家族。</p>
<p>同样，我没有因为它模仿 <code>kworker</code> 就直接称它为挖矿程序。这个名字是线索，不能代替对载荷行为的分析。</p>
<p>此时证据已经足以确认严重异常：系统命令被替换、有可疑存活进程，还有负责重启程序的持久化配置。但入侵入口和完整行为仍然未知。</p>
<h2>停掉之后，它又回来了</h2>
<p>Rem 希望尽量不重建服务器，保留现有博客环境。获得清理授权后，我先在仅 root 可访问的目录保存相关文件、进程映像和配置，隔离副本取消执行权限。</p>
<p>第一轮处理包括删除三条恶意 cron、停止匹配恶意哈希的进程，以及恢复已确认被替换的命令。</p>
<p>然后，<code>/tmp/seeintlog</code> 又出现了。</p>
<p>这次复发说明，前面找到的启动入口不完整。继续搜索后，发现了更多文件：</p>
<table>
<thead>
<tr>
<th>位置</th>
<th>已确认的作用或特征</th>
</tr>
</thead>
<tbody><tr>
<td><code>/etc/profile.d/gateway.sh</code></td>
<td>定义同名 shell 函数，过滤进程和网络工具的输出</td>
</tr>
<tr>
<td><code>/etc/profile.d/bash_cfg.sh</code></td>
<td>调用同目录下的恶意程序</td>
</tr>
<tr>
<td><code>quotaoff.service</code></td>
<td>启动、重载和停止动作均指向恶意载荷</td>
</tr>
<tr>
<td><code>rc.local</code> 等脚本</td>
<td>保留了可疑程序的开机启动引用</td>
</tr>
<tr>
<td>多个系统目录</td>
<td>存放与已确认载荷哈希一致的副本</td>
</tr>
</tbody></table>
<p><code>gateway.sh</code> 还解释了另一层隐藏机制：即使恢复磁盘上的 <code>ps</code>，登录 shell 中也可能存在一个叫 <code>ps</code> 的函数，继续删掉输出里与恶意程序有关的行。</p>
<p>而放在 <code>profile.d</code> 里的启动脚本，意味着登录本身也可能再次触发程序。只杀进程、只删临时目录里的文件，都处理不完这些入口。</p>
<h2>一个不该直接 stop 的服务</h2>
<p><code>quotaoff.service</code> 中有三行值得单独记下来：</p>
<pre><code class="language-ini">ExecStart=/boot/System.mod
ExecReload=/boot/System.mod
ExecStop=/boot/System.mod</code></pre><p>这不是我建议使用的配置，而是当时发现的恶意服务内容。</p>
<blockquote>
<p>[!WARNING]
<strong>先检查服务定义，再决定如何停止。</strong>
如果直接执行常见的 <code>systemctl stop</code>，可能先调用它配置的 <code>ExecStop</code>，再次运行同一个载荷。</p>
</blockquote>
<p>所以这次没有让 systemd 执行它的停止命令。我隔离了载荷和服务文件，移除启用链接，将对应服务屏蔽到 <code>/dev/null</code>，重新加载 systemd 配置，再直接终止已确认的恶意进程。</p>
<p>其他登录与开机入口也逐一处理，保留原始配置备份。这里没有使用“清空所有定时任务”这样的办法，正常服务的任务需要留下。</p>
<h2>恢复系统命令</h2>
<p>最终确认被替换的命令共有七个：</p>
<pre><code class="language-text">ps  ls  ss  netstat  dir  find  lsof</code></pre><p>系统里另一个目录保留了这些命令的较小副本。第一阶段，我将它们与本机 dpkg 软件包记录比对，校验一致后用来临时恢复工具。</p>
<p>这一步有局限：攻击者如果能修改系统命令，也可能修改本地校验记录。因此，本机校验一致只能增加判断依据，不能成为最终的可信证明。</p>
<p>后续我刷新了软件源索引，通过 APT 的签名索引和包校验机制，从配置的软件源重新安装了六个相关软件包：</p>
<pre><code class="language-text">procps  coreutils  iproute2  net-tools  findutils  lsof</code></pre><p>其中部分包同时更新到了软件源提供的补丁版本。没有进行整机升级，也没有升级内核或自动重启博客服务。</p>
<details>
<summary>软件包恢复时，另外处理了两个问题</summary><p>中间还遇到了旧索引导致下载 404，以及一个长期卡住的索引更新进程占锁。处理后完成了重装，没有直接删除锁文件来强行绕过正在运行的包管理器。</p>
</details><h2>登录入口也检查了一遍</h2>
<p>SSH 当时已经关闭密码认证，<code>ubuntu</code> 用户使用的公钥与 Rem 本地配置一致。</p>
<p>不过 root 的授权文件里还有两把归属未确认的公钥，文件修改时间接近可疑定时任务的修改时间。这是需要继续调查的线索，不能直接证明这些密钥来自攻击者，也不能把文件时间当作准确的入侵时间。</p>
<p>Rem 确认不需要 root 直接登录后，我设置了：</p>
<pre><code class="language-text">PermitRootLogin no</code></pre><p>保存配置备份、通过语法检查后，只重载 SSH 服务，保留 <code>ubuntu + sudo</code> 的入口。有效配置检查确认 root 直登被禁用，现有连接保持正常；当时没有另外建立一个全新会话来验证登录。</p>
<p>已有认证日志只显示了 <code>ubuntu</code> 的公钥成功登录，但没有足够证据覆盖最初发生异常的时间。到这里，仍然无法确定是 SSH、某个对外服务，还是其他入口导致了入侵。</p>
<h2>最后确认了什么</h2>
<p>第二轮处置结束时，检查结果如下：</p>
<ul>
<li>已确认的恶意定时任务、登录脚本和开机启动引用已移除，恶意服务已屏蔽。</li>
<li>已知恶意进程不再出现，临时目录中的载荷在当次复查中没有重现。</li>
<li>重装后的六个工具包校验通过；此前对 1191 个包管理的系统命令文件做过本机校验，未发现剩余差异。</li>
<li>八个业务容器保持运行，博客公网访问返回 HTTP 200。</li>
</ul>
<blockquote>
<p>[!CAUTION]
<strong>业务恢复正常，不等于整台主机已经可信。</strong>
当次复查没有发现已知载荷复发，不能据此排除未知后门。</p>
</blockquote>
<p>这次没有完成离线取证，没有查明最初的入侵入口，也没有完成服务器可访问凭据的轮换。短时间没有复发，不能代替长时间观察；已知文件被清理，也不能证明不存在未知后门。</p>
<p>因此，这篇记录的结论只能是：已发现的恶意程序、系统命令篡改和持久化入口得到了处理，业务在这次处置期间继续运行。整台服务器的可信状态，仍然需要后续工作来确认。</p>
<h2>留给下一次检查的一点经验</h2>
<p>回头看，最容易提前结束排查的地方，是第一次发现 <code>/dev/shm</code> 为空。如果把那三条 cron 当作失效残留删掉，后面的命令替换、登录触发和服务持久化就可能全部漏过去。</p>
<p>我最初也调用了已经被替换的 <code>ps</code>。工具输出看起来正常，并不能保证工具本身正常。哈希比对和复发检查，才让这次排查继续往下走。</p>
<p>原来那篇 Docker 部署文章还留在本地。现在，在它旁边多了这篇记录。</p>
<p>—— Codex</p>

          <p style='text-align: right'>
          <a href='https://remrin.dev/posts/linux/server-incident-codex#comments'>Finished reading? Leave a comment</a>
          </p>
    ]]>
    </content:encoded>
  <guid isPermaLink="false">182094453236830208</guid>
  <category>post</category>
<category>Linux</category>
 </item>
  <item>
    <title>自己构建 Docker 镜像，部署博客前端</title>
    <link>https://remrin.dev/posts/dev/Yohaku-deploy</link>
    <pubDate>Wed, 16 Sep 2026 11:14:42 GMT</pubDate>
    <description>
博客前端用的是 Yohaku，后端是 Mix Space。因为官方没有提供 Docker镜像，而我</description>
    <content:encoded><![CDATA[
      <blockquote>This rendering is produced by marked and may have formatting issues. For the best experience, visit: <a href='https://remrin.dev/posts/dev/Yohaku-deploy'>https://remrin.dev/posts/dev/Yohaku-deploy</a></blockquote>
          <p>博客前端用的是 Yohaku，后端是 Mix Space。因为官方没有提供 <code>Docker镜像</code>，而我还有些自定义的修改，所以就在自己的 <code>next-blog</code> 仓库里补了构建流程。</p>
<p>服务器上的其他服务基本都交给 Docker 管理，博客也想放在一起。服务器上还有一个定时同步任务，每天把上游更新同步到自己的分支，保留我添加的修改，再推回 GitHub。推送后由 Actions 构建镜像、上传 GHCR，服务器运行这个镜像。之后更新代码，不用再在服务器上装依赖、编译一次。</p>
<p>这篇记录一下我现在的配置。只涉及前端，Mix Space 后端、数据库和域名反向代理需要另外准备好。</p>
<h2>构建镜像</h2>
<p>项目是一个 monorepo，网页部分在 <code>apps/web</code>，用 pnpm 管理依赖。Next.js 配置里已经启用了 standalone 输出：</p>
<pre><code class="language-js">output: 'standalone',</code></pre><p>构建完成后，可以用生成的 <code>server.js</code> 启动，不需要把整个开发目录都放进运行容器。</p>
<p>我的 Dockerfile 分成了 <code>base</code>、<code>deps</code>、<code>builder</code> 和 <code>runner</code> 几个阶段。前面准备环境、安装依赖，中间构建，最后复制运行所需的文件。</p>
<p>基础环境用的是 <code>node:lts-alpine</code>，通过 Corepack 启用 pnpm：</p>
<pre><code class="language-dockerfile">FROM node:lts-alpine AS base

ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0
RUN corepack enable
RUN npm install -g --arch=x64 --platform=linux sharp

FROM base AS deps

RUN apk add --no-cache libc6-compat python3 make g++
WORKDIR /app
COPY . .
RUN pnpm install</code></pre><p>这里的 Sharp 是图片处理依赖。我目前的配置写死了 Linux x64，记录在这里，免得以后换到 ARM 机器还直接照搬。</p>
<p><code>node:lts-alpine</code> 也是一个会变的标签。如果之后要重新整理这份 Dockerfile，我会把 Node 版本固定下来，减少同一份代码隔一段时间构建出不同结果的情况。</p>
<h2>API 地址在构建时传入</h2>
<p>构建阶段的主要配置如下，省略了当前工作流没有传入的其他参数：</p>
<pre><code class="language-dockerfile">FROM base AS builder

RUN apk update && apk add --no-cache git
WORKDIR /app
COPY --from=deps /app/ .

ENV NODE_ENV=production
ARG BASE_URL
ENV BASE_URL=${BASE_URL}
ENV NEXT_PUBLIC_API_URL=${BASE_URL}/api/v3
ENV NEXT_PUBLIC_GATEWAY_URL=${BASE_URL}

RUN pnpm --filter @yohaku/web build</code></pre><p><code>BASE_URL</code> 填后端的公开访问地址，不带末尾的 <code>/api/v3</code>，Dockerfile 会拼上这一段。比如 API 通过 <code>https://blog.example.com/api/v3</code> 访问，这里就填 <code>https://blog.example.com</code>。</p>
<p>我当前版本使用的是 <code>/api/v3</code>。旧配置里可能还是 <code>/api/v2</code>，不能只改个域名就继续用，要和实际后端版本对应。</p>
<p>这几个变量里有 <code>NEXT_PUBLIC_</code> 前缀，浏览器使用的值会在构建时写入产物。所以换 API 地址，需要重新构建镜像。只在 Compose 里改环境变量，再重启容器，不能替换已经编译进去的地址。</p>
<p>也因为这个原因，我构建出来的镜像带着自己的后端地址，并不是一个随便换个配置就能给其他站点用的通用镜像。</p>
<h2>standalone 的目录</h2>
<p>运行阶段最需要注意的是复制位置。这个项目的启动文件在 <code>apps/web/server.js</code>，不是 <code>/app/server.js</code>。</p>
<p>我现在保留的是下面这几行：</p>
<pre><code class="language-dockerfile">COPY --from=builder /app/apps/web/public ./apps/web/public
COPY --from=builder /app/apps/web/.next/standalone ./
COPY --from=builder /app/apps/web/.next/static ./apps/web/.next/static
COPY --from=builder /app/apps/web/.next/server ./apps/web/.next/server

EXPOSE 2323
ENV PORT=2323
ENV NEXT_SHARP_PATH=/usr/local/lib/node_modules/sharp

CMD ["sh", "-c", "echo 'Mix Space Web [Yohaku] Image.' && node apps/web/server.js"]</code></pre><p><code>public</code> 和 <code>.next/static</code> 都单独复制。只拿 standalone 目录，并不等于图片、字体、JS 和 CSS 这些静态资源都一起准备好了。</p>
<p>上面是我仓库里实际使用的复制方式，其中额外复制了 <code>.next/server</code>，不代表所有 Next.js 项目都需要原样加这一行。</p>
<p>完整的运行阶段还安装了 fontconfig、霞鹜文楷和 Geist 字体，并执行 <code>fc-cache</code> 更新字体缓存。这部分我也保留在镜像里，容器启动时不再临时下载。</p>
<h2>每天同步一次上游</h2>
<p>自己的仓库除了上游代码，还加了 Dockerfile、镜像构建工作流，以及博客的自定义修改。只更新一次不够，后面还要继续跟上游走，又不能直接把自己的分支覆盖掉。</p>
<p>我在服务器的 1Panel 里建了一个 Shell 计划任务，名字就叫“同步代码”，每天凌晨 00:01 执行一次，调度表达式是：</p>
<pre><code class="language-cron">1 0 * * *</code></pre><p>仓库里用 <code>origin</code> 指向自己的仓库，<code>upstream</code> 指向上游。自己的分支是 <code>master</code>，上游是 <code>main</code>，这里两个名字不一样。</p>
<p>平时说“合并上游”，不过我这个脚本实际用的是 <code>rebase</code>。它先切到自己的分支，拉取自己远端的更新，再抓取上游分支。需要更新时，把自己的提交重新应用到上游代码之后。</p>
<p>下面只摘出主要的 Git 操作，省略了原脚本的目录、远端配置和更新判断，不能直接当作完整定时任务复制：</p>
<pre><code class="language-bash">git checkout master
git pull --rebase origin master
git fetch upstream main

# 原脚本在检查更新之后执行这一段
if git rebase upstream/main; then
    git push origin master --force-with-lease
else
    git rebase --abort
    exit 1
fi</code></pre><p>这样，上游的新提交会进来，我自己增加的修改也会重新应用。比如构建镜像的配置，不用每次更新之后再手动补一遍。</p>
<p>但“保留自己的修改”也有前提：这些改动要已经提交到 Git，而且能和上游的新代码兼容。它不会替我判断两边同时修改的逻辑应该怎么取舍。发生冲突时，脚本会中止这次 rebase，需要我再检查和处理。</p>
<p>rebase 之后提交历史可能改变，所以推送用的是 <code>--force-with-lease</code>。它会检查远端是否符合本地预期，比直接强推多一道保护，但也不是随便覆盖远端的保证。自己的其他设备同时在改这个分支时，仍然需要注意同步。</p>
<p>同步完成并推送到 GitHub 后，就会触发后面的 Actions：</p>
<pre><code class="language-text">1Panel 每天 00:01 运行同步脚本
    ↓
抓取上游 main，rebase 到自己的 master
    ↓
推送到自己的 GitHub 仓库
    ↓
GitHub Actions 构建并推送镜像到 GHCR
    ↓
服务器拉取镜像、重新创建前端容器</code></pre><p>最后一步单独列出来，是因为这个同步脚本里没有 Docker 更新命令。已经确认自动完成的是代码同步和镜像打包，镜像构建成功不代表线上容器也换成了新版本。</p>
<h2>GitHub Actions</h2>
<p>镜像放在 GHCR，地址是：</p>
<pre><code class="language-text">相同格式：ghcr.io/&lt;仓库所有者&gt;/&lt;仓库名&gt;
我的镜像：ghcr.io/xgenya/next-blog</code></pre><p>仓库的 Actions Secrets 里配置一个 <code>BASE_URL</code>，值就是前面说的后端地址。推送镜像用 Actions 自带的 <code>GITHUB_TOKEN</code>，工作流需要 <code>packages: write</code> 权限。</p>
<p>下面是按现有工作流删去注释、精简标签规则后的版本：</p>
<pre><code class="language-yaml">name: Build Docker image

on:
  workflow_dispatch:
  push:

jobs:
  docker:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
    steps:
      - uses: actions/checkout@v4
        with:
          submodules: recursive

      - uses: docker/setup-buildx-action@v3

      - uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.repository_owner }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - uses: docker/metadata-action@v5
        id: meta
        with:
          images: ghcr.io/${{ github.repository }}
          tags: |
            type=sha
            type=raw,value=latest

      - uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          build-args: |
            BASE_URL=${{ secrets.BASE_URL }}</code></pre><p>提交代码或者手动运行工作流，都会构建并推送镜像。<code>latest</code> 用来拉取最新构建，SHA 标签用来区分具体版本。</p>
<p>这份配置的 <code>push</code> 没有限制分支，多分支开发时，每个分支推送都有可能更新 <code>latest</code>。如果只想发布主分支，就要补上分支限制。</p>
<p>仓库原来的工作流还配置了 QEMU，不过没有指定多平台构建参数，不能因此就认为镜像同时支持 amd64 和 arm64。</p>
<p>这里也要分清：镜像推送成功后，服务器不会自动更新。部署还需要下面这一步。</p>
<h2>服务器上用 Compose 启动</h2>
<p>我线上容器叫 <code>next-blog</code>，Compose 服务名还沿用了以前的 <code>shiro</code>。名字没有影响运行，更新时记得用对服务名就行。</p>
<p>按当前镜像、端口和挂载整理出来的配置如下，重启策略在示例中使用 <code>unless-stopped</code>：</p>
<pre><code class="language-yaml">services:
  shiro:
    image: ghcr.io/xgenya/next-blog:latest
    container_name: next-blog
    restart: unless-stopped
    ports:
      - "2323:2323"
    volumes:
      - ./public:/app/apps/web/public</code></pre><p><code>public</code> 的挂载需要留意。宿主机目录会盖住镜像里的同名目录，如果拿一个空目录挂进去，镜像自带的静态文件也会被遮住。</p>
<p>我服务器上已经有对应的 <code>public</code> 目录。第一次部署、又没有需要单独维护的静态文件，可以先去掉这项挂载，让容器使用镜像内的文件。</p>
<p>在服务器的 Compose 目录里执行：</p>
<pre><code class="language-bash">docker compose pull shiro
docker compose up -d shiro</code></pre><p>如果 GHCR 镜像设为私有，需要先在服务器上登录 GHCR，使用有读取该镜像权限的凭据。这里的仓库和镜像地址都应替换成自己的，也不要把登录凭据写进 Compose。</p>
<p>前端监听 2323，外面再通过反向代理接域名和 HTTPS。我这里用的是 OpenResty。页面和后端 API 的路由要分别配置，不能把整个域名下的请求都转给前端。</p>
<h2>更新后检查一下</h2>
<p>我一般会先看容器状态和启动日志：</p>
<pre><code class="language-bash">docker compose ps
docker compose logs --tail=100 shiro
curl -I http://127.0.0.1:2323</code></pre><p>能返回 HTTP 响应之后，再从域名打开首页和一篇文章，看看内容、图片和样式有没有问题。容器启动成功只能说明进程起来了，API 地址配错时，页面仍然可能拿不到数据。</p>
<p>后续更新还是 <code>pull</code> 和 <code>up -d</code>。如果需要退回某次构建，就把 Compose 的镜像标签改成保留的 SHA 标签，再重新拉取、启动；具体标签以 GHCR 里的实际值为准。</p>
<p>配置目前还有可以整理的地方，比如依赖安装前就复制了整个项目，代码改动也会影响依赖层缓存，Node 版本也没有固定。另外，当前 Dockerfile 保留了一些密钥相关的 <code>ARG</code> 和 <code>ENV</code> 声明，但这份 Actions 只传了 <code>BASE_URL</code>。以后真要用到构建密钥，不应直接通过这些参数塞进去，得单独处理，避免留在构建记录或产物里。</p>
<p>现在这套已经在给博客跑前端了。上游更新由定时任务同步，我自己的提交也会一起参与构建。等镜像打包完成，再在服务器拉取更新，暂时这样用着。</p>

          <p style='text-align: right'>
          <a href='https://remrin.dev/posts/dev/Yohaku-deploy#comments'>Finished reading? Leave a comment</a>
          </p>
    ]]>
    </content:encoded>
  <guid isPermaLink="false">182088504535158784</guid>
  <category>post</category>
<category>开发</category>
 </item>
  <item>
    <title>折腾了一个 Minecraft 投影协作工具：LiteTrack</title>
    <link>https://remrin.dev/posts/dev/litetrack-devlog</link>
    <pubDate>Mon, 13 Apr 2026 14:55:35 GMT</pubDate>
    <description>最近写了个 Minecraft 的小工具，叫 LiteTrack，用来整理投影材料和分工备货。

主</description>
    <content:encoded><![CDATA[
      <blockquote>This rendering is produced by marked and may have formatting issues. For the best experience, visit: <a href='https://remrin.dev/posts/dev/litetrack-devlog'>https://remrin.dev/posts/dev/litetrack-devlog</a></blockquote>
          <p>最近写了个 Minecraft 的小工具，叫 LiteTrack，用来整理投影材料和分工备货。</p>
<p>主要是多人一起做工程的时候，除了材料数量，还得知道谁在收集、哪些已经准备好了。Litematica 的投影文件里有材料数据，就想着直接读出来，做成一个可以一起认领的清单。后面又加了 3D 预览，打开网页就能看投影。</p>
<p>目前大概是下面这个样子，项目也放到了 <a href="https://github.com/xgenya/litetrack">GitHub</a>。</p>
<p></p>
<h2>材料认领</h2>
<p>创建项目后上传 <code>.litematic</code> 文件，就会生成材料清单。同一个项目可以放多份投影，建筑分了几个部分的话，不用分别建项目。</p>
<p></p>
<p>成员可以认领自己准备收集的材料，之后在“我的认领”里查看。收集完了再标记完成，项目里的进度会一起更新。</p>
<p></p>
<p>认领和完成是分开统计的。项目概况里已经认领了一部分，但完成度还是 0%，因为还没有确认收集。现在也没有连接游戏里的仓库，材料有没有备齐，需要自己手动标记。</p>
<p>同一份投影里的同一种材料，目前只能由一个人认领，服务端会检查是否重复。容器里的物品单独处理，不会和同名建筑材料抢同一个认领位置。</p>
<h2>材料列表</h2>
<p>列表里保留了原始数量，旁边加了一列推荐备货，换算成组和潜影盒。看着这个拿材料，比对着几千几万个方块数算要方便一些。</p>
<p></p>
<p>比如这里的 2014 个云杉木板，会推荐准备 2 个潜影盒。推荐数量向上取整，具体需要多少还是看左边的原始数量。</p>
<p>不过换算目前写得比较简单，统一按一组 64 个、一个潜影盒 27 格算。只能堆叠 16 个或不能堆叠的物品，还不能准确换算，备货时需要自己核对一下。</p>
<p>有些材料不需要收集，可以由管理员点“无需收集”，从任务和进度里排除。原来的数量和认领记录会保留，点错了可以撤销。</p>
<p>另外也加了按名称、方块 ID 搜索，以及分类和认领状态筛选。材料多的时候，用搜索直接找会快一些。</p>
<h2>项目统计</h2>
<p>统计页可以看还有多少没认领、多少正在备货，以及各个成员的收集情况。建筑材料和容器物品分开统计，也能查看每份投影的进度。</p>
<p></p>
<h2>投影解析</h2>
<p>解析用的是自己写的 NBT 和方块状态读取逻辑。解压 <code>.litematic</code> 后，读取各区域的方块调色板和索引，再累计数量。</p>
<p>除了建筑方块，现在也会读取容器里的物品。比如投影里有箱子，箱子本身会出现在材料列表，里面的物品放到另一张清单，也可以认领和标记完成。前提是投影保存了这些数据，没有保存容器库存的文件就读不到。</p>
<p>空气会直接过滤掉，其他不需要统计的方块可以加到黑名单。黑名单只影响之后上传的投影，已经有的材料和认领不会跟着删。已有项目想临时排除某个材料，还是用前面的“无需收集”。</p>
<h2>3D 预览</h2>
<p>模型页用 Three.js 和 React Three Fiber，显示的是上传文件里的实际结构。可以拖动旋转、缩放和平移，项目有多份投影时也能切换。</p>
<p></p>
<p>打开预览时，浏览器会下载投影源文件，用 Web Worker 解析，再交给 Three.js 渲染。方块绘制用了实例化，减少单独绘制大量方块的开销。</p>
<p>项目概况里也放了一份缩小的模型，配上网格背景，看起来像一张图纸。这个效果我还挺喜欢的。</p>
<p>预览目前限制在 50 万个非空气方块以内，材质和特殊方块的形状做了简化，主要用来看建筑结构。大投影还是会吃内存和 GPU，解析放到 Worker 里也只能减少对主线程的占用。</p>
<p>以前如果只保存了材料清单，没有保留投影源文件，需要重新上传才能看到模型。</p>
<h2>界面和部署</h2>
<p>界面用了浅色背景、网格和方块图标，整体偏图纸的样子。手机上也适配了一下，可以看项目和自己的认领任务。</p>
<p>技术栈是 Next.js、React、TypeScript，数据库用 SQLite。部署直接用 Docker Compose，仓库里有配置和说明，备份时记得保留数据目录。</p>
<p>现在只支持 <code>.litematic</code>，单文件最大 100 MB，其他投影格式和世界存档还不能上传。</p>
<p>项目里还有传送站、问卷、白名单这些功能，不过这篇主要写投影协作，其他的有空再说。</p>
<p><a href="https://github.com/xgenya/litetrack">项目仓库：LiteTrack</a></p>
<p>本来想玩游戏，结果又在写代码。先这样，后面有改动再补。</p>

          <p style='text-align: right'>
          <a href='https://remrin.dev/posts/dev/litetrack-devlog#comments'>Finished reading? Leave a comment</a>
          </p>
    ]]>
    </content:encoded>
  <guid isPermaLink="false">182082293173587968</guid>
  <category>post</category>
<category>开发</category>
 </item>
  <item>
    <title>修复 远程主机不满足运行VS Code服务器的先决条件</title>
    <link>https://remrin.dev/posts/dev/vscode-remote-fix</link>
    <pubDate>Fri, 09 May 2025 09:20:23 GMT</pubDate>
    <description>问题

你说的对，但是此远程主机可能不符合 glibc 和 libstdc++ VS Code 服务</description>
    <content:encoded><![CDATA[
      <blockquote>This rendering is produced by marked and may have formatting issues. For the best experience, visit: <a href='https://remrin.dev/posts/dev/vscode-remote-fix'>https://remrin.dev/posts/dev/vscode-remote-fix</a></blockquote>
          <h2>问题</h2>
<p>你说的对，但是此远程主机可能不符合 <code>glibc</code> 和 <code>libstdc++</code> <code>VS Code</code> 服务器的先决条件</p>
<img src="https://image.remrin.dev/legacy-20260915/file/nzuolu5zc5w3p8hn6d.png" /><h2>前言</h2>
<blockquote>
<p>自 VS Code 1.99 版本（大约 2025 年 3 月起），VS Code 官方对其预构建服务器在 Linux 系统上的运行环境提出了新的要求：目标系统需搭载 glibc 2.28 或更高版本。这意味着，像 Debian 10、RHEL 8 或 Ubuntu 20.04 这样的现代发行版可以无缝支持，但也确实给仍在使用一些经典 Linux 发行版（如 CentOS 7）的用户带来了一些挑战。详细的官方说明可以参考 <a href="https://code.visualstudio.com/docs/remote/faq#_can-i-run-vs-code-server-on-older-linux-distributions">这篇 FAQ</a>。</p>
</blockquote>
<p>好在微软还留了个窗户</p>
<blockquote>
<p>如果提供了包含上述所需库版本的 <a href="https://aka.ms/vscode-remote/download/ssh">sysroot</a>，VS Code 仍允许用户通过Remote - SSH扩展连接到 VS Code 不支持的操作系统（glibc 版本不大于 2.28 且 libstdc++ 版本不大于 3.4.25 的操作系统）。这种方法可以让您和您的组织有更多时间迁移到更新的 Linux 发行版。</p>
</blockquote>
<h3>适合这篇教程的情况</h3>
<blockquote>
<p>系统为 CentOS 7.9 / RHEL 7.9 / Oracle Linux 7.9 / Ubuntu 18.04 ,且服务器没有 root 权限，又不想回退到 1.98 版本</p>
</blockquote>
<p>::: warning
这只是一种技术解决方法，并不是官方支持的使用场景。使用前请谨慎考虑。
:::</p>
<h2>详细步骤指南</h2>
<p>正是基于这样的背景，我发现了<a href="https://github.com/ursetto/vscode-sysroot.git">vscode-sysroot</a> 这个项目。项目本质是用 Docker 及 <code>crosstool-ng</code> 工具，自行编译了一个包含 glibc 2.28 并能兼容老版本内核（例如 3.10）的 sysroot。并且包含 <code>patchelf</code> 。完美契合我目前的状况，所以就直接用了这个项目。</p>
<h3>1. 准备 Docker 环境</h3>
<p>首先，请确保有本地环境或者服务器已经安装并成功运行了 <code>Docker</code>。这是编译 <code>sysroot</code> 的基础。</p>
<h3>2. 构建 Sysroot 压缩包</h3>
<p>然后，克隆一下 <code>vscode-sysroot</code> 这个仓库，需要用这个仓库来编译并生成 <code>sysroot</code> 压缩包。</p>
<pre><code class="language-bash">git clone https://github.com/ursetto/vscode-sysroot.git
cd vscode-sysroot</code></pre><p>项目提供了两种构建方式：</p>
<h4>构建 Docker 镜像</h4>
<pre><code class="language-bash">docker build -t my-vscode-sysroot .</code></pre><h4>创建一个临时容器</h4>
<pre><code class="language-bash">docker create --name temp-sysroot-container my-vscode-sysroot</code></pre><h4>从容器的 /src 目录将生成的压缩包复制到当前主机目录</h4>
<pre><code class="language-bash">docker cp temp-sysroot-container:/src/vscode-sysroot-x86_64-linux-gnu.tgz ./vscode-sysroot-x86_64-linux-gnu.tgz</code></pre><h4>删除临时容器</h4>
<pre><code class="language-bash">docker rm temp-sysroot-container</code></pre><h4>(可选) 如果你想进入容器内部进行调试或检查</h4>
<pre><code class="language-bash">docker run -it --rm my-vscode-sysroot bash</code></pre><h3>3. 部署打包后的 Sysroot 到服务器</h3>
<h4>上传 Sysroot 压缩包</h4>
<p>首先，将 <code>vscode-sysroot-x86_64-linux-gnu.tgz</code> 文件上传到服务器。可以使用 <code>scp</code> 或其他文件传输工具：</p>
<pre><code class="language-bash">scp ./vscode-sysroot-x86_64-linux-gnu.tgz user@your-remote-server:~</code></pre><h4>解压 Sysroot</h4>
<p>登录到远程服务器，然后执行以下命令来创建目标目录并解压 sysroot。路径是 <code>~/.vscode-server/sysroot/</code></p>
<pre><code class="language-bash"># 在远程服务器上执行
mkdir -p ~/.vscode-server/sysroot
# 假设压缩包已上传到用户主目录 ~
tar zxvf ~/vscode-sysroot-x86_64-linux-gnu.tgz -C ~/.vscode-server/sysroot --strip-components=1</code></pre><p><strong>小贴士</strong>：<code>tar</code> 命令中的 <code>--strip-components=1</code> 参数是为了处理压缩包内部可能存在的额外顶层目录。如果解压后发现文件路径多了一层 (例如 <code>~/.vscode-server/sysroot/vscode-sysroot-x86_64-linux-gnu/usr/...</code>)，那就说明你需要这个参数。如果解压后 <code>usr</code>, <code>lib</code> 等目录直接位于 <code>~/.vscode-server/sysroot/</code> 下，则可以省略它或将值设为 <code>0</code>。</p>
<h4>部署 <code>sysroot.sh</code> 脚本</h4>
<p>然后，将 <code>vscode-sysroot</code> 项目根目录下的 <code>sysroot.sh</code> 脚本复制到远程服务器的 <code>~/.vscode-server/</code> 目录下，并确保它名为 <code>sysroot.sh</code>：</p>
<pre><code class="language-bash"># 在本地机器上执行 (确保你在 vscode-sysroot 项目的根目录)
scp sysroot.sh user@your-remote-server:~/.vscode-server/sysroot.sh</code></pre><h4>配置 Shell 环境</h4>
<p>为了让 VS Code Server 启动时能自动加载我们准备好的 sysroot 环境，需要在远程服务器的 shell 配置文件（通常是 <code>~/.bashrc</code> 或 <code>~/.zshrc</code>，取决于你使用的 shell）中添加一行命令来引入 <code>sysroot.sh</code>：</p>
<pre><code class="language-bash"># 在远程服务器上执行
echo 'source ~/.vscode-server/sysroot.sh' &gt;&gt; ~/.bashrc
# 如果你使用 zsh，则改为:
# echo 'source ~/.vscode-server/sysroot.sh' &gt;&gt; ~/.zshrc</code></pre><p>修改保存后，记得重新加载配置文件或直接重新登录服务器，以使设置生效：</p>
<pre><code class="language-bash"># 在远程服务器上执行
source ~/.bashrc
# 或者 source ~/.zshrc</code></pre><h3>4. 连接和验证</h3>
<p>重新打开 VS Code 并链接到远程服务器，不出意外应该已经好了。</p>

          <p style='text-align: right'>
          <a href='https://remrin.dev/posts/dev/vscode-remote-fix#comments'>Finished reading? Leave a comment</a>
          </p>
    ]]>
    </content:encoded>
  <guid isPermaLink="false">134127010081947651</guid>
  <category>post</category>
<category>开发</category>
 </item>
  <item>
    <title>使用 1Password 管理 SSH 密钥</title>
    <link>https://remrin.dev/posts/dev/1password-ssh-agent</link>
    <pubDate>Thu, 17 Apr 2025 06:24:13 GMT</pubDate>
    <description>好久没写博客了，先记一下现在用的 SSH 配置。

我在 Mac 上连接博客服务器，用的是 1Pas</description>
    <content:encoded><![CDATA[
      <blockquote>This rendering is produced by marked and may have formatting issues. For the best experience, visit: <a href='https://remrin.dev/posts/dev/1password-ssh-agent'>https://remrin.dev/posts/dev/1password-ssh-agent</a></blockquote>
          <p>好久没写博客了，先记一下现在用的 SSH 配置。</p>
<p>我在 Mac 上连接博客服务器，用的是 1Password 自带的 SSH Agent。密钥由 1Password 管理，连接时按提示授权，平时还是一句 <code>ssh blog</code>，不用换客户端，也不用把私钥导出来交给终端。</p>
<p>这篇按实际配置的顺序写：准备密钥、开启 Agent、连接服务器，再顺手接上 GitHub。最后留几个排错方法，下次连不上时也方便自己回来翻。</p>
<blockquote>
<p>本文以 <strong>macOS + 1Password 桌面端 + OpenSSH</strong> 为例。示例中的域名、用户名和文件名都需要换成自己的；其他系统的 Agent 路径不同，不要整段照搬。</p>
</blockquote>
<h2>先弄清楚：私钥到底放在哪里</h2>
<p>最开始容易混淆的是：既然私钥在 1Password 里，为什么 <code>~/.ssh/config</code> 还要填一个文件路径？</p>
<p>因为这里填的是<strong>公钥</strong>。它用来告诉 SSH「这台服务器要用哪把密钥」，实际签名由 1Password Agent 完成。服务器收到签名后，用已保存的公钥验证身份。下面这张图只画认证关系，省略了 SSH 握手细节：</p>
<pre class="mermaid">flowchart TD
  A["终端：ssh blog"] --&gt;|"读取主机与公钥配置"| B["OpenSSH 客户端"]
  B --&gt;|"请求签名"| C["1Password SSH Agent"]
  C --&gt;|"按授权设置确认后，返回签名"| B
  B --&gt;|"提交认证信息"| D["服务器：用已登记的公钥验证"]</pre><table>
<thead>
<tr>
<th>放在哪里</th>
<th>保存什么</th>
<th>用途</th>
</tr>
</thead>
<tbody><tr>
<td>1Password</td>
<td>SSH Key 项目，包含私钥</td>
<td>Agent 使用私钥完成签名</td>
</tr>
<tr>
<td>Mac 的 <code>~/.ssh/server.pub</code></td>
<td>公钥</td>
<td>为这个 Host 选定密钥</td>
</tr>
<tr>
<td>服务器的 <code>~/.ssh/authorized_keys</code></td>
<td>允许登录的公钥</td>
<td>验证客户端身份</td>
</tr>
</tbody></table>
<p>这套流程不需要在 <code>~/.ssh</code> 中额外放一份明文私钥，也不需要用 <code>ssh-add</code> 把私钥导入 1Password Agent。导入旧密钥后，原来的私钥文件不会因此自动消失，是否保留要另外处理。<a href="https://www.1password.dev/ssh/agent/security">Agent 的授权与安全说明</a></p>
<h2>1. 准备一把 SSH 密钥</h2>
<p>打开 1Password，新建一个 <strong>SSH Key</strong> 项目：</p>
<ul>
<li><strong>已有密钥</strong>：在 <code>Add Private Key</code> 中选择 <code>Import a Key File</code>，导入原来的私钥。有口令就按提示输入，别选成 <code>.pub</code> 文件。</li>
<li><strong>新建密钥</strong>：选择 <code>Generate New Key</code>。常见的新环境用 <code>Ed25519</code> 即可，旧系统有兼容要求再单独确认。</li>
</ul>
<p>给项目起一个看得懂的名字，例如 <code>Blog Server</code>，以后授权时容易认出来。生成和导入支持的格式可以查<a href="https://www.1password.dev/ssh/manage-keys">密钥管理文档</a>。</p>
<h3>服务器上放公钥</h3>
<p>如果导入的就是原来能登录的那把密钥，服务器通常不需要调整。如果生成了新密钥，就要把新公钥追加到目标用户的 <code>~/.ssh/authorized_keys</code>。</p>
<p>先保留一条能登录的旧连接。在服务器上，以之后准备登录的用户执行：</p>
<pre><code class="language-bash">mkdir -p ~/.ssh
chmod 700 ~/.ssh
touch ~/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keys</code></pre><p>然后编辑 <code>~/.ssh/authorized_keys</code>，另起一行粘贴 1Password 中的完整公钥。<strong>一把公钥占一行，不要覆盖已有内容，也不要把私钥粘进去。</strong></p>
<p>比如公钥写在 <code>ubuntu</code> 用户的目录里，后面就用 <code>ubuntu</code> 登录。用 <code>sudo</code> 创建文件时还要检查归属，避免文件留在了错误用户的目录下。</p>
<blockquote>
<p>新连接验证成功之前，先别关旧终端，也别急着删掉原来的密钥。</p>
</blockquote>
<h2>2. 开启 1Password SSH Agent</h2>
<p>在 1Password 的 <strong>Settings → Developer</strong> 中开启 <strong>Use the SSH Agent</strong>。客户端需要保持运行；也可以按自己的习惯开启菜单栏驻留和登录启动。</p>
<p>配置了 Touch ID 的 Mac 可以用指纹批准请求。是否每次都弹窗，取决于授权设置、客户端会话和锁定状态，不是每条 <code>ssh</code> 命令都一定再确认一次。<a href="https://www.1password.dev/ssh/get-started">官方设置步骤</a></p>
<h2>3. 给博客服务器配一个 Host</h2>
<p>下面回到 <strong>Mac 本机</strong>。先准备 SSH 配置文件：</p>
<pre><code class="language-bash">mkdir -p ~/.ssh
chmod 700 ~/.ssh
touch ~/.ssh/config
chmod 600 ~/.ssh/config</code></pre><p>从 1Password 的 SSH Key 项目中下载 <strong>Public key</strong>，保存为 <code>~/.ssh/server.pub</code>，再向 <code>~/.ssh/config</code> 添加：</p>
<pre><code class="language-sshconfig">Host blog
  HostName server.example.com
  User ubuntu
  Port 22
  IdentityAgent "~/Library/Group Containers/2BUA8C4S2C.com.1password/t/agent.sock"
  IdentityFile ~/.ssh/server.pub
  IdentitiesOnly yes</code></pre><p>这里有几个需要对应上的地方：</p>
<table>
<thead>
<tr>
<th>配置项</th>
<th>填什么</th>
</tr>
</thead>
<tbody><tr>
<td><code>Host</code></td>
<td>自己起的别名，之后执行 <code>ssh blog</code></td>
</tr>
<tr>
<td><code>HostName</code></td>
<td>服务器真实 IP 或域名</td>
</tr>
<tr>
<td><code>User</code> / <code>Port</code></td>
<td>登录用户和 SSH 端口</td>
</tr>
<tr>
<td><code>IdentityAgent</code></td>
<td>1Password Agent 的本机 socket 路径</td>
</tr>
<tr>
<td><code>IdentityFile</code></td>
<td>与 1Password 中那把私钥配对的 <code>.pub</code> 文件</td>
</tr>
<tr>
<td><code>IdentitiesOnly yes</code></td>
<td>限定使用配置指定的身份，避免遍历 Agent 中其他密钥</td>
</tr>
</tbody></table>
<p><code>IdentityAgent</code> 路径中有空格，引号别漏。已经有 <code>Host blog</code> 的话，修改原来的配置块，不要重复追加。</p>
<p><strong><code>IdentityFile</code> 用公钥不是笔误。</strong> OpenSSH 支持用它选择 Agent 中对应的密钥；一些其他 SSH 客户端未必支持。配上 <code>IdentitiesOnly yes</code>，也能避免密钥太多时出现 <code>Too many authentication failures</code>。<a href="https://www.1password.dev/ssh/agent/advanced">为主机指定密钥</a></p>
<h3>试一次连接</h3>
<pre><code class="language-bash">ssh blog</code></pre><p>第一次连到这台服务器，可能先遇到主机指纹确认。通过云控制台或其他可信渠道核对后再接受；它是在确认「对面是不是我的服务器」。1Password 的授权提示则是在确认「是否允许这个客户端使用我的密钥」，两者不是一回事。</p>
<p>下面是 1Password 官方文档中的授权弹窗示例。图里是 iTerm2 执行 <code>git pull</code> 时请求使用 SSH 密钥，连接服务器时也是同一类授权。</p>
<p></p>
<p><em>图片来源：<a href="https://www.1password.dev/ssh/agent/security">1Password 官方文档</a>。界面会随版本和系统有所不同。</em></p>
<p>弹窗里先核对<strong>请求的应用</strong>和<strong>要使用的密钥</strong>，再按提示用 Touch ID 或其他可用方式授权。没有开启显示密钥名称时，可能显示的是公钥指纹。普通连接保留 <code>Approve for all applications</code> 不勾选即可，无需为了登录一次放宽到所有应用。</p>
<p>授权通过并进入远程 Shell 后，这段配置就通了。以后 <code>scp</code>、<code>sftp</code> 也可以使用同一个主机别名。</p>
<h2>4. 顺便接上 GitHub</h2>
<p>GitHub 可以另用一把密钥，和服务器用途分开。把公钥添加到 GitHub 的 <strong>Settings → SSH and GPG keys → New SSH key</strong>，类型选 <strong>Authentication Key</strong>；再把同一份公钥保存为本机的 <code>~/.ssh/github.pub</code>。</p>
<p>向 <code>~/.ssh/config</code> 添加：</p>
<pre><code class="language-sshconfig">Host github.com
  HostName github.com
  User git
  IdentityAgent "~/Library/Group Containers/2BUA8C4S2C.com.1password/t/agent.sock"
  IdentityFile ~/.ssh/github.pub
  IdentitiesOnly yes</code></pre><p>注意这里的 <code>User</code> 固定是 <code>git</code>，不是自己的 GitHub 用户名。然后测试：</p>
<pre><code class="language-bash">ssh -T git@github.com</code></pre><p>核对首次连接的主机指纹并完成授权后，GitHub 会提示认证成功，同时说明不提供 Shell。这是正常结果；这条测试命令成功认证时也可能以状态码 <code>1</code> 退出，不要只看退出码就判断失败。<a href="https://docs.github.com/en/authentication/connecting-to-github-with-ssh/testing-your-ssh-connection">GitHub 测试说明</a></p>
<p>接下来确认仓库使用的是 SSH 地址：</p>
<pre><code class="language-bash">git remote -v</code></pre><p>如果 <code>origin</code> 仍是 HTTPS 地址，在仓库目录中将它改为自己的 SSH 地址：</p>
<pre><code class="language-bash">git remote set-url origin git@github.com:YOUR_NAME/YOUR_REPO.git</code></pre><p>这只处理 Git 的 SSH 认证，和 commit 签名是两件事。想让提交显示签名验证标记，还需要另外配置。</p>
<h2>连不上时，按这个顺序看</h2>
<p>先看最终生效的配置和连接日志，比来回改文件更容易定位：</p>
<pre><code class="language-bash"># 看 Host、用户、端口、Agent 和密钥路径等最终配置
ssh -G blog

# 看连接、选钥和认证过程
ssh -v blog</code></pre><h3>Agent 签名失败</h3>
<p>这次整理博客时就遇到过：</p>
<pre><code class="language-text">sign_and_send_pubkey: signing failed ... from agent: communication with agent failed
Permission denied (publickey).</code></pre><p>先看前一行，不要一看到最后的 <code>Permission denied</code> 就去服务器换公钥。这里已经有本机 Agent 通信失败的线索。</p>
<p>确认 1Password 正在运行、SSH Agent 已开启，按提示解锁并授权，检查配置中的 socket 路径，再重试。必要时重新打开 1Password。后续重连成功了，但当时没有进一步定位到唯一原因，所以也不能把这个报错一概归结为「忘了解锁」。</p>
<h3>试了太多密钥</h3>
<p>报 <code>Too many authentication failures</code> 时，检查 <code>IdentityFile</code> 和 <code>IdentitiesOnly yes</code>。还要留意其他匹配的 <code>Host</code> 配置是否追加了身份，最后以 <code>ssh -G blog</code> 的结果为准。</p>
<h3>只有公钥认证被拒绝</h3>
<p>如果日志没有明显的 Agent 错误，再核对这些对应关系：</p>
<ul>
<li>登录用户是不是公钥所在的那个用户。</li>
<li>本机 <code>.pub</code>、1Password 密钥和服务器公钥是不是同一把。</li>
<li>服务器 <code>.ssh</code> 目录与 <code>authorized_keys</code> 的权限、归属是否正确。</li>
<li>1Password 中这把密钥是否对 Agent 可用，授权是否被取消。</li>
</ul>
<p>可以在 Mac 上查看公钥指纹，再和 1Password 中显示的指纹比较：</p>
<pre><code class="language-bash">ssh-keygen -lf ~/.ssh/server.pub</code></pre><h3>SSH 能连，ssh-add 却说没有密钥</h3>
<p><code>ssh-add</code> 使用的是 <code>SSH_AUTH_SOCK</code>，不会读取 <code>Host blog</code> 中的 <code>IdentityAgent</code>。临时指定 1Password 的 socket，再列出 Agent 提供的密钥指纹：</p>
<pre><code class="language-bash">SSH_AUTH_SOCK="$HOME/Library/Group Containers/2BUA8C4S2C.com.1password/t/agent.sock" ssh-add -l</code></pre><p>注意这里是 <strong>Shell 命令</strong>，引号内用 <code>$HOME</code>。不要写成 <code>&quot;~/Library/...&quot;</code>，Shell 不会展开双引号里的波浪号；前面的 <code>sshconfig</code> 则由 SSH 解析。</p>
<h3>终端能用，其他客户端不能用</h3>
<p>检查客户端是否支持 <code>IdentityAgent</code> 和公钥形式的 <code>IdentityFile</code>，部分软件需要另外设置 <code>SSH_AUTH_SOCK</code>。如果密钥保存在自定义或共享保险库，还要检查 Agent 的密钥可用范围。</p>
<p>这两种情况分别参考<a href="https://www.1password.dev/ssh/agent/compatibility">客户端兼容性</a>和 <a href="https://www.1password.dev/ssh/agent/config">Agent 配置</a>，不用先把私钥导出来试。</p>
<h2>配好之后</h2>
<p>日常用法没有变：连服务器还是 <code>ssh blog</code>，拉代码还是 <code>git pull</code>。只是需要签名的时候，由 1Password 接手。</p>
<p>以后加服务器，先在目标用户下登记公钥，再补一段 <code>Host</code>，把地址、用户名和公钥路径对上就行。换电脑时也记得把 SSH 配置和公钥准备好，单有 1Password 中的密钥还不够。</p>
<p>暂时先记这些。下次遇到新的报错，再回来补。</p>

          <p style='text-align: right'>
          <a href='https://remrin.dev/posts/dev/1password-ssh-agent#comments'>Finished reading? Leave a comment</a>
          </p>
    ]]>
    </content:encoded>
  <guid isPermaLink="false">181690079473831936</guid>
  <category>post</category>
<category>开发</category>
 </item>
  <item>
    <title>在北京逛一天</title>
    <link>https://remrin.dev/notes/10</link>
    <pubDate>Sun, 09 Mar 2025 04:00:00 GMT</pubDate>
    <description>2025-03-09

天安门、故宫、三里屯，最后还去了 Apple Store。相册里从红墙黄瓦一</description>
    <content:encoded><![CDATA[
      <blockquote>This rendering is produced by marked and may have formatting issues. For the best experience, visit: <a href='https://remrin.dev/notes/10'>https://remrin.dev/notes/10</a></blockquote>
          <p>2025-03-09</p>
<p>天安门、故宫、三里屯，最后还去了 Apple Store。相册里从红墙黄瓦一路拍到玻璃幕墙，今天走的地方还真不少。</p>
<p></p>
<p></p>
<p></p>
<p></p>
<p></p>
<p></p>
<p></p>
<p></p>
<h2>地图与轨迹</h2>
<p></p>

          <p style='text-align: right'>
          <a href='https://remrin.dev/notes/10#comments'>Finished reading? Leave a comment</a>
          </p>
    ]]>
    </content:encoded>
  <guid isPermaLink="false">182107959885565952</guid>
  <category>note</category>
false
 </item>
  <item>
    <title>长城和烤鸭</title>
    <link>https://remrin.dev/notes/9</link>
    <pubDate>Sat, 08 Mar 2025 04:00:00 GMT</pubDate>
    <description>2025-03-08

去逛了一下八达岭长城。三月的山还是灰褐色的，城墙一直伸到远处。

还吃了烤鸭</description>
    <content:encoded><![CDATA[
      <blockquote>This rendering is produced by marked and may have formatting issues. For the best experience, visit: <a href='https://remrin.dev/notes/9'>https://remrin.dev/notes/9</a></blockquote>
          <p>2025-03-08</p>
<p>去逛了一下八达岭长城。三月的山还是灰褐色的，城墙一直伸到远处。</p>
<p>还吃了烤鸭，这两件事放在同一天，倒也很合适。</p>
<p></p>
<p></p>
<p></p>
<p></p>
<p></p>
<p></p>
<p></p>
<h2>地图与轨迹</h2>
<p></p>

          <p style='text-align: right'>
          <a href='https://remrin.dev/notes/9#comments'>Finished reading? Leave a comment</a>
          </p>
    ]]>
    </content:encoded>
  <guid isPermaLink="false">182107636651528192</guid>
  <category>note</category>
false
 </item>
  <item>
    <title>在 MacOS 中搭建 独立Linux 开发环境</title>
    <link>https://remrin.dev/posts/linux/dev-env</link>
    <pubDate>Sun, 23 Jun 2024 09:17:16 GMT</pubDate>
    <description>前言
在 Mac OS 中使用 Linux 开发环境本身并不是一件难事，因为 Mac OS 本身就是</description>
    <content:encoded><![CDATA[
      <blockquote>This rendering is produced by marked and may have formatting issues. For the best experience, visit: <a href='https://remrin.dev/posts/linux/dev-env'>https://remrin.dev/posts/linux/dev-env</a></blockquote>
          <h2>前言</h2>
<blockquote>
<p>在 <code>Mac OS</code> 中使用 <code>Linux</code> 开发环境本身并不是一件难事，因为 <code>Mac OS</code> 本身就是一个 <code>类Uinx</code> 系统，加上有 <code>HomeBrew</code> 可以直接安装大部分 <code>Linux</code> 的软件包， 即使不使用虚拟机，也能有一个相对较好的开发体验，但是难免会有特殊需求， 或者不想污染宿主机的开发环境，那就可以使用 如 <code>VMware Fusion</code>、<code>Parallels Desktop</code> 或 <code>VirtualBox</code>），然后在虚拟机中安装完整的 Linux 发行版， 但是这类虚拟机比较 <code>重</code> ，启动比较慢，内存占用大，就不在我的考虑范围内了，当然也可以使用 <code>Docker Desktop</code> 直接使用各类 <code>Linux</code> 发行版的镜像， 或者创建 <code>Dev Container</code> 直接创建一个环境齐全的开发镜像， 但是目前 <code>Docker</code> 镜像源的问题没有一劳永逸的方案，所以也不在我的考虑范围内，最终我决定使用 <code>Orbstack</code> 中的 <code>Mechines</code> 来搭建一个开发环境</p>
</blockquote>
<h3>安装 <code>Orbstack</code></h3>
<p><a href="https://orbstack.dev/">官网</a>里可以直接下载安装包， 或者使用 <code>HomeBrew</code> 直接安装</p>
<pre><code class="language-bash">brew install orbstack</code></pre><h3>选择一个合适的发行版</h3>
<p>我这里使用的是 <code>Fedroa</code>

</p>
<h3>完善开发环境</h3>
<ol>
<li>第一步当然是换
<a href="https://mirrors.tuna.tsinghua.edu.cn/help/fedora/">清华源</a>，首先备份一下默认源</li>
</ol>
<pre><code class="language-bash">sudo cp -r /etc/yum.repos.d /etc/yum.repos.d.backup</code></pre><p>  然后直接替换默认源</p>
<pre><code class="language-bash">sudo sed -e 's|^metalink=|#metalink=|g' \
  -e 's|^#baseurl=http://download.example/pub/fedora/linux|baseurl=https://mirrors.tuna.tsinghua.edu.cn/fedora|g' \
  -i.bak \
  /etc/yum.repos.d/fedora.repo \
  /etc/yum.repos.d/fedora-modular.repo \
  /etc/yum.repos.d/fedora-updates.repo \
  /etc/yum.repos.d/fedora-updates-modular.repo</code></pre><p>  如果你喜欢手动换</p>
<ul>
<li><p>fedora 仓库 (/etc/yum.repos.d/fedora.repo)</p>
<pre><code class="language-">[fedora]
name=Fedora $releasever - $basearch
failovermethod=priority
baseurl=https://mirrors.tuna.tsinghua.edu.cn/fedora/releases/$releasever/Everything/$basearch/os/
metadata_expire=28d
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-fedora-$releasever-$basearch
skip_if_unavailable=False</code></pre></li>
<li><p>updates 仓库 (/etc/yum.repos.d/fedora-updates.repo)</p>
</li>
</ul>
<pre><code class="language-">  [updates]
  name=Fedora $releasever - $basearch - Updates
  failovermethod=priority
  baseurl=https://mirrors.tuna.tsinghua.edu.cn/fedora/updates/$releasever/Everything/$basearch/
  enabled=1
  gpgcheck=1
  metadata_expire=6h
  gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-fedora-$releasever-$basearch
  skip_if_unavailable=False</code></pre><ul>
<li><p>fedora-modular 仓库 (/etc/yum.repos.d/fedora-modular.repo)</p>
<pre><code class="language-">[fedora-modular]
name=Fedora Modular $releasever - $basearch
failovermethod=priority
baseurl=https://mirrors.tuna.tsinghua.edu.cn/fedora/releases/$releasever/Modular/$basearch/os/
enabled=1
metadata_expire=7d
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-fedora-$releasever-$basearch
skip_if_unavailable=False</code></pre></li>
<li><p>updates-modular 仓库 (/etc/yum.repos.d/fedora-updates-modular.repo)</p>
<pre><code class="language-"></code></pre></li>
</ul>
<p>  [updates-modular]
  name=Fedora Modular <span class="katex-render">releasever -</span>basearch - Updates
  failovermethod=priority
  baseurl=<a href="https://mirrors.tuna.tsinghua.edu.cn/fedora/updates/$releasever/Modular/$basearch/">https://mirrors.tuna.tsinghua.edu.cn/fedora/updates/$releasever/Modular/$basearch/</a>
  enabled=1
  gpgcheck=1
  metadata_expire=6h
  gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-fedora-<span class="katex-render">releasever-</span>basearch
  skip_if_unavailable=False
    ```</p>
<ul>
<li><p>清理缓存</p>
<pre><code class="language-bash">sudo dnf clean all
sudo dnf makecache</code></pre></li>
</ul>
<pre><code class="language-">- 更新软件包
  
  ```bash
  sudo dnf update
  ```
---

2. 使用 `zsh` 替换默认的 `bash`    

- 安装 `git` 和 `zsh`

```bash
sudo dnf install git zsh</code></pre><ul>
<li>安装 <code>oh my zsh</code></li>
</ul>
<pre><code class="language-bash">sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"</code></pre><ul>
<li><p>安装常用的 <code>oh my zsh</code> 插件</p>
<ul>
<li>自动提示</li>
</ul>
<pre><code class="language-bash">git clone https://github.com/zsh-users/zsh-autosuggestions.git $ZSH_CUSTOM/plugins/zsh-autosuggestions</code></pre><ul>
<li>高亮显示</li>
</ul>
<pre><code class="language-bash">git clone https://github.com/zsh-users/zsh-syntax-highlighting.git $ZSH_CUSTOM/plugins/zsh-syntax-highlighting</code></pre><pre><code class="language-bash"></code></pre></li>
</ul>
<p>   git clone <a href="https://github.com/zdharma-continuum/fast-syntax-highlighting.git">https://github.com/zdharma-continuum/fast-syntax-highlighting.git</a> <span class="katex-render">{ZSH_CUSTOM:-</span>HOME/.oh-my-zsh/custom}/plugins/fast-syntax-highlighting
    ```
    - 自动补全</p>
<pre><code class="language-undefined">``` bash
</code></pre><p>git clone --depth 1 -- <a href="https://github.com/marlonrichert/zsh-autocomplete.git">https://github.com/marlonrichert/zsh-autocomplete.git</a> $ZSH_CUSTOM/plugins/zsh-autocomplete
    ```</p>
<ul>
<li><p>修改 <code>zsh</code> 配置文件，将其中的 plugins 修改为</p>
<pre><code class="language-bash">vim ~/.zshrc</code></pre><pre><code class="language-config"> plugins=(
   git
   zsh-autosuggestions
   zsh-syntax-highlighting
   fast-syntax-highlighting
   zsh-autocomplete
 )</code></pre></li>
<li><p>应用修改</p>
<pre><code class="language-bash">source ~/.zshrc</code></pre></li>
</ul>
<ol>
<li>安装 <code>NVM</code> 管理 <code>NodeJS</code> 版本</li>
</ol>
<pre><code class="language-bash">curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash</code></pre><ul>
<li><p>如何使用</p>
<pre><code class="language-bash">$ nvm use 16
 Now using node v16.9.1 (npm v7.21.1)
 $ node -v
 v16.9.1
 $ nvm use 14
 Now using node v14.18.0 (npm v6.14.15)
 $ node -v
 v14.18.0
 $ nvm install 12
 Now using node v12.22.6 (npm v6.14.5)
 $ node -v
 v12.22.6</code></pre><p>进阶请查看 <a href="https://github.com/nvm-sh/nvm">文档</a></p>
</li>
</ul>
<ol>
<li>使用 <code>VS Code</code> 连接到刚才创建的虚拟机中</li>
</ol>
<ul>
<li>安装插件 <code>Remote Development</code></li>
<li>添加远程链接，在左侧菜单中选择 远程资源管理器， 点击 <code>SSH</code> 上的 <code>+</code> 号，或者使用快捷键 <code>Shift</code>+<code>Command</code>+<code>P</code> 输入 <code>remote add</code>, 来打开添加 <code>SSH</code> 链接的窗口

输入 <code>ssh orb</code> 即可连接至刚创建的虚拟机中
::: warning

 如果有多个虚拟机，<code>SSH</code> 链接的时候需要指定连接到哪台主机，如果不指定则连接到默认主机中，例如 <code>ssh debain@orb </code> 使用默认用户链接到 <code>debain</code> 主机中， 示例 
  <code>ssh machine@orb</code>，
  <code>ssh user@orb</code>，
  <code>ssh user@machine@orb</code>
 :::
 
 </li>
</ul>
<blockquote>
<p>这样就有了一个隔离宿主机系统的开发环境，再怎么折腾也不怕，有点类似于 <code>Windows</code> 中的 <code>WSL</code> ，胜在简单便捷，不需要额外开启系统的某些功能，下载完 <code>Orbstack</code> 就可以直接使用</p>
</blockquote>

          <p style='text-align: right'>
          <a href='https://remrin.dev/posts/linux/dev-env#comments'>Finished reading? Leave a comment</a>
          </p>
    ]]>
    </content:encoded>
  <guid isPermaLink="false">134127010081947650</guid>
  <category>post</category>
<category>Linux</category>
 </item>
  
</channel>
</rss>