Skip to content

feat: Add ConnectionRef to DataSource for pluggable external credential resolution - #6642

Open
ntkathole wants to merge 1 commit into
feast-dev:masterfrom
ntkathole:credentials
Open

feat: Add ConnectionRef to DataSource for pluggable external credential resolution#6642
ntkathole wants to merge 1 commit into
feast-dev:masterfrom
ntkathole:credentials

Conversation

@ntkathole

@ntkathole ntkathole commented Jul 24, 2026

Copy link
Copy Markdown
Member

What this PR does / why we need it:

Feast DataSources today have no mechanism to reference external credentials - authentication relies on ambient environment variables or a single global
offline_store config in feature_store.yaml.

This PR introduces ConnectionRef - which backend to use, how to authenticate, and where to connect - an optional reference on DataSource that points to an external credential store (Kubernetes Secrets, HashiCorp Vault, cloud secret managers, or environment variables). Credentials are resolved
at runtime by a pluggable CredentialProvider interface.

This enables:

  • HybridOfflineStore at runtime: Users configure a HybridOfflineStore via CR/feature_store.yaml, then add new data sources at runtime with their own connection_ref specifying connection_type, credentials, and connection params - no server restart needed.
  • Per-source credential isolation: Each DataSource resolves its own credentials from K8s Secrets, Vault, or other providers, removing the need for a single shared credential set.
  • Self-describing data sources: Non-sensitive connection parameters (account, warehouse, endpoint) live on the DataSource alongside the credential reference, making the source fully portable.

@ntkathole ntkathole self-assigned this Jul 24, 2026
@ntkathole
ntkathole requested a review from a team as a code owner July 24, 2026 11:11
@ntkathole
ntkathole marked this pull request as draft July 24, 2026 11:14
@codecov-commenter

codecov-commenter commented Jul 24, 2026

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 47.87234% with 147 lines in your changes missing coverage. Please review.
✅ Project coverage is 46.81%. Comparing base (39d408d) to head (9c5b573).
⚠️ Report is 1 commits behind head on master.

Files with missing lines Patch % Lines
sdk/python/feast/credentials.py 72.85% 38 Missing ⚠️
sdk/python/feast/infra/offline_stores/snowflake.py 7.14% 26 Missing ⚠️
sdk/python/feast/infra/offline_stores/duckdb.py 0.00% 18 Missing ⚠️
...k/python/feast/infra/offline_stores/file_source.py 11.11% 16 Missing ⚠️
sdk/python/feast/infra/offline_stores/redshift.py 12.50% 14 Missing ⚠️
sdk/python/feast/infra/offline_stores/dask.py 16.66% 8 Missing and 2 partials ⚠️
sdk/python/feast/infra/offline_stores/bigquery.py 10.00% 9 Missing ⚠️
sdk/python/feast/data_source.py 58.33% 3 Missing and 2 partials ⚠️
...ine_stores/contrib/ray_offline_store/ray_source.py 0.00% 2 Missing ⚠️
...ores/contrib/athena_offline_store/athena_source.py 50.00% 1 Missing ⚠️
... and 8 more
❗ Your organization needs to install the Codecov GitHub app to enable full functionality.
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##           master    #6642      +/-   ##
==========================================
+ Coverage   46.79%   46.81%   +0.02%     
==========================================
  Files         415      416       +1     
  Lines       50395    50676     +281     
  Branches     7215     7261      +46     
