Files
vault-plugin-secrets-litellm/backend.go
unkinben ab3b02a48e Add LiteLLM dynamic secrets engine implementation
Populate the repo with the Vault/OpenBao dynamic secrets engine that mints
LiteLLM virtual keys scoped by model, spending limit, and lease TTL.

- Secrets backend: config, roles, creds paths and a revocable litellm_key type
- LiteLLM API client (generate/update/delete/info) with master-key auth
- Unit tests (mock LiteLLM) and a docker-compose e2e against both Vault and
  OpenBao proving the same binary works on each
- Makefile, woodpecker CI (build/test/pre-commit), pre-commit config
2026-07-02 23:22:18 +10:00

119 lines
2.7 KiB
Go

package litellm
import (
"context"
"strings"
"sync"
"github.com/hashicorp/vault/sdk/framework"
"github.com/hashicorp/vault/sdk/logical"
)
// litellmBackend is the Vault secrets backend that manages LiteLLM virtual keys.
type litellmBackend struct {
*framework.Backend
lock sync.RWMutex
client *litellmClient
}
// Factory returns a configured LiteLLM secrets backend.
func Factory(ctx context.Context, conf *logical.BackendConfig) (logical.Backend, error) {
b := backend()
if err := b.Setup(ctx, conf); err != nil {
return nil, err
}
return b, nil
}
func backend() *litellmBackend {
b := &litellmBackend{}
b.Backend = &framework.Backend{
Help: strings.TrimSpace(backendHelp),
BackendType: logical.TypeLogical,
PathsSpecial: &logical.Paths{
LocalStorage: []string{},
SealWrapStorage: []string{
configStoragePath,
},
},
Paths: framework.PathAppend(
[]*framework.Path{
pathConfig(b),
pathRole(b),
pathRolesList(b),
pathCredentials(b),
},
),
Secrets: []*framework.Secret{
b.litellmKey(),
},
Invalidate: b.invalidate,
WALRollback: nil,
}
return b
}
// reset drops the cached LiteLLM client so it is rebuilt from storage on the
// next request. Called when the config changes.
func (b *litellmBackend) reset() {
b.lock.Lock()
defer b.lock.Unlock()
b.client = nil
}
// invalidate clears the cached client when the config is written from another
// cluster node.
func (b *litellmBackend) invalidate(_ context.Context, key string) {
if key == configStoragePath {
b.reset()
}
}
// getClient returns a cached LiteLLM client, building one from stored config if
// necessary.
func (b *litellmBackend) getClient(ctx context.Context, s logical.Storage) (*litellmClient, error) {
b.lock.RLock()
if b.client != nil {
defer b.lock.RUnlock()
return b.client, nil
}
b.lock.RUnlock()
b.lock.Lock()
defer b.lock.Unlock()
// Re-check after acquiring the write lock.
if b.client != nil {
return b.client, nil
}
config, err := getConfig(ctx, s)
if err != nil {
return nil, err
}
if config == nil {
return nil, errBackendNotConfigured
}
client, err := newClient(config)
if err != nil {
return nil, err
}
b.client = client
return b.client, nil
}
const backendHelp = `
The LiteLLM secrets backend dynamically generates LiteLLM virtual API keys
scoped to specific models, with a spending limit and a lease-bound TTL.
After mounting this backend, configure it with the connection details for the
LiteLLM proxy using the "config" path, then define one or more roles that
constrain the generated keys. Reading from the "creds/<role>" path issues a new
virtual key whose lifetime is managed by Vault: revoking the lease revokes the
key in LiteLLM.
`