Skip to main content

Virtual Keys vs API Keys

The word "key" shows up in two completely different places in the Ferro Labs AI Gateway, and conflating them is a common source of confusion. They are unrelated:

  • A virtual_key is a routing identifier inside your config. It names a provider and its credential set. It is not a secret and is never sent by clients.
  • An admin API key (fgw_...) is a client credential. Clients send it as a Bearer token to authenticate to the gateway.

This page keeps the two straight.

virtual_key โ€” the config target identifierโ€‹

Every entry in targets[] carries a virtual_key (Target.VirtualKey in config.go). It is the unique identifier the gateway uses to pick which provider, with which credentials, handles a request.

Resolution happens before routing and plugin execution: the gateway looks up g.providers[virtual_key] to find the provider instance that was built for that target. Each provider instance is constructed from a ProviderEntry (providers/providers_list.go) and loads its credentials either from environment variables (OSS self-hosted) or from an injected credential map (managed deployments). So the virtual_key is the name of a credential set โ€” the actual secret (for example OPENAI_API_KEY) lives in the environment, never in the virtual_key itself.

strategy:
mode: fallback

targets:
- virtual_key: openai # selects the OpenAI provider + OPENAI_API_KEY
retry:
attempts: 3
on_status_codes: [429, 502, 503]
- virtual_key: anthropic # selects the Anthropic provider + ANTHROPIC_API_KEY
circuit_breaker:
failure_threshold: 5
timeout: 30s

The same identifier is what routing strategies reference as target_key. For example, conditional, content-based, and A/B-test strategies all route to a target_key that must match a virtual_key declared in targets[]:

strategy:
mode: conditional
conditions:
- key: model
value: gpt-4o
target_key: openai # must match a targets[].virtual_key
- key: model
value: claude-3-5-sonnet-20241022
target_key: anthropic
note

A virtual_key is plaintext configuration. It carries no authentication weight and is safe to commit. The provider's real API key is resolved separately from the environment at request time.

A target can also declare models[] alongside its virtual_key โ€” model IDs the operator asserts that target serves, additive to whatever the model catalog and live discovery already report. It's the provider-agnostic way to route a model a provider doesn't otherwise advertise (a preview ID, a self-hosted deployment, a provider with no /models endpoint):

targets:
- virtual_key: gemini
models:
- gemini-2.5-flash # joins the routing index and /v1/models

See Configuration for the full targets[] schema.

Admin API keys (fgw_...) โ€” client credentialsโ€‹

Admin API keys are the credentials clients use to authenticate to the gateway. They are minted via POST /admin/keys and returned as a string with an fgw_ prefix. The admin control plane that issues and validates them is split across three packages under internal/admin/ โ€” model (the APIKey type and scope rules), repository (the key store), and handlers (the HTTP surface) โ€” there's no single keys.go file to point to.

A client then presents the key as a Bearer token on inference routes:

curl http://localhost:8080/v1/chat/completions \
-H "Authorization: Bearer fgw_8f3c...." \
-H "Content-Type: application/json" \
-d '{"model":"gpt-4o","messages":[{"role":"user","content":"Hello"}]}'

Each key is a record with a lifecycle:

  • Scopes โ€” admin or read_only. When none are supplied at creation, the key defaults to read_only โ€” least privilege, not admin.
  • Expiry โ€” an optional expires_at. A key past its expiry stops authenticating. PUT /admin/keys/{id} can extend it by sending a new expires_at, or remove it entirely with "clear_expiration": true.
  • Rotation โ€” POST /admin/keys/{id}/rotate swaps the key string in place (stamping rotated_at) while keeping the same ID, scopes, and usage history.
  • Revocation vs. deletion โ€” these are not the same operation. POST /admin/keys/{id}/revoke marks the key inactive so it immediately stops validating, but keeps the record (and its usage history) in GET /admin/keys and in request-log attribution. DELETE /admin/keys/{id} removes the record outright โ€” it's irreversible, and existing request-log rows keep the now-orphaned api_key_id but can no longer resolve it to a name. Prefer revoke when you want the trail; delete when you don't need it.
  • Usage โ€” every successful validation bumps usage_count and last_used_at.

A key can't delete, revoke, or de-scope itself, and the last remaining admin key can't be deleted, revoked, or stripped of the admin scope โ€” both are refused with 409 to prevent a self-inflicted lockout.

When listed, the stored key string is masked to its first 8 characters (fgw_...) so the full secret is never echoed back by the admin API.

Dashboard sessions are not API keysโ€‹

POST /admin/session exchanges an API key (or MASTER_KEY) for a short-lived dashboard session token, prefixed fgws_ โ€” deliberately distinct from an API key's fgw_ prefix so the two can never be confused. A session is a separate record from the APIKey it was minted from: it carries its own ID, inherits the source key's scopes at mint time, and expires on its own clock (24h absolute, or 1h after the last request โ€” whichever comes first). Signing out (DELETE /admin/session) or an admin revoking one (DELETE /admin/sessions/{id}, or all at once with DELETE /admin/sessions) deletes the session row rather than marking it, so it stops validating immediately. Revoking a session never touches the API key it came from.

Side-by-sideโ€‹

virtual_key (config)Admin API key (fgw_...)
Defined intargets[].virtual_key in configIssued at runtime via POST /admin/keys
PurposeNames a provider + credential setAuthenticates a client to the gateway
Is it a secret?No โ€” plaintext, safe to commitYes โ€” a Bearer credential
Who uses itThe gateway, internally, before routingClients, in the Authorization header
Resolved toA provider instance (g.providers[...])A validated APIKey record + scopes
Lifecycle opsEdit config and reloadCreate, rotate, revoke, expire, delete
Example valueopenai, anthropic, vertex-aifgw_8f3c...