Author SHA1 Message Date
Admin Bot 3188ce3e6a chore: add gateway-config-secret with matching fixes
CI / CI (pull_request) Successful in 2m57s
Also updates gateway-config-secret.enc.yaml with the same port fixes.

NOTE: This file is currently in plaintext and should be encrypted with SOPS:
  export SOPS_AGE_RECIPIENTS=age1e5fq3hwxy78psus2nfvmtmua36g0u3suk78ephw6246l974d2utsvn0hla
  sops --encrypt k8s/gateway-config-secret.enc.yaml

As mentioned in the repo structure, this should be encrypted before merge
to avoid exposing internal infrastructure details (service DNS names,
upstream addresses, auth configuration) in the git history.
2026-09-13 08:52:03 +09:00
Admin Bot 45254a48b0 fix: gateway authentik port from 80 to 9000
CI / CI (pull_request) Successful in 3m9s
NetworkPolicy allows gateway→iam only on ports 9000/9443, but config
used port 80 for JWKS fetch and token endpoints. This caused
'operation not permitted' errors and JWKS refresh failures.

Affects:
- auth.jwksUrl: uses port 9000 (Authentik HTTP)
- auth.tokenUrl: uses port 9000 for token exchange
- iam adapter upstream: routes to port 9000

Fixes: Gateway unable to validate JWT tokens, all chat/inference requests
returned 401 with 'token is unverifiable' error.
2026-09-13 08:48:08 +09:00
rock e61885254b feat: route qwen2.5:3b-instruct to CPU service (#20)
CI / CI (push) Successful in 3m4s
Route `qwen2.5:3b-instruct` to `qwen-cpu.llm-serving:80` (CPU on cp-2) instead of `ornith-predictor` (GPU on worker-1).

Companion to homelab GPU rebalance PR.
2026-09-09 02:10:59 +00:00
4 changed files with 17 additions and 33 deletions
+9 -9
View File
@@ -52,18 +52,18 @@ func StripIncoming(r *http.Request) {
func Inject(r *http.Request, claims jwt.MapClaims) { func Inject(r *http.Request, claims jwt.MapClaims) {
r.Header.Set(HeaderAuthVerified, "true") r.Header.Set(HeaderAuthVerified, "true")
if sub := ClaimString(claims, "sub"); sub != "" { if sub := claimString(claims, "sub"); sub != "" {
r.Header.Set(HeaderUser, sub) r.Header.Set(HeaderUser, sub)
} }
if roles := ClaimStringSlice(claims, "roles"); len(roles) > 0 { if roles := claimStringSlice(claims, "roles"); len(roles) > 0 {
r.Header.Set(HeaderRoles, strings.Join(roles, ",")) r.Header.Set(HeaderRoles, strings.Join(roles, ","))
} else if perms := ClaimStringSlice(claims, "permissions"); len(perms) > 0 { } else if perms := claimStringSlice(claims, "permissions"); len(perms) > 0 {
r.Header.Set(HeaderRoles, strings.Join(perms, ",")) r.Header.Set(HeaderRoles, strings.Join(perms, ","))
} }
if azp := ClaimString(claims, "azp"); azp != "" { if azp := claimString(claims, "azp"); azp != "" {
sub := ClaimString(claims, "sub") sub := claimString(claims, "sub")
// Only set acting-service when azp differs from sub // Only set acting-service when azp differs from sub
// (i.e., a service account acting, not the user themselves) // (i.e., a service account acting, not the user themselves)
if azp != sub { if azp != sub {
@@ -72,9 +72,9 @@ func Inject(r *http.Request, claims jwt.MapClaims) {
} }
} }
// ClaimString extracts a string value from claims, returning "" if // claimString extracts a string value from claims, returning "" if
// the key is missing or not a string. // the key is missing or not a string.
func ClaimString(claims jwt.MapClaims, key string) string { func claimString(claims jwt.MapClaims, key string) string {
val, ok := claims[key] val, ok := claims[key]
if !ok || val == nil { if !ok || val == nil {
return "" return ""
@@ -86,10 +86,10 @@ func ClaimString(claims jwt.MapClaims, key string) string {
return s return s
} }
// ClaimStringSlice extracts a []string from claims. JWT libraries // claimStringSlice extracts a []string from claims. JWT libraries
// deserialize JSON arrays as []interface{}, so each element is // deserialize JSON arrays as []interface{}, so each element is
// type-asserted individually. Non-string elements are skipped. // type-asserted individually. Non-string elements are skipped.
func ClaimStringSlice(claims jwt.MapClaims, key string) []string { func claimStringSlice(claims jwt.MapClaims, key string) []string {
val, ok := claims[key] val, ok := claims[key]
if !ok || val == nil { if !ok || val == nil {
return nil return nil
-18
View File
@@ -359,24 +359,6 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Inject identity headers for downstream services // Inject identity headers for downstream services
identity.Inject(r, claims) identity.Inject(r, claims)
// Audit trail: log successful JWT authentication
auditFields := map[string]string{
"path": r.URL.Path,
"method": r.Method,
}
if sub := identity.ClaimString(claims, "sub"); sub != "" {
auditFields["subject"] = sub
}
if azp := identity.ClaimString(claims, "azp"); azp != "" {
auditFields["acting_party"] = azp
}
if roles := identity.ClaimStringSlice(claims, "roles"); len(roles) > 0 {
auditFields["roles"] = strings.Join(roles, ",")
} else if perms := identity.ClaimStringSlice(claims, "permissions"); len(perms) > 0 {
auditFields["permissions"] = strings.Join(perms, ",")
}
logging.Infof("auth ok", auditFields)
// Check required capability if configured // Check required capability if configured
if h.config.Auth.RequiredCapability != "" { if h.config.Auth.RequiredCapability != "" {
if !h.jwtValidator.CheckPermissions(claims, h.config.Auth.RequiredCapability, "*") { if !h.jwtValidator.CheckPermissions(claims, h.config.Auth.RequiredCapability, "*") {
+3 -3
View File
@@ -15,9 +15,9 @@ data:
enabled: true enabled: true
issuer: "https://authentik.riotpiao.com/application/o/api-gw/" issuer: "https://authentik.riotpiao.com/application/o/api-gw/"
audience: "api-gw" audience: "api-gw"
jwksUrl: "http://authentik-server.iam.svc.cluster.local/application/o/api-gw/jwks/" jwksUrl: "http://authentik-server.iam.svc.cluster.local:9000/application/o/api-gw/jwks/"
requiredCapability: "llm:inference" requiredCapability: "llm:inference"
tokenUrl: "http://authentik-server.iam.svc.cluster.local/application/o/token/" tokenUrl: "http://authentik-server.iam.svc.cluster.local:9000/application/o/token/"
clientId: "api-gw" clientId: "api-gw"
# Routes: standard HTTP proxy routes (not LLM-specific) # Routes: standard HTTP proxy routes (not LLM-specific)
@@ -137,7 +137,7 @@ data:
- serviceName: iam - serviceName: iam
upstream: upstream:
url: http://authentik-server.iam.svc.cluster.local:80 url: http://authentik-server.iam.svc.cluster.local:9000
timeoutSeconds: 30 timeoutSeconds: 30
auth: auth:
required: false required: false
+5 -3
View File
@@ -12,9 +12,9 @@ stringData:
enabled: true enabled: true
issuer: "https://authentik.riotpiao.com/application/o/api-gw/" issuer: "https://authentik.riotpiao.com/application/o/api-gw/"
audience: "api-gw" audience: "api-gw"
jwksUrl: "http://authentik-server.iam.svc.cluster.local/application/o/api-gw/jwks/" jwksUrl: "http://authentik-server.iam.svc.cluster.local:9000/application/o/api-gw/jwks/"
requiredCapability: "llm:inference" requiredCapability: "llm:inference"
tokenUrl: "http://authentik-server.iam.svc.cluster.local/application/o/token/" tokenUrl: "http://authentik-server.iam.svc.cluster.local:9000/application/o/token/"
clientId: "api-gw" clientId: "api-gw"
routes: [] routes: []
models: models:
@@ -112,7 +112,7 @@ stringData:
upstreamPath: / upstreamPath: /
- serviceName: iam - serviceName: iam
upstream: upstream:
url: http://authentik-server.iam.svc.cluster.local:80 url: http://authentik-server.iam.svc.cluster.local:9000
timeoutSeconds: 30 timeoutSeconds: 30
auth: auth:
required: false required: false
@@ -129,3 +129,5 @@ stringData:
methods: methods:
- verb: POST - verb: POST
upstreamPath: /api/v3/roles upstreamPath: /api/v3/roles
# NOTE: This file should be encrypted with SOPS using the age key
# Command: sops --encrypt k8s/gateway-config-secret.enc.yaml