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 用」的那個應用。
運作方式
步驟 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,請找管理員代為撤銷。
輪替,請照這個順序(千萬別反過來,先撤銷等於製造停機):
- 在
/account/api-keys建立替代金鑰。
- 更新每個消費者的 secret,並確認可以正常運作。
- 看一下舊金鑰的 最後使用 欄位;如果還在往前走,表示您漏掉了一個消費者。這一步要在撤銷 之前 做 — 已撤銷的金鑰不會再更新 最後使用,事後那個欄位是凍住的,什麼都看不出來。
- 撤銷舊金鑰。
金鑰一旦外洩,先撤銷再追問。 然後建立新的一把 — 輪替客戶端應用的 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 請撤掉金鑰,不要放到過期為止 |
相關文件
- 開始使用
- 裝置流程 — 互動式 CLI 的正確選擇
- 用戶端憑證流程 — 服務身分的正確選擇
- Token 與撤銷 — introspection、撤銷、速率限制
- JWT 驗證 — 給 JWT access token 用(不是金鑰)
- 錯誤處理
管理 API 權限
啟用管理 API 後,/api/v1/me 接受明確授予 account:read 的個人金鑰。建立頁提供選填管理權限,逐把金鑰獨立保存;既有金鑰沒有管理權限。有效權限受目前應用程式授權限制,管理員操作另檢查目前管理員角色。管理 API 不使用一般驗證快取,撤銷、停用或收窄會在下一次請求生效。管理 grant 綁定伺服器設定的精確 resource,但不新增 JWT claims,也不改變 tokeninfo/introspect 的 scope。僅選擇獲准的應用程式不會自動取得管理權限。