litellm/terraform/provider/docs/resources/jwt_key_mapping.md
Louis Vauterin 3f7a344337 feat(jwt-key-mapping): accept token_id as an alternative to the plaintext key
A JWT key mapping can now name its virtual key by the SHA-256 hash the proxy
already stores, instead of only by the plaintext key.

litellm_key makes its generated key write-only so raw keys stay out of Terraform
state, and write-only attributes cannot be referenced at all, so the natural
wiring fails while planning, in every apply ordering:

  Error: Missing required argument
    with litellm_jwt_key_mapping.example
    key = litellm_key.example.key
    The argument "key" is required, but no definition was found.

The only way out today is supplying the plaintext from a variable or a secret
manager, which means the mapped key cannot be one the proxy generated and the
configuration has to carry a credential. The value the mapping stores is
hash_token(key), which is the same hash litellm_key already exports as
token_id, and a hash is not a credential, so accepting it closes the gap:

  resource "litellm_jwt_key_mapping" "service" {
    jwt_claim_name  = "client_id"
    jwt_claim_value = "reporting-service"
    token_id        = litellm_key.service.token_id
  }

CreateJWTKeyMappingRequest and UpdateJWTKeyMappingRequest gain an optional
token. Create requires exactly one of key or token, update accepts at most one,
and omitting both still leaves the mapped key alone. A supplied token must be 64
lowercase hex characters, because hash_token() hashes unconditionally and a
plaintext key sent as token would be stored as a hash of a hash, then silently
match nothing at auth time. Both rejections are 400s raised before the row is
written.

On the provider side, key becomes Optional with ExactlyOneOf{key, token_id} and
token_id is added next to it. token_id is not marked sensitive since a hash is
not a credential, both fields are omitempty on the wire so the proxy receives
only the one that was configured, and a failed update reverts token_id for the
same reason it already reverts key.

key keeps working unchanged and existing state is untouched. The only change to
it is Required to Optional, which no existing configuration can violate.
2026-09-15 23:15:20 +00:00

4.9 KiB

litellm_jwt_key_mapping

Maps a JWT claim value to a LiteLLM virtual key. Every JWT client identified by a claim, typically client_id, azp or sub, then gets the model restrictions, budgets, rate limits, guardrails and spend tracking of the virtual key it maps to, without that key ever being handed to the client.

The mappings only take effect once JWT auth is enabled on the proxy, which is configuration rather than API state:

general_settings:
  enable_jwt_auth: True
  litellm_jwtauth:
    virtual_key_claim_field: "client_id"
    unregistered_jwt_client_behavior: "fallback_team_mapping"

See JWT to virtual key mapping for the proxy side of the feature

Example Usage

The mapped virtual key has to exist already and its value has to be known to Terraform, so it comes from a variable or a secret manager rather than from a litellm_key resource. litellm_key deliberately made its generated key write-only, to avoid storing raw API keys in state, so referencing it here does not merely read back null: Terraform's write-only enforcement turns key = litellm_key.foo.key into a static Missing required argument error at terraform plan, before any API call, in every apply ordering, including a first apply where both resources are created together:

variable "alice_key" {
  type      = string
  sensitive = true
}

resource "litellm_jwt_key_mapping" "alice" {
  jwt_claim_name  = "client_id"
  jwt_claim_value = "dev-alice"
  key             = var.alice_key
}

Per-client limits live on the virtual key, so one mapping per client is how each JWT client gets its own budget and quota:

resource "litellm_jwt_key_mapping" "billing_service" {
  jwt_claim_name  = "client_id"
  jwt_claim_value = "billing-service"
  key             = var.billing_service_key
  description     = "Billing service JWT client"
  is_active       = true
}

Several clients at once, with the key values coming from a map of secrets:

variable "jwt_client_keys" {
  type      = map(string)
  sensitive = true
}

resource "litellm_jwt_key_mapping" "developer" {
  for_each = var.jwt_client_keys

  jwt_claim_name  = "client_id"
  jwt_claim_value = each.key
  key             = each.value
  description     = "Developer JWT client ${each.key}"
}

Argument Reference

  • jwt_claim_name - (Required, ForceNew) Name of the JWT claim to match on, for example client_id, azp or sub. Must match virtual_key_claim_field in the proxy JWT config
  • jwt_claim_value - (Required, ForceNew) Value of the claim identifying the JWT client. Unique together with jwt_claim_name, so a second mapping for the same pair fails with a 409
  • key - (Optional, Sensitive) The virtual key this claim value maps to, as plaintext. It has to exist already, otherwise the proxy rejects the mapping with The provided key does not match an existing virtual key. Exactly one of key or token_id is required. litellm_key marks its generated key write-only, so this cannot reference a litellm_key resource -- use token_id for that, or supply the plaintext from a variable or a secret manager
  • token_id - (Optional) The SHA-256 hash of the virtual key this claim value maps to, which is what the proxy stores. litellm_key exposes it as token_id, so unlike key it can be referenced directly from a litellm_key resource. Not a secret, so it is not marked sensitive. Exactly one of key or token_id is required
  • description - (Optional) Description of the mapping
  • is_active - (Optional) Whether the mapping is active. Inactive mappings are ignored during JWT auth. Defaults to true

Attribute Reference

  • id - The mapping ID assigned by LiteLLM
  • created_at - Timestamp when the mapping was created
  • updated_at - Timestamp when the mapping was last updated
  • created_by - User who created the mapping
  • updated_by - User who last updated the mapping

Notes

The proxy stores only a hash of key and never returns it, so drift on that attribute cannot be detected and Terraform tracks the value from your configuration. Changing key rotates the mapping onto the new virtual key in place, with no replacement. Like the other secrets this provider accepts, such as credential_values and model_api_key, the configured value is kept in state, so treat the state as sensitive

Only proxy admins can create, update or delete mappings, so the provider api_key has to be a master key or an admin key

Import

Mappings are imported by their mapping ID:

terraform import litellm_jwt_key_mapping.alice 297a5536-1aeb-4cf1-b666-b3809c2750a8

Because the API does not return the mapped key, key is empty in state right after an import, so the first plan shows an in-place update that pushes the configured key back to the proxy. That update is harmless, the proxy just rehashes the same value when the key has not actually changed