Information Disclosure
Description
The change tightens the scope of the RuleSequence informer used by alerting rules. Previously, the informer could watch all namespaces (all-namespaces), which in cloud deployments could lead to information exposure across tenants by accessing alerting rule data from multiple namespaces. The fix fences the watcher to a single stack namespace in cloud environments (and uses all-namespaces only for on-prem). This reduces cross-namespace information disclosure risk and tenant isolation violations in cloud setups.
Commit Details
Author: Mustafa Sencer Özcan
Date: 2026-06-08 13:10 UTC
Message:
fix: make alerting rule sequence informer watch calls cluster scoped (#125947)
* fix: namespace scoped
* chore: reduce comments
Triage Assessment
Vulnerability Type: Information Disclosure
Confidence: MEDIUM
Reasoning:
The change constrains the RuleSequence informer to a specific namespace in cloud deployments and ensures that all-namespaces watching is only allowed in on-prem setups. This reduces cross-namespace exposure of alerting rules data and mitigates potential information disclosure or tenant isolation risks when running in cloud environments.
Verification Assessment
Vulnerability Type: Information Disclosure
Confidence: MEDIUM
Affected Versions: < 12.4.0 (pre-fix releases; prior to this commit which scopes the RuleSequence informer)
Code Diff
diff --git a/apps/alerting/rules/pkg/app/app.go b/apps/alerting/rules/pkg/app/app.go
index 38b71636e8b50..2eee8a7134d72 100644
--- a/apps/alerting/rules/pkg/app/app.go
+++ b/apps/alerting/rules/pkg/app/app.go
@@ -30,6 +30,11 @@ func New(cfg app.Config) (app.App, error) {
Mutator: buildKindMutator(kind, runtimeCfg),
Watcher: buildKindWatcher(kind, runtimeCfg),
}
+ // Only kinds with a watcher run an informer (RuleSequence), so this
+ // scopes that watch to WatchNamespace; empty means all namespaces.
+ if managedKind.Watcher != nil && runtimeCfg.WatchNamespace != "" {
+ managedKind.ReconcileOptions.Namespace = runtimeCfg.WatchNamespace
+ }
managedKinds = append(managedKinds, managedKind)
}
}
diff --git a/apps/alerting/rules/pkg/app/config/runtime.go b/apps/alerting/rules/pkg/app/config/runtime.go
index ed1695ed48bff..26dd68789ec98 100644
--- a/apps/alerting/rules/pkg/app/config/runtime.go
+++ b/apps/alerting/rules/pkg/app/config/runtime.go
@@ -42,4 +42,8 @@ type RuntimeConfig struct {
// RuleSequence (if any) owns a rule UID. The default implementation is a
// watch-backed in-memory index that provides O(1) lookups.
MembershipResolver RuleSequenceMembershipResolver
+ // WatchNamespace scopes the RuleSequence informer to one namespace. Empty
+ // watches all namespaces (on-prem default); in cloud it must be the stack
+ // namespace, else the all-namespace watch is rejected as a mismatch.
+ WatchNamespace string
}
diff --git a/pkg/registry/apps/alerting/rules/register.go b/pkg/registry/apps/alerting/rules/register.go
index b480b7a65de6b..58f8df082307e 100644
--- a/pkg/registry/apps/alerting/rules/register.go
+++ b/pkg/registry/apps/alerting/rules/register.go
@@ -71,6 +71,7 @@ func RegisterAppInstaller(
ResolveRuleRef: newRuleRefResolver(ng),
MembershipResolver: membershipIndex,
NotificationSettingsValidator: newNotificationSettingsValidator(ng),
+ WatchNamespace: watchNamespace(cfg),
}
provider := simple.NewAppProvider(rulesManifest.LocalManifest(), appSpecificConfig, rulesApp.New)
@@ -89,6 +90,18 @@ func RegisterAppInstaller(
return installer, nil
}
+// watchNamespace returns the namespace the RuleSequence informer should watch.
+// In cloud each instance serves one stack namespace and its storage identity is
+// scoped to it, so an all-namespace watch is rejected as a mismatch; on-prem
+// (no stack ID) returns "" to watch all namespaces.
+func watchNamespace(cfg *setting.Cfg) string {
+ if cfg == nil || cfg.StackID == "" {
+ return ""
+ }
+ // The cloud mapper ignores the org ID and returns the stack namespace.
+ return reqns.GetNamespaceMapper(cfg)(0)
+}
+
func resolveOrgID(ctx context.Context) int64 {
orgID, err := reqns.OrgIDForList(ctx)
if err != nil || orgID < 1 {
diff --git a/pkg/registry/apps/alerting/rules/register_test.go b/pkg/registry/apps/alerting/rules/register_test.go
index 852a8494a6e6f..5e1af084a2079 100644
--- a/pkg/registry/apps/alerting/rules/register_test.go
+++ b/pkg/registry/apps/alerting/rules/register_test.go
@@ -9,6 +9,24 @@ import (
"github.com/stretchr/testify/require"
)
+func TestWatchNamespace(t *testing.T) {
+ tests := []struct {
+ name string
+ cfg *setting.Cfg
+ want string
+ }{
+ {name: "nil cfg watches all namespaces", cfg: nil, want: ""},
+ {name: "on-prem (no stack id) watches all namespaces", cfg: &setting.Cfg{}, want: ""},
+ {name: "cloud scopes to the stack namespace", cfg: &setting.Cfg{StackID: "42"}, want: "stacks-42"},
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ require.Equal(t, tt.want, watchNamespace(tt.cfg))
+ })
+ }
+}
+
func TestRegisterAppInstaller_UnifiedAlertingEnabled(t *testing.T) {
tests := []struct {
name string