Skip to content

Fix secrets masking in Rendered Templates for complex objects#61394

Open
amoghrajesh wants to merge 1 commit intoapache:mainfrom
astronomer:fix-rtif-complex-obj
Open

Fix secrets masking in Rendered Templates for complex objects#61394
amoghrajesh wants to merge 1 commit intoapache:mainfrom
astronomer:fix-rtif-complex-obj

Conversation

@amoghrajesh
Copy link
Contributor

@amoghrajesh amoghrajesh commented Feb 3, 2026


Was generative AI tooling used to co-author this PR?
  • No

Problem

Secrets stored in complex objects (e.g., Kubernetes V1EnvVar, boto3 models) were not masked in the Rendered Templates UI when values contained spaces or exceeded certain lengths.

The cause here is that when _serialize_template_field() encounters non JSON serializable objects, it uses str() for serialization. Python __repr__ for these objects breaks long string values across multiple lines:

'value': 'This is a longer test phrase. We are checking if this handles '
          'regular sentences.'

Now if this value was registered with the masker without new lines in between, redaction wont happen since patterns do not match exactly.

Fix

The fix is to attempt converting such objects to serializable formats by json. To do this, added a to_serializable() helper function that converts objects with .to_dict() methods (standard in Kubernetes, boto3, and other similar such objects) to plain dictionaries before serialization. Then use json.dumps() instead of str() to produce clean json output.

It works because json spec doesn't allow line breaks inside string values and json.dumps() works on these converted objects. This leads now to secret patterns matching correctly -> masking succeeds.

Testing

Use a simple macro like this:

def mask_str(value: str):
    """
    Mask a literal string value so it isn't exposed in logs or task instance details.
    """
    mask_secret(value)
    return value


class MyMacroMacros(AirflowPlugin):
    name = "my_macro"
    macros = [mask_str]

DAG:

import logging
from datetime import datetime

from airflow.providers.standard.operators.bash import BashOperator
from airflow.sdk import DAG
from airflow.providers.cncf.kubernetes.operators.pod import KubernetesPodOperator
from airflow.providers.standard.operators.empty import EmptyOperator

logger = logging.getLogger(__name__)

DAG_ID = "my_dag"
default_args = {
    "start_date": datetime(2017, 8, 9, hour=5),
}


def create_dag(dag_id: str = DAG_ID) -> DAG:
    dag = DAG(
        dag_id=dag_id,
        default_args=default_args,
        schedule=None,
        doc_md=__doc__,
    )

    empty_task = EmptyOperator(dag=dag, task_id="empty_task")

    cmds = ["/bin/bash", "-c"]

    kpo = {
        "TEST_PHRASE1": f"{{{{ macros.my_macro.mask_str('This is a test phrase.') }}}}",
        "TEST_PHRASE2": f"{{{{ macros.my_macro.mask_str('This is a longer test phrase. We are checking if this handles regular sentences.') }}}}",
        "TEST_PHRASE3": f"{{{{ macros.my_macro.mask_str('Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.') }}}}"
    }
    for i in range(50):
        kpo[f"TEST_URL_{i}"] = (
            f"{{{{ macros.my_macro.mask_str('postgresql+psycopg2://username:testpass123@test.domain.com/testdb') }}}}"
        )
    arguments = [f"echo '{key}: {value}'" for key, value in secrets_for_kpo.items()]
    debug_secret_for_kpo = KubernetesPodOperator(
        task_id="kpo-task",
        image="ubuntu",
        cmds=cmds,
        arguments=[" && ".join(arguments)],
        env_vars=kpo,
        dag=dag,
    )

    empty_task >> debug_secret_for_kpo

    return dag


dag = create_dag()

Earlier:

Screenshot 2026-01-29 at 1 09 47 PM

Now:

