Skip to content

Hide team scoped values of options registered as sensitive - #70755

Merged
vatsrahul1001 merged 5 commits into
apache:mainfrom
potiuk:hide-team-scoped-sensitive-config-values
Aug 4, 2026
Merged

Hide team scoped values of options registered as sensitive#70755
vatsrahul1001 merged 5 commits into
apache:mainfrom
potiuk:hide-team-scoped-sensitive-config-values

Conversation

@potiuk

@potiuk potiuk commented Jul 30, 2026

Copy link
Copy Markdown
Member

Options are registered as sensitive under their base section, but a team scoped
override lives elsewhere:

  • config file — a [<team>=<section>] section
  • environment — AIRFLOW__<TEAM>___<SECTION>__<KEY>

Every sensitivity decision was a direct membership test against the registered base
pairs, and the masking pass iterated those pairs and looked each section up
verbatim. A team scoped section was therefore never visited, and its value was
returned in clear text while the same option in the base section was correctly
hidden.

The environment spelling misses by a further step: _include_envs splits
AIRFLOW__TEAM_A___CELERY__BROKER_URL on __ with maxsplit=2, yielding section
team_a and key _celery__broker_url — a pair that is neither the base nor the
team scoped section name, so no base keyed loop could reach it either.

Approach

Sensitivity decisions now resolve the team scoped spelling back to the base option.

  • team_section_name() builds the config file section name, and is used at both
    construction sites so the two representations cannot drift
  • base_section_name() recovers the base section from it
  • is_sensitive_option() tests a pair directly, then via the base section, then via
    the tail an environment variable contributes — so it can only ever recognise
    more options as sensitive, never fewer

Two deliberate choices:

The team name is never parsed out of an environment variable name. A team name
may contain underscores, so splitting AIRFLOW__<TEAM>___<SECTION>__<KEY> is
ambiguous. The name is matched against the tail each registered sensitive option
contributes instead. No section ends with _ or . and no key starts with _
anywhere in core or providers, so this cannot collide with a global variable.

The config file section is split on the last separator. Base section names
never contain = (checked across all sections in config.yml and every config:
block in every provider.yaml), so the base section is recovered even for a team
name that contains the separator itself. airflow teams sync creates teams from
dag bundle config, whose team_name field is not validated against the CLI's
charset, so such a name is conceivable.

routes/public/config.py needed its own two line change: get_config_value never
goes through as_dict, so the parser change cannot reach it.

_get_cmd_option_from_config_sources and _get_secret_option_from_config_sources
are intentionally left alone. They are only called with base pairs, and making them
team aware would mean executing team scoped commands and fetching team secrets,
which the parser deliberately refuses. Those fallbacks are hidden instead.

Behaviour changes

  1. Team scoped values of sensitive options now read < hidden > in as_dict,
    GET /config, GET /config/section/{section}/option/{option} and
    write(hide_sensitive=True) — symmetric with how the base option already
    behaved. Anyone using the API to read a team's real value will now get the mask.
  2. Team scoped _cmd / _secret entries are replaced with < hidden > in place
    rather than resolved and deleted as in a base section, since resolution is not
    supported for a team. The command string itself is no longer shown.
  3. write() masking now covers team sections, so airflow config list is included.
  4. Additive public API on the shared parser; no signature changes.
  5. No change for non team configuration, and display_sensitive=True still returns
    real values.

as_dict cost 0.62 ms versus 0.48 ms baseline at 750 options with 40 sensitive
values.

Test plan

  • shared/configuration/tests/configuration/test_parser.py — 6 tests: base_section_name parametrized including a separator inside the team name; is_sensitive_option asserting spoofing fails in both directions; as_dict for a file backed team section, with display_source and display_sensitive=True variants; team _cmd/_secret fallbacks; team environment variable; write()
  • 68/68 pass in that module; the behavioural ones fail against the unmodified parser
  • airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_config.pyTestTeamScopedOptionMasking, covering the single option path masked and a non sensitive team scoped option still readable
  • ruff check and format clean; mypy reports the same single pre-existing error as baseline

Deferred

