Skip to content

Commit 851b73b

Browse files
authored
[2.2] feat: added reference grant mode (#14209) (#14239)
Signed-off-by: Andy Fong <andy.fong@solo.io>
1 parent 505431b commit 851b73b

19 files changed

Lines changed: 1366 additions & 7 deletions

api/settings/settings.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,39 @@ func (v *ValidationMode) Decode(value string) error {
4040
}
4141
}
4242

43+
// ReferenceGrantMode controls how strictly cross-namespace references are validated
44+
// via ReferenceGrant across the control plane.
45+
type ReferenceGrantMode string
46+
47+
const (
48+
// ReferenceGrantOff disables all ReferenceGrant validation. All cross-namespace
49+
// references are permitted without any grant. Use only in environments where
50+
// namespace isolation is enforced by other means.
51+
ReferenceGrantOff ReferenceGrantMode = "OFF"
52+
53+
// ReferenceGrantPermissive enforces ReferenceGrant for cross-namespace
54+
// BackendRef and SecretRef references (current default behavior). Cross-namespace
55+
// ExtensionRef references are permitted without a grant.
56+
ReferenceGrantPermissive ReferenceGrantMode = "PERMISSIVE"
57+
58+
// ReferenceGrantStrict enforces ReferenceGrant for all cross-namespace references,
59+
// including ExtensionRef (e.g., TrafficPolicy referencing a GatewayExtension in
60+
// another namespace).
61+
ReferenceGrantStrict ReferenceGrantMode = "STRICT"
62+
)
63+
64+
// Decode implements envconfig.Decoder.
65+
func (r *ReferenceGrantMode) Decode(value string) error {
66+
mode := ReferenceGrantMode(strings.ToUpper(value))
67+
switch mode {
68+
case ReferenceGrantOff, ReferenceGrantPermissive, ReferenceGrantStrict:
69+
*r = mode
70+
return nil
71+
default:
72+
return fmt.Errorf("invalid reference grant mode: %q", value)
73+
}
74+
}
75+
4376
// DnsLookupFamily controls the DNS lookup family for all static clusters created via Backend resources.
4477
type DnsLookupFamily string
4578

@@ -253,6 +286,13 @@ type Settings struct {
253286
// kgateway refuses to start against an unsupported Gateway API version
254287
// and points the operator to the docs.
255288
SkipGatewayAPIVersionCheck bool `split_words:"true" default:"false"`
289+
290+
// ReferenceGrantMode controls how cross-namespace references are validated via ReferenceGrant.
291+
// Supported values are:
292+
// - "OFF": No ReferenceGrant validation. All cross-namespace references are permitted.
293+
// - "PERMISSIVE": ReferenceGrant required for BackendRef and SecretRef (default behavior).
294+
// - "STRICT": ReferenceGrant required for all cross-namespace references including ExtensionRef.
295+
ReferenceGrantMode ReferenceGrantMode `split_words:"true" default:"PERMISSIVE"`
256296
}
257297

258298
// BuildSettings returns a zero-valued Settings obj if error is encountered when parsing env

api/settings/settings_test.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ func allEnvVarsSet() map[string]string {
5252
"KGW_XDS_TLS": "true",
5353
"KGW_ENABLE_EXPERIMENTAL_GATEWAY_API_FEATURES": "false",
5454
"KGW_SKIP_GATEWAY_API_VERSION_CHECK": "true",
55+
"KGW_REFERENCE_GRANT_MODE": string(ReferenceGrantStrict),
5556
}
5657
}
5758

