feat(api): 内部面 token 按调用方拆分为 velocity/limbo/build/ops 并按路由限定调用方,审计来源区分调用方,安装器生成并下发各自 Secret,新增 felis rotate-token 轮换命令

This commit is contained in:
Lemon-miaow committed 2026-09-25 15:21:41 +08:00
1 parent d3769b5c31
commit a883c1fe07
38 files changed
+1359 -209

No files matched your search

+24 -19
View File
@@ -50,20 +50,18 @@ import (
// kubernetes.io/hostname selector and PV node affinity) or the CronJob could
// schedule on a node where the worlds-root is empty.
const (
// configSecretName / serviceTokenSecretName are referenced BY NAME and NEVER
// rendered into the bundle: felis.toml carries the database URL (a credential)
// and the service token is a credential, so writing either into a checked-in
// manifest is a hard red line. The deployment provisions both Secrets
// out-of-band before applying these workloads.
// configSecretName and the caller token Secrets (naming.CallerTokens) are
// referenced BY NAME and NEVER rendered into the bundle: felis.toml carries the
// database URL (a credential) and each token is a credential, so writing any of
// them into a checked-in manifest is a hard red line. The deployment provisions
// these Secrets out-of-band before applying these workloads.
configSecretName = "felis-config"
configSecretKey = "felis.toml"
configMountPath = "/etc/felis"
configFilePath = "/etc/felis/felis.toml"
felisBinaryPath = "/usr/local/bin/felis"
// Single-sourced with the operator, which injects the same Secret into the
// login system server's pod (see internal/naming).
serviceTokenSecretName = naming.ServiceTokenSecretName
serviceTokenSecretKey = naming.ServiceTokenSecretKey
// The key every caller token Secret stores its value under (internal/naming).
serviceTokenSecretKey = naming.ServiceTokenSecretKey
// Ports, single-sourced with the entrypoints (cmd/felis). The api external
// port must match server.listen in felis.toml (default 0.0.0.0:8080); that
@@ -323,8 +321,8 @@ func retentionEnabled(p Params) bool {
// fence and is asserted in workloads_test.go.
//
// felis.toml is mounted read-only from a Secret (it carries the database URL, a
// credential, so it must never be a ConfigMap); FELIS_SERVICE_TOKEN comes from a
// second Secret by reference. FELIS_IMAGE is the felis image itself, so the
// credential, so it must never be a ConfigMap); each internal caller's token
// (naming.CallerTokens) comes from its own Secret by reference. FELIS_IMAGE is the felis image itself, so the
// restore executor launches `felis restore` with the same image. FELIS_BACKUP_PVC
// is rendered only when a backup PVC is named — otherwise the restore endpoint
// degrades to 503 rather than enqueuing a Job that cannot mount its backup.
@@ -336,22 +334,29 @@ func retentionEnabled(p Params) bool {
func APIDeployment(p Params) *appsv1.Deployment {
p = p.withDefaults()
env := []corev1.EnvVar{
{
Name: "FELIS_SERVICE_TOKEN",
var env []corev1.EnvVar
// Every internal caller's token, each from its own Secret. Required: the
// installer applies all four before this Deployment, and a missing one should
// stall the rollout on the old pods rather than start an api that turns that
// caller away.
for _, ct := range naming.CallerTokens {
env = append(env, corev1.EnvVar{
Name: ct.APIEnv,
ValueFrom: &corev1.EnvVarSource{
SecretKeyRef: &corev1.SecretKeySelector{
LocalObjectReference: corev1.LocalObjectReference{Name: serviceTokenSecretName},
LocalObjectReference: corev1.LocalObjectReference{Name: ct.Secret},
Key: serviceTokenSecretKey,
},
},
},
{Name: "FELIS_IMAGE", Value: p.FelisImage},
})
}
env = append(env,
corev1.EnvVar{Name: "FELIS_IMAGE", Value: p.FelisImage},
// The api's own internal-face base URL, so it derives the submission
// context URLs that build Pods fetch through it. Same value the login gate
// is handed; one address for one face.
{Name: naming.EnvAPIBaseURL, Value: InternalAPIBaseURL(p.ControlNamespace)},
}
corev1.EnvVar{Name: naming.EnvAPIBaseURL, Value: InternalAPIBaseURL(p.ControlNamespace)},
)
if p.BackupPVC != "" {
env = append(env, corev1.EnvVar{Name: "FELIS_BACKUP_PVC", Value: p.BackupPVC})
}
+22 -7
View File
@@ -233,13 +233,28 @@ func TestAPIDeployment_Wiring(t *testing.T) {
if v := envValue(c.Env, "FELIS_API_BASE_URL"); v != InternalAPIBaseURL(p.ControlNamespace) {
t.Errorf("FELIS_API_BASE_URL = %q, want %q", v, InternalAPIBaseURL(p.ControlNamespace))
}
// FELIS_SERVICE_TOKEN must come from a Secret, never a literal value.
tok := envVar(c.Env, "FELIS_SERVICE_TOKEN")
if tok == nil || tok.ValueFrom == nil || tok.ValueFrom.SecretKeyRef == nil {
t.Fatal("FELIS_SERVICE_TOKEN must be sourced from a secretKeyRef")
}
if tok.Value != "" {
t.Error("FELIS_SERVICE_TOKEN must not carry a literal value")
// Each internal caller's token comes from its own Secret, never a literal
// value, and none is optional: a missing Secret must hold the rollout back.
for env, secret := range map[string]string{
"FELIS_SERVICE_TOKEN": "felis-service-token",
"FELIS_LIMBO_TOKEN": "felis-limbo-token",
"FELIS_BUILD_TOKEN": "felis-build-token",
"FELIS_OPS_TOKEN": "felis-ops-token",
} {
tok := envVar(c.Env, env)
if tok == nil || tok.ValueFrom == nil || tok.ValueFrom.SecretKeyRef == nil {
t.Fatalf("%s must be sourced from a secretKeyRef", env)
}
ref := tok.ValueFrom.SecretKeyRef
if ref.Name != secret || ref.Key != "token" {
t.Errorf("%s reads %s/%s, want %s/token", env, ref.Name, ref.Key, secret)
}
if ref.Optional != nil && *ref.Optional {
t.Errorf("%s is optional; the api must not start without it", env)
}
if tok.Value != "" {
t.Errorf("%s must not carry a literal value", env)
}
}
// felis.toml carries the DB URL, so its volume must be a Secret (NOT a