The synthetic per-key secrets-backend options in api_fastapi/core_api/services/public/config.py match literal section names only, so they stay unaware of a team scoped spelling. Nothing leaks today because _get_custom_secret_backend is not team aware; tracked in #71037, and linked from a comment at that site.

Was generative AI tooling used to co-author this PR?
  • Yes — Claude Opus 5 (1M context)

Generated-by: Claude Opus 5 (1M context) following the guidelines at
https://github.com/apache/airflow/blob/main/contributing-docs/05_pull_requests.rst#gen-ai-assisted-contributions

@potiuk potiuk added this to the Airflow 3.3.1 milestone Jul 30, 2026
@potiuk potiuk added the backport-to-v3-3-test Backport to v3-3-test label Jul 30, 2026

@amoghrajesh amoghrajesh left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Separate issue found while reviewing: https://github.com/apache/airflow/blob/main/airflow-core/src/airflow/api_fastapi/core_api/services/public/config.py#L40-L59 only matches literal secrets/workers section names, not their team scoped spelling. Same root cause, different mechanism, untouched here. _get_custom_secret_backend is not team aware either so nothing real leaks today but worth a follow up (cc: @vincbeck / @o-nikolas)

Although I am ok with this PR, would like either @vincbeck or @o-nikolas to review as well.

Comment thread shared/configuration/src/airflow_shared/configuration/parser.py
Comment thread shared/configuration/tests/configuration/test_parser.py
potiuk added 3 commits August 4, 2026 01:38
Options are registered as sensitive under their base section, but a team scoped
override lives in a `[<team>=<section>]` config file section, or in an
`AIRFLOW__<TEAM>___<SECTION>__<KEY>` environment variable. Every sensitivity
decision was a direct membership test against the registered base pairs, and the
masking pass iterated those pairs and looked each section up verbatim, so a team
scoped section was never visited and its value was returned in clear.

Resolve the team scoped spelling back to the base option before deciding whether
a value is sensitive. `team_section_name` now builds the config file section name
at both construction sites so the two representations cannot drift,
`base_section_name` recovers the base section from it, and `is_sensitive_option`
tests a pair directly, then via the base section, then via the tail an
environment variable contributes -- so it can only ever recognise more options as
sensitive, never fewer.

The team name is not parsed out of an environment variable name, because a team
name may contain underscores; the name is matched against the tail each
registered option contributes instead. The config file section name is split on
the last separator, so the base section is recovered even for a team name that
contains the separator itself.

Four call sites use the predicate: the masking pass, the environment collection,
`write`, and the single option config route, which does not go through `as_dict`
and so needed its own change.

Team scoped `_cmd` and `_secret` entries are hidden in place rather than resolved
into their value, because resolving them is not supported for a team.

Generated-by: Claude Opus 5 (1M context) following the guidelines at
https://github.com/apache/airflow/blob/main/contributing-docs/05_pull_requests.rst#gen-ai-assisted-contributions
…touched gap

The per-key secrets-backend options are matched by literal section name and stay
unaware of a team scoped spelling. Nothing leaks while the secrets backend itself
is not team aware, so it is recorded rather than fixed here.
@potiuk
potiuk force-pushed the hide-team-scoped-sensitive-config-values branch from d782298 to 9429d55 Compare August 3, 2026 23:42
@potiuk

potiuk commented Aug 4, 2026

Copy link
Copy Markdown
Member Author

Both threads addressed and resolved — duplicated rationale trimmed to one site in each case.

The separate gap you spotted in services/public/config.py is filed as #71037 and linked from a comment at that site; it is unreachable today since the secrets backend is not team aware, so it is recorded rather than fixed here.

@amoghrajesh


Drafted-by: Claude Code (Opus 5); reviewed by @potiuk before posting

@potiuk
potiuk requested a review from amoghrajesh August 4, 2026 00:48

@amoghrajesh amoghrajesh left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I have a qn and few nits.

Comment thread shared/configuration/tests/configuration/test_parser.py Outdated
Comment thread airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_config.py Outdated
Comment thread shared/configuration/src/airflow_shared/configuration/parser.py
potiuk and others added 2 commits August 4, 2026 13:36
Co-authored-by: Amogh Desai <amoghrajesh1999@gmail.com>
…t_config.py