==========================================
+ Hits        23581    23724     +143     
- Misses      25162    25296     +134     
- Partials     1652     1656       +4     
Flag Coverage Δ
go-feature-server 30.58% <ø> (ø)
python-unit 48.13% <47.87%> (+0.01%) ⬆️
Files with missing lines Coverage Δ
..._sources/contrib/iceberg_catalog/iceberg_source.py 25.00% <100.00%> (+0.75%) ⬆️
...es/contrib/iceberg_catalog/unity_catalog_source.py 33.33% <100.00%> (+1.62%) ⬆️
...thon/feast/infra/offline_stores/bigquery_source.py 61.98% <100.00%> (+0.63%) ⬆️
...thon/feast/infra/offline_stores/redshift_source.py 63.44% <100.00%> (+0.51%) ⬆️
...hon/feast/infra/offline_stores/snowflake_source.py 57.51% <100.00%> (+0.56%) ⬆️
...ores/contrib/athena_offline_store/athena_source.py 45.76% <50.00%> (+0.07%) ⬆️
...trib/clickhouse_offline_store/clickhouse_source.py 55.68% <50.00%> (-0.14%) ⬇️
...ontrib/couchbase_offline_store/couchbase_source.py 42.14% <50.00%> (+0.11%) ⬆️
...ne_stores/contrib/mongodb_offline_store/mongodb.py 72.99% <50.00%> (-0.10%) ⬇️
.../contrib/mssql_offline_store/mssqlserver_source.py 47.82% <50.00%> (+0.04%) ⬆️
... and 13 more

... and 1 file with indirect coverage changes


Continue to review full report in Codecov by Harness.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 39d408d...9c5b573. Read the comment docs.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@ntkathole
ntkathole force-pushed the credentials branch 2 times, most recently from 4f3809d to 948b262 Compare August 4, 2026 05:17
@ntkathole ntkathole changed the title feat: Add CredentialRef to DataSource for pluggable external credential resolution feat: Add ConnectionRef to DataSource for pluggable external credential resolution Aug 4, 2026
@ntkathole
ntkathole force-pushed the credentials branch 4 times, most recently from ed8fece to 35e9afc Compare August 4, 2026 06:26
@ntkathole
ntkathole marked this pull request as ready for review August 4, 2026 08:04
@ntkathole

Copy link
Copy Markdown
Member Author

@jyejare @patelchaitany @aniketpalu @Vperiodt Please review

Comment thread sdk/python/feast/credentials.py

@jyejare jyejare left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR adds ConnectionRef to DataSource for pluggable external credential resolution, enabling per-datasource credentials via external providers like Kubernetes Secrets, HashiCorp Vault, and cloud secret managers. The implementation is well-architected with comprehensive documentation, but has some critical security concerns and missing integration points that need addressing.

Comment on lines +325 to +338
addr: str = field(default_factory=lambda: os.environ.get("VAULT_ADDR", ""))
token: str = field(default_factory=lambda: os.environ.get("VAULT_TOKEN", ""))
role: str = field(default_factory=lambda: os.environ.get("VAULT_ROLE", ""))
auth_method: str = field(
default_factory=lambda: os.environ.get("VAULT_AUTH_METHOD", "token")
)


class VaultProvider(CredentialProvider):
"""Reads credentials from HashiCorp Vault KV v2 secrets engine.

``ref.name`` is the Vault secret path (e.g. ``"secret/data/feast/my-conn"``).
``ref.namespace`` is the Vault mount point (defaults to ``"secret"``).

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] Unsafe credentials returned from Kubernetes secrets without authentication validation

The KubernetesSecretProvider base64-decodes and returns credentials without validating the service account's permissions. This could lead to privilege escalation if a malicious actor can modify the Secret or if the Pod has broader access than intended. The provider should validate that the current service account has explicit read access to the specific Secret before returning credentials.

Suggested:

Suggested change
addr: str = field(default_factory=lambda: os.environ.get("VAULT_ADDR", ""))
token: str = field(default_factory=lambda: os.environ.get("VAULT_TOKEN", ""))
role: str = field(default_factory=lambda: os.environ.get("VAULT_ROLE", ""))
auth_method: str = field(
default_factory=lambda: os.environ.get("VAULT_AUTH_METHOD", "token")
)
class VaultProvider(CredentialProvider):
"""Reads credentials from HashiCorp Vault KV v2 secrets engine.
``ref.name`` is the Vault secret path (e.g. ``"secret/data/feast/my-conn"``).
``ref.namespace`` is the Vault mount point (defaults to ``"secret"``).
# Validate service account has read access to this secret
try:
v1.read_namespaced_secret(name=ref.name, namespace=namespace, exact=True)
except client.exceptions.ApiException as exc:
if exc.status == 403:
raise CredentialResolutionError(
f"Service account lacks read access to Secret '{ref.name}' "
f"in namespace '{namespace}'"
) from exc
raise
return {
key: base64.b64decode(value).decode("utf-8")
for key, value in (secret.data or {}).items()
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It delegates authentication and authorization entirely to the Kubernetes API server, if the service account lacks get permission on secrets in the target namespace, the call returns 403.

Comment on lines +351 to +365
except ImportError as exc:
raise CredentialResolutionError(
"hvac package is required for the 'vault' credential provider. "
"Install it with: pip install hvac"
) from exc

vault_client = hvac.Client(url=self._config.addr, token=self._config.token)
if not vault_client.is_authenticated():
raise CredentialResolutionError(
"Vault client is not authenticated. "
"Set VAULT_ADDR and VAULT_TOKEN, or configure auth_method."
)

mount_point = ref.namespace or "secret"
try:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] VaultProvider exposes all secret data without field-level access control

