API 金鑰(Personal API Keys)

Client App 的 Owner 或 Admin 必須在 App 設定中啟用 Personal API Key,且 App 必須通過審核並處於啟用狀態。新建及升級前既有 App 預設關閉;CIMD App 不可使用。若沒有可用 App,請聯絡 Owner,或前往「我的 App」啟用後重試。關閉設定會立即停止新申請及既有 Key 的使用。重新啟用只會恢復尚未到期且未撤銷的 Key;暫停中的 Key 仍可查看及撤銷。

個人 API 金鑰 是由已登入使用者自行建立的不透明憑證(前綴 sgk_),給那些跑不了 OAuth 流程的呼叫端用:shell script、CI job、cron 排程,或是沒有瀏覽器、也沒地方安放 client secret 的老舊系統。

送法跟 bearer token 一樣,但它 不是 JWT — 沒有任何 claim、不能刷新、也無法離線驗證。Resource server 必須問 Signet 這把金鑰還有效嗎,而這正是撤銷能即時生效的原因。

何時該用金鑰,何時不該

情境 該用
CI job、cron 排程,或一次性 script 以您的身分 執行 個人 API 金鑰
互動式 CLI 或無頭裝置,但有人能開瀏覽器 裝置流程
後端服務以 自己的身分 呼叫,完全沒有使用者 用戶端憑證
任何有使用者又有瀏覽器的場景 授權碼 + PKCE

只有在沒有任何流程適用時 才動用金鑰。流程給您的是短命 token、可離線 JWKS 驗證、磁碟上沒有長期祕密;金鑰把這三項全部放棄,換來的是一行 curl 就能跑。另外請注意金鑰綁在 您的 帳號上 — 您離職那天,所有靠它運作的東西一起停擺。屬於團隊而非個人的東西,請管理員開一個 Client Credentials 客戶端。

您的部署可能整個關掉了這項功能(PERSONAL_API_KEYS_ENABLED=false)。若 /account/api-keys 回 404,原因就在這 — 請找管理員。

金鑰屬性

屬性 值
格式 sgk_ + 52 個小寫 base32 字元(共 56 字元)。裡面沒有編碼任何資訊 — 它只是一個隨機查表用的把手
顯示 只有一次,就在建立完成後那一頁。之後只會再看到 sgk_ab12…wxyz 這樣的片段
綁定 恰好一個 客戶端應用,建立時選定
Scope 該客戶端應用的 scope,在 Signet 填查詢快取時從應用讀取 — 金鑰上不存任何副本
到期 必填,建立時選定,並受部署上限約束(預設上限 90 天)。不存在永不過期的金鑰。
數量 每位使用者有上限(預設 10 把有效金鑰)
撤銷 立即生效 — 下一次驗證就會失敗
驗證方式 只能線上驗(/oauth/tokeninfo 或 /oauth/introspect)— 沒有 JWKS、無法本地驗證

Scope 跟著應用走,不跟著金鑰走。 一般 tokeninfo/introspect scope 不逐把金鑰保存副本:驗證所回報的 scope,是 Signet 上次為這把金鑰填查詢快取時該客戶端應用所擁有的 scope。管理員把應用的 scope 放寬,綁在上面的每把金鑰跟著放寬;收窄同理 — 但不是立即生效,所以千萬別把「改了應用的 scope」當成「旗下金鑰立刻降權」。請挑「剛好夠您的 script 用」的那個應用。

運作方式

sequenceDiagram participant User as 使用者 participant Signet participant Script as 腳本 participant API as Resource Server User->>Signet: /account/api-keys → 建立(名稱、客戶端應用、到期) Signet-->>User: sgk_…(只顯示這一次) note over User,Script: 存進 secrets manager / CI secret Script->>API: GET /resource(Authorization: Bearer sgk_…) API->>Signet: GET /oauth/tokeninfo(Bearer sgk_…) Signet-->>API: {active, user_id, scope, token_type: personal_api_key} API-->>Script: 200 OK note over User,Signet: 在 /account/api-keys 撤銷 → 下次驗證即 401

步驟 1:建立金鑰

前往 /account/api-keys → + 建立金鑰。三個欄位:

