Skip to content

Commit a13ed7b

Browse files
feat: Pass optional OIDC audience and issuer through the operator (#6677)
* feat: Pass optional OIDC audience and issuer through the operator Follow-up to #6670, requested in review: add audience and issuer to OidcOptionalSecretProperties so operators can set them in the referenced OIDC Secret and have them flow into the generated feature_store.yaml auth section, enabling the new opt-in claim verification on the feature server. Absent keys change nothing. Documents the two optional Secret keys in the operator security guide with a pointer to the OIDC authorization page for the token-claims vs discovery-metadata caveat. Signed-off-by: Larry Singleton <166439969+larrysingleton007@users.noreply.github.com> * fix: Coerce numeric OIDC audience and issuer Secret values to strings Secret values are YAML-parsed on extraction, so an all-digits audience (e.g. a numeric IdP application ID) arrives as a Go int, renders unquoted in the generated feature_store.yaml, and fails the SDK's Optional[str] validation at server startup. Coerce the two claim keys back to strings in the allowlist copy, with a regression test. Also extend the reconcile-level envtest fixtures with both keys so they exercise real Secret extraction and YAML serialization rather than only the mocked property map. Signed-off-by: Larry Singleton <166439969+larrysingleton007@users.noreply.github.com> * docs: Sharpen operator OIDC audience/issuer docs and fix dead links State precisely which claims are and are not checked, scope the checks to the standard OIDC/JWKS path (ServiceAccount and intra-server tokens follow separate paths), distinguish the Secret's issuer key from the CR's issuerUrl, and document three operational caveats: pre-existing Secret keys activate on operator upgrade, the IdP must mint matching claims for Feast's own client token flows, and Secret edits apply on the next reconcile. Point the two dead ../reference/auth links at real pages and add the new optional keys to the linked sample Secret as commented lines. Signed-off-by: Larry Singleton <166439969+larrysingleton007@users.noreply.github.com> --------- Signed-off-by: Larry Singleton <166439969+larrysingleton007@users.noreply.github.com>
1 parent 4f6b821 commit a13ed7b

7 files changed

Lines changed: 57 additions & 7 deletions

File tree

.secrets.baseline

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1172,7 +1172,7 @@
11721172
"filename": "infra/feast-operator/internal/controller/services/repo_config.go",
11731173
"hashed_secret": "e2fb052132fd6a07a56af2013e0b62a1f510572c",
11741174
"is_verified": false,
1175-
"line_number": 224
1175+
"line_number": 235
11761176
}
11771177
],
11781178
"infra/feast-operator/internal/controller/services/services.go": [
@@ -1564,5 +1564,5 @@
15641564
}
15651565
]
15661566
},
1567-
"generated_at": "2026-07-30T09:40:48Z"
1567+
"generated_at": "2026-07-30T16:22:49Z"
15681568
}

docs/how-to-guides/feast-operator/05-security.md

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ to subjects using standard Kubernetes `ClusterRoleBinding` or `RoleBinding` reso
3939
> Kubernetes auth requires all services to be exposed as servers (the controller rejects
4040
> partial configurations where some services are local while RBAC is enabled).
4141

42-
**SDK docs**: [Feast RBAC](../reference/auth/rbac.md)
42+
**SDK docs**: [Feast RBAC](../../getting-started/architecture/rbac.md)
4343

4444
---
4545