The VaultProvider returns all key-value pairs from a Vault secret path without any field-level filtering or access validation. This violates the principle of least privilege and could expose unintended credentials. The provider should either accept a list of required fields or implement field-level access validation.

Suggested:

Suggested change
except ImportError as exc:
raise CredentialResolutionError(
"hvac package is required for the 'vault' credential provider. "
"Install it with: pip install hvac"
) from exc
vault_client = hvac.Client(url=self._config.addr, token=self._config.token)
if not vault_client.is_authenticated():
raise CredentialResolutionError(
"Vault client is not authenticated. "
"Set VAULT_ADDR and VAULT_TOKEN, or configure auth_method."
)
mount_point = ref.namespace or "secret"
try:
data = response.get("data", {}).get("data", {})
# Only return explicitly requested fields if specified
if ref.params and "fields" in ref.params:
requested_fields = ref.params["fields"].split(",")
data = {k: v for k, v in data.items() if k in requested_fields}
return {k: str(v) for k, v in data.items()}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this can be nice to have enhancement. This is consistent with how every Vault client operates, it's not a security gap - it's a usability improvement

Comment thread sdk/python/feast/infra/offline_stores/snowflake.py
Comment on lines +276 to +291
self.connection_ref = connection_ref
now = _utc_now()
self.created_timestamp = now
self.last_updated_timestamp = now

def resolve_credentials(self) -> Optional[Dict[str, str]]:
"""Resolve credentials from the ``connection_ref`` if set.

Returns ``None`` when no ``connection_ref`` is configured (ambient
credentials are used instead).
"""
if self.connection_ref is None:
return None
from feast.credentials import resolve_credentials

return resolve_credentials(self.connection_ref)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Warning] Missing credential validation in resolve_credentials method

The resolve_credentials method directly calls the credential provider without any validation or error handling for malformed or missing credentials. This could lead to runtime errors that are difficult to debug. The method should validate the resolved credentials and provide informative error messages.

Suggested:

Suggested change
self.connection_ref = connection_ref
now = _utc_now()
self.created_timestamp = now
self.last_updated_timestamp = now
def resolve_credentials(self) -> Optional[Dict[str, str]]:
"""Resolve credentials from the ``connection_ref`` if set.
Returns ``None`` when no ``connection_ref`` is configured (ambient
credentials are used instead).
"""
if self.connection_ref is None:
return None
from feast.credentials import resolve_credentials
return resolve_credentials(self.connection_ref)
def resolve_credentials(self) -> Optional[Dict[str, str]]:
"""Resolve credentials from the ``connection_ref`` if set.
Returns ``None`` when no ``connection_ref`` is configured (ambient
credentials are used instead).
Raises:
CredentialResolutionError: If credentials cannot be resolved.
"""
if self.connection_ref is None:
return None
from feast.credentials import resolve_credentials, CredentialResolutionError
try:
creds = resolve_credentials(self.connection_ref)
if not creds:
raise CredentialResolutionError(
f"No credentials returned from provider '{self.connection_ref.provider}' "
f"for name '{self.connection_ref.name}'"
)
return creds
except Exception as e:
raise CredentialResolutionError(
f"Failed to resolve credentials for DataSource '{self.name}': {e}"
) from e

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolve_credentials function is intentionally a thin wrapper - it delegates to providers that already handle their own error cases.

