Skip to content

Add a user to the gateway

By the end of this guide a teammate, application or agent has its own virtual key for the gateway, capped in spend and in rate, restricted to the models it needs, and revocable without touching anything else.

For what a virtual key is and why one shared key costs you both attribution and control, see the overview. For the same thing in the console, see the console walkthrough.

Prerequisites

Decide the scope before you create anything

A key answers four questions. Settle them before you create it. A key can be changed afterwards, but one that has already been handed out with no limits is hard to withdraw.

Question Setting
Who holds it? one holder, named in name and description
How much may it spend? budget_id
How fast? rate_limit_id
Which models may it reach? provider_configs.allowed_models

1. Define the limits

Budgets and rate limits are named objects that keys point at, so define each once and reuse it. A budget caps total spend over a window; a rate limit caps how fast that window can be consumed.

values.yaml (excerpt)
bifrost:
  governance:
    budgets:
    - id: standard-budget
      max_limit: 10 # (1)!
      reset_duration: 1M # (2)!
    - id: premium-budget
      max_limit: 100
      reset_duration: 1M
    rateLimits:
    - id: standard-rate # (3)!
      request_max_limit: 100
      request_reset_duration: 1h
      token_max_limit: 100000 # (4)!
      token_reset_duration: 1d
    - id: premium-rate
      request_max_limit: 1000
      request_reset_duration: 1h
      token_max_limit: 1000000
      token_reset_duration: 1d
  1. Tune this. The spend ceiling for one period, in the gateway's billing units. This is the total damage a key can do before it stops working.

  2. Tune this. How often the budget resets - 1M is monthly, 1d daily. Shorter periods bound the damage more tightly and generate more interruptions.

  3. Leave as is. The id keys refer to. Writing a limit once and attaching it to many keys is the point of the split - changing the limit then changes every key at once.

  4. Tune this. Requests and tokens are capped separately, over different windows, and both matter: a hundred requests an hour sounds modest until each one carries a 100k-token context.

A budget alone does not stop a runaway loop

A retry loop can spend a month's budget in minutes. The budget bounds the damage; the rate limit bounds how fast it arrives, and is what gives you time to notice.

2. Generate the key value

Generate it. Do not compose one by hand, and do not encode anything meaningful in it:

echo "sk-$(openssl rand -hex 24)"

3. Create the virtual key

values.yaml (excerpt)
bifrost:
  governance:
    virtualKeys:
    - id: vk-<holder> # (1)!
      name: <Human readable name>
      description: <who holds this and what it is for>
      is_active: true # (2)!
      budget_id: standard-budget # (3)!
      rate_limit_id: standard-rate
      value: <generated-key> # (4)!
  1. Edit this. The key's stable identifier, and what you will use to revoke or deactivate it later. Name it after the holder, not the person who created it.

  2. Leave as is for now. Setting it to false disables the key without deleting it, which is the reversible way to cut someone off - see Change.

  3. Edit this. Which budget and rate limit apply, by id from step 1. Omit them and the key is unlimited, which is rarely what you want.

  4. Edit this, and keep it out of Git. The credential itself. Read it from your secret store when you render the values file - a key committed to a repository is a key you have to rotate.

That key can reach every model the gateway offers. To restrict it - the usual case - add a model scope:

values.yaml (excerpt)
    - id: vk-<holder> # (1)!
      name: <Human readable name>
      description: <who holds this and what it is for> # (2)!
      is_active: true # (3)!
      budget_id: standard-budget # (4)!
      rate_limit_id: standard-rate
      value: <generated-key> # (5)!
      provider_configs:
      - provider: <provider>
        weight: 1
        allowed_models: # (6)!
        - <model-name>
  1. Edit this. A stable identifier for the key. You will use it to find, deactivate and delete this key later, so make it say who holds it.

  2. Edit this. Write it for whoever reads this file in six months trying to work out whether the key is still needed. "Ana's laptop, OpenCode" beats "dev key".

  3. Leave as is. Setting this to false later disables the key without deleting it - reversible, and it keeps the spend history. Reach for that before deletion.

  4. Edit this. Must match an id from the budgets you defined. Note that budgets are shared: two keys pointing at standard-budget consume the same ceiling.

  5. Edit this. The generated value from step 2. This is the credential the holder uses - treat the file it lands in accordingly.

  6. Edit this. The gateway's public model names this key may reach, not the provider's model ids. Omit the whole provider_configs block to allow everything - but a key that only needs a small model should not be able to reach an expensive one.

