Information Disclosure

MEDIUM
grafana/grafana
Commit: 1f08b76c1d2a
Affected: < 12.4.0 (pre-fix releases; prior to this commit which scopes the RuleSequence informer)
2026-06-08 13:44 UTC

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
← Back to Alerts View on GitHub →