Co-authored-by: Amogh Desai <amoghrajesh1999@gmail.com>
@vatsrahul1001
vatsrahul1001 merged commit d41ac7b into apache:main Aug 4, 2026
84 checks passed
@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Backport successfully created: v3-3-test

Note: As of Merging PRs targeted for Airflow 3.X
the committer who merges the PR is responsible for backporting the PRs that are bug fixes (generally speaking) to the maintenance branches.

In matter of doubt please ask in #release-management Slack channel.

Status Branch Result
v3-3-test PR Link

github-actions Bot pushed a commit to aws-mwaa/upstream-to-airflow that referenced this pull request Aug 4, 2026
apache#70755)

* Hide team scoped values of options registered as sensitive

Options are registered as sensitive under their base section, but a team scoped
override lives in a `[<team>=<section>]` config file section, or in an
`AIRFLOW__<TEAM>___<SECTION>__<KEY>` environment variable. Every sensitivity
decision was a direct membership test against the registered base pairs, and the
masking pass iterated those pairs and looked each section up verbatim, so a team
scoped section was never visited and its value was returned in clear.

Resolve the team scoped spelling back to the base option before deciding whether
a value is sensitive. `team_section_name` now builds the config file section name
at both construction sites so the two representations cannot drift,
`base_section_name` recovers the base section from it, and `is_sensitive_option`
tests a pair directly, then via the base section, then via the tail an
environment variable contributes -- so it can only ever recognise more options as
sensitive, never fewer.

The team name is not parsed out of an environment variable name, because a team
name may contain underscores; the name is matched against the tail each
registered option contributes instead. The config file section name is split on
the last separator, so the base section is recovered even for a team name that
contains the separator itself.

Four call sites use the predicate: the masking pass, the environment collection,
`write`, and the single option config route, which does not go through `as_dict`
and so needed its own change.

Team scoped `_cmd` and `_secret` entries are hidden in place rather than resolved
into their value, because resolving them is not supported for a team.

Generated-by: Claude Opus 5 (1M context) following the guidelines at
https://github.com/apache/airflow/blob/main/contributing-docs/05_pull_requests.rst#gen-ai-assisted-contributions

* Add newsfragment for the team scoped sensitive option masking change

* Keep one copy of the team scoped masking rationale, and record the untouched gap

The per-key secrets-backend options are matched by literal section name and stay
unaware of a team scoped spelling. Nothing leaks while the secrets backend itself
is not team aware, so it is recorded rather than fixed here.

* Update shared/configuration/tests/configuration/test_parser.py

Co-authored-by: Amogh Desai <amoghrajesh1999@gmail.com>

* Update airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_config.py

Co-authored-by: Amogh Desai <amoghrajesh1999@gmail.com>

---------
(cherry picked from commit d41ac7b)

Co-authored-by: Jarek Potiuk <jarek@potiuk.com>
Co-authored-by: Amogh Desai <amoghrajesh1999@gmail.com>
aws-airflow-bot pushed a commit to aws-mwaa/upstream-to-airflow that referenced this pull request Aug 4, 2026
apache#70755)

* Hide team scoped values of options registered as sensitive

Options are registered as sensitive under their base section, but a team scoped
override lives in a `[<team>=<section>]` config file section, or in an
`AIRFLOW__<TEAM>___<SECTION>__<KEY>` environment variable. Every sensitivity
decision was a direct membership test against the registered base pairs, and the
masking pass iterated those pairs and looked each section up verbatim, so a team
scoped section was never visited and its value was returned in clear.

Resolve the team scoped spelling back to the base option before deciding whether
a value is sensitive. `team_section_name` now builds the config file section name
at both construction sites so the two representations cannot drift,
`base_section_name` recovers the base section from it, and `is_sensitive_option`
tests a pair directly, then via the base section, then via the tail an
environment variable contributes -- so it can only ever recognise more options as
sensitive, never fewer.

The team name is not parsed out of an environment variable name, because a team
name may contain underscores; the name is matched against the tail each
registered option contributes instead. The config file section name is split on
the last separator, so the base section is recovered even for a team name that
contains the separator itself.

Four call sites use the predicate: the masking pass, the environment collection,
`write`, and the single option config route, which does not go through `as_dict`
and so needed its own change.