欄位 說明
名稱 最多 100 字元。寫清楚誰會用它,例如 CI deploy、nightly backup — 日後就是靠這個對號入座
用戶端應用程式 輸入關鍵字搜尋啟用中的應用。金鑰屬於這個應用,並繼承它的 scope
到期時間 3 小時 / 1 天 / 7 天 / 30 天 / 自訂… 天數。全部受部署上限約束(預設 90 天),超過上限的預設選項不會出現

下一頁 只會顯示一次 完整金鑰。請直接複製進 secrets manager 或 CI secret store — 該頁帶有 Cache-Control: no-store,按上一頁救不回來,而 Signet 只保存雜湊值。弄丟了?撤銷後重建一把,沒有「再看一次」這個選項。

離開那一頁之前,先跑一次頁面上的 現在就驗證它 區塊。它會給你一段可以直接貼上、且帶有真實金鑰的指令,並印出你這把金鑰應該回傳的完整內容——你的 user_id、你的 client_id、你的權限範圍、你的 exp。把兩邊對起來,是你唯一能證明金鑰完整複製下來的機會,而代價只是貼上一次。

怎麼挑到期時間:挑您有能力自動化處理的最短壽命。一次性搬遷用的 3 小時金鑰,出事的代價遠低於掛在 CI runner 上的 90 天金鑰。若撞到每人上限(列表頁會顯示「已使用 3 / 10 把金鑰」),請撤掉沒在用的那把,而不是去要更高的上限。

什麼會佔用上限:只有「有效」金鑰會佔名額,也就是尚未撤銷、也還沒到期的那些。已撤銷和已過期的金鑰仍會留在列表中,作為曾經存在過的紀錄,但它們會立刻釋出名額——這也是為什麼列表的列數和標題上的數字經常對不起來。讓一把金鑰自然到期,跟主動撤銷它一樣能空出名額。

步驟 2:使用金鑰

當成 bearer token 送出:

curl -H "Authorization: Bearer $SIGNET_API_KEY" https://api.example.com/resource

在 CI 裡放進 secret store,以環境變數注入:

# GitHub Actions
- name: Deploy
  env:
    SIGNET_API_KEY: ${{ secrets.SIGNET_API_KEY }}
  run: ./deploy.sh

保管規則 — 金鑰等同一組長期密碼:

  • 絕對不要 放進 URL query string、redirect、或任何 GET 參數。URL 會進 access log、proxy 與瀏覽器歷史。
  • 絕對不要 直接寫在命令列參數上 — argv 可以被 ps 看見,也會進 shell 歷史。請從環境變數或檔案讀取。
  • 不要 commit、不要貼到 issue,並把它從 CI log 裡遮蔽掉(把變數設為 masked)。
  • 一個消費者一把金鑰。三支 script 共用一把,撤銷時三支一起死,而 最後使用 也無法告訴您是誰在用。

步驟 3:驗證金鑰(Resource Server 端)

沒有任何離線驗證的可能。下面兩個端點都會回 token_type: "personal_api_key",讓您能套用不同政策 — 例如部署 API 接受金鑰,但 OIDC 敏感面一律拒絕。

GET /oauth/tokeninfo

最簡單的做法:金鑰本身就是這次呼叫的憑證,不需要客戶端憑證。

curl -H "Authorization: Bearer sgk_..." https://your-signet/oauth/tokeninfo
{
  "active": true,
  "user_id": "5f6e...",
  "client_id": "d4c3...",
  "scope": "deploy",
  "exp": 1769472000,
  "iss": "https://your-signet",
  "subject_type": "user",
  "token_type": "personal_api_key"
}

user_id 是建立這把金鑰的人 — 請把這次呼叫視為 以該使用者身分 發出,並在放行操作前檢查 scope。另外注意金鑰沒有 aud 欄位:audience binding(RFC 8707)是 JWT 的功能,所以金鑰無法被綁定到單一 resource server。如果您的 API 靠 aud 防止 token 被重放到隔壁服務,那金鑰就是錯的憑證選擇。

任何失敗 — 不存在、格式錯誤、已撤銷、已過期、綁定的應用被停用、或功能被關閉 — 都回 同一個 結果:

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer error="invalid_token", resource_metadata="…/.well-known/oauth-protected-resource"

{"error": "invalid_token", "error_description": "Token is invalid or expired"}