@@ -97,6 +98,7 @@ func TestSettings(t *testing.T) {
9798
EnableEnvoy: true,
9899
WeightedRoutePrecedence: false,
99100
ValidationMode: ValidationStandard,
101+
ReferenceGrantMode: ReferenceGrantPermissive,
100102
EnableBuiltinDefaultMetrics: false,
101103
GlobalPolicyNamespace: "",
102104
DisableLeaderElection: false,
@@ -156,6 +158,7 @@ func TestSettings(t *testing.T) {
156158
},
157159
},
158160
SkipGatewayAPIVersionCheck: true,
161+
ReferenceGrantMode: ReferenceGrantStrict,
159162
},
160163
},
161164
{
@@ -193,6 +196,13 @@ func TestSettings(t *testing.T) {
193196
},
194197
expectedErrorStr: `invalid validation mode: "invalid"`,
195198
},
199+
{
200+
name: "errors on invalid reference grant mode",
201+
envVars: map[string]string{
202+
"KGW_REFERENCE_GRANT_MODE": "invalid",
203+
},
204+
expectedErrorStr: `invalid reference grant mode: "invalid"`,
205+
},
196206
{
197207
name: "errors on invalid gatewayclass parameters refs: missing name",
198208
envVars: map[string]string{
@@ -234,6 +244,7 @@ func TestSettings(t *testing.T) {
234244
EnableEnvoy: true,
235245
WeightedRoutePrecedence: false,
236246
ValidationMode: ValidationStandard,
247+
ReferenceGrantMode: ReferenceGrantPermissive,
237248
PolicyMerge: "{}",
238249
XdsAuth: true,
239250
XdsTLS: false,
Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
# ReferenceGrant Enforcement Modes
2+
3+
## Overview
4+
5+
The [Gateway API ReferenceGrant](https://gateway-api.sigs.k8s.io/api-types/referencegrant/)
6+
mechanism controls whether a resource in one namespace may reference a resource
7+
in another namespace. Without a ReferenceGrant in the target namespace, the
8+
reference is denied.
9+
10+
kgateway supports three enforcement modes, configurable via the
11+
`KGW_REFERENCE_GRANT_MODE` environment variable. The default is `PERMISSIVE`,
12+
which preserves backward-compatible behavior.
13+
14+
## Modes
15+
16+
### Off
17+
18+
```
19+
KGW_REFERENCE_GRANT_MODE=OFF
20+
```
21+
22+
> **Warning: Off mode breaks Gateway API compliance and has serious security
23+
> implications. Do not use in multi-tenant or production environments.**
24+
>
25+
> - **Gateway API non-compliance.** The Gateway API specification requires
26+
> ReferenceGrant for all cross-namespace references. Off mode violates this
27+
> requirement, meaning conformance tests will fail and behavior diverges from
28+
> the spec in ways that may surprise users and operators.
29+
>
30+
> - **Namespace isolation bypassed.** Any resource in any namespace can
31+
> reference backends, secrets, and GatewayExtensions in any other namespace
32+
> without restriction. A tenant with write access to one namespace can
33+
> silently route traffic through or read credentials from a completely
34+
> unrelated namespace.
35+
>
36+
> - **Secret exfiltration risk.** A TrafficPolicy referencing a GatewayExtension
37+
> that holds an OAuth2 `ClientSecretRef` gains indirect read access to that
38+
> secret. With Off mode there is no grant check at either hop, so any
39+
> namespace can access any OAuth2 client secret in the cluster.
40+
41+
Disables all ReferenceGrant validation. Every cross-namespace reference is
42+
permitted unconditionally. Only appropriate when namespace isolation is enforced
43+
entirely by an external mechanism (e.g., a service mesh or network policy layer)
44+
and Gateway API conformance is not required.
45+
46+
### Permissive (default)
47+
48+
```
49+
KGW_REFERENCE_GRANT_MODE=PERMISSIVE
50+
```
51+
52+
Enforces ReferenceGrant for BackendRef and SecretRef cross-namespace references,
53+
matching today's behavior. Cross-namespace `ExtensionRef` references (TrafficPolicy
54+
-> GatewayExtension) are **not** checked and are always permitted.
55+
56+
### Strict
57+
58+
```
59+
KGW_REFERENCE_GRANT_MODE=STRICT
60+
```
61+
62+
Enforces ReferenceGrant for all cross-namespace references, including
63+
`ExtensionRef` (TrafficPolicy -> GatewayExtension). A missing grant in Strict
64+
mode causes the referencing policy to be rejected and its routes to report a
65+
`RouteRuleReplaced` condition with reason `missing reference grant`.
66+
67+
## Reference type coverage by mode
68+
69+
| Reference | From | To | Off | Permissive | Strict |
70+
|---|---|---|---|---|---|
71+
| BackendRef | HTTPRoute / GRPCRoute | Service / Backend | allowed | checked | checked |
72+
| SecretRef | TrafficPolicy (BasicAuth, APIKeyAuth) | Secret | allowed | checked | checked |
73+
| BackendRef | GatewayExtension (ExtAuth, ExtProc, RateLimit, OAuth2) | Service | allowed | checked | checked |
74+
| ExtensionRef | TrafficPolicy | GatewayExtension (same ns) | allowed | allowed | allowed |
75+
| ExtensionRef | TrafficPolicy | GatewayExtension (cross-ns) | allowed | allowed | checked |
76+
77+
Same-namespace references always pass, regardless of mode.
78+
79+
## Why ExtensionRef matters in Strict mode
80+
81+
A TrafficPolicy in namespace A that references a GatewayExtension in namespace B
82+
gains indirect read access to everything that GatewayExtension configures,
83+
including its `ClientSecretRef` secret. Even though the GatewayExtension ->
84+
Secret hop is same-namespace (and therefore never requires a grant), the
85+
unchecked first hop creates a transitive exposure path:
86+
87+
```
88+
TrafficPolicy (ns A) --[ExtensionRef, no grant]--> GatewayExtension (ns B)
89+
|
90+
[ClientSecretRef, same-ns]
91+
|
92+
Secret (ns B)
93+
```
94+
95+
Strict mode closes this gap by requiring a ReferenceGrant for the first hop.
96+
Among the GatewayExtension sub-types, only OAuth2 currently holds secrets
97+
(`ClientSecretRef`). ExtAuth, ExtProc, and RateLimit only reference Service
98+
backends, which are already checked by the BackendRef enforcement above.
99+
100+
## ReferenceGrant example for Strict mode
101+
102+
To allow a TrafficPolicy in `app-ns` to reference a GatewayExtension in
103+
`infra-ns`, create a ReferenceGrant in the **target** namespace (`infra-ns`):
104+
105+
```yaml
106+
apiVersion: gateway.networking.k8s.io/v1beta1
107+
kind: ReferenceGrant
108+
metadata:
109+
name: allow-trafficpolicy-to-gwext
110+
namespace: infra-ns
111+
spec:
112+
from:
113+
- group: gateway.kgateway.dev
114+
kind: TrafficPolicy
115+
namespace: app-ns
116+
to:
117+
- group: gateway.kgateway.dev
118+
kind: GatewayExtension
119+
```
120+
121+
## Code flow
122+
123+
### Off mode — all checks bypassed
124+
125+
The mode is stored on `RefGrantIndex` (created once at startup in
126+
`pkg/pluginsdk/collections/collections.go`). `ReferenceAllowed()` returns
127+
`true` immediately when the mode is `Off`, so all existing call sites
128+
(BackendRef, SecretRef) are bypassed without any changes to those call sites.
129+
130+
```
131+
ReferenceAllowed() pkg/krtcollections/policy.go
132+
if mode == Off -> return true (short-circuits all callers)
133+
```
134+
135+
### Permissive mode — BackendRef and SecretRef checked
136+
137+
The normal check flow applies for BackendRef and SecretRef. ExtensionRef
138+
resolution in `FetchGatewayExtension` does a raw `krt.FetchOne` without calling
139+
`ReferenceAllowed`, so cross-namespace ExtensionRefs pass through.
140+
141+
```
142+
HTTPRoute translation (KRT collection evaluation)
143+
transformHttpRoute() pkg/krtcollections/policy.go
144+
getBackends()
145+
GetBackendFromRef()
146+
ReferenceAllowed() -> grant required
147+
148+
TrafficPolicy -> IR (KRT collection evaluation)
149+
ConstructIR()
150+
constructExtProc/ExtAuth/...
151+
FetchGatewayExtension() pkg/.../trafficpolicy/constructor.go
152+
krt.FetchOne() -> no grant check
153+
```
154+
155+
### Strict mode — ExtensionRef also checked
156+
157+
`FetchGatewayExtension` calls `ReferenceAllowed` before `krt.FetchOne` when the
158+
mode is `Strict` and the target namespace differs from the source namespace:
159+
160+
```
161+
FetchGatewayExtension() pkg/.../trafficpolicy/constructor.go
162+
if mode == Strict:
163+
RefGrants.ReferenceAllowed(
164+
from: TrafficPolicy GK, fromNs,
165+
to: GatewayExtension GK, targetNs, name
166+
)
167+
-> ErrMissingReferenceGrant if denied
168+
krt.FetchOne(gatewayExtensions, ...)
169+
```
170+
171+
Because `FetchGatewayExtension` runs inside a KRT collection evaluation, the
172+
`ReferenceAllowed` call registers a KRT dependency on the matching
173+
ReferenceGrant objects. Adding or removing a ReferenceGrant automatically
174+
triggers recomputation of any affected TrafficPolicy IR — no manual cache
175+
invalidation is needed.
176+
177+
### Key files
178+
179+
| File | Role |
180+
|---|---|
181+
| `api/settings/settings.go` | `ReferenceGrantMode` type and `Settings.ReferenceGrantMode` field |
182+
| `pkg/krtcollections/policy.go` | `RefGrantIndex`, `NewRefGrantIndex`, `ReferenceAllowed` |
183+
| `pkg/pluginsdk/collections/collections.go` | Wires mode from settings into `NewRefGrantIndex` |
184+
| `pkg/kgateway/extensions2/plugins/trafficpolicy/constructor.go` | `FetchGatewayExtension` — Strict-mode ExtensionRef check |
185+
| `pkg/krtcollections/secrets.go` | SecretRef enforcement via `GetSecret` -> `ReferenceAllowed` |
186+
187+
## Tests
188+
189+
Translator golden tests covering each mode live under:
190+
191+
```
192+
pkg/kgateway/translator/gateway/testutils/inputs/reference-grant-mode/
193+
pkg/kgateway/translator/gateway/testutils/outputs/reference-grant-mode/
194+
```
195+
196+
| Test name | Mode | Resource | Expected outcome |
197+
|---|---|---|---|
198+
| `off-backendref-no-grant` | Off | HTTPRoute BackendRef to Service in `other` ns, no grant | Route accepted; cluster `kube_other_backend-svc_80` present |
199+
| `permissive-extensionref-no-grant` | Permissive | TrafficPolicy ExtProc ExtensionRef to GatewayExtension in `other-ns`, no grant | Policy accepted; `ext_proc/other-ns/extproc-ext` filter applied |
200+
| `strict-extensionref-no-grant` | Strict | TrafficPolicy JWT ExtensionRef to GatewayExtension in `other-ns`, no grant | Policy rejected; route reports `jwt: missing reference grant` |
201+
| `strict-extensionref-with-grant` | Strict | TrafficPolicy ExtAuth ExtensionRef to GatewayExtension in `other-ns`, with grant | Policy accepted; `ext_auth/other-ns/extauth-ext` filter applied |
202+
203+
Run a specific test:
204+
205+
```bash
206+
go test -run "^TestBasic$/^ReferenceGrantMode" ./pkg/kgateway/translator/gateway/
207+
```
208+
209+
Regenerate golden files after changing translation output:
210+
211+
```bash
212+
REFRESH_GOLDEN=true go test -run "^TestBasic$/^ReferenceGrantMode" ./pkg/kgateway/translator/gateway/
213+
```
214+
215+
## Audit: reference types not covered by any mode
216+
217+
The following references are same-namespace only by type and therefore never
218+
require a ReferenceGrant regardless of mode:
219+
220+
| CRD | Field | Type | Reason |
221+
|---|---|---|---|
222+
| `Backend` | `Aws.Auth.SecretRef` | `corev1.LocalObjectReference` | Name only, no namespace field |
223+
| `BackendConfigPolicy` | `TLS.SecretRef` | `corev1.LocalObjectReference` | Name only, no namespace field |
224+
| `GatewayExtension` | `OAuth2.Credentials.ClientSecretRef` | `corev1.LocalObjectReference` | Always same-namespace as the GatewayExtension |
225+
| `GatewayExtension` | `JWT.Providers[].JWKS.LocalJWKS.ConfigMapRef` | `corev1.LocalObjectReference` | Always same-namespace as the GatewayExtension |

pkg/kgateway/extensions2/plugins/trafficpolicy/constructor.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,11 @@ import (
99
"k8s.io/utils/ptr"
1010
gwv1 "sigs.k8s.io/gateway-api/apis/v1"
1111

12+
apisettings "github.com/kgateway-dev/kgateway/v2/api/settings"
1213
"github.com/kgateway-dev/kgateway/v2/api/v1alpha1/kgateway"
1314
"github.com/kgateway-dev/kgateway/v2/api/v1alpha1/shared"
15+
"github.com/kgateway-dev/kgateway/v2/pkg/kgateway/wellknown"
16+
"github.com/kgateway-dev/kgateway/v2/pkg/krtcollections"
1417
"github.com/kgateway-dev/kgateway/v2/pkg/pluginsdk/collections"
1518
"github.com/kgateway-dev/kgateway/v2/pkg/pluginsdk/ir"
1619
)
@@ -134,6 +137,22 @@ func (c *TrafficPolicyConstructor) FetchGatewayExtension(krtctx krt.HandlerConte
134137
namespace = gwv1.Namespace(ns)
135138
}
136139

140+
// In Strict mode, cross-namespace ExtensionRef requires a ReferenceGrant.
141+
if c.commoncol.Settings.ReferenceGrantMode == apisettings.ReferenceGrantStrict {
142+
if !c.commoncol.RefGrants.ReferenceAllowed(krtctx,
143+
wellknown.TrafficPolicyGVK.GroupKind(),
144+
ns,
145+
ir.ObjectSource{
146+
Group: wellknown.GatewayExtensionGVK.Group,
147+
Kind: wellknown.GatewayExtensionGVK.Kind,
148+
Namespace: string(namespace),
149+
Name: string(extensionRef.Name),
150+
},
151+
) {
152+
return nil, krtcollections.ErrMissingReferenceGrant
153+
}
154+
}
155+
137156
gwExtNN := types.NamespacedName{Name: string(extensionRef.Name), Namespace: string(namespace)}
138157
gatewayExtension := krt.FetchOne(krtctx, c.gatewayExtensions, krt.FilterObjectName(gwExtNN))
139158
if gatewayExtension == nil {

pkg/kgateway/query/query_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1130,7 +1130,7 @@ func newQueries(t test.Failer, initObjs ...client.Object) query.GatewayQueries {
11301130
}
11311131
mock := krttest.NewMock(t, anys)
11321132
services := krttest.GetMockCollection[*corev1.Service](mock)
1133-
refgrants := krtcollections.NewRefGrantIndex(krttest.GetMockCollection[*gwv1b1.ReferenceGrant](mock))
1133+
refgrants := krtcollections.NewRefGrantIndex(krttest.GetMockCollection[*gwv1b1.ReferenceGrant](mock), apisettings.ReferenceGrantPermissive)
11341134

11351135
policies := krtcollections.NewPolicyIndex(krtutil.KrtOptions{}, sdk.ContributesPolicies{}, apisettings.Settings{})
11361136
upstreams := krtcollections.NewBackendIndex(krtutil.KrtOptions{}, policies, refgrants)

0 commit comments

Comments
 (0)