@@ -62,8 +62,20 @@ stringData:
6262
client_secret: <your-client-secret>
6363
username: <service-account-username> # used for client-credentials flow
6464
password: <service-account-password>
65+
audience: <expected-aud-claim> # optional: reject tokens whose aud claim differs
66+
issuer: <expected-iss-claim> # optional: reject tokens whose iss claim differs
6567
```
6668

69+
The optional `audience` and `issuer` keys enable audience and issuer claim verification on the standard OIDC/JWKS validation path; when omitted, the `aud` and `iss` claims are not checked. Set them to the values your IdP puts in the token itself, which are not always the ones in the discovery document (see [OIDC Authorization](../../getting-started/components/authz_manager.md#oidc-authorization)). The Secret key `issuer` is distinct from the CR's `issuerUrl`, which selects the discovery endpoint and plays no part in claim verification. Kubernetes ServiceAccount tokens (validated via TokenReview) and intra-server communication follow separate paths and are not subject to these checks.
70+
71+
{% hint style="warning" %}
72+
Before enabling these, three operational caveats:
73+
74+
* **Existing Secret keys take effect on operator upgrade.** Keys named `audience` or `issuer` already present in the referenced Secret were previously ignored; after upgrading they are forwarded to every Feast pod.
75+
* **Your IdP must mint matching tokens for Feast's own clients.** Feast's client-credentials flow requests no audience, so in multi-service topologies (e.g. a remote registry) and for the UI's browser tokens, the IdP must be configured to issue tokens carrying the expected claims (e.g. a Keycloak audience mapper), or inter-service calls will be rejected.
76+
* **Secret edits are not watched.** Changes to these keys apply on the next reconcile or pod restart, not immediately.
77+
{% endhint %}
78+
6779
Reference the Secret from the CR:
6880

6981
```yaml
@@ -92,7 +104,7 @@ authz:
92104
caCertConfigMap: oidc-ca-cert # ConfigMap with CA cert for SSL verification
93105
```
94106

95-
**SDK docs**: [Feast OIDC Auth](../reference/auth/oidc.md)
107+
**SDK docs**: [Feast OIDC Auth](../../getting-started/components/authz_manager.md#oidc-authorization)
96108

97109
---
98110

infra/feast-operator/config/samples/v1_featurestore_oidc_auth.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,7 @@ stringData:
1919
client_secret: client_secret
2020
username: username
2121
password: password
22+
# Optional: enable audience/issuer claim verification on the servers.
23+
# Values must match the claims in the tokens your IdP issues.
24+
# audience: api://feast-feature-server
25+
# issuer: https://idp.example.com/realms/feast

infra/feast-operator/internal/controller/featurestore_controller_oidc_auth_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -493,6 +493,8 @@ func expectedServerOidcAuthorizConfig() services.AuthzConfig {
493493
string(services.OidcClientSecret): "client-secret",
494494
string(services.OidcUsername): "username",
495495
string(services.OidcPassword): "password",
496+
string(services.OidcAudience): "api://feast-feature-server",
497+
string(services.OidcIssuer): "https://keycloak.example.com/realms/test",
496498
},
497499
}
498500
}
@@ -509,6 +511,8 @@ func validOidcSecretMap() map[string]string {
509511
string(services.OidcClientSecret): "client-secret",
510512
string(services.OidcUsername): "username",
511513
string(services.OidcPassword): "password",
514+
string(services.OidcAudience): "api://feast-feature-server",
515+
string(services.OidcIssuer): "https://keycloak.example.com/realms/test",
512516
}
513517
}
514518

infra/feast-operator/internal/controller/services/repo_config.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,17 @@ func getBaseServiceRepoConfig(
138138
}
139139
for _, prop := range OidcOptionalSecretProperties {
140140
if val, exists := secretProperties[string(prop)]; exists {
141+
// Secret values are YAML-parsed on extraction, so an
142+
// all-digits audience or issuer arrives as an int and
143+
// would render unquoted, which the SDK's OidcAuthConfig
144+
// rejects (Optional[str]). Coerce the claim keys back to
145+
// strings; the five original keys keep their historical
146+
// typing.
147+
if prop == OidcAudience || prop == OidcIssuer {
148+
if _, isString := val.(string); !isString {
149+
val = fmt.Sprintf("%v", val)
150+
}
151+
}
141152
oidcParameters[string(prop)] = val
142153
}
143154
}

infra/feast-operator/internal/controller/services/repo_config_test.go

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -212,23 +212,40 @@ var _ = Describe("Repo Config", func() {
212212
string(OidcClientId): clientIDValue,
213213
string(OidcClientSecret): "client-secret",
214214
string(OidcUsername): "username",
215-
string(OidcPassword): "password"})
215+
string(OidcPassword): "password",
216+
string(OidcAudience): "api://feast-feature-server",
217+
string(OidcIssuer): "https://login.example.com/realms/master"})
216218
repoConfig, err = getServiceRepoConfig(featureStore, secretExtractionFunc, emptyMockExtractConfigFromConfigMap, false)
217219
Expect(err).NotTo(HaveOccurred())
218220
Expect(repoConfig.AuthzConfig.Type).To(Equal(OidcAuthType))
219-
Expect(repoConfig.AuthzConfig.OidcParameters).To(HaveLen(5))
221+
Expect(repoConfig.AuthzConfig.OidcParameters).To(HaveLen(7))
220222
Expect(repoConfig.AuthzConfig.OidcParameters).To(HaveKey(string(OidcClientId)))
221223
Expect(repoConfig.AuthzConfig.OidcParameters).To(HaveKey(string(OidcAuthDiscoveryUrl)))
222224
Expect(repoConfig.AuthzConfig.OidcParameters).To(HaveKey(string(OidcClientSecret)))
223225
Expect(repoConfig.AuthzConfig.OidcParameters).To(HaveKey(string(OidcUsername)))
224226
Expect(repoConfig.AuthzConfig.OidcParameters).To(HaveKey(string(OidcPassword)))
227+
Expect(repoConfig.AuthzConfig.OidcParameters).To(HaveKeyWithValue(string(OidcAudience), "api://feast-feature-server"))
228+
Expect(repoConfig.AuthzConfig.OidcParameters).To(HaveKeyWithValue(string(OidcIssuer), "https://login.example.com/realms/master"))
225229
Expect(repoConfig.OfflineStore).To(Equal(expectedOfflineConfig))
226230
Expect(repoConfig.OnlineStore).To(Equal(defaultOnlineStoreConfig(featureStore)))
227231
Expect(repoConfig.Registry).To(Equal(defaultRegistryConfig(featureStore)))
228232

229233
repoConfig = getClientRepoConfig(featureStore, nil)
230234
Expect(repoConfig.AuthzConfig.Type).To(Equal(OidcAuthType))
231235

236+
By("Coercing numeric audience and issuer Secret values to strings")
237+
secretExtractionFunc = mockOidcConfigFromSecret(map[string]interface{}{
238+
string(OidcAuthDiscoveryUrl): "discovery-url",
239+
string(OidcClientId): clientIDValue,
240+
// Secret extraction YAML-parses values, so an all-digits
241+
// audience/issuer reaches this code as an int.
242+
string(OidcAudience): 1234567890,
243+
string(OidcIssuer): 9876543210})
244+
repoConfig, err = getServiceRepoConfig(featureStore, secretExtractionFunc, emptyMockExtractConfigFromConfigMap, false)
245+
Expect(err).NotTo(HaveOccurred())
246+
Expect(repoConfig.AuthzConfig.OidcParameters).To(HaveKeyWithValue(string(OidcAudience), "1234567890"))
247+
Expect(repoConfig.AuthzConfig.OidcParameters).To(HaveKeyWithValue(string(OidcIssuer), "9876543210"))
248+
232249
By("Having oidc authorization with issuerUrl only (no Secret)")
233250
featureStore.Spec.AuthzConfig = &feastdevv1.AuthzConfig{
234251
OidcAuthz: &feastdevv1.OidcAuthz{

infra/feast-operator/internal/controller/services/services_types.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,8 @@ const (
111111
OidcTokenEnvVar OidcPropertyType = "token_env_var"
112112
OidcVerifySsl OidcPropertyType = "verify_ssl"
113113
OidcCaCertPath OidcPropertyType = "ca_cert_path"
114+
OidcAudience OidcPropertyType = "audience"
115+
OidcIssuer OidcPropertyType = "issuer"
114116

115117
OidcMissingSecretError string = "missing OIDC secret: %s"
116118

@@ -274,7 +276,7 @@ var (
274276
},
275277
}
276278

277-
OidcOptionalSecretProperties = []OidcPropertyType{OidcAuthDiscoveryUrl, OidcClientId, OidcClientSecret, OidcUsername, OidcPassword}
279+
OidcOptionalSecretProperties = []OidcPropertyType{OidcAuthDiscoveryUrl, OidcClientId, OidcClientSecret, OidcUsername, OidcPassword, OidcAudience, OidcIssuer}
278280
)
279281

280282
// Feast server types: Reserved only for server types like Online, Offline, and Registry servers. Should not be used for client types like the UI, etc.

0 commit comments

Comments
 (0)