這種一致性是刻意的:掃描者不該有辦法得知金鑰「為什麼」失敗。代價是 您自己也看不出來 — 請看下方〈為什麼我的金鑰突然不能用了〉。

POST /oauth/introspect(RFC 7662)

當您的 resource server 本身就是註冊過的 Signet 客戶端,而且想要符合 RFC 形狀的輸出時使用:

curl -X POST https://your-signet/oauth/introspect \
  -u "$CLIENT_ID:$CLIENT_SECRET" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "token=sgk_..."
{
  "active": true,
  "scope": "deploy",
  "client_id": "d4c3...",
  "token_type": "personal_api_key",
  "exp": 1769472000,
  "iat": 1769385600,
  "sub": "5f6e...",
  "username": "alice",
  "iss": "https://your-signet",
  "jti": "key-uuid"
}

無效的金鑰依 RFC 7662 就只是 {"active": false}(絕不會是 4xx)。

這裡同樣有 ownership 閘門。 預設(INTROSPECTION_REQUIRE_OWNERSHIP=true)只有當您以 金鑰所綁定的那個客戶端應用 身分認證時,才拿得到上面的完整 metadata。其他客戶端只會看到被清空的 {"active": true}。所以一個共用的 API gateway 去 introspect 綁在許多不同應用上的金鑰,會什麼有用資訊都拿不到 — 那種拓樸請改用 /oauth/tokeninfo。

快取驗證結果

能避免的話,不要每個進來的請求都打一次 Signet。金鑰是*唯一*真的需要往返的憑證 — JWT access token 必須用 JWKS 本地驗證,絕不能每個請求都打這兩個端點。兩個端點都有速率限制:/oauth/tokeninfo 每 IP(預設每分鐘 600 次,您整個 egress IP 共用),/oauth/introspect 每個已認證的客戶端應用(各每分鐘 600 次,後面再加每 IP 1200 次的上限);見 Token 與撤銷。Signet 自己的快取保護的是 Signet 的資料庫,不是您的請求額度 — 收到 429 就代表您這邊少了一層驗證結果快取。

// Go 偽程式碼 — 快取「結果」,不是快取金鑰
if v, ok := cache.Get(sha256(key)); ok {
    return v
}
v := callTokenInfo(key)          // 401 也要快取成負面結果
cache.Set(sha256(key), v, 60*time.Second)
  • 快取鍵請用金鑰的雜湊,絕不用金鑰本身,也絕不寫進 log。
  • TTL 要短 — 您的快取 TTL 就是您的撤銷延遲。30~60 秒是合理的折衷;快取到 exp 等於把線上驗證這件事的意義整個丟掉。
  • 負面結果也快取一小段時間,這樣設定壞掉的客戶端重試迴圈才不會同時打爆您和 Signet。

哪些端點吃金鑰

端點 sgk_ 金鑰 行為
GET /oauth/tokeninfo 接受 驗證用。token_type: personal_api_key
POST /oauth/introspect 接受 驗證用,RFC 7662 形狀。需客戶端認證,且有 ownership 閘門
POST /oauth/revoke 接受 自助撤銷 — 持有金鑰即是授權。一律回 200
POST /oauth/token(四種 grant 皆然) 拒絕 金鑰不是 code、不是 refresh token,也不是 device code → invalid_grant(device_code 是 access_denied)
GET /api/v1/me 有條件接受 管理 API 已啟用;此金鑰明確取得 account:read,且目前 client/resource 授權仍有效
GET /oauth/userinfo 拒絕 OIDC 面,只吃 JWT access token → 401 invalid_token
本地 JWKS 驗證 拒絕 不是 JWT,沒東西可驗。請改打上面兩個端點

撤銷與輪替

從介面:/account/api-keys → 撤銷。立即生效,且無法復原;若部署是多節點又沒有共用快取,請預留最多約 1 分鐘讓每個節點跟上。

從腳本 — /oauth/revoke 不需要客戶端憑證就接受金鑰,因為持有金鑰本身 就是 授權。很適合在工作結束時順手拆掉一把短命金鑰:

curl -X POST https://your-signet/oauth/revoke \
  -H "Content-Type: application/x-www-form-urlencoded" \
  --data-urlencode "token=$SIGNET_API_KEY"

