Skip to content

Refuse Akeyless secret ids that address another team's namespace - #72646

Merged
eladkal merged 1 commit into
apache:mainfrom
potiuk:security/akeyless-team-scope-guard
Sep 10, 2026
Merged

eladkal merged 1 commit into
apache:mainfrom
potiuk:security/akeyless-team-scope-guard

Conversation

@potiuk

@potiuk potiuk commented Sep 7, 2026

Copy link
Copy Markdown
Member

Akeyless secret names are built by joining <base path><sep><key>, and the
key was not validated.

In multi-team mode the team-scoped lookup is tried first and, when it misses,
the team-agnostic fallback resolves <base path><sep><key> — which is the
prefix every other team's secrets are stored under. A caller in team alpha
requesting the key beta/db_password therefore reached team beta's secret.
The key is Dag-author controlled and the execution API variables route is
declared with a :path converter, so a separator survives the round trip.

Scope of the refusal

The separator here is the ordinary path separator and nested keys are a
documented layout, so the refusal is kept as narrow as the defect — it applies
only where this backend actually crosses a namespace (multi-team on,
team-scoped paths in use, and a team name supplied):

  • use_team_secrets_path=False builds no team path, so nothing is refused and
    a flat nested layout keeps working under multi-team mode.
  • get_config() takes no team name, and Airflow does not perform team-scoped
    config lookups through a secrets backend, so that path has no boundary to
    cross and is not guarded. Subfolder config layouts keep resolving.
  • A caller with no team name resolves in the shared namespace directly rather
    than falling back into it.

The key is never parsed to determine which team it names, because it cannot
be — nothing distinguishes a nested key in the shared namespace from one
naming another team.

Also in this change

core.multi_team is now read with getboolean. It was read with conf.get,
which returns the string "False" — truthy — so the multi-team branches were
selected even with multi-team disabled.

Tests

The escape tests wire the backend so the cross-team path would return a
value, and assert that it does not come back — rather than asserting that a
guard ran. Against unpatched sources they fail with
assert 'beta-secret' is None.

Compatibility is covered too: nested config ids under multi-team, nested keys
with use_team_secrets_path=False, nested keys for a caller with no team, and
ordinary team-scoped lookups.


Was generative AI tooling used to co-author this PR?
  • Yes (please specify the tool below)

Generated-by: Claude Code following the guidelines

Secret names are built by joining <base path><sep><key>, and the key was not
validated. In multi-team mode the team-scoped lookup is tried first and, when
it misses, the team-agnostic fallback resolves <base path><sep><key> -- the
prefix under which every other team's secrets are stored. A caller in team
alpha requesting the key 'beta/db_password' therefore reached team beta's
secret. The key is Dag-author controlled and the execution API variables route
is declared with a ':path' converter, so a separator survives the round trip.

Refuse such a key only where this backend actually crosses a namespace: when
multi-team mode is on, team-scoped paths are in use, and a team name was
supplied. The separator here is the ordinary path separator and nested keys
are a documented layout, so the refusal is kept as narrow as the defect:

* use_team_secrets_path=False builds no team path, so nothing is refused and
  a flat nested layout keeps working under multi-team mode.
* get_config takes no team name and Airflow does not perform team-scoped
  config lookups through a secrets backend, so that path has no boundary to
  cross and is not guarded; subfolder config layouts keep resolving.
* A caller with no team name resolves in the shared namespace directly rather
  than falling back into it. Whether global scope should be able to name a
  team's namespace is a separate question and is not decided here.

The key is never parsed to determine which team it names, because it cannot
be -- nothing distinguishes a nested key in the shared namespace from one
naming another team.

Also read core.multi_team with getboolean. It was read with conf.get, which
returns the string 'False' -- truthy -- so the multi-team branches were
selected even with multi-team disabled.

