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 1b43dce4466 Mask sensitive values in airflow config update dry-run
output (#73265)
1b43dce4466 is described below
commit 1b43dce4466229125cfcd07283a3df3d622a8230
Author: Keith <[email protected]>
AuthorDate: Mon Nov 2 09:38:05 2026 +0900
Mask sensitive values in airflow config update dry-run output (#73265)
The dry run, the default mode of airflow config update, printed the
regenerated airflow.cfg with real values of sql_alchemy_conn, fernet_key and
other sensitive options, and that preview output easily ends up in CI logs and
bug reports.
closes: #73255
Generated-by: Claude Opus 5
---
airflow-core/src/airflow/cli/cli_config.py | 11 ++++
.../src/airflow/cli/commands/config_command.py | 13 +++++
airflow-core/src/airflow/configuration.py | 18 ++++++-
.../tests/unit/cli/commands/test_config_command.py | 59 ++++++++++++++++++++++
4 files changed, 100 insertions(+), 1 deletion(-)
diff --git a/airflow-core/src/airflow/cli/cli_config.py
b/airflow-core/src/airflow/cli/cli_config.py
index 6f9be99f3c2..7bc1517700f 100644
--- a/airflow-core/src/airflow/cli/cli_config.py
+++ b/airflow-core/src/airflow/cli/cli_config.py
@@ -1039,6 +1039,16 @@ ARG_UPDATE_ALL_RECOMMENDATIONS = Arg(
action="store_true",
)
+ARG_UPDATE_CONFIG_SHOW_SENSITIVE = Arg(
+ ("--show-sensitive",),
+ help=(
+ "Show real values of sensitive options (passwords, keys, tokens, etc.)
in the dry-run "
+ "preview instead of masking them as '< hidden >'. Has no effect
together with --fix, "
+ "which always writes real values to airflow.cfg."
+ ),
+ action="store_true",
+)
+
# jobs check
ARG_JOB_TYPE_FILTER = Arg(
@@ -2087,6 +2097,7 @@ CONFIG_COMMANDS = (
ARG_VERBOSE,
ARG_UPDATE_CONFIG_FIX,
ARG_UPDATE_ALL_RECOMMENDATIONS,
+ ARG_UPDATE_CONFIG_SHOW_SENSITIVE,
),
),
)
diff --git a/airflow-core/src/airflow/cli/commands/config_command.py
b/airflow-core/src/airflow/cli/commands/config_command.py
index 4dc38a38cc7..3a85cc18100 100644
--- a/airflow-core/src/airflow/cli/commands/config_command.py
+++ b/airflow-core/src/airflow/cli/commands/config_command.py
@@ -978,6 +978,12 @@ def update_config(args) -> None:
Comma-separated list of configuration options to ignore during
update.
Example: --ignore-option check_slas
+ --show-sensitive: flag (optional)
+ Show real values of sensitive options in the dry-run preview. By
default sensitive
+ values (passwords, keys, tokens, etc.) are masked as '< hidden >'.
Has no effect
+ together with --fix, which always writes real values to
airflow.cfg.
+ Example: --show-sensitive
+
Examples:
1. Dry-run mode (print the changes in modified airflow.cfg) showing
only breaking changes:
airflow config update
@@ -1075,12 +1081,19 @@ def update_config(args) -> None:
if dry_run:
console.print("[blue]Dry-run mode enabled. No changes will be written
to airflow.cfg.[/blue]")
+ hide_sensitive = not args.show_sensitive
+ if hide_sensitive:
+ console.print(
+ "[blue]Sensitive values are shown as '< hidden >'. Use
`--show-sensitive` to "
+ "display them (never paste unmasked output into logs or bug
reports).[/blue]"
+ )
with StringIO() as config_output:
conf.write_custom_config(
file=config_output,
comment_out_defaults=True,
include_descriptions=True,
modifications=modifications,
+ hide_sensitive=hide_sensitive,
)
new_config = config_output.getvalue()
console.print(new_config)
diff --git a/airflow-core/src/airflow/configuration.py
b/airflow-core/src/airflow/configuration.py
index 2fe2e5e2f00..f8a554a5dc3 100644
--- a/airflow-core/src/airflow/configuration.py
+++ b/airflow-core/src/airflow/configuration.py
@@ -63,6 +63,12 @@ ConfigSourcesType = dict[str, ConfigSectionSourcesType]
ENV_VAR_PREFIX = "AIRFLOW__"
+_AIRFLOW_V2_SENSITIVE_OPTIONS = {
+ ("core", "dataset_manager_kwargs"),
+ ("core", "internal_api_secret_key"),
+ ("smtp", "smtp_password"),
+}
+
class _SecretKeys:
"""Holds the secret keys used in Airflow during runtime."""
@@ -282,6 +288,7 @@ class AirflowConfigParser(_SharedAirflowConfigParser):
include_descriptions: bool = True,
extra_spacing: bool = True,
modifications: ConfigModifications | None = None,
+ hide_sensitive: bool = False,
) -> None:
"""
Write a configuration file using a ConfigModifications object.
@@ -298,6 +305,8 @@ class AirflowConfigParser(_SharedAirflowConfigParser):
:param include_descriptions: Whether to include section descriptions.
:param extra_spacing: Whether to insert an extra blank line after each
option.
:param modifications: ConfigModifications instance with rename,
remove, and default updates.
+ :param hide_sensitive: If True, values of options registered as
sensitive are written as
+ ``< hidden >``. Only suitable for preview output, never for a real
configuration file.
"""
modifications = modifications or ConfigModifications()
output: dict[str, list[tuple[str, str, bool, str]]] = {}
@@ -327,8 +336,15 @@ class AirflowConfigParser(_SharedAirflowConfigParser):
default_value = self.get_default_value(effective_section,
effective_option, fallback="")
is_default = str(value) == str(default_value)
+ value_to_write = str(value)
+ if hide_sensitive and (
+ key in _AIRFLOW_V2_SENSITIVE_OPTIONS
+ or self.is_sensitive_option(section, option)
+ or self.is_sensitive_option(effective_section,
effective_option)
+ ):
+ value_to_write = "< hidden >"
output.setdefault(effective_section.lower(), []).append(
- (effective_option, str(value), is_default, mod_comment)
+ (effective_option, value_to_write, is_default, mod_comment)
)
for section, options in output.items():
diff --git a/airflow-core/tests/unit/cli/commands/test_config_command.py
b/airflow-core/tests/unit/cli/commands/test_config_command.py
index 5d634bb13bf..90daf86e182 100644
--- a/airflow-core/tests/unit/cli/commands/test_config_command.py
+++ b/airflow-core/tests/unit/cli/commands/test_config_command.py
@@ -741,3 +741,62 @@ class TestCliConfigUpdate:
output = capsys.readouterr().out
assert f"'{present_key}'" in output
assert f"'{absent_key}'" not in output
+
+ @conf_vars(
+ {
+ ("database", "sql_alchemy_conn"):
"postgresql://airflow:S3cr3tPassword@db:5432/airflow",
+ ("core", "dags_folder"): "/opt/airflow/dags",
+ ("core", "dataset_manager_kwargs"): '{"password":
"LegacyDatasetSecret"}',
+ ("core", "internal_api_secret_key"): "LegacyInternalApiSecret",
+ ("smtp", "smtp_password"): "LegacySmtpSecret",
+ }
+ )
+ def test_update_config_dry_run_masks_sensitive_values(self, tmp_path,
monkeypatch, capsys):
+ cfg_file = tmp_path / "airflow.cfg"
+ cfg_file.write_text("[core]\nexecutor = SequentialExecutor\n")
+ monkeypatch.setattr(config_command, "AIRFLOW_CONFIG", str(cfg_file))
+
+ parser = cli_parser.get_parser()
+ args = parser.parse_args(["config", "update"])
+
+ config_command.update_config(args)
+
+ output = capsys.readouterr().out
+ assert "S3cr3tPassword" not in output
+ assert "LegacyDatasetSecret" not in output
+ assert "LegacyInternalApiSecret" not in output
+ assert "LegacySmtpSecret" not in output
+ assert "< hidden >" in output
+ assert "/opt/airflow/dags" in output, "Non-sensitive values must stay
visible in the preview."
+ assert "Use `--show-sensitive` to" in output
+
+ @conf_vars({("database", "sql_alchemy_conn"):
"postgresql://airflow:S3cr3tPassword@db:5432/airflow"})
+ def test_update_config_dry_run_show_sensitive_reveals_values(self,
tmp_path, monkeypatch, capsys):
+ cfg_file = tmp_path / "airflow.cfg"
+ cfg_file.write_text("[core]\nexecutor = SequentialExecutor\n")
+ monkeypatch.setattr(config_command, "AIRFLOW_CONFIG", str(cfg_file))
+
+ parser = cli_parser.get_parser()
+ args = parser.parse_args(["config", "update", "--show-sensitive"])
+
+ config_command.update_config(args)
+
+ output = capsys.readouterr().out
+ assert "S3cr3tPassword" in output
+ assert "Use `--show-sensitive` to" not in output
+
+ @conf_vars({("database", "sql_alchemy_conn"):
"postgresql://airflow:S3cr3tPassword@db:5432/airflow"})
+ def test_update_config_fix_always_writes_real_values(self, tmp_path,
monkeypatch, capsys):
+ cfg_file = tmp_path / "airflow.cfg"
+ cfg_file.write_text("[core]\nexecutor = SequentialExecutor\n")
+ monkeypatch.setattr(config_command, "AIRFLOW_CONFIG", str(cfg_file))
+ monkeypatch.setattr(shutil, "copy2", lambda src, dst: None)
+
+ parser = cli_parser.get_parser()
+ args = parser.parse_args(["config", "update", "--fix"])
+
+ config_command.update_config(args)
+
+ written_cfg = cfg_file.read_text()
+ assert "S3cr3tPassword" in written_cfg, "--fix must write real values
to airflow.cfg."
+ assert "< hidden >" not in written_cfg