This is an automated email from the ASF dual-hosted git repository.

potiuk pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/main by this push:
     new bfa23909190 Mask registered secrets at any nesting depth (#73793)
bfa23909190 is described below

commit bfa2390919046ddcee5e568362adff7725672886
Author: Jarek Potiuk <[email protected]>
AuthorDate: Mon Oct 19 16:56:10 2026 +0200

    Mask registered secrets at any nesting depth (#73793)
    
    * Mask registered secrets at any nesting depth
    
    Pattern-based masking of registered secrets stopped at max_depth: a
    string nested deeper than that was returned without the replacer being
    applied, while key-name masking already worked at any depth. Apply the
    replacer to string leaves wherever they sit. max_depth still bounds the
    fail-closed walk of a value already known to be sensitive.
    
    Generated-by: Claude Opus 5
    
    * Add newsfragment
    
    Generated-by: Claude Opus 5
---
 airflow-core/newsfragments/73793.improvement.rst          |  1 +
 .../src/airflow_shared/secrets_masker/secrets_masker.py   | 15 ++++++---------
 .../tests/secrets_masker/test_secrets_masker.py           | 11 ++++++++---
 3 files changed, 15 insertions(+), 12 deletions(-)

diff --git a/airflow-core/newsfragments/73793.improvement.rst 
b/airflow-core/newsfragments/73793.improvement.rst
new file mode 100644
index 00000000000..fa43fd229c7
--- /dev/null
+++ b/airflow-core/newsfragments/73793.improvement.rst
@@ -0,0 +1 @@
+Mask registered secrets at any nesting depth. Secret values registered with 
the masker are now replaced wherever they appear in a nested structure, instead 
of only up to the masker's maximum depth.
diff --git 
a/shared/secrets_masker/src/airflow_shared/secrets_masker/secrets_masker.py 
b/shared/secrets_masker/src/airflow_shared/secrets_masker/secrets_masker.py
index b28d0d1a358..178b451c10b 100644
--- a/shared/secrets_masker/src/airflow_shared/secrets_masker/secrets_masker.py
+++ b/shared/secrets_masker/src/airflow_shared/secrets_masker/secrets_masker.py
@@ -363,10 +363,12 @@ class SecretsMasker(logging.Filter):
         self, item: Redactable, name: str | None, depth: int, max_depth: int, 
replacement: str = "***"
     ) -> Redacted:
         try:
-            # Key-name-based redaction is unbounded by depth — sensitive keys
-            # must fail closed at any nesting level. The depth cutoff below is
-            # only used to bound the work of pattern-based string masking and
-            # to terminate recursion through self-referential iterables.
+            # Both key-name-based and pattern-based redaction apply at any
+            # nesting depth: a registered secret or a sensitive key must fail
+            # closed wherever it sits in the structure. ``max_depth`` only 
bounds
+            # the fail-closed walk of a value already known to be sensitive
+            # (``_redact_all``); self-referential structures are stopped by
+            # Python's recursion limit and the except clause below.
             if name and self.should_hide_value_for_key(name):
                 return self._redact_all(item, depth, max_depth, 
replacement=replacement)
             # Always walk dicts so deeper sensitive keys are still caught;
@@ -402,11 +404,6 @@ class SecretsMasker(logging.Filter):
                     )
                     for subval in item
                 ]
-            # The depth cutoff only bounds the work of pattern-based string
-            # masking below — key-name redaction (dicts and iterables above) is
-            # unbounded so sensitive keys fail closed at any depth.
-            if depth > max_depth:
-                return item
             if isinstance(item, Enum):
                 return self._redact(
                     item=item.value, name=name, depth=depth, 
max_depth=max_depth, replacement=replacement
diff --git a/shared/secrets_masker/tests/secrets_masker/test_secrets_masker.py 
b/shared/secrets_masker/tests/secrets_masker/test_secrets_masker.py
index ebecfc7279d..f993a723eba 100644
--- a/shared/secrets_masker/tests/secrets_masker/test_secrets_masker.py
+++ b/shared/secrets_masker/tests/secrets_masker/test_secrets_masker.py
@@ -675,9 +675,14 @@ class TestSecretsMasker:
             (["abcdef"], ["***"], 1),
             ([[[["abcdef"]]]], [[[["***"]]]], None),
             ([[[[["abcdef"]]]]], [[[[["***"]]]]], None),
-            # Items below max depth aren't redacted
-            ([[[[[["abcdef"]]]]]], [[[[[["abcdef"]]]]]], None),
-            ([["abcdef"]], [["abcdef"]], 1),
+            # Registered secrets are redacted at any depth, whatever max_depth 
says
+            ([[[[[["abcdef"]]]]]], [[[[[["***"]]]]]], None),
+            ([["abcdef"]], [["***"]], 1),
+            (
+                {"a": {"b": {"c": {"d": {"e": {"f": {"g": 
"prefix-abcdef-suffix"}}}}}}},
+                {"a": {"b": {"c": {"d": {"e": {"f": {"g": 
"prefix-***-suffix"}}}}}}},
+                None,
+            ),
         ],
     )
     def test_redact_max_depth(self, val, expected, max_depth):

Reply via email to