The escape tests wire the backend so the cross-team path would return a value
and assert it does not come back. Against unpatched sources they fail with
"assert 'beta-secret' is None".
@potiuk
potiuk force-pushed the security/akeyless-team-scope-guard branch from 10f6b72 to 1cb91b8 Compare September 7, 2026 18:24
@eladkal

eladkal commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

cc @baraka-akeyless for review

@baraka-akeyless

Copy link
Copy Markdown
Contributor

Thanks @potiuk for this fix — reviewed and looks good to me.

@eladkal
eladkal merged commit cf76cd0 into apache:main Sep 10, 2026
79 checks passed
imrichardwu pushed a commit to imrichardwu/airflow that referenced this pull request Sep 11, 2026
…che#72646)

Secret names are built by joining <base path><sep><key>, and the key was not
validated. In multi-team mode the team-scoped lookup is tried first and, when
it misses, the team-agnostic fallback resolves <base path><sep><key> -- the
prefix under which every other team's secrets are stored. A caller in team
alpha requesting the key 'beta/db_password' therefore reached team beta's
secret. The key is Dag-author controlled and the execution API variables route
is declared with a ':path' converter, so a separator survives the round trip.

Refuse such a key only where this backend actually crosses a namespace: when
multi-team mode is on, team-scoped paths are in use, and a team name was
supplied. The separator here is the ordinary path separator and nested keys
are a documented layout, so the refusal is kept as narrow as the defect:

* use_team_secrets_path=False builds no team path, so nothing is refused and
  a flat nested layout keeps working under multi-team mode.
* get_config takes no team name and Airflow does not perform team-scoped
  config lookups through a secrets backend, so that path has no boundary to
  cross and is not guarded; subfolder config layouts keep resolving.
* A caller with no team name resolves in the shared namespace directly rather
  than falling back into it. Whether global scope should be able to name a
  team's namespace is a separate question and is not decided here.

The key is never parsed to determine which team it names, because it cannot
be -- nothing distinguishes a nested key in the shared namespace from one
naming another team.

Also read core.multi_team with getboolean. It was read with conf.get, which
returns the string 'False' -- truthy -- so the multi-team branches were
selected even with multi-team disabled.

The escape tests wire the backend so the cross-team path would return a value
and assert it does not come back. Against unpatched sources they fail with
"assert 'beta-secret' is None".
xvega pushed a commit to xvega/airflow that referenced this pull request Sep 13, 2026
…che#72646)

Secret names are built by joining <base path><sep><key>, and the key was not
validated. In multi-team mode the team-scoped lookup is tried first and, when
it misses, the team-agnostic fallback resolves <base path><sep><key> -- the
prefix under which every other team's secrets are stored. A caller in team
alpha requesting the key 'beta/db_password' therefore reached team beta's
secret. The key is Dag-author controlled and the execution API variables route
is declared with a ':path' converter, so a separator survives the round trip.

Refuse such a key only where this backend actually crosses a namespace: when
multi-team mode is on, team-scoped paths are in use, and a team name was
supplied. The separator here is the ordinary path separator and nested keys
are a documented layout, so the refusal is kept as narrow as the defect:

* use_team_secrets_path=False builds no team path, so nothing is refused and
  a flat nested layout keeps working under multi-team mode.
* get_config takes no team name and Airflow does not perform team-scoped
  config lookups through a secrets backend, so that path has no boundary to
  cross and is not guarded; subfolder config layouts keep resolving.
* A caller with no team name resolves in the shared namespace directly rather
  than falling back into it. Whether global scope should be able to name a
  team's namespace is a separate question and is not decided here.

The key is never parsed to determine which team it names, because it cannot
be -- nothing distinguishes a nested key in the shared namespace from one
naming another team.

Also read core.multi_team with getboolean. It was read with conf.get, which
returns the string 'False' -- truthy -- so the multi-team branches were
selected even with multi-team disabled.

The escape tests wire the backend so the cross-team path would return a value
and assert it does not come back. Against unpatched sources they fail with
"assert 'beta-secret' is None".
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.

3 participants