← All posts
Guides

Virtual Keys vs Master Key: Scoping a Key Per Job

A management credential and an inference credential are different tools with different blast radii. Here is how to issue one virtual key per environment-and-service pair, narrow it with the four scope fields, cap it, and rotate it without downtime.

nRouter team · 11 min read
Virtual Keys vs Master Key: Scoping a Key Per Job

The short answer: two credentials, two jobs. A management credential administers the account and never makes a model call; a virtual key (sk-nrouter-…) makes model calls and can do nothing else. Issue one virtual key per environment-and-service pair, narrow it with the four scope fields, attach a budget, and a leak becomes a revoke instead of an incident.

The short answer

Every API key is a liability the moment it leaves your control. The useful question is not "will a key leak" — over a long enough horizon one will, in a container image, a log line, a repository, a screenshot in a ticket — but "how much can the leaked one do?"

Good key design answers that with the smallest number you can arrange in advance, and the arrangement has to happen before the leak. There are only two moments you get to decide a key's blast radius: when you create it, and never again. Everything below is work you do in the first of those moments.

nRouter splits credentials into two kinds with deliberately unequal power. The management credential administers the account. Virtual keys make model calls. The asymmetry is the design: the powerful credential stays put, and the credentials that travel are not powerful.

When you need this

You are about to put a key in a container image, a CI secret, or a serverless function's environment. That key is now in more places than you can enumerate, and it will outlive your memory of putting it there — which is the whole subject of the OWASP Secrets Management Cheat Sheet.

One key already serves everything and somebody asked which service spent the money. A shared key produces one undifferentiated line item. Attribution is decided at request time; it cannot be reconstructed afterwards from a total. The full attribution model is in Attribute LLM spend by team, customer, and feature.

A key leaked and the response was "rotate everything". That sentence is a symptom, not a plan. If revoking one credential requires touching every service you own, the credentials were never scoped — and the outage caused by the rotation will be larger than the leak.

What you need first

  • An organization with a funded balance. Signup is card-required and takes a real charge; the minimum credit purchase is $5, and a first qualifying purchase carries a $10 bonus — load $5, get $15. See Pricing.
  • The right role. Owners, admins and members can create keys; only owners and admins can edit, revoke or rotate one, and viewers can see keys and usage without changing anything. That split is itself a control — check yours on Team Management.
  • A secret manager, or at minimum an environment variable. The value you are about to generate is displayed once. A Slack message to yourself is not storage.
  • NROUTER_API_KEY exported for the verification steps, against https://api.nrouter.ai/v1.

Step 1 — Know which credential does which job

Management credentialVirtual key (sk-nrouter-…)
How manyOneMany — one per environment and service
What it can doAccount and key administrationMake model calls, and nothing else
Used byAccount administration only, server-sideYour apps, SDKs, the playground
Rate limited / budgetedNot applicableYes, per key
Spend attributedNot applicableYes, per key
Where it may liveNever in client code, never in a browserServer-side, secret manager, CI secret
If it leaksAdministrative compromise — treat as an incidentBounded — revoke the one key

The operating rule follows from the second row and is not negotiable: every model call authenticates with a virtual key. Requests from your backend, from an SDK, from a notebook, from the Playground — all of them carry a virtual key. The management credential is for administration and is kept out of anything a client can observe.

A management credential in client code is not a bill, it is a breach

A leaked virtual key means somebody can spend up to a cap you already set, on models you already allowed, until you revoke it. A leaked management credential means somebody can administer the account. That is why one of them never travels and never appears in an inference example — including the ones below.

Step 2 — Create one key per environment-and-service pair

Open the Keys page (/[organization]/keys) and click Generate Key. The naming convention does more work than it looks like it does, because the dashboard sorts alphabetically and a prefix groups environments visually:

EnvironmentPrefixExample name
Local developmentdev-dev-checkout-service
Stagingstage-stage-checkout-service
Productionprod-prod-checkout-service

One key per (environment, service) pair — not per person, and emphatically not per organization. Sharing a single key across environments also shares its rate limits, its budget cap and its audit trail, which is how a runaway staging job exhausts the budget a production service depends on.

Each key is bound to exactly one team and inherits that team's permissions, so a key can never reach a model or a limit its team is not entitled to. The scope hierarchy behind that is described in Org, team, member.