[{"name": "TEST_PHRASE1", "value": "***", "value_from": null}, {"name": "TEST_PHRASE2", "value": "***", "value_from": null}, {"name": "TEST_PHRASE3", "value": "***", "value_from": null}, {"name": "RANDOM_100", "value": "***", "value_from": null}, {"name": "RANDOM_500", "value": "***", "value_from": null}, {"name": "RANDOM_1000", "value": "***", "value_from": null}, {"name": "RANDOM_1500", "value": "***", "value_from": null}, {"name": "RANDOM_2500", "value": "***", "value_from": null}, {"name": "TEST_URL_0", "value": "***", "value_from": null}, {"name": "TEST_URL_1", "value": "***", "value_from": null}, {"name": "TEST_URL_2", "value": "***", "value_from": null}, {"name": "TEST_URL_3", "value": "***", "value_from": null}, {"name": "TEST_URL_4", "value": "***", "value_from": null}, {"name": "TEST_URL_5", "value": "***", "value_from": null}, {"name": "TEST_URL_6", "value": "***", "value_from": null}, {"name": "TEST_URL_7", "value": "***", "value_from": null}, {"name": "TEST_URL_8", "value": "***", "value_from": null}, {"name": "TEST_URL_9", "value": "***", "value_from": null}, {"name": "TEST_URL_10", "value": "***", "value_from": null}, {"name": "TEST_URL_11", "value": "***", "value_from": null}, {"name": "TEST_URL_12", "value": "***", "value_from": null}, {"name": "TEST_URL_13", "value": "***", "value_from": null}, {"name": "TEST_URL_14", "value": "***", "value_from": null}, {"name": "TEST_URL_15", "value": "***", "value_from": null}, {"name": "TEST_URL_16", "value": "***", "value_from": null}, {"name": "TEST_URL_17", "value": "***", "value_from": null}, {"name": "TEST_URL_18", "value": "***", "value_from": null}, {"name": "TEST_URL_19", "value": "***", "value_from": null}, {"name": "TEST_URL_20", "value": "***", "value_from": null}, {"name": "TEST_URL_21", "value": "***", "value_from": null}, {"name": "TEST_URL_22", "value": "***", "value_from": null}, {"name": "TEST_URL_23", "value": "***", "value_from": null}, {"name": "TEST_URL_24", "value": "***", "value_from": null}, {"name": "TEST_URL_25", "value": "***", "value_from": null}, {"name": "TEST_URL_26", "value": "***", "value_from": null}, {"name": "TEST_URL_27", "value": "***", "value_from": null}, {"name": "TEST_URL_28", "value": "***", "value_from": null}, {"name": "TEST_URL_29", "value": "***", "value_from": null}, {"name": "TEST_URL_30", "value": "***", "value_from": null}, {"name": "TEST_URL_31", "value": "***", "value_from": null}, {"name": "TEST_URL_32", "value": "***", "value_from": null}, {"name": "TEST_URL_33", "value": "***", "value_from": null}, {"name": "TEST_URL_34", "value": "***", "value_from": null}, {"name": "TEST_URL_35", "value": "***", "value_from": null}, {"name": "TEST_URL_36", "value": "***", "value_from": null}, {"name": "TEST_URL_37", "value": "***", "value_from": null}, {"name": "TEST_URL_38", "value": "***", "value_from": null}, {"name": "TEST_URL_39", "value": "***", "value_from": null}, {"name": "TEST_URL_40", "value": "***", "value_from": null}, {"name": "TEST_URL_41", "value": "***", "value_from": null}, {"name": "TEST_URL_42", "value": "***", "value_from": null}, {"name": "TEST_URL_43", "value": "***", "value_from": null}, {"name": "TEST_URL_44", "value": "***", "value_from": null}, {"name": "TEST_URL_45", "value": "***", "value_from": null}, {"name": "TEST_URL_46", "value": "***", "value_from": null}, {"name": "TEST_URL_47", "value": "***", "value_from": null}, {"name": "TEST_URL_48", "value": "***", "value_from": null}, {"name": "TEST_URL_49", "value": "***", "value_from": null}]

  • Read the Pull Request Guidelines for more information. Note: commit author/co-author name and email in commits become permanently public when merged.
  • For fundamental code changes, an Airflow Improvement Proposal (AIP) is needed.
  • When adding dependency, check compliance with the ASF 3rd Party License Policy.
  • For significant user-facing changes create newsfragment: {pr_number}.significant.rst or {issue_number}.significant.rst, in airflow-core/newsfragments.

@amoghrajesh amoghrajesh added this to the Airflow 3.1.8 milestone Feb 3, 2026
@amoghrajesh
Copy link
Contributor Author

We may be able to get this one in for 3.1.8

@amoghrajesh amoghrajesh self-assigned this Feb 3, 2026
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.

1 participant