# Aether Forgejo Creds

> Aether 环境下 Forgejo 凭据的决策 / 诊断 / 轮换指南。回答"该用哪个 token"、 拆解人机两账号模型 (simonfish 人 / 10cg-ci-bot 机)、辨别 CF Access 与 forgejo PAT 两个凭据平面、诊断误导性 403 ("Only signed in user")、安全轮换 (先枚举全部 store)、 凭据卫生红线。深度内容指向 docs/guides/forgejo-token-map.md + .aether/pat-inventory.yaml。 使用场景: "该用哪个 forgejo token"、"which forgejo token"、"forgejo 401"、 "forgejo 403"、"Only signed in user"、"docker login forgejo"、"CF Access 403"、 "token rotation"、"凭据轮换"、"FORGEJO_TOKEN"、"forgejo credential"、 "人机账号"、"registry pull 401"、"docker push unauthorized"

- Skill: `10cg/aether-forgejo-creds` (Agent Skill)
- Install (CLI): `npx skillmds@latest add 10cg/aether-forgejo-creds`
- Raw SKILL.md: https://api.skillmd.com/api/skills/10cg/aether-forgejo-creds/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: DevOps & Infra
- Author: 10cg (https://skillmd.com/u/10cg)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/10cg/aether-forgejo-creds

---


# Aether Forgejo 凭据指南 (aether-forgejo-creds)

> **版本**: 0.2.0 | **优先级**: P1 | **事故根因**: H1 (#189, 2026-07-01 一 token N 用级联)
> **机读 SoT**: `.aether/pat-inventory.yaml` · **人读权威**: `docs/guides/forgejo-token-map.md`
> **AB Benchmark**: **3/3 evals WITH_BETTER (2026-08-09, suite v1.2.0, Aether #275, 首个基线)**
> —— WITHOUT 臂在 eval-3 critical FAIL (把 org 级 entry 押在 Tier 1 上, 第一步即撞 `ORG_LEVEL_UNSUPPORTED`)。

## 核心原则 (先记这三条)

1. **人机分离**: 只有两个主账号 —— `simonfish`(人) / `10cg-ci-bot`(机器)。人的活用
   simonfish，机器的活 (CI / Nomad job / host config) 用 10cg-ci-bot。**绝不交叉**。
2. **两个平面**: 访问外网 `forgejo.10cg.pub` 要过两道**独立**的门 —— Cloudflare Access
   (CF header) + forgejo PAT (Authorization: token)。它们各自失效、各自诊断，别混。
3. **轮换前枚举全部 store**: 一处 fingerprint = 假完整。H1 就是只看一个 store 就 revoke，
   级联断了 8+ 仓 CI。

> 这是 investigation-first skill: 先判断"是决策 / 诊断 / 轮换 哪类问题"，再走对应段落。
> 不确定就读 `docs/guides/forgejo-token-map.md` (权威人读版) 或
> `.aether/pat-inventory.yaml` (机读 SoT)。**不要凭记忆编造 token/账号**。

> ⚠️ **范围边界**: 本 skill **仅**覆盖 Forgejo / git / registry 凭据 (T1–T7，见 §3)。应用 /
> 数据库 secret (连接串密码等) **不在本 skill 范围** —— 走 Nomad Variables +
> `template{env=true}` 注入模式，不硬编码进 HCL，详见 `aether-conventions` skill。

---

## 1. 两账号模型 (who)

| 账号 | id | 身份 | 用途 | 绝不 |
|------|----|------|------|------|
| **`simonfish`** | 2 | 人 (org owner) | 交互 dev/AI 会话: 写 code、开 PR、发 issue、跑 `aether` 只读命令 | ❌ 进 CI / Nomad job / host config |
| **`10cg-ci-bot`** | 6 | 机器 | 一切自动化: CI、runtime 拉镜像、build push、issue 自动化 | ❌ 进人的 `~/.forgejo_env` / dev shell |

> `10cg-ci-bot` 是 `aria-runner-bot` 的新名 (renamed 2026-07-01)。任何地方再看到
> `aria-runner-bot` 用户名 = **stale，要改**。**不新增第三个主账号**(除非强隔离需求)；
> 专用永久 bot 令牌 (如 `ci-runner-image-mirror`，见 §3 T5) 另计。

**账号规则 (永远)**:
- `docker login` / basic-auth: username **应当**匹配 token 归属账号 (`10cg-ci-bot` 的
  token → `-u 10cg-ci-bot`)。理由是**日志可读 + 记录准确**，**不是**"否则会 401" —— 见下方实测。
- `curl -H "Authorization: token …"` (拉 package / API): **只看 token 不看 user**。

> ☠️ **registry 401 不要查 username**。Forgejo 容器 registry 的 `/v2/token` 端点**只校验
> PAT，完全忽略 basic-auth username** —— 错误账号名、甚至不存在的账号名，配有效 token
> 都能签出 token 并 push (2026-07-02 / 07-12 / 08-08 **三次独立实测**)。故:
> - `401` → **先查 token 死活** (已 revoke / 过期)；查 username 是已知的时间黑洞
> - `403` on push → 查 **scope** (例如 `read:package` 的 token 去 push)
>
> **边界**: 上述实测只覆盖 **registry 路径** (`/v2/token` + blob upload)。若某消费方拿
> `$FORGEJO_USER` 拼 URL 或调**非 registry** 的 Forgejo API，行为未验证 —— 这才是仍要保持
> username 正确的理由。
>
> ⚠️ 「username 不匹配会致 401」这条断言**已被实测杀死两次又复活两次** (跟踪
> [#291](https://forgejo.10cg.pub/10CG/Aether/issues/291))；仓内别处若仍有旧表述，**以本条为准**。

---

## 2. 两个凭据平面 + 误导性 403 诊断

访问外网 URL `forgejo.10cg.pub` 要过两道独立的门:

| 平面 | 凭据 | header | 谁管 |
|------|------|--------|------|
| ① **Cloudflare Access 网关** | `CF_ACCESS_CLIENT_ID` / `CF_ACCESS_CLIENT_SECRET` | `CF-Access-Client-Id/Secret` | Cloudflare 后台 (**不是 forgejo token，不归本 skill / aether-rotate-pat**) |
| ② **forgejo 账号认证** | `FORGEJO_TOKEN` (PAT) | `Authorization: token …` | 本 skill + aether-rotate-pat |

> **内网端点 `192.168.69.200:3000` 完全绕过 CF 门** —— 调试 forgejo PAT 时走内网，把 CF 平面隔离掉。

### 陷阱: `"Only signed in user is allowed to call APIs."` 403

这个 403 长得像 CF 门 / access-gate 拒绝，**实为 forgejo 拒绝一个失效或缺失的 PAT**。
**别去轮换 CF token —— 该刷新 forgejo PAT。**

诊断表 (对外网 URL 逐步加 header 观察):

| 请求 | 结果 | 判读 |
|------|------|------|
| 无 CF header | `302` 重定向到 CF 登录 | CF 门在挡 → 缺 CF header |
| 有 CF header + 无/死 PAT | `403` + forgejo JSON `Only signed in user…` | **PAT 缺失/失效 → 刷 forgejo PAT** |
| 有 CF header + 有效 PAT | `200` | 两道门都过 |

对照口诀: **302 = CF 层问题; 403-json = forgejo PAT 层问题; 200 = 通。**
走内网 `192.168.69.200:3000` 复测可直接排除 CF 平面。

### 401 vs 403 速判

- `401 unauthorized` on `docker login`/push/pull → **token 死了或值错** (已 revoke / 过期 /
  抄错)。**不是 username 问题** —— registry 忽略 username，见 §1 实测。查 §3 是否用错 token。
- `403` on push/pull → **scope 不足** (例如 `read:package` 的 token 去 push)。
- `403 Only signed in user` → §2 上表，刷 PAT。

---

## 3. 决策矩阵: 我要做 X，用哪个 token?

> 完整 token 清单 (scope · store · 消费方) 见 `docs/guides/forgejo-token-map.md §2`。
> ⚠ 下面速览是 doc/inventory 的**镜像，可能滞后** —— 与 `docs/guides/forgejo-token-map.md`
> 或 `.aether/pat-inventory.yaml` 冲突时**以后者为准**。

| 场景 | 用哪个 | 从哪取 |
|------|--------|--------|
| 交互开 PR / 发 issue / `forgejo` CLI / `aether` 只读命令 | **T1 simonfish** | shell 自动继承 `$FORGEJO_TOKEN` (from `~/.forgejo_env`); 验证 `login=simonfish` |
| CI 拉内网二进制 (golangci/nomad/gitleaks/ossutil) | **T3** | `secrets.FORGEJO_TOKEN` 自动注入 |
| CI / deploy job `docker login` + push 镜像 | **T3** | `-u $FORGEJO_USER=10cg-ci-bot -p $FORGEJO_TOKEN` 自动注入 |
| Nomad job 运行时拉容器镜像 | **T2** | Nomad var `docker_auth_password` (`aether env set` 管) |
| act_runner 拉 runner-images | **T2** | heavy `/root/.docker/config.json` (`docker login -u 10cg-ci-bot`) |
| aria-build push aria-runner 镜像 | **T4** | Nomad var `FORGEJO_BOT_PAT` (`nomad/jobs/aria-build`) |
| todo-web CI push package | **T6** (repo 级 override，不继承 org T3) | todo-web repo 级 Actions secret |
| 手动 `aether doctor` 查 registry (需 read:package) | **T1 可用** —— 2026-07-02 起已加 `read:package` (只读) | shell 里的 `$FORGEJO_TOKEN` 直接用；**仍无 `write:package`**，push 场景要换 T2/T3 |

### Token 清单速览 (账号 · scope · store)

| # | 用途 | 账号 | scope | store |
|---|------|------|-------|-------|
| T1 | 人的 dev-shell | simonfish | read:user+write:issue+write:repository+**`read:package`** (2026-07-02 加, 只读; **绝无 write:package**) | `~/.forgejo_env` (600), `.bashrc`+`.profile` source |
| T2 | runtime registry pull | 10cg-ci-bot | `read:package` | 18 Nomad var `docker_auth_password` + 3 heavy `/root/.docker/config.json` |
| T3 | org CI 全用途 | 10cg-ci-bot | issue+repo+package+read:user | org `10CG` Actions secret `FORGEJO_TOKEN` (+`FORGEJO_USER`) |
| T4 | aria-build DooD push | 10cg-ci-bot | `write:package` | Nomad var `FORGEJO_BOT_PAT` (aria-build, **自定义 key** → Tier 1 拒绝, `VAR_KEY_UNSUPPORTED`)。✅ **已解耦 (2026-07-02)**: 专用 token，不再共用 org T3 |
| T5 | runner-image mirror push | `ci-runner-image-mirror` (专用永久 bot) | `write:package` (**永不过期**) | heavy-1 `/opt/forgejo-runner/.docker-mirror/config.json` |
| T6 | todo-web CI | todo-web own | `write:package` | todo-web **repo 级** Actions secret (override org T3) |
| T7 | rotation bootstrap | ops | `write:user` | 不部署 (引导凭据) |

> ☠️ `ca32267` (旧 `aether-deploy` token, simonfish) 已 **revoke** —— H1 一 token N 用根因，
> 不要复活。silknode ACR 用的是 Aliyun 凭据 (`docker_registry_password`)，**非 forgejo**，不在本范围。

---

## 4. 轮换: 先枚举全部 store，再动手

**revoke / 轮换任何 token 前，枚举它的全部 store。** 一处 fingerprint = 假完整 (H1 教训:
`ca32267` revoke 级联到 8+ 仓)。四类 store 逐一核:

- [ ] **① Nomad Variables**: `docker_auth_password` + 自定义 key (`FORGEJO_BOT_PAT` 等)
  → `aether registry-auth list`
- [ ] **② Forgejo Actions secrets**: **repo 级 AND org 级** 都查
  → `aether doctor --check forgejo_actions_secret_drift` (cli-v1.16.43+)
- [ ] **③ host docker configs**: heavy `/root/.docker/config.json` (runner-pull) +
  `/opt/forgejo-runner/.docker-mirror` (mirror push)
  → 远端 `python3 base64 decode + sha256` 指纹，**不打印 token**
- [ ] **④ shell env**: `~/.forgejo_env` (simonfish 单一来源，login+interactive 都 source 它)

> ✅ **T3 (org FORGEJO_TOKEN) 与 aria-build (T4) 已于 2026-07-02 解耦** —— T4 改用专用
> `write:package` token，轮换 org token **不再**连带断 aria-build。(此前"二者复用同一物理
> token"的记载已过时；权威以 `.aether/pat-inventory.yaml` T4 entry 为准。)
>
> 但 **blast-radius 仍要逐条枚举**：解耦只解了这一条已知耦合，不等于 T3 没有别的消费方。

工具:
- **Tier 1 自动轮换**: `aether registry-auth rotate` (nomad-variables + forgejo-secrets **repo 级**；`docker_auth_password` 单 key) —— 详见 `aether-rotate-pat` skill
- **手动**: 账号切换 / 自定义 var key (`VAR_KEY_UNSUPPORTED`) / **org 级** secret
  (`ORG_LEVEL_UNSUPPORTED`) / host docker config / 永久 bot → 手动 (token-map §2 定位 store +
  `docs/guides/forgejo-pat-rotation.md` **Mode 6/7** / `forgejo-pat-emergency-rotation.md`)
  - cli-v1.16.74+ 在 `--dry-run` 阶段就以这两个码拒绝，**拿到码即知走哪条手工路径**
  - 手工写值一律 `aether env set --job <job> <KEY> --from-file <file>`，**绝不**把 token
    放进命令行参数 (argv 进 shell history / `ps aux` / AI transcript，#282)
- **审计**: `aether doctor --check pat_age` (到期) + `--check pat_inventory_drift` (Nomad var 漂移) + `--check forgejo_actions_secret_drift` (org Actions secret 漂移)

> scope 决定消费面: 换的 token scope 不足 (例如 issue-only 却给需要 package 的消费方) 会留隐性断裂。
> 换前核对该 store 消费方需要的最小 scope。

---

## 5. 凭据卫生 + 红线

**凭据卫生 (绝不违反)**:
- ❌ **绝不 `grep`/`cat`/`echo` 含 token 的文件或变量** (`~/.forgejo_env`、`.bashrc`、`.env`、docker config…)。
- ✅ 验证"换没换"用 **sha256 指纹** (前 16 字符): `sha256sum <<<"$VAR" | cut -c1-16` 在 subshell 内，读完 scrub。
- ✅ 验证"有效性"用 subshell 读 + 立即清；**绝不把 token 值贴进对话** (transcript 持久化)。

**红线**:
1. 绝不一 token N 用**跨账号边界** (H1 根因)。
2. 机器 token 绝不进人的 shell，人的 token 绝不进 CI/job/host (反之亦然)。
3. revoke 前枚举**全部** store (§4)。
4. 新 token / 新 store 必须登记进 `.aether/pat-inventory.yaml` —— `forgejo_actions_secret_drift` 会揪未登记的 org secret。
5. 账号切换时 user + token **同步改** —— 保持记录一致、日志可读。**注意**: 理由不是
   "不同步会 401" (registry 忽略 username, 见 §1 实测)，而是非 registry 消费方的行为未验证。

---

## 关联

- **权威人读版**: `docs/guides/forgejo-token-map.md` (完整 token 清单 + 决策矩阵 + 待办红线)
- **机读 SoT**: `.aether/pat-inventory.yaml` (登记全部 token/store，drift check 依据)
- **审计**: `aether doctor --check forgejo_actions_secret_drift` / `--check pat_age` / `--check pat_inventory_drift`
- **轮换执行**: `aether-rotate-pat` skill · `docs/guides/forgejo-pat-rotation.md` · `forgejo-pat-emergency-rotation.md`
- **事故全账**: [#189](https://forgejo.10cg.pub/10CG/Aether/issues/189) (H1 一 token N 用级联)
- **Nomad docker auth**: `docs/guides/nomad-variables-docker-auth.md`

