---
title: "實測 boxd.sh 的 GitHub Token 注入：gh 走 cache，git 走 metadata"
url: https://geminixiang.com/posts/boxd-github-token-injection/
description: "記錄一次對 exe.dev / boxd.sh 免費試用 VM 的觀察：gh CLI 透過 /run/boxd/github-token 做 5 分鐘 cache，但 git credential helper 會直接向 metadata API 取 GitHub token。"
published: 2026-06-28
category: "Security"
tags: ["boxd.sh", "exe.dev", "GitHub Actions", "GH_TOKEN", "Sandbox", "資安"]
---

# 實測 boxd.sh 的 GitHub Token 注入：gh 走 cache，git 走 metadata

最近在替 [mikan](https://github.com/geminixiang/mikan) 設計 secret 注入機制時遇到一個問題。

mikan 是一個 coding agent，會在隔離的 sandbox 環境裡執行使用者交辦的任務。這類 agent 幾乎都需要存取外部服務：push 程式碼到 GitHub、呼叫部署 API、讀取私有套件庫……這些操作都需要帶 credential（認證金鑰）。

問題是：credential 要怎麼安全地交到 sandbox 手上？

最直覺的做法是直接把 token 塞進環境變數（`export GITHUB_TOKEN=xxx`），但環境變數會被所有子 process 繼承，agent spawn 出來的每個工具、腳本、甚至 `env` 指令都能看到它。另一個選項是把 token 寫進 workspace，但 agent 一不小心就可能讀到、commit 進去，或印到 log 裡。

所以我開始看其他 sandbox runtime 怎麼解這個問題。

## boxd.sh 是什麼

[exe.dev](https://exe.dev/) 和 [boxd.sh](https://boxd.sh/) 都是提供持久性 Linux VM 的服務，面向開發者和 AI agent。兩者都能透過 SSH 連進去，VM 的狀態會保留，重新連線時環境還在。功能定位相近，是直接競品。

這類服務通常會替使用者預先設定好 GitHub 授權，讓 VM 裡的 `gh` 和 `git` 指令不需要手動登入就能直接存取 GitHub。

我想弄清楚的是：它怎麼把 GitHub token 交給 VM？

---

連進免費試用的 VM 之後，我原本以為 token 只從一個地方來。實測後要修正：

![boxd credential injection flow](../../assets/images/posts/boxd-credential-flow.svg)

> [!IMPORTANT]
> **`gh` CLI 走 `/run/boxd/github-token` 這個本地 cache；`git` 的 credential helper 則直接向 metadata API 取 token。**
>
> 這是兩條不同的整合路徑，不能混為一談。

## 第一條路：`gh` CLI

### `boxd-github-token`：帶 cache 的取 token 工具

VM 裡有一個二進位工具 `/usr/local/bin/boxd-github-token`，工作是拿到當下有效的 GitHub token：

```sh title="/usr/local/bin/boxd-github-token"
CACHE=/run/boxd/github-token
mkdir -p /run/boxd

if [ -f "$CACHE" ]; then
  AGE=$(( $(date +%s) - $(stat -c %Y "$CACHE" 2>/dev/null || echo 0) ))
  if [ "$AGE" -lt 300 ] && [ -s "$CACHE" ]; then
    cat "$CACHE"   # cache hit：直接輸出檔案
    exit 0
  fi
fi

# cache miss：向 metadata API 拿新 token，寫回 cache 再輸出
TOKEN=$(wget -qO- http://10.20.0.1:9002/v1/github/token 2>/dev/null) || exit 1
if [ -n "$TOKEN" ]; then
  printf '%s' "$TOKEN" > "$CACHE"
  printf '%s' "$TOKEN"
else
  exit 1
fi
```

邏輯：

- **cache hit**：`/run/boxd/github-token` 存在且距上次寫入不超過 300 秒（5 分鐘），直接輸出檔案內容
- **cache miss**：向 `http://10.20.0.1:9002/v1/github/token` 取新 token，寫回 cache 再輸出

`10.20.0.1:9002` 是 boxd 的 **metadata API**——類似 AWS EC2 的 `169.254.169.254`，是 VM host 開放給 VM 查詢自己 metadata（包含臨時 credential）的內部端點，只有 VM 內部能打到。

> [!NOTE]
> `/run/boxd/github-token` 本身只是一個 cache 檔案，不是 token 的來源。真正的上游是 metadata API。

### `gh` 怎麼用這個 helper

`gh` CLI 被 `/etc/profile.d/boxd-github.sh` 包了一層 shell function：

```sh title="/etc/profile.d/boxd-github.sh"
gh() {
  _token=$(boxd-github-token 2>/dev/null)
  if [ -z "$_token" ]; then
    command gh "$@"
    return $?
  fi
  GH_TOKEN="$_token" command gh "$@"  # token 只注入這一個指令的環境
  unset _token
}
```

也就是說，在 login shell 裡執行 `gh repo view`，實際的路徑是：

```text
gh function → boxd-github-token → GH_TOKEN=<token> command gh ...
```

注意：`GH_TOKEN` 不是預先 export 在整個 session 的環境變數，而是只在執行那一個 `gh` command 時才臨時注入。在 shell 裡跑 `env`，不會看到 `GH_TOKEN`。

## 第二條路：`git`

Git 有一套叫做 **credential helper** 的機制：當 `git` 需要對 HTTPS URL 做認證時，它會呼叫設定好的 helper 程式，把 `protocol`、`host` 傳進去，helper 再回傳 `username` 和 `password`。

boxd 透過 `/etc/gitconfig` 設定讓所有 GitHub 操作都走自己的 helper：

```ini title="/etc/gitconfig"
[credential "https://github.com"]
    helper = boxd

[url "https://github.com/"]
    insteadOf = git@github.com:   # SSH URL 自動轉為 HTTPS，讓 credential helper 也能接管
```

helper 的核心邏輯：

```sh title="/usr/local/bin/git-credential-boxd"
if [ "$GITHUB" = "1" ] && [ "$HTTPS" = "1" ]; then
  TOKEN=$(wget -qO- http://10.20.0.1:9002/v1/github/token 2>/dev/null)
  printf 'protocol=https\nhost=github.com\nusername=x-access-token\npassword=%s\n\n' "$TOKEN"
fi
```

> [!NOTE]
> 這裡**沒有讀 `/run/boxd/github-token`，也沒有寫 cache**。每次 `git` 需要 GitHub credential，helper 都直接打 metadata API 拿新 token。

## 兩條路徑的比較

| | `gh` CLI | `git` over HTTPS |
|---|---|---|
| 進入點 | `/etc/profile.d/boxd-github.sh`（shell function） | `/etc/gitconfig` credential.helper |
| 取 token | `boxd-github-token`（有 cache） | `git-credential-boxd`（無 cache） |
| cache | `/run/boxd/github-token`，TTL 5 分鐘 | 無 |
| 上游來源 | metadata API（cache miss 時） | metadata API（每次直接打） |

這是這次實測最重要的結論：`/run/boxd/github-token` 不是整個 VM 唯一的 GitHub token 輸出點，它只是 `gh` 這條路徑的本地 cache。

## 值得注意的安全點

### 1. `/run/boxd/github-token` 是 644

```text
drwxr-xr-x boxd boxd /run/boxd
-rw-r--r-- boxd boxd /run/boxd/github-token
```

Owner 是 `boxd`，但 mode `644` 代表同機其他 user 也能讀這個檔案。如果 VM 裡只有一個使用者，影響可能有限；如果存在多個 user 或不受信任的 process，這個設計值得留意。

### 2. token 不在預設 env

一般 shell session 裡，`GH_TOKEN` 不是常駐的環境變數。這比直接 `export GITHUB_TOKEN=xxx` 好：token 不會被所有子 process 自動繼承，只有在 `gh` function 執行的那一刻才存在於環境中。

### 3. `gh` 和 `git` 行為不同，容易誤判

只看 `/run/boxd/github-token` 這個檔案，只能理解 `gh` 這條路徑。`git clone`、`git push` 走的是另一條完全獨立的機制，不會經過這個 cache。

### 4. log 洩漏仍然是最大的攻擊面

不管 token 從 cache 來還是 metadata API 來，最後都會進到某個指令的環境或 credential response 裡。

> [!WARNING]
> 最直接的風險是把 token 印出來。常見的觸發方式：
>
> ```bash
> set -x  # 會把每個指令和它的參數都輸出到 stderr
> ```
>
> 任何會把 token 帶進 stdout / stderr / CI log / agent transcript 的操作，才是實際上最需要防守的地方。

## 小結

boxd.sh 把 GitHub token 注入 VM 的方式有兩條路：

- **`gh` CLI**：透過 shell function 呼叫 `boxd-github-token`，後者用 5 分鐘 cache 減少 metadata API 請求
- **`git`**：透過 credential helper 直接打 metadata API，每次都拿新 token

兩條路共用同一個 metadata endpoint 作為上游來源，但 cache 策略完全不同。設計 sandbox 的 credential 注入機制時，要把這兩條路分開考慮，不能只看其中一條。

---

## About the author

**Ying Xiang Zhao** (geminixiang) — DevOps Engineer, Taipei, Taiwan.

Ying Xiang Zhao（geminixiang）是住在台北的 DevOps 工程師，工作橫跨雲原生基礎設施、CI/CD、ML 模型部署與全端開發。這個部落格記錄 infra、資安與 AI coding agent 的實戰筆記；作品包含開源專案 mikan（把 AI coding agent 帶進 Slack / Telegram / Discord）與 tiny-agent（用四種語言實作的極簡 coding agent），瀏覽器內的影片編輯器 Studio，並經營旅遊分帳工具 SentriPay。

**Open to work** — DevOps Engineer / Platform Engineer / Site Reliability Engineer / AI Infrastructure Engineer · Taipei, Taiwan / Remote. Résumé: https://geminixiang.com/about/ (JSON: https://geminixiang.com/resume.json)

Topics: DevOps, Cloud Native, Kubernetes, Docker, Terraform, Google Cloud, CI/CD, Security, AI coding agents, ML model deployment, TypeScript, Python

Projects:
- [mikan](https://github.com/geminixiang/mikan) — Self-hosted AI coding agent for Slack, Telegram, Discord and GitHub with per-conversation sandboxes
- [tiny-agent](https://github.com/geminixiang/tiny-agent) — Minimal AI coding agents in TypeScript, Go, Python and Rust, plus a book on how they work
- [SentriPay](https://sentripay.com) — Travel expense splitter with multi-currency conversion and shareable, live-synced trip ledgers
- [Studio](https://studio.geminixiang.com) — Browser-based video editor built on WebCodecs

[About](https://geminixiang.com/about/) · [Résumé](https://geminixiang.com/about/) · [LinkedIn](https://linkedin.com/in/geminixiang/) · [GitHub](https://github.com/geminixiang) · [Twitter](https://x.com/geminixiang)
