Skip to content

Rotate an inference key without resetting quota

Issue a replacement key, verify it, and revoke the old key while preserving access and quota.

Updated View as Markdown
Audience: application-backendPlane: adminAuth: management-keyStatus: beta

Goal

无损轮换一名用户的 Inference Key。

Outcome

新 Key 仍绑定同一 WorkspaceAccess;既有 Plan、QuotaPeriod 和历史 Usage 不复制、不归零。验证新 Key 后才撤销旧 Key。

Prerequisites

  • Access ID、旧 Key ID、Management Key。
  • 既有 Gateway Base URL 和目标 Provider 接受的模型名。

Lifecycle note

Key 只负责凭证安全生命周期。轮换不会延长 WorkspaceAccess.accessEndsAt,也不会改变 QuotaPeriod.periodEnd

1. Issue, verify, and cut over

rotate-inference-key.mjsjs
import { writeFile } from 'node:fs/promises';

const adminBaseUrl = required('NEXUS_ADMIN_BASE_URL').replace(/\/$/, '');
const managementKey = required('NEXUS_MANAGEMENT_KEY');
const accessId = required('NEXUS_ACCESS_ID');
const oldKeyId = required('NEXUS_OLD_KEY_ID');
const gatewayBaseUrl = required('NEXUS_GATEWAY_BASE_URL').replace(/\/$/, '');
const model = required('NEXUS_MODEL');

async function admin(path, { method = 'GET', body } = {}) {
  const response = await fetch(`${adminBaseUrl}${path}`, {
    method,
    headers: {
      Authorization: `Bearer ${managementKey}`,
      ...(body === undefined ? {} : { 'Content-Type': 'application/json' }),
    },
    ...(body === undefined ? {} : { body: JSON.stringify(body) }),
  });
  if (!response.ok) throw new Error(`${method} ${path} failed: ${response.status} ${await response.text()}`);
  return response.status === 204 ? undefined : response.json();
}

const before = await admin(`/admin/v1/workspace-accesses/${accessId}/quota`);
const replacement = await admin(`/admin/v1/workspace-accesses/${accessId}/api-keys`, {
  method: 'POST',
  body: { name: 'rotation-replacement', expiresInSeconds: 2592000 },
});
await writeFile('replacement-inference-key.txt', `${replacement.key}\n`, { mode: 0o600, flag: 'wx' });

const smoke = await fetch(`${gatewayBaseUrl}/chat/completions`, {
  method: 'POST',
  headers: { Authorization: `Bearer ${replacement.key}`, 'Content-Type': 'application/json' },
  body: JSON.stringify({ model, messages: [{ role: 'user', content: 'Key rotation smoke test.' }] }),
});
if (!smoke.ok) throw new Error(`replacement key smoke test failed: ${smoke.status} ${await smoke.text()}`);

await admin(`/admin/v1/workspace-accesses/${accessId}/api-keys/${oldKeyId}`, { method: 'DELETE' });
const after = await admin(`/admin/v1/workspace-accesses/${accessId}/quota`);
if (before.quotaPeriodId !== after.quotaPeriodId || before.grantedUnits !== after.grantedUnits) {
  throw new Error('key rotation unexpectedly changed the quota period');
}
console.log(JSON.stringify({ newKeyId: replacement.id, quotaPeriodId: after.quotaPeriodId }));

function required(name) {
  const value = process.env[name]?.trim();
  if (!value) throw new Error(`${name} is required`);
  return value;
}

Check your work

  • 新 Key 的烟雾测试成功后旧 Key 才被撤销。
  • quotaPeriodIdgrantedUnits 不变;烟雾测试本身可以正常增加 consumedUnits
  • replacement-inference-key.txt 权限为 0600,明文不会再次从 Admin API 读取。

Failure modes

  • 新 Key 测试失败:保留旧 Key,不执行撤销步骤。
  • workspace_access_denied:修复 Access,而不是继续轮换 Key。
  • Key 到期时间超出 Access 上限:选择更短 TTL;Key 有效期不能替代 Access 有效期。

Security notes

切流后从部署 Secret 中移除旧 Key,再撤销服务端记录。不要同时撤销所有可用 Key,除非这是明确的紧急处置。

Next steps

复核 Credential modelSecurity

Navigation

Type to search…

↑↓ navigate↵ selectEsc close