The full value is shown exactly once, at creation. Afterwards the dashboard shows the key name and last four characters — sk-…a1b2 — and there is no reveal control, because the full value is not retrievable by anyone, including us. Lose it and you rotate; there is no recovery path, which is the same property that means an attacker has no recovery path either.

export NROUTER_API_KEY="sk-nrouter-..."   # the value you copied at creation
curl -sS https://api.nrouter.ai/v1/models \
  -H "Authorization: Bearer $NROUTER_API_KEY" | head -c 300

A model list means the key is live. A 401 means it is wrong, revoked or expired — see Authentication. The Authorization: Bearer form is the ordinary OAuth 2.0 bearer scheme of RFC 6750, which is why any HTTP client you already own can carry it.

Step 3 — Narrow the key with the four scope fields

A key that can do everything your team can do is only marginally better than a shared one. Four fields on the create/edit form turn it into a credential shaped like one job.

FieldWhat it doesGood default
Allowed modelsPer-key allowlist of model namesList them; empty means everything the team may use
Allowed endpointsRestrict to /chat/completions, /embeddings, etc.An embedding job needs one endpoint, not seven
IP allowlistComma-separated CIDR blocksYour VPC's NAT range for services; blank for developers
ExpirationNever, 30 days, 90 days, or a custom datetime90 days for service-to-service; 30 for human-held

Bounding a credential's life is not a house style. NIST SP 800-57 Part 1 Rev. 5 makes a defined cryptoperiod part of key management precisely because the probability that a secret has escaped rises with the time it has existed, and the OWASP Key Management Cheat Sheet says the same thing for application keys.

Two properties are worth internalising because they change what a leak is worth.

First, the IP allowlist is evaluated before the key is consumed against your budget, so a request from an unexpected source is rejected at the door and costs zero credits. A key exfiltrated from a container and replayed from an attacker's network is inert if that network is not in the list.

Second, a model outside the allowlist is rejected before it reaches a provider. That bounds not only spend but capability: a key issued for a cheap summarisation model cannot be turned on an expensive frontier model, no matter what the caller puts in the request body. Full field reference: API Key Management.

Note what is not in that table: nowhere do you paste a provider credential. nRouter holds the provider keys; you hold an nRouter key and credits. That is a deliberate design decision with its own trade-offs, argued in Why we don't do BYOK, and its security consequence is that a leaked virtual key can never become a leaked OpenAI or Anthropic key — the credentials those providers tell you to guard (OpenAI, Anthropic) are not in your estate at all.

Step 4 — Cap it, so a leak is bounded before you notice it

Scoping bounds what a key can do. A budget and a rate limit bound how much, and they are what protect you during the hours between a leak and its discovery — which is the interval that actually matters, because you will not be watching.

Create the cap on the Budgets page (/[organization]/budgets) → Create Budget, attach it to the key, and set Duration to Daily for a service key. A per-key budget returns 429 with code key_budget_exceeded when it fires — note that this differs from organization, team and user budgets, which return 402 with budget_exceeded. Your retry logic must branch on the code field, not the status; the full playbook is Handling 429 and 402 errors.

Then add per-key RPM and TPM limits, because a dollar cap and a velocity cap stop different things. A monthly budget is a total and cannot prevent a stolen key from burning it in twenty minutes; a rate limit is a boundary the caller cannot raise, which is exactly the property you want against an attacker. The distinction gets its own post in Budgets vs rate limits, and the four independent gates a request clears are laid out in the four pre-flight gates.

The result is that the worst case for a leaked, capped, allowlisted key is a known number of dollars on a known list of models from a known network, all of it attributed to one key in the logs.

Step 5 — Rotate with two keys and an explicit hand-off

There is deliberately no in-place secret swap behind a single key id. That sounds like a missing feature and is a control: swapping the secret under one id would let a consumer you forgot about keep working after you believed you had cut it off. Two keys with two ids force the hand-off to be explicit.

  1. Create the replacement with the same scopes — the Duplicate action on the existing key clones them, so you do not re-derive the allowlists by hand.
  2. Roll it out to the environment variable or secret manager entry the service reads.
  3. Confirm the new key is actually being called. The Keys page shows a last-used timestamp and recent request count per key. Do not skip this — it is the step that distinguishes a rotation from an outage.
  4. Revoke the old key. Every create, rotate and revoke event is recorded with the actor, source IP and timestamp, and each key row links to its own activity; the trail is at /[organization]/audit and documented in Audit Log.

