|
| 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 | |
0 commit comments