依 RFC 7009,無論金鑰是否存在都回 200,所以這個回應什麼都證明不了。有一種情況它並 不會 真的撤銷:若該部署把個人 API 金鑰整個關掉了(PERSONAL_API_KEYS_ENABLED=false),這個呼叫照樣回 200,但金鑰仍在資料庫裡,等維運人員把功能開回來就又能用。真的要緊時(例如金鑰外洩)請到 /account/api-keys 確認;若那頁回 404,請找管理員代為撤銷。

輪替,請照這個順序(千萬別反過來,先撤銷等於製造停機):

  1. 在 /account/api-keys 建立替代金鑰。
  2. 更新每個消費者的 secret,並確認可以正常運作。
  3. 看一下舊金鑰的 最後使用 欄位;如果還在往前走,表示您漏掉了一個消費者。這一步要在撤銷 之前 做 — 已撤銷的金鑰不會再更新 最後使用,事後那個欄位是凍住的,什麼都看不出來。
  4. 撤銷舊金鑰。

金鑰一旦外洩,先撤銷再追問。 然後建立新的一把 — 輪替客戶端應用的 secret 對金鑰毫無作用,別以為那樣就「洗乾淨」了。

為什麼我的金鑰突然不能用了

下面每一種都會產生同一個 401 invalid_token,所以請打開 /account/api-keys 逐項排除:

原因 從哪看得出來 怎麼救
已超過到期時間 狀態是 已過期 建立新金鑰
您或管理員撤銷了它 狀態是 已撤銷 建立新金鑰 — 撤銷是永久的
您的帳號被停用 您連登入都登不進來 您所有金鑰都已被撤銷。帳號重新啟用 不會 讓它們復活
綁定的客戶端應用被停用 金鑰仍列在列表上,既未過期也未撤銷 不用做任何事 — 管理員把應用重新啟用後金鑰會 自動恢復(傳播最多約 1 分鐘)
綁定的客戶端應用被刪除 金鑰變成 已撤銷 永久失效。請改綁其他應用建立新金鑰
整個部署關掉了這項功能 /account/api-keys 回 404 找管理員
金鑰被截斷或貼錯 列表上看起來一切正常卻還是 401 重新複製一次 — 金鑰恰好 56 字元,而畫面上顯示的片段(sgk_ab12…wxyz)不能 當憑證使用
Scope 不再足夠 Resource server 回 403,不是 401 該客戶端應用的 scope 變了。請找應用的擁有者

還有誰看得到您的金鑰:您綁定的那個客戶端應用的擁有者,以及管理員,能看到這把金鑰存在 — 名稱、片段、到期時間、最後使用時間,旁邊還有您的使用者名稱與 email。兩者都看不到金鑰本身。應用擁有者不能撤銷您的金鑰;管理員可以強制撤銷。

安全檢查清單

要求 細節
優先用流程 只有在沒有 OAuth 流程適用時才用金鑰 — 見本頁最上面那張表
到期時間取最短可行值 以天為單位,不是以月。部署上限是天花板,不是目標
一個消費者一把金鑰 可獨立撤銷,且 最後使用 才有判讀價值
放 secrets manager / CI secret 絕不進版控、argv、URL 或 log
綁最小權限的客戶端應用 金鑰繼承應用的 scope — 綁在剛好夠用的那個應用上
線上驗證,短快取 驗證結果快取 30~60 秒;TTL 就是撤銷延遲
處理 429 tokeninfo 每 IP 限制,introspect 每客戶端應用限制 — 若有 Retry-After 就遵守,退避加抖動
外洩先撤銷,再輪替 立刻撤銷,然後照「建立 → 部署 → 撤舊」
收尾 退役的 script 請撤掉金鑰,不要放到過期為止

相關文件

管理 API 權限

啟用管理 API 後,/api/v1/me 接受明確授予 account:read 的個人金鑰。建立頁提供選填管理權限,逐把金鑰獨立保存;既有金鑰沒有管理權限。有效權限受目前應用程式授權限制,管理員操作另檢查目前管理員角色。管理 API 不使用一般驗證快取,撤銷、停用或收窄會在下一次請求生效。管理 grant 綁定伺服器設定的精確 resource,但不新增 JWT claims,也不改變 tokeninfo/introspect 的 scope。僅選擇獲准的應用程式不會自動取得管理權限。