Set expirations so this is a routine you have practised rather than an emergency procedure you are improvising. A rotation you have never run is not a capability.

Verifying it worked

  1. Confirm the display rule. Reload the Keys page and check that the full value is gone and only the name and last four remain. If you can read a full key off any screen, something is wrong.
  2. Confirm the scope bites. Call a model that is not on the key's allowed list and confirm the rejection, then confirm on /[organization]/logs that no cost was recorded.
  3. Confirm the cap bites. Attach a $0.01 Total budget to a throwaway key and send two calls; the second returns 429 with key_budget_exceeded and a message naming the budget.
  4. Confirm attribution. Send one call and read the headers back — the response carries the request id and, when the call was priceable, the settled cost.
curl -sS -D - -o /dev/null https://api.nrouter.ai/v1/chat/completions \
  -H "Authorization: Bearer $NROUTER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model":"gpt-5.4-mini","messages":[{"role":"user","content":"ping"}]}' \
  | grep -i '^x-nr-'

x-nr-request-cost is absent when a call could not be priced. Absent means unknown, not zero — client code that defaults it to 0.0 will under-report spend in your own dashboards while the ledger stays correct. See What an LLM request log should contain.

  1. Confirm the row is attributed to the right key at /[organization]/logs. Per-key attribution is what makes step 4's incident response possible; if two services share a key, this is where you find out.

Handling a leak: the twenty-minute runbook

Because the key was scoped in advance, the response is a procedure rather than a judgement call.

  1. Revoke the leaked key first, from /[organization]/keys. Every other key keeps working. Do not begin with an investigation — revoke, then investigate.
  2. Read what it did. Filter /[organization]/logs by that key: every request, model, token count, status and settled cost is attributed to it. This is the answer to "what was accessed", and it exists only because the key was per-service.
  3. Reconcile against the ledger for the spend figure you will actually quote to somebody — How to read your LLM credit ledger.
  4. Issue the replacement with the same scopes via Duplicate, and deploy it.
  5. Record the timeline from /[organization]/audit, which already carries the actor, source IP and timestamp for the revoke and the create. If you are assembling evidence for an auditor, the criterion-by-criterion version is SOC 2 for an LLM gateway.

Nothing in that list is "rotate everything", and nothing in it requires a maintenance window.

What goes wrong

One key for everything. It works until it does not, and then the rotation is an outage. It also destroys per-service attribution permanently — you cannot retroactively split one key's history into five services.

A key in client-side code. A browser bundle, a mobile binary and a public notebook are all public, and a credential read out of one is the shortest route to the sensitive-information-disclosure entry in the OWASP Top 10 for LLM Applications. Model calls belong on your server; the browser talks to your backend, which holds the key. The one sanctioned exception is the Playground, where the key you paste is your own and is held in the browser session only.

"Never" expiry on a service credential. Never is the default and the wrong choice for anything machine-held. A key with no expiry is a key that outlives the service, the repository, and the person who created it.

The cost header read as zero. An absent x-nr-request-cost means the call could not be priced. Defaulting it to 0.0 under-reports your own spend silently, while the ledger — the authoritative store — stays correct. Why we report unknown rather than zero: Cost honesty.

Rotation without the last-used check. Revoking the old key before confirming the new one is live turns a hygiene task into an incident. The timestamp is on the Keys page precisely so you do not have to guess.

Try it

Keys are unlimited on every plan, and so are the controls around them — per-key budgets, RPM and TPM limits, model and endpoint allowlists, IP allowlists, expiry, rotation and the audit trail. Plans vary the platform fee (4% on Pay as you go, 0% on Pro at $50/mo or $500/yr) and the default rate limits, never the feature set; the reasoning is in Every Feature on Every Plan: We Charge a Fee, Not a Gate.

Load your first $5 and get a $10 bonus — $15 in API credits — then create one key per service rather than one key, and set a daily cap on each before you deploy anything. Start at /signup, or send a first call from the browser in the Playground.

Questions about scoping a key for a workload you are not sure how to split? Ask in the nRouter community.

See also

Sources

Verified 2026-06-11; the external references were re-checked on 2026-08-23. Every field name, dashboard path, role rule and status code above comes from nRouter's own documentation. If something has drifted, email hello@nrouter.ai and we will correct it.

Share
Written by nRouter teamEngineering, product, and company posts from the nRouter team — code-first, cost-honest, no vendor-marketing fluff.