Repository navigation
Refuse Akeyless secret ids that address another team's namespace - #72646
Merged
Merged
Conversation
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
force-pushed
the
security/akeyless-team-scope-guard
branch
from
September 7, 2026 18:24
10f6b72 to
1cb91b8
Compare
Contributor
|
cc @baraka-akeyless for review |
Contributor
|
Thanks @potiuk for this fix — reviewed and looks good to me. |
eladkal
approved these changes
Sep 10, 2026
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".
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Akeyless secret names are built by joining
<base path><sep><key>, and thekey 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 theprefix every other team's secrets are stored under. A caller in team
alpharequesting the key
beta/db_passwordtherefore reached teambeta's secret.The key is Dag-author controlled and the execution API variables route is
declared with a
:pathconverter, 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=Falsebuilds no team path, so nothing is refused anda flat nested layout keeps working under multi-team mode.
get_config()takes no team name, and Airflow does not perform team-scopedconfig lookups through a secrets backend, so that path has no boundary to
cross and is not guarded. Subfolder config layouts keep resolving.
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_teamis now read withgetboolean. It was read withconf.get,which returns the string
"False"— truthy — so the multi-team branches wereselected 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, andordinary team-scoped lookups.
Was generative AI tooling used to co-author this PR?
Generated-by: Claude Code following the guidelines