From 28e3609f9c8b764536254d3cc9aa089950dba810 Mon Sep 17 00:00:00 2001 From: Larry Singleton <166439969+larrysingleton007@users.noreply.github.com> Date: Thu, 30 Jul 2026 10:10:02 -0500 Subject: [PATCH 1/3] 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> --- docs/how-to-guides/feast-operator/05-security.md | 4 ++++ .../internal/controller/services/repo_config_test.go | 8 ++++++-- .../internal/controller/services/services_types.go | 4 +++- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/docs/how-to-guides/feast-operator/05-security.md b/docs/how-to-guides/feast-operator/05-security.md index 4c28aba9805..996d50890dd 100644 --- a/docs/how-to-guides/feast-operator/05-security.md +++ b/docs/how-to-guides/feast-operator/05-security.md @@ -62,8 +62,12 @@ stringData: client_secret: username: # used for client-credentials flow password: + audience: # optional: reject tokens whose aud claim differs + issuer: # optional: reject tokens whose iss claim differs ``` +The optional `audience` and `issuer` keys enable strict claim verification on the feature server; when omitted, only signature and expiry are verified. 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)). + Reference the Secret from the CR: ```yaml diff --git a/infra/feast-operator/internal/controller/services/repo_config_test.go b/infra/feast-operator/internal/controller/services/repo_config_test.go index 89941c6beb5..519b0eb6536 100644 --- a/infra/feast-operator/internal/controller/services/repo_config_test.go +++ b/infra/feast-operator/internal/controller/services/repo_config_test.go @@ -212,16 +212,20 @@ var _ = Describe("Repo Config", func() { string(OidcClientId): clientIDValue, string(OidcClientSecret): "client-secret", string(OidcUsername): "username", - string(OidcPassword): "password"}) + string(OidcPassword): "password", + string(OidcAudience): "api://feast-feature-server", + string(OidcIssuer): "https://login.example.com/realms/master"}) repoConfig, err = getServiceRepoConfig(featureStore, secretExtractionFunc, emptyMockExtractConfigFromConfigMap, false) Expect(err).NotTo(HaveOccurred()) Expect(repoConfig.AuthzConfig.Type).To(Equal(OidcAuthType)) - Expect(repoConfig.AuthzConfig.OidcParameters).To(HaveLen(5)) + Expect(repoConfig.AuthzConfig.OidcParameters).To(HaveLen(7)) Expect(repoConfig.AuthzConfig.OidcParameters).To(HaveKey(string(OidcClientId))) Expect(repoConfig.AuthzConfig.OidcParameters).To(HaveKey(string(OidcAuthDiscoveryUrl))) Expect(repoConfig.AuthzConfig.OidcParameters).To(HaveKey(string(OidcClientSecret))) Expect(repoConfig.AuthzConfig.OidcParameters).To(HaveKey(string(OidcUsername))) Expect(repoConfig.AuthzConfig.OidcParameters).To(HaveKey(string(OidcPassword))) + Expect(repoConfig.AuthzConfig.OidcParameters).To(HaveKeyWithValue(string(OidcAudience), "api://feast-feature-server")) + Expect(repoConfig.AuthzConfig.OidcParameters).To(HaveKeyWithValue(string(OidcIssuer), "https://login.example.com/realms/master")) Expect(repoConfig.OfflineStore).To(Equal(expectedOfflineConfig)) Expect(repoConfig.OnlineStore).To(Equal(defaultOnlineStoreConfig(featureStore))) Expect(repoConfig.Registry).To(Equal(defaultRegistryConfig(featureStore))) diff --git a/infra/feast-operator/internal/controller/services/services_types.go b/infra/feast-operator/internal/controller/services/services_types.go index 06e614de087..f9b3303e3de 100644 --- a/infra/feast-operator/internal/controller/services/services_types.go +++ b/infra/feast-operator/internal/controller/services/services_types.go @@ -111,6 +111,8 @@ const ( OidcTokenEnvVar OidcPropertyType = "token_env_var" OidcVerifySsl OidcPropertyType = "verify_ssl" OidcCaCertPath OidcPropertyType = "ca_cert_path" + OidcAudience OidcPropertyType = "audience" + OidcIssuer OidcPropertyType = "issuer" OidcMissingSecretError string = "missing OIDC secret: %s" @@ -274,7 +276,7 @@ var ( }, } - OidcOptionalSecretProperties = []OidcPropertyType{OidcAuthDiscoveryUrl, OidcClientId, OidcClientSecret, OidcUsername, OidcPassword} + OidcOptionalSecretProperties = []OidcPropertyType{OidcAuthDiscoveryUrl, OidcClientId, OidcClientSecret, OidcUsername, OidcPassword, OidcAudience, OidcIssuer} ) // 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. From 543f9d05334f5eba64ed867334837f4860296cf5 Mon Sep 17 00:00:00 2001 From: Larry Singleton <166439969+larrysingleton007@users.noreply.github.com> Date: Thu, 30 Jul 2026 11:23:03 -0500 Subject: [PATCH 2/3] 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> --- .secrets.baseline | 4 ++-- .../featurestore_controller_oidc_auth_test.go | 4 ++++ .../internal/controller/services/repo_config.go | 11 +++++++++++ .../controller/services/repo_config_test.go | 13 +++++++++++++ 4 files changed, 30 insertions(+), 2 deletions(-) diff --git a/.secrets.baseline b/.secrets.baseline index d2838a1317f..a0536178ad8 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -1172,7 +1172,7 @@ "filename": "infra/feast-operator/internal/controller/services/repo_config.go", "hashed_secret": "e2fb052132fd6a07a56af2013e0b62a1f510572c", "is_verified": false, - "line_number": 224 + "line_number": 235 } ], "infra/feast-operator/internal/controller/services/services.go": [ @@ -1564,5 +1564,5 @@ } ] }, - "generated_at": "2026-07-30T09:40:48Z" + "generated_at": "2026-07-30T16:22:49Z" } diff --git a/infra/feast-operator/internal/controller/featurestore_controller_oidc_auth_test.go b/infra/feast-operator/internal/controller/featurestore_controller_oidc_auth_test.go index e15f8ecfa8a..0f99f6d0479 100644 --- a/infra/feast-operator/internal/controller/featurestore_controller_oidc_auth_test.go +++ b/infra/feast-operator/internal/controller/featurestore_controller_oidc_auth_test.go @@ -493,6 +493,8 @@ func expectedServerOidcAuthorizConfig() services.AuthzConfig { string(services.OidcClientSecret): "client-secret", string(services.OidcUsername): "username", string(services.OidcPassword): "password", + string(services.OidcAudience): "api://feast-feature-server", + string(services.OidcIssuer): "https://keycloak.example.com/realms/test", }, } } @@ -509,6 +511,8 @@ func validOidcSecretMap() map[string]string { string(services.OidcClientSecret): "client-secret", string(services.OidcUsername): "username", string(services.OidcPassword): "password", + string(services.OidcAudience): "api://feast-feature-server", + string(services.OidcIssuer): "https://keycloak.example.com/realms/test", } } diff --git a/infra/feast-operator/internal/controller/services/repo_config.go b/infra/feast-operator/internal/controller/services/repo_config.go index 5df6bba3fbc..b4a01b312b1 100644 --- a/infra/feast-operator/internal/controller/services/repo_config.go +++ b/infra/feast-operator/internal/controller/services/repo_config.go @@ -138,6 +138,17 @@ func getBaseServiceRepoConfig( } for _, prop := range OidcOptionalSecretProperties { if val, exists := secretProperties[string(prop)]; exists { + // Secret values are YAML-parsed on extraction, so an + // all-digits audience or issuer arrives as an int and + // would render unquoted, which the SDK's OidcAuthConfig + // rejects (Optional[str]). Coerce the claim keys back to + // strings; the five original keys keep their historical + // typing. + if prop == OidcAudience || prop == OidcIssuer { + if _, isString := val.(string); !isString { + val = fmt.Sprintf("%v", val) + } + } oidcParameters[string(prop)] = val } } diff --git a/infra/feast-operator/internal/controller/services/repo_config_test.go b/infra/feast-operator/internal/controller/services/repo_config_test.go index 519b0eb6536..e87efdf7dec 100644 --- a/infra/feast-operator/internal/controller/services/repo_config_test.go +++ b/infra/feast-operator/internal/controller/services/repo_config_test.go @@ -233,6 +233,19 @@ var _ = Describe("Repo Config", func() { repoConfig = getClientRepoConfig(featureStore, nil) Expect(repoConfig.AuthzConfig.Type).To(Equal(OidcAuthType)) + By("Coercing numeric audience and issuer Secret values to strings") + secretExtractionFunc = mockOidcConfigFromSecret(map[string]interface{}{ + string(OidcAuthDiscoveryUrl): "discovery-url", + string(OidcClientId): clientIDValue, + // Secret extraction YAML-parses values, so an all-digits + // audience/issuer reaches this code as an int. + string(OidcAudience): 1234567890, + string(OidcIssuer): 9876543210}) + repoConfig, err = getServiceRepoConfig(featureStore, secretExtractionFunc, emptyMockExtractConfigFromConfigMap, false) + Expect(err).NotTo(HaveOccurred()) + Expect(repoConfig.AuthzConfig.OidcParameters).To(HaveKeyWithValue(string(OidcAudience), "1234567890")) + Expect(repoConfig.AuthzConfig.OidcParameters).To(HaveKeyWithValue(string(OidcIssuer), "9876543210")) + By("Having oidc authorization with issuerUrl only (no Secret)") featureStore.Spec.AuthzConfig = &feastdevv1.AuthzConfig{ OidcAuthz: &feastdevv1.OidcAuthz{ From d8ef562be3ad28322c89a23f52cc7d6c1ab0a1a2 Mon Sep 17 00:00:00 2001 From: Larry Singleton <166439969+larrysingleton007@users.noreply.github.com> Date: Thu, 30 Jul 2026 11:23:05 -0500 Subject: [PATCH 3/3] 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> --- docs/how-to-guides/feast-operator/05-security.md | 14 +++++++++++--- .../config/samples/v1_featurestore_oidc_auth.yaml | 4 ++++ 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/docs/how-to-guides/feast-operator/05-security.md b/docs/how-to-guides/feast-operator/05-security.md index 996d50890dd..002c3a9f846 100644 --- a/docs/how-to-guides/feast-operator/05-security.md +++ b/docs/how-to-guides/feast-operator/05-security.md @@ -39,7 +39,7 @@ to subjects using standard Kubernetes `ClusterRoleBinding` or `RoleBinding` reso > Kubernetes auth requires all services to be exposed as servers (the controller rejects > partial configurations where some services are local while RBAC is enabled). -**SDK docs**: [Feast RBAC](../reference/auth/rbac.md) +**SDK docs**: [Feast RBAC](../../getting-started/architecture/rbac.md) --- @@ -66,7 +66,15 @@ stringData: issuer: # optional: reject tokens whose iss claim differs ``` -The optional `audience` and `issuer` keys enable strict claim verification on the feature server; when omitted, only signature and expiry are verified. 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 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. + +{% hint style="warning" %} +Before enabling these, three operational caveats: + +* **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. +* **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. +* **Secret edits are not watched.** Changes to these keys apply on the next reconcile or pod restart, not immediately. +{% endhint %} Reference the Secret from the CR: @@ -96,7 +104,7 @@ authz: caCertConfigMap: oidc-ca-cert # ConfigMap with CA cert for SSL verification ``` -**SDK docs**: [Feast OIDC Auth](../reference/auth/oidc.md) +**SDK docs**: [Feast OIDC Auth](../../getting-started/components/authz_manager.md#oidc-authorization) --- diff --git a/infra/feast-operator/config/samples/v1_featurestore_oidc_auth.yaml b/infra/feast-operator/config/samples/v1_featurestore_oidc_auth.yaml index 7ef676d0297..97f325bb0bd 100644 --- a/infra/feast-operator/config/samples/v1_featurestore_oidc_auth.yaml +++ b/infra/feast-operator/config/samples/v1_featurestore_oidc_auth.yaml @@ -19,3 +19,7 @@ stringData: client_secret: client_secret username: username password: password + # Optional: enable audience/issuer claim verification on the servers. + # Values must match the claims in the tokens your IdP issues. + # audience: api://feast-feature-server + # issuer: https://idp.example.com/realms/feast