allowed_models is checked before routing, so a request for anything else is refused at the gateway rather than billed.

Key values are credentials, wherever they live

A key written into a values file is a credential in that file. Keep gateway configuration out of public repositories, and prefer a secret store where one is available - see Security & identity.

4. Apply

Apply the configuration the way the gateway is deployed - through your GitOps pipeline, or directly while developing:

oc apply -k <gateway-config-path> --enable-helm
oc rollout status deploy/<gateway> -n <gateway-project>

5. Hand it over

The recipient needs exactly two things: the gateway base URL, and their key. Send the key the way you would send a password, and never in the same message as the URL if that channel is shared.

If the recipient is a CLI coding agent rather than a person, see Connect a coding agent.

Verify

Test the key from the recipient's position, not yours.

It is accepted:

curl -sS <base-url>/models \
  -H "Authorization: Bearer <the-key>"

It can reach what it should:

curl -sS <base-url>/chat/completions \
  -H "Authorization: Bearer <the-key>" \
  -H "Content-Type: application/json" \
  -d '{"model":"<allowed-model>","messages":[{"role":"user","content":"ping"}]}'

It cannot reach what it should not - the test people skip, and the only one that proves the scope works. Ask for a model outside allowed_models and confirm it is refused:

curl -sS <base-url>/chat/completions \
  -H "Authorization: Bearer <the-key>" \
  -H "Content-Type: application/json" \
  -d '{"model":"<a-model-not-allowed>","messages":[{"role":"user","content":"ping"}]}'

The request was attributed - check the gateway logs show the call against this key with token counts:

oc logs deploy/<gateway> -n <gateway-project> --tail=50

A 200 response does not prove the virtual key was checked

Some gateway deployments disable authentication on inference during a proof of concept. If that is set, an inference call succeeds whether or not the key is valid. Confirm authentication is enforced before concluding anything from a 200.

List

oc get configmap,secret -n <gateway-project>

The keys themselves come from the gateway's configuration - read that rather than the cluster, since it is the source of truth for what exists.

Change

Adjust a limit by pointing the key at a different budget or rate limit, or by changing the numbers on the one it uses. Remember that changing a shared budget changes it for every key referencing it.

Deactivate without deleting - reversible, and it keeps the spend history:

values.yaml (excerpt)
    - id: vk-<holder>
      is_active: false

Rotate a key. Never edit a value in place; something is using it. Overlap instead:

  1. Add a second entry with a new id and a fresh value.
  2. Update the holder's configuration.
  3. Confirm traffic arrives on the new key.
  4. Set the old key is_active: false and wait.
  5. Remove the old entry once nothing has broken.

Delete

Find the holder first. A key with no obvious owner is usually configured somewhere rather than genuinely unused:

grep -rn "<key-id>" .

Then remove its entry from virtualKeys and apply. Deactivating first, waiting, and deleting afterwards turns a possible outage into a reversible one.

Troubleshoot

401 or 403 on every request. The key is not active, the value does not match, or the configuration did not reload. Confirm the rollout completed and the gateway restarted.

The model is refused but you expected it to work. allowed_models matches the gateway's public model name, not the provider's model id. Check the two are the same string - see Add a model provider.

Requests refused with a budget or rate error and the numbers look wrong. More than one key can share a budget. Check whether the ceiling is being consumed by another holder.

The key works but nothing appears in the logs. Request logging is not always durable - some deployments run the gateway without persistent storage, in which case logs do not survive a restart. Confirm before relying on them for attribution.

Next

Connect a coding agent → - hand this key to a CLI agent, or Add a model provider to widen what it can reach.