Comment on lines +37 to +45
message ConnectionRef {
// Credential provider type: "kubernetes", "vault", "aws-secrets-manager",
// "gcp-secret-manager", "azure-key-vault", "env".
string provider = 1;

// Provider-specific name: K8s Secret name, Vault path, env var prefix, etc.
string name = 2;

// Optional scope qualifier: K8s namespace, Vault mount, AWS region, etc.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] ConnectionRef proto should include validation constraints

The ConnectionRef proto definition lacks validation constraints that could prevent common misconfigurations. Adding field validation (e.g., required provider, valid provider types, namespace format) would catch configuration errors early rather than at runtime. This could be part of next improvement PR though.

Optional Comment!

Suggested:

Suggested change
message ConnectionRef {
// Credential provider type: "kubernetes", "vault", "aws-secrets-manager",
// "gcp-secret-manager", "azure-key-vault", "env".
string provider = 1;
// Provider-specific name: K8s Secret name, Vault path, env var prefix, etc.
string name = 2;
// Optional scope qualifier: K8s namespace, Vault mount, AWS region, etc.
message ConnectionRef {
// Credential provider type: "kubernetes", "vault", "aws-secrets-manager",
// "gcp-secret-manager", "azure-key-vault", "env".
// Required field.
string provider = 1 [(validate.rules).string.min_len = 1];
// Provider-specific name: K8s Secret name, Vault path, env var prefix, etc.
// Required field.
string name = 2 [(validate.rules).string.min_len = 1];

Comment on lines +93 to +110
f"{TAG_PREFIX}provider": self.provider,
f"{TAG_PREFIX}name": self.name,
}
if self.namespace:
tags[f"{TAG_PREFIX}namespace"] = self.namespace
if self.connection_type:
tags[f"{TAG_PREFIX}connection-type"] = self.connection_type
if self.auth_type and self.auth_type != "secret":
tags[f"{TAG_PREFIX}auth-type"] = self.auth_type
for key, value in self.params.items():
tags[f"{TAG_PREFIX}param.{key}"] = value
return tags

@classmethod
def from_tags(cls, tags: Dict[str, str]) -> Optional["ConnectionRef"]:
"""Deserialize from DataSource ``tags``. Returns *None* when no
connection-ref tags are present."""
provider = tags.get(f"{TAG_PREFIX}provider")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] ConnectionRef to_tags serialization should validate tag key limits

The to_tags method doesn't validate that the generated tag keys don't exceed storage limits or contain invalid characters. Some data source backends may have restrictions on tag key length or character sets that could cause silent failures during serialization.

Suggested:

Suggested change
f"{TAG_PREFIX}provider": self.provider,
f"{TAG_PREFIX}name": self.name,
}
if self.namespace:
tags[f"{TAG_PREFIX}namespace"] = self.namespace
if self.connection_type:
tags[f"{TAG_PREFIX}connection-type"] = self.connection_type
if self.auth_type and self.auth_type != "secret":
tags[f"{TAG_PREFIX}auth-type"] = self.auth_type
for key, value in self.params.items():
tags[f"{TAG_PREFIX}param.{key}"] = value
return tags
@classmethod
def from_tags(cls, tags: Dict[str, str]) -> Optional["ConnectionRef"]:
"""Deserialize from DataSource ``tags``. Returns *None* when no
connection-ref tags are present."""
provider = tags.get(f"{TAG_PREFIX}provider")
def to_tags(self) -> Dict[str, str]:
"""Serialize into DataSource ``tags`` dict entries."""
tags: Dict[str, str] = {
f"{TAG_PREFIX}provider": self.provider,
f"{TAG_PREFIX}name": self.name,
}
if self.namespace:
tags[f"{TAG_PREFIX}namespace"] = self.namespace
# Validate tag key lengths (common limit is 128 chars)
for key in tags.keys():
if len(key) > 128:
raise ValueError(f"Tag key too long: {key} (max 128 chars)")
return tags

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The same type used across all Feast objects (FeatureView.tags, Entity.tags, etc.), none of which validate tag keys.

Comment thread sdk/python/feast/infra/offline_stores/bigquery.py
Comment thread sdk/python/feast/credentials.py
Comment on lines 266 to +291

@staticmethod
def create_filesystem_and_path(
path: str, s3_endpoint_override: str
path: str,
s3_endpoint_override: str,
resolved_credentials: Optional[Dict[str, str]] = None,
) -> Tuple[Optional[FileSystem], str]:
if path.startswith("s3://"):
s3fs = S3FileSystem(
endpoint_override=s3_endpoint_override if s3_endpoint_override else None
)
kwargs: Dict[str, Optional[str]] = {}
if s3_endpoint_override:
kwargs["endpoint_override"] = s3_endpoint_override

if resolved_credentials:
access_key = resolved_credentials.get("AWS_ACCESS_KEY_ID", "")
secret_key = resolved_credentials.get("AWS_SECRET_ACCESS_KEY", "")
session_token = resolved_credentials.get("AWS_SESSION_TOKEN")
region = resolved_credentials.get("AWS_DEFAULT_REGION")
if access_key and secret_key:
kwargs["access_key"] = access_key
kwargs["secret_key"] = secret_key
if session_token:
kwargs["session_token"] = session_token
if region:
kwargs["region"] = region

s3fs = S3FileSystem(**kwargs)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] S3 credential handling should support all AWS credential types

The S3FileSystem credential handling only supports access_key/secret_key authentication but AWS supports many other credential types (IAM roles, STS, OIDC, etc.). The implementation should be more flexible to support the full range of AWS authentication methods.

Again this could be the part of future improvements. Or we should at least document that only access_key / secret_key type auth.

Suggested:

Suggested change
@staticmethod
def create_filesystem_and_path(
path: str, s3_endpoint_override: str
path: str,
s3_endpoint_override: str,
resolved_credentials: Optional[Dict[str, str]] = None,
) -> Tuple[Optional[FileSystem], str]:
if path.startswith("s3://"):
s3fs = S3FileSystem(
endpoint_override=s3_endpoint_override if s3_endpoint_override else None
)
kwargs: Dict[str, Optional[str]] = {}
if s3_endpoint_override:
kwargs["endpoint_override"] = s3_endpoint_override
if resolved_credentials:
access_key = resolved_credentials.get("AWS_ACCESS_KEY_ID", "")
secret_key = resolved_credentials.get("AWS_SECRET_ACCESS_KEY", "")
session_token = resolved_credentials.get("AWS_SESSION_TOKEN")
region = resolved_credentials.get("AWS_DEFAULT_REGION")
if access_key and secret_key:
kwargs["access_key"] = access_key
kwargs["secret_key"] = secret_key
if session_token:
kwargs["session_token"] = session_token
if region:
kwargs["region"] = region
s3fs = S3FileSystem(**kwargs)
if resolved_credentials:
# Support multiple AWS credential formats
access_key = resolved_credentials.get("AWS_ACCESS_KEY_ID") or resolved_credentials.get("access_key_id")
secret_key = resolved_credentials.get("AWS_SECRET_ACCESS_KEY") or resolved_credentials.get("secret_access_key")
session_token = resolved_credentials.get("AWS_SESSION_TOKEN") or resolved_credentials.get("session_token")
region = resolved_credentials.get("AWS_DEFAULT_REGION") or resolved_credentials.get("region")
if access_key and secret_key:
kwargs["access_key"] = access_key
kwargs["secret_key"] = secret_key
# Support role-based authentication
elif "role_arn" in resolved_credentials:
kwargs["role_arn"] = resolved_credentials["role_arn"]
if "external_id" in resolved_credentials:
kwargs["external_id"] = resolved_credentials["external_id"]

@jyejare jyejare left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR introduces a well-architected ConnectionRef feature for pluggable external credential resolution in Feast DataSources. The implementation follows good patterns with comprehensive documentation, built-in providers for Kubernetes/Vault/environment variables, and proper integration across all data source types. However, there are several critical security concerns around credential handling and some missing test coverage.

Comment thread sdk/python/feast/credentials.py
Comment thread sdk/python/feast/credentials.py Outdated
Comment thread sdk/python/feast/credentials.py
…al resolution

Signed-off-by: ntkathole <nikhilkathole2683@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants