Skip to content

Add OIDC workload identity for Dag processor session credentials - #74700

Open
ephraimbuddy wants to merge 6 commits into
apache:mainfrom
astronomer:dag-processor-memory-credentials
Open

ephraimbuddy wants to merge 6 commits into
apache:mainfrom
astronomer:dag-processor-memory-credentials

Conversation

@ephraimbuddy

@ephraimbuddy ephraimbuddy commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

Allow Dag processor integrations to exchange an OIDC workload identity for short-lived Airflow session tokens kept only in memory. The API server validates the identity and configured bundle grants; existing file-based provisioning remains supported.

This builds on #74370 and adds API and client support. Wiring the standard airflow dag-processor command to this client remains separate work.

Performance check for the follow-up fixes (previous PR head a051dcd460 versus 0f527b0cc1): medians from five alternating runs in Breeze, using the same environment and an unchanged control.

Operation Before After
Cached identity authentication 269.05 µs 253.42 µs
Mock JWKS refresh, without network I/O 138.09 µs 117.09 µs
Unchanged JWT decode control 166.86 µs 166.54 µs

The control changed by 0.19%; no regression was measured. Concurrent test load caused sample variation, so these results do not establish precise production speedups.


Was generative AI tooling used to co-author this PR?
  • Yes — Codex (GPT-6)

Generated-by: Codex (GPT-6) following the guidelines

Comment thread airflow-core/src/airflow/dag_processing/credentials.py
Comment thread airflow-core/src/airflow/dag_processing/credentials.py Outdated
Comment thread airflow-core/docs/security/jwt_token_authentication.rst Outdated
Comment thread airflow-core/docs/security/jwt_token_authentication.rst Outdated
Comment thread airflow-core/src/airflow/api_fastapi/auth/tokens.py
NonemptyString = Annotated[str, Field(min_length=1)]


class WorkloadIdentityConfig(BaseModel):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This takes a single issuer, and grants are keyed on bare sub. Kubernetes issuers are per cluster, so Dag processors in a second cluster (per-team clusters in a multi-team setup, say) can't be trusted by the same API server. Should the top level be a list of issuer entries, each with its own grants, before the option ships and the shape becomes something we have to keep? If single issuer is intended, saying so in the option description would help.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Single issuer is intended for now. The option description and the docs now say so, including that Dag processors whose identities come from different issuers, such as separate Kubernetes clusters, can't all use one API server. Several issuers can be added later without breaking this shape, for example by also accepting a list of these objects, each with its own grants.

def __init__(self, config: WorkloadIdentityConfig, *, client: httpx.AsyncClient):
if config.audience in conf.getlist("execution_api", "jwt_audience"):
raise ValueError("Workload identity must use a dedicated audience")
if unknown := frozenset().union(*config.grants.values()) - get_configured_bundle_names():

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Since the core lifespan enters every sub-app lifespan in one AsyncExitStack, a grant naming an unconfigured bundle stops the whole API server, not just this exchange. Renaming a bundle and forgetting the grant would take down the UI and the Task Execution API that workers heartbeat against. Is that intended, or would logging the error and returning 503 from the exchange be enough?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Not intended; that blast radius is too wide. A grant naming an unconfigured bundle no longer stops startup. The verifier logs an error naming both settings and refuses only that subject's exchanges with a 403. I went with 403 rather than 503 so processors don't spend their retry budget on an error only a config change can fix. Malformed trust config (the JSON shape, or reusing the runtime audience) still fails startup, since it is wrong from the moment it's written. Covered by test_unconfigured_bundle_grant_refuses_only_its_subject.

Comment thread airflow-core/src/airflow/api_fastapi/execution_api/workload_identity.py Outdated
Comment thread airflow-core/tests/unit/api_fastapi/execution_api/test_token_scope_boundaries.py Outdated
@ephraimbuddy
ephraimbuddy force-pushed the dag-processor-memory-credentials branch 3 times, most recently from 0f527b0 to f56f9d2 Compare October 26, 2026 09:38
@ephraimbuddy
ephraimbuddy requested a review from kaxil October 26, 2026 11:47
Dag processor integrations currently need a trusted provisioner to write
and rotate an Airflow session token on disk. Deployments that already
provide renewable workload identities should not need a second credential
file or an Airflow signing key on the processor.

An opt-in identity exchange lets these integrations keep Airflow session
credentials in memory while retaining the existing bundle grants and Job
ownership model. Stable sessions avoid retiring active Jobs when the
platform rotates an identity, and bounded lifetimes keep issued
credentials within the authority of that identity.

This is a follow-up to the Dag processor API foundation. The existing
file-based provisioning path remains supported; wiring the standard Dag
processor command to the HTTP client is separate work.
A grant naming a Dag bundle the API server does not have can only be fixed
by correcting the configuration and restarting. Reporting it as a retryable
503 on every exchange made each processor spend its retry budget on an error
that could never clear, and the cause only surfaced on the processor side.

Components that host the in-process Execution API, such as the Dag processor
manager and the triggerer, can never serve the exchange. They still loaded
its trust settings from shared configuration, so a component with a narrower
bundle list than the API server would have failed to start.

A token with an unknown signing key forces a JWKS refresh, and anyone can
send one. While that refresh waited on a slow identity provider, it also
held up exchanges whose keys were already cached.
The Task SDK client moved to httpx2 while this change was in review, and its
responses, timeouts and errors do not interoperate with httpx objects. The
session token provider is built on that client, so it has to use httpx2
types for its timeouts and error handling to keep working. The API server's
JWKS fetcher still uses httpx, as the rest of its token validation does.
The opt-out stopped the reissue middleware from re-validating the external
workload identity as if it were an Airflow token. The middleware now only
renews tokens that Airflow's own JWT check has already validated, and the
exchange never runs that check, so the opt-out no longer prevents anything.
After a failed cold-cache JWKS fetch the API server waits 10 seconds before
fetching again, and the SDK's default backoff retried every registration
attempt inside that window more than half the time. A brief identity
provider outage therefore failed registration, although the 503 told the
client to retry.

An identity supplier that raises its own exception type, or a JWKS endpoint
that returns something other than a JSON object, turned a recoverable
condition into failed requests, even for keys that were already cached.

A grant naming an unconfigured Dag bundle stopped the whole API server,
taking down the UI and the API that workers heartbeat against, when only the
subject holding that grant needs to be refused. Catching type errors at the
route also reported verifier bugs as invalid identities, when only older
PyJWT releases raise them for malformed time claims.
Processors must stop trusting keys withdrawn by their issuer and recover from temporary identity-provider failures without exhausting retries during its cooldown. Authentication must also remain the boundary for incoming trace context.
@ephraimbuddy
ephraimbuddy force-pushed the dag-processor-memory-credentials branch from f56f9d2 to 287d7f9 Compare October 27, 2026 09:49

This branch has not been deployed

No deployments
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.

2 participants