feat(auth): wire JWT validation into /v1/* LLM endpoints
This commit is contained in:
@@ -22,6 +22,8 @@ type Config struct {
|
|||||||
Models map[string]*ModelUpstream
|
Models map[string]*ModelUpstream
|
||||||
// Adapters holds service adapter definitions for X-Service routing.
|
// Adapters holds service adapter definitions for X-Service routing.
|
||||||
Adapters []*serviceadapter.ServiceAdapter
|
Adapters []*serviceadapter.ServiceAdapter
|
||||||
|
// Auth holds JWT authentication configuration for /v1/* endpoints.
|
||||||
|
Auth AuthConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
// ModelUpstream holds upstream configuration for a specific model.
|
// ModelUpstream holds upstream configuration for a specific model.
|
||||||
@@ -32,6 +34,22 @@ type ModelUpstream struct {
|
|||||||
Address string
|
Address string
|
||||||
// Path is the upstream path for this model (e.g., "/v1/chat/completions").
|
// Path is the upstream path for this model (e.g., "/v1/chat/completions").
|
||||||
Path string
|
Path string
|
||||||
|
// AuthRequired indicates whether this model requires JWT authentication.
|
||||||
|
AuthRequired bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthConfig holds JWT authentication configuration.
|
||||||
|
type AuthConfig struct {
|
||||||
|
// Enabled globally enables/disables auth for /v1/* endpoints.
|
||||||
|
Enabled bool
|
||||||
|
// Issuer is the expected JWT issuer (iss claim).
|
||||||
|
Issuer string
|
||||||
|
// Audience is the expected JWT audience (aud claim).
|
||||||
|
Audience string
|
||||||
|
// JWKSURL is the URL to fetch JSON Web Key Set for signature validation.
|
||||||
|
JWKSURL string
|
||||||
|
// RequiredCapability is the permission required for LLM inference (e.g., "llm:inference").
|
||||||
|
RequiredCapability string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Route represents a single route and its upstream configuration.
|
// Route represents a single route and its upstream configuration.
|
||||||
@@ -98,18 +116,20 @@ func Load() (*Config, error) {
|
|||||||
shutdownTimeout = d
|
shutdownTimeout = d
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load routes, models, and adapters from config file
|
// Load routes, models, adapters, and auth from config file
|
||||||
routes := make(map[string]*Route)
|
routes := make(map[string]*Route)
|
||||||
models := make(map[string]*ModelUpstream)
|
models := make(map[string]*ModelUpstream)
|
||||||
var adapters []*serviceadapter.ServiceAdapter
|
var adapters []*serviceadapter.ServiceAdapter
|
||||||
|
var authConfig AuthConfig
|
||||||
if configPath, ok := os.LookupEnv("CONFIG_PATH"); ok {
|
if configPath, ok := os.LookupEnv("CONFIG_PATH"); ok {
|
||||||
loadedRoutes, loadedModels, loadedAdapters, err := LoadRoutesAndModelsFromFile(configPath)
|
loadedRoutes, loadedModels, loadedAdapters, loadedAuth, err := LoadRoutesAndModelsFromFile(configPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
routes = loadedRoutes
|
routes = loadedRoutes
|
||||||
models = loadedModels
|
models = loadedModels
|
||||||
adapters = loadedAdapters
|
adapters = loadedAdapters
|
||||||
|
authConfig = loadedAuth
|
||||||
}
|
}
|
||||||
|
|
||||||
return &Config{
|
return &Config{
|
||||||
@@ -118,5 +138,6 @@ func Load() (*Config, error) {
|
|||||||
Routes: routes,
|
Routes: routes,
|
||||||
Models: models,
|
Models: models,
|
||||||
Adapters: adapters,
|
Adapters: adapters,
|
||||||
|
Auth: authConfig,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
+46
-20
@@ -15,6 +15,16 @@ type rawConfig struct {
|
|||||||
Routes []rawRoute `yaml:"routes"`
|
Routes []rawRoute `yaml:"routes"`
|
||||||
Models []rawModel `yaml:"models"`
|
Models []rawModel `yaml:"models"`
|
||||||
Adapters []rawAdapter `yaml:"adapters"`
|
Adapters []rawAdapter `yaml:"adapters"`
|
||||||
|
Auth rawAuth `yaml:"auth"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// rawAuth represents auth configuration in YAML.
|
||||||
|
type rawAuth struct {
|
||||||
|
Enabled bool `yaml:"enabled"`
|
||||||
|
Issuer string `yaml:"issuer"`
|
||||||
|
Audience string `yaml:"audience"`
|
||||||
|
JWKSURL string `yaml:"jwksUrl"`
|
||||||
|
RequiredCapability string `yaml:"requiredCapability"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// rawRoute represents a single route in the YAML configuration.
|
// rawRoute represents a single route in the YAML configuration.
|
||||||
@@ -25,9 +35,10 @@ type rawRoute struct {
|
|||||||
|
|
||||||
// rawModel represents a single model entry in the YAML configuration.
|
// rawModel represents a single model entry in the YAML configuration.
|
||||||
type rawModel struct {
|
type rawModel struct {
|
||||||
Name string `yaml:"name"`
|
Name string `yaml:"name"`
|
||||||
Address string `yaml:"address"`
|
Address string `yaml:"address"`
|
||||||
Path string `yaml:"path"`
|
Path string `yaml:"path"`
|
||||||
|
AuthRequired *bool `yaml:"authRequired"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// rawAdapter represents a service adapter in the YAML configuration.
|
// rawAdapter represents a service adapter in the YAML configuration.
|
||||||
@@ -64,32 +75,32 @@ type rawUpstream struct {
|
|||||||
AuthRequired *bool `yaml:"authRequired"`
|
AuthRequired *bool `yaml:"authRequired"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadRoutesAndModelsFromFile loads route, model, and adapter configuration from a YAML file.
|
// LoadRoutesAndModelsFromFile loads route, model, adapter, and auth configuration from a YAML file.
|
||||||
func LoadRoutesAndModelsFromFile(path string) (map[string]*Route, map[string]*ModelUpstream, []*serviceadapter.ServiceAdapter, error) {
|
func LoadRoutesAndModelsFromFile(path string) (map[string]*Route, map[string]*ModelUpstream, []*serviceadapter.ServiceAdapter, AuthConfig, error) {
|
||||||
data, err := os.ReadFile(path)
|
data, err := os.ReadFile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, nil, fmt.Errorf("failed to read config file %q: %w", path, err)
|
return nil, nil, nil, AuthConfig{}, fmt.Errorf("failed to read config file %q: %w", path, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var raw rawConfig
|
var raw rawConfig
|
||||||
if err := yaml.Unmarshal(data, &raw); err != nil {
|
if err := yaml.Unmarshal(data, &raw); err != nil {
|
||||||
return nil, nil, nil, fmt.Errorf("failed to parse config file %q: %w", path, err)
|
return nil, nil, nil, AuthConfig{}, fmt.Errorf("failed to parse config file %q: %w", path, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load routes
|
// Load routes
|
||||||
routes := make(map[string]*Route)
|
routes := make(map[string]*Route)
|
||||||
for _, rawRoute := range raw.Routes {
|
for _, rawRoute := range raw.Routes {
|
||||||
if rawRoute.Name == "" {
|
if rawRoute.Name == "" {
|
||||||
return nil, nil, nil, fmt.Errorf("route has empty name")
|
return nil, nil, nil, AuthConfig{}, fmt.Errorf("route has empty name")
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, exists := routes[rawRoute.Name]; exists {
|
if _, exists := routes[rawRoute.Name]; exists {
|
||||||
return nil, nil, nil, fmt.Errorf("duplicate route: \"%s\"", rawRoute.Name)
|
return nil, nil, nil, AuthConfig{}, fmt.Errorf("duplicate route: \"%s\"", rawRoute.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
upstream, err := parseUpstream(rawRoute.Name, rawRoute.Upstream)
|
upstream, err := parseUpstream(rawRoute.Name, rawRoute.Upstream)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, nil, err
|
return nil, nil, nil, AuthConfig{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
routes[rawRoute.Name] = &Route{
|
routes[rawRoute.Name] = &Route{
|
||||||
@@ -102,21 +113,27 @@ func LoadRoutesAndModelsFromFile(path string) (map[string]*Route, map[string]*Mo
|
|||||||
models := make(map[string]*ModelUpstream)
|
models := make(map[string]*ModelUpstream)
|
||||||
for _, rawModel := range raw.Models {
|
for _, rawModel := range raw.Models {
|
||||||
if rawModel.Name == "" {
|
if rawModel.Name == "" {
|
||||||
return nil, nil, nil, fmt.Errorf("model has empty name")
|
return nil, nil, nil, AuthConfig{}, fmt.Errorf("model has empty name")
|
||||||
}
|
}
|
||||||
if _, exists := models[rawModel.Name]; exists {
|
if _, exists := models[rawModel.Name]; exists {
|
||||||
return nil, nil, nil, fmt.Errorf("duplicate model: \"%s\"", rawModel.Name)
|
return nil, nil, nil, AuthConfig{}, fmt.Errorf("duplicate model: \"%s\"", rawModel.Name)
|
||||||
}
|
}
|
||||||
if rawModel.Address == "" {
|
if rawModel.Address == "" {
|
||||||
return nil, nil, nil, fmt.Errorf("model \"%s\": field 'address' is required", rawModel.Name)
|
return nil, nil, nil, AuthConfig{}, fmt.Errorf("model \"%s\": field 'address' is required", rawModel.Name)
|
||||||
}
|
}
|
||||||
if _, _, err := net.SplitHostPort(rawModel.Address); err != nil {
|
if _, _, err := net.SplitHostPort(rawModel.Address); err != nil {
|
||||||
return nil, nil, nil, fmt.Errorf("model \"%s\": invalid address \"%s\": %w", rawModel.Name, rawModel.Address, err)
|
return nil, nil, nil, AuthConfig{}, fmt.Errorf("model \"%s\": invalid address \"%s\": %w", rawModel.Name, rawModel.Address, err)
|
||||||
|
}
|
||||||
|
// Default authRequired to global auth.enabled if not specified per-model
|
||||||
|
authRequired := false
|
||||||
|
if rawModel.AuthRequired != nil {
|
||||||
|
authRequired = *rawModel.AuthRequired
|
||||||
}
|
}
|
||||||
models[rawModel.Name] = &ModelUpstream{
|
models[rawModel.Name] = &ModelUpstream{
|
||||||
Name: rawModel.Name,
|
Name: rawModel.Name,
|
||||||
Address: rawModel.Address,
|
Address: rawModel.Address,
|
||||||
Path: rawModel.Path,
|
Path: rawModel.Path,
|
||||||
|
AuthRequired: authRequired,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,7 +141,7 @@ func LoadRoutesAndModelsFromFile(path string) (map[string]*Route, map[string]*Mo
|
|||||||
adapters := make([]*serviceadapter.ServiceAdapter, 0, len(raw.Adapters))
|
adapters := make([]*serviceadapter.ServiceAdapter, 0, len(raw.Adapters))
|
||||||
for _, ra := range raw.Adapters {
|
for _, ra := range raw.Adapters {
|
||||||
if ra.ServiceName == "" {
|
if ra.ServiceName == "" {
|
||||||
return nil, nil, nil, fmt.Errorf("adapter has empty serviceName")
|
return nil, nil, nil, AuthConfig{}, fmt.Errorf("adapter has empty serviceName")
|
||||||
}
|
}
|
||||||
a := &serviceadapter.ServiceAdapter{
|
a := &serviceadapter.ServiceAdapter{
|
||||||
Name: ra.ServiceName,
|
Name: ra.ServiceName,
|
||||||
@@ -152,13 +169,22 @@ func LoadRoutesAndModelsFromFile(path string) (map[string]*Route, map[string]*Mo
|
|||||||
adapters = append(adapters, a)
|
adapters = append(adapters, a)
|
||||||
}
|
}
|
||||||
|
|
||||||
return routes, models, adapters, nil
|
// Parse auth config
|
||||||
|
authConfig := AuthConfig{
|
||||||
|
Enabled: raw.Auth.Enabled,
|
||||||
|
Issuer: raw.Auth.Issuer,
|
||||||
|
Audience: raw.Auth.Audience,
|
||||||
|
JWKSURL: raw.Auth.JWKSURL,
|
||||||
|
RequiredCapability: raw.Auth.RequiredCapability,
|
||||||
|
}
|
||||||
|
|
||||||
|
return routes, models, adapters, authConfig, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadRoutesFromFile loads route configuration from a YAML file.
|
// LoadRoutesFromFile loads route configuration from a YAML file.
|
||||||
// Deprecated: Use LoadRoutesAndModelsFromFile instead.
|
// Deprecated: Use LoadRoutesAndModelsFromFile instead.
|
||||||
func LoadRoutesFromFile(path string) (map[string]*Route, error) {
|
func LoadRoutesFromFile(path string) (map[string]*Route, error) {
|
||||||
routes, _, _, err := LoadRoutesAndModelsFromFile(path)
|
routes, _, _, _, err := LoadRoutesAndModelsFromFile(path)
|
||||||
return routes, err
|
return routes, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ models:
|
|||||||
tmpFile.WriteString(data)
|
tmpFile.WriteString(data)
|
||||||
tmpFile.Close()
|
tmpFile.Close()
|
||||||
|
|
||||||
_, models, _, err := LoadRoutesAndModelsFromFile(tmpFile.Name())
|
_, models, _, _, err := LoadRoutesAndModelsFromFile(tmpFile.Name())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to load config: %v", err)
|
t.Fatalf("failed to load config: %v", err)
|
||||||
}
|
}
|
||||||
@@ -94,7 +94,7 @@ models:
|
|||||||
tmpFile.WriteString(data)
|
tmpFile.WriteString(data)
|
||||||
tmpFile.Close()
|
tmpFile.Close()
|
||||||
|
|
||||||
_, _, _, err := LoadRoutesAndModelsFromFile(tmpFile.Name())
|
_, _, _, _, err := LoadRoutesAndModelsFromFile(tmpFile.Name())
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Errorf("expected error for duplicate model name, got nil")
|
t.Errorf("expected error for duplicate model name, got nil")
|
||||||
}
|
}
|
||||||
@@ -127,7 +127,7 @@ models:
|
|||||||
tmpFile.WriteString(data)
|
tmpFile.WriteString(data)
|
||||||
tmpFile.Close()
|
tmpFile.Close()
|
||||||
|
|
||||||
_, _, _, err := LoadRoutesAndModelsFromFile(tmpFile.Name())
|
_, _, _, _, err := LoadRoutesAndModelsFromFile(tmpFile.Name())
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Errorf("expected error for empty model name, got nil")
|
t.Errorf("expected error for empty model name, got nil")
|
||||||
}
|
}
|
||||||
@@ -157,7 +157,7 @@ models:
|
|||||||
tmpFile.WriteString(data)
|
tmpFile.WriteString(data)
|
||||||
tmpFile.Close()
|
tmpFile.Close()
|
||||||
|
|
||||||
_, _, _, err := LoadRoutesAndModelsFromFile(tmpFile.Name())
|
_, _, _, _, err := LoadRoutesAndModelsFromFile(tmpFile.Name())
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Errorf("expected error for missing address, got nil")
|
t.Errorf("expected error for missing address, got nil")
|
||||||
}
|
}
|
||||||
@@ -190,7 +190,7 @@ models:
|
|||||||
tmpFile.WriteString(data)
|
tmpFile.WriteString(data)
|
||||||
tmpFile.Close()
|
tmpFile.Close()
|
||||||
|
|
||||||
_, _, _, err := LoadRoutesAndModelsFromFile(tmpFile.Name())
|
_, _, _, _, err := LoadRoutesAndModelsFromFile(tmpFile.Name())
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Errorf("expected error for invalid address, got nil")
|
t.Errorf("expected error for invalid address, got nil")
|
||||||
}
|
}
|
||||||
@@ -219,7 +219,7 @@ models:
|
|||||||
tmpFile.WriteString(data)
|
tmpFile.WriteString(data)
|
||||||
tmpFile.Close()
|
tmpFile.Close()
|
||||||
|
|
||||||
_, models, _, err := LoadRoutesAndModelsFromFile(tmpFile.Name())
|
_, models, _, _, err := LoadRoutesAndModelsFromFile(tmpFile.Name())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to load config: %v", err)
|
t.Fatalf("failed to load config: %v", err)
|
||||||
}
|
}
|
||||||
@@ -261,7 +261,7 @@ models:
|
|||||||
tmpFile.WriteString(data)
|
tmpFile.WriteString(data)
|
||||||
tmpFile.Close()
|
tmpFile.Close()
|
||||||
|
|
||||||
routes, models, _, err := LoadRoutesAndModelsFromFile(tmpFile.Name())
|
routes, models, _, _, err := LoadRoutesAndModelsFromFile(tmpFile.Name())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to load config: %v", err)
|
t.Fatalf("failed to load config: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"forgejo.riotpiao.com/rock/homelab-frontend/internal/auth"
|
||||||
"forgejo.riotpiao.com/rock/homelab-frontend/internal/config"
|
"forgejo.riotpiao.com/rock/homelab-frontend/internal/config"
|
||||||
"forgejo.riotpiao.com/rock/homelab-frontend/internal/logging"
|
"forgejo.riotpiao.com/rock/homelab-frontend/internal/logging"
|
||||||
"forgejo.riotpiao.com/rock/homelab-frontend/internal/tracing"
|
"forgejo.riotpiao.com/rock/homelab-frontend/internal/tracing"
|
||||||
@@ -25,6 +26,8 @@ type Handler struct {
|
|||||||
transports map[string]*http.Transport
|
transports map[string]*http.Transport
|
||||||
// config holds the gateway configuration (for model registry, etc.)
|
// config holds the gateway configuration (for model registry, etc.)
|
||||||
config *config.Config
|
config *config.Config
|
||||||
|
// jwtValidator validates JWT tokens for authenticated endpoints
|
||||||
|
jwtValidator *auth.Validator
|
||||||
// Default timeouts for synthesized routes (model-based dispatch)
|
// Default timeouts for synthesized routes (model-based dispatch)
|
||||||
defaultConnectTimeout time.Duration
|
defaultConnectTimeout time.Duration
|
||||||
defaultReadTimeout time.Duration
|
defaultReadTimeout time.Duration
|
||||||
@@ -73,6 +76,15 @@ func New(cfg *config.Config) *Handler {
|
|||||||
defaultMaxBodySize: 100 * 1024 * 1024,
|
defaultMaxBodySize: 100 * 1024 * 1024,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize JWT validator if auth is enabled
|
||||||
|
if cfg.Auth.Enabled && cfg.Auth.JWKSURL != "" {
|
||||||
|
h.jwtValidator = auth.NewValidator(
|
||||||
|
cfg.Auth.Issuer,
|
||||||
|
cfg.Auth.Audience,
|
||||||
|
cfg.Auth.JWKSURL,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
for name, route := range cfg.Routes {
|
for name, route := range cfg.Routes {
|
||||||
// Create a transport per unique upstream address for connection reuse
|
// Create a transport per unique upstream address for connection reuse
|
||||||
transport := h.getOrCreateTransport(route.Upstream.Address, &route.Upstream)
|
transport := h.getOrCreateTransport(route.Upstream.Address, &route.Upstream)
|
||||||
@@ -291,6 +303,51 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// JWT Authentication for /v1/* endpoints
|
||||||
|
if h.jwtValidator != nil && strings.HasPrefix(r.URL.Path, "/v1/") {
|
||||||
|
authHeader := r.Header.Get("Authorization")
|
||||||
|
if authHeader == "" {
|
||||||
|
writeProblemDetail(w, http.StatusUnauthorized,
|
||||||
|
"https://api.example.com/problems/unauthorized",
|
||||||
|
"Unauthorized",
|
||||||
|
"Authorization header required",
|
||||||
|
nil)
|
||||||
|
logging.Errorf("auth failed", fmt.Errorf("missing auth header"), map[string]string{
|
||||||
|
"path": r.URL.Path,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
claims, err := h.jwtValidator.ValidateBearerToken(authHeader)
|
||||||
|
if err != nil {
|
||||||
|
writeProblemDetail(w, http.StatusForbidden,
|
||||||
|
"https://api.example.com/problems/forbidden",
|
||||||
|
"Forbidden",
|
||||||
|
fmt.Sprintf("JWT validation failed: %v", err),
|
||||||
|
nil)
|
||||||
|
logging.Errorf("auth failed", err, map[string]string{
|
||||||
|
"path": r.URL.Path,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check required capability if configured
|
||||||
|
if h.config.Auth.RequiredCapability != "" {
|
||||||
|
if !h.jwtValidator.CheckPermissions(claims, h.config.Auth.RequiredCapability, "*") {
|
||||||
|
writeProblemDetail(w, http.StatusForbidden,
|
||||||
|
"https://api.example.com/problems/insufficient-permissions",
|
||||||
|
"Insufficient Permissions",
|
||||||
|
fmt.Sprintf("Required capability: %s", h.config.Auth.RequiredCapability),
|
||||||
|
nil)
|
||||||
|
logging.Errorf("auth failed", fmt.Errorf("insufficient permissions"), map[string]string{
|
||||||
|
"path": r.URL.Path,
|
||||||
|
"required": h.config.Auth.RequiredCapability,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Note: Body size checking already happened in RouteRequest (body was read for model dispatch).
|
// Note: Body size checking already happened in RouteRequest (body was read for model dispatch).
|
||||||
// For other paths, we still need to enforce the cap.
|
// For other paths, we still need to enforce the cap.
|
||||||
// For /v1/chat/completions, the body was already read and validated.
|
// For /v1/chat/completions, the body was already read and validated.
|
||||||
|
|||||||
@@ -10,6 +10,14 @@ data:
|
|||||||
# Gateway configuration - loaded at startup, never compiled in
|
# Gateway configuration - loaded at startup, never compiled in
|
||||||
# See REQUIREMENTS.md for full specification
|
# See REQUIREMENTS.md for full specification
|
||||||
|
|
||||||
|
# JWT Authentication for /v1/* endpoints (LLM API)
|
||||||
|
auth:
|
||||||
|
enabled: true
|
||||||
|
issuer: "https://authentik.riotpiao.com/application/o/local-llm/"
|
||||||
|
audience: "local-llm"
|
||||||
|
jwksUrl: "https://authentik.riotpiao.com/application/o/local-llm/jwks/"
|
||||||
|
requiredCapability: "llm:inference"
|
||||||
|
|
||||||
# Routes: standard HTTP proxy routes (not LLM-specific)
|
# Routes: standard HTTP proxy routes (not LLM-specific)
|
||||||
# These are for non-LLM services (agent-pod/console, etc.)
|
# These are for non-LLM services (agent-pod/console, etc.)
|
||||||
routes: []
|
routes: []
|
||||||
|
|||||||
Reference in New Issue
Block a user