Team scoped `_cmd` and `_secret` entries are hidden in place rather than resolved
into their value, because resolving them is not supported for a team.

Generated-by: Claude Opus 5 (1M context) following the guidelines at
https://github.com/apache/airflow/blob/main/contributing-docs/05_pull_requests.rst#gen-ai-assisted-contributions

* Add newsfragment for the team scoped sensitive option masking change

* Keep one copy of the team scoped masking rationale, and record the untouched gap

The per-key secrets-backend options are matched by literal section name and stay
unaware of a team scoped spelling. Nothing leaks while the secrets backend itself
is not team aware, so it is recorded rather than fixed here.

* Update shared/configuration/tests/configuration/test_parser.py

Co-authored-by: Amogh Desai <amoghrajesh1999@gmail.com>

* Update airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_config.py

Co-authored-by: Amogh Desai <amoghrajesh1999@gmail.com>

---------
(cherry picked from commit d41ac7b)

Co-authored-by: Jarek Potiuk <jarek@potiuk.com>
Co-authored-by: Amogh Desai <amoghrajesh1999@gmail.com>
vatsrahul1001 pushed a commit that referenced this pull request Aug 4, 2026
#70755) (#71099)

* Hide team scoped values of options registered as sensitive

Options are registered as sensitive under their base section, but a team scoped
override lives in a `[<team>=<section>]` config file section, or in an
`AIRFLOW__<TEAM>___<SECTION>__<KEY>` environment variable. Every sensitivity
decision was a direct membership test against the registered base pairs, and the
masking pass iterated those pairs and looked each section up verbatim, so a team
scoped section was never visited and its value was returned in clear.

Resolve the team scoped spelling back to the base option before deciding whether
a value is sensitive. `team_section_name` now builds the config file section name
at both construction sites so the two representations cannot drift,
`base_section_name` recovers the base section from it, and `is_sensitive_option`
tests a pair directly, then via the base section, then via the tail an
environment variable contributes -- so it can only ever recognise more options as
sensitive, never fewer.

The team name is not parsed out of an environment variable name, because a team
name may contain underscores; the name is matched against the tail each
registered option contributes instead. The config file section name is split on
the last separator, so the base section is recovered even for a team name that
contains the separator itself.

Four call sites use the predicate: the masking pass, the environment collection,
`write`, and the single option config route, which does not go through `as_dict`
and so needed its own change.

Team scoped `_cmd` and `_secret` entries are hidden in place rather than resolved
into their value, because resolving them is not supported for a team.

Generated-by: Claude Opus 5 (1M context) following the guidelines at
https://github.com/apache/airflow/blob/main/contributing-docs/05_pull_requests.rst#gen-ai-assisted-contributions

* Add newsfragment for the team scoped sensitive option masking change

* Keep one copy of the team scoped masking rationale, and record the untouched gap

The per-key secrets-backend options are matched by literal section name and stay
unaware of a team scoped spelling. Nothing leaks while the secrets backend itself
is not team aware, so it is recorded rather than fixed here.

* Update shared/configuration/tests/configuration/test_parser.py



* Update airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_config.py



---------
(cherry picked from commit d41ac7b)

Co-authored-by: Jarek Potiuk <jarek@potiuk.com>
Co-authored-by: Amogh Desai <amoghrajesh1999@gmail.com>
abhishekmauryaKsolves pushed a commit to abhishekmauryaKsolves/airflow that referenced this pull request Aug 5, 2026
… as sensitive

_is_per_key_sensitive_option and _mask_per_key_sensitive_options matched
literal section names ('secrets', 'workers') only, so a team scoped
spelling of the same option -- the [<team>=secrets] config-file section,
or the AIRFLOW__<TEAM>___SECRETS__BACKEND_KWARG__* env var, both of which
are reported under a section named after the team -- was never recognised
as sensitive.

Resolve the section to its base section via base_section_name (the same
helper AirflowConfigParser.is_sensitive_option uses for registered options,
introduced in apache#70755) before deciding sensitivity, in both
_is_per_key_sensitive_option and _mask_per_key_sensitive_options.

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

Labels

area:API Airflow's REST/HTTP API backport-to-v3-3-test Backport to v3-3-test

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants