Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .secrets.baseline
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand Down Expand Up @@ -1564,5 +1564,5 @@
}
]
},
"generated_at": "2026-07-30T09:40:48Z"
"generated_at": "2026-07-30T16:22:49Z"
}
16 changes: 14 additions & 2 deletions docs/how-to-guides/feast-operator/05-security.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

---

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

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:

```yaml
Expand Down Expand Up @@ -92,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)

---

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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",
},
}
}
Expand All @@ -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",
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -212,23 +212,40 @@ 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)))

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{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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.
Expand Down
Loading