fix(operator): RCON Secret 改走无缓存按名读取,去掉 Secret watch,RBAC 收窄为 secrets get/create,不再缓存全命名空间 Secret

This commit is contained in:
Lemon-miaow committed 2026-09-25 19:25:24 +08:00
1 parent 6ec1b2726c
commit a977e229ca
6 files changed
+110 -18

No files matched your search

+7 -7
View File
@@ -150,7 +150,7 @@ func APIBuildRole(p Params) *rbacv1.Role {
// status (Status().Update — `update` only) and patches spec.desiredState to
// Stopped for idle auto-stop (spec §8 — the one spec field it may write, using
// the same merge patch as the reaper's Stop: without the grant the auto-stop
// call fails closed with a 403), and reads RCON Secrets. Jobs are list-only,
// call fails closed with a 403), and reads RCON Secrets by name, uncached. Jobs are list-only,
// through the manager's uncached API reader: before scaling a server up from zero
// the operator checks that no restore/backup/file-write Job holds its world
// (internal/maintenance). Pods are delete-only: a start that timed out is retried
@@ -164,13 +164,13 @@ func OperatorRole(p Params) *rbacv1.Role {
rule([]string{groupFelis}, []string{"minecraftservers/status"}, []string{"update"}),
rule([]string{groupApps}, []string{"statefulsets"}, []string{"get", "list", "watch", "create", "update"}),
rule([]string{groupCore}, []string{"services"}, []string{"get", "list", "watch", "create", "update"}),
// create is here for the per-server RCON password Secret the operator
// provisions on first reconcile (internal/operator.ensureRconSecret). It is a
// smaller grant than it looks: this identity already holds get/list/watch on
// every Secret in this namespace, so being able to add one grants no read it
// did not already have. No update/delete — the password is written once and
// get by name, through the uncached API reader (Reconciler.Secrets), of the
// RCON password Secret a server's spec names; create for the one the operator
// provisions on first reconcile (internal/operator.ensureRconSecret). No
// list/watch, so there is no Secret informer and nothing enumerates the
// namespace's Secrets. No update/delete — the password is written once and
// removed by garbage collection through its controller reference.
rule([]string{groupCore}, []string{"secrets"}, []string{"get", "list", "watch", "create"}),
rule([]string{groupCore}, []string{"secrets"}, []string{"get", "create"}),
// list only: an uncached List (no informer, so no watch) of the world-volume
// maintenance Jobs; the operator never creates or deletes a Job.
rule([]string{groupBatch}, []string{"jobs"}, []string{"list"}),
+12
View File
@@ -194,6 +194,18 @@ func TestOperatorRole_ScopeExact(t *testing.T) {
t.Errorf("operator must NOT touch core/%s", res)
}
}
// RCON Secrets are read by name through the uncached reader and created once;
// no list/watch, so no informer mirrors the namespace's Secrets into it.
for _, v := range []string{"get", "create"} {
if !hasRule(op, groupCore, "secrets", v) {
t.Errorf("operator must have secrets:%s", v)
}
}
for _, v := range []string{"list", "watch", "update", "patch", "delete", "deletecollection", "*"} {
if hasRule(op, groupCore, "secrets", v) {
t.Errorf("operator secrets rule must be get+create only, found %s", v)
}
}
// Pods are delete-only: the bounded retry of a timed-out start.
if !hasRule(op, groupCore, "pods", "delete") {
t.Error("operator must have pods:delete (auto-restart of a timed-out start)")
+5 -2
View File
@@ -540,8 +540,11 @@ func operatorMetricsService(p Params) *corev1.Service {
// the felis-operator SA and carries controlPlanePodLabels(operator), the second
// pod the allow-rcon peer admits (the readiness prober dials RCON). It takes NO
// config Secret: the operator reads everything from flags + the in-cluster API,
// so it never holds the database URL — a deliberately smaller attack surface than
// the api. It watches the minecraft namespace (--namespace) while running in the
// so it never loads the database URL — a deliberately smaller attack surface than
// the api. Its secrets:get in the minecraft namespace is by name and uncached
// (no list, no informer), yet namespaced RBAC cannot exclude a name, so a
// compromised operator could still fetch the felis-config mirror the Jobs and
// the reaper mount there. It watches the minecraft namespace (--namespace) while running in the
// control namespace, exactly the split cmd/felis/operator.go documents.
func OperatorDeployment(p Params) *appsv1.Deployment {
p = p.withDefaults()