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

kaxilnaik pushed a commit to branch v2-11-test
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/v2-11-test by this push:
     new b68510efb25 [v2-11-test] Ensure that the the generated airflow.cfg 
contains a ran… (#47755)
b68510efb25 is described below

commit b68510efb25e0e6ff6ae690173739e7462d99cb4
Author: Jens Scheffler <[email protected]>
AuthorDate: Wed Apr 30 21:20:46 2025 +0200

    [v2-11-test] Ensure that the the generated airflow.cfg contains a ran… 
(#47755)
    
    Ensure that the the generated airflow.cfg contains a random jwt_secret and 
fernet_key (#46966)
    
    * Ensure that the the generated airflow.cfg contains a random jwt_secret 
and fernet_key
    
    I don't know exactly when this got broken, and unforutnaltey it wasn't 
tested,
    but I suspect it might have been around the time we swapped the default 
config
    from a config file to the yaml based values. I.e. a while ago!
    
    To make sure it doesn't get broken I've gone and added some unit tests
    
    And to make my next PR and test easier I have done the same thing with the
    `auth_jwt_secret` that we do for fernet_key -- of only set it in the config
    file if we're generating that file, not always in memory.
    
    * Improve upgrade path by generating and warning about the missing config
    
    (cherry picked from commit 106b8726724fa712176dfc17ae192dbf5be83e17)
    
    Co-authored-by: Ash Berlin-Taylor <[email protected]>
    
    * Missing another one...
    
    * Fixup!
    
    * Fix error about missing section
    
    * Remove not-needed test
    
    ---------
    (cherry picked from commit 106b8726724fa712176dfc17ae192dbf5be83e17)
    
    Co-authored-by: Ash Berlin-Taylor <[email protected]>
---
 airflow/api_internal/endpoints/rpc_api_endpoint.py |  4 ++--
 airflow/api_internal/internal_api_call.py          |  4 ++--
 airflow/configuration.py                           | 10 ++++++---
 airflow/utils/jwt_signer.py                        | 18 +++++++++++++++
 airflow/utils/log/file_task_handler.py             |  4 ++--
 airflow/utils/serve_logs.py                        |  4 ++--
 airflow/www/app.py                                 |  3 ++-
 tests/core/test_configuration.py                   | 26 ++++++++++++++++++++++
 8 files changed, 61 insertions(+), 12 deletions(-)

diff --git a/airflow/api_internal/endpoints/rpc_api_endpoint.py 
b/airflow/api_internal/endpoints/rpc_api_endpoint.py
index c3d8b671fbb..a3c3c48d82e 100644
--- a/airflow/api_internal/endpoints/rpc_api_endpoint.py
+++ b/airflow/api_internal/endpoints/rpc_api_endpoint.py
@@ -42,7 +42,7 @@ from airflow.models.taskinstance import 
_record_task_map_for_downstreams
 from airflow.models.xcom_arg import _get_task_map_length
 from airflow.sensors.base import _orig_start_date
 from airflow.serialization.serialized_objects import BaseSerialization
-from airflow.utils.jwt_signer import JWTSigner
+from airflow.utils.jwt_signer import JWTSigner, get_signing_key
 from airflow.utils.session import create_session
 
 if TYPE_CHECKING:
@@ -178,7 +178,7 @@ def internal_airflow_api(body: dict[str, Any]) -> 
APIResponse:
     auth = request.headers.get("Authorization", "")
     clock_grace = conf.getint("core", "internal_api_clock_grace", fallback=30)
     signer = JWTSigner(
-        secret_key=conf.get("core", "internal_api_secret_key"),
+        secret_key=get_signing_key("core", "internal_api_secret_key"),
         expiration_time_in_seconds=clock_grace,
         leeway_in_seconds=clock_grace,
         audience="api",
diff --git a/airflow/api_internal/internal_api_call.py 
b/airflow/api_internal/internal_api_call.py
index 064834d7c86..f52b0323cbc 100644
--- a/airflow/api_internal/internal_api_call.py
+++ b/airflow/api_internal/internal_api_call.py
@@ -33,7 +33,7 @@ from airflow.configuration import conf
 from airflow.exceptions import AirflowConfigException, AirflowException
 from airflow.settings import _ENABLE_AIP_44, 
force_traceback_session_for_untrusted_components
 from airflow.typing_compat import ParamSpec
-from airflow.utils.jwt_signer import JWTSigner
+from airflow.utils.jwt_signer import JWTSigner, get_signing_key
 
 PS = ParamSpec("PS")
 RT = TypeVar("RT")
@@ -139,7 +139,7 @@ def internal_api_call(func: Callable[PS, RT]) -> 
Callable[PS, RT]:
     )
     def make_jsonrpc_request(method_name: str, params_json: str) -> bytes:
         signer = JWTSigner(
-            secret_key=conf.get("core", "internal_api_secret_key"),
+            secret_key=get_signing_key("core", "internal_api_secret_key"),
             expiration_time_in_seconds=conf.getint("core", 
"internal_api_clock_grace", fallback=30),
             audience="api",
         )
diff --git a/airflow/configuration.py b/airflow/configuration.py
index afb4b5f3808..3affeb3c24d 100644
--- a/airflow/configuration.py
+++ b/airflow/configuration.py
@@ -1949,7 +1949,7 @@ def get_airflow_config(airflow_home: str) -> str:
 
 
 def get_all_expansion_variables() -> dict[str, Any]:
-    return {k: v for d in [globals(), locals()] for k, v in d.items()}
+    return {k: v for d in [globals(), locals()] for k, v in d.items() if not 
k.startswith("_")}
 
 
 def _generate_fernet_key() -> str:
@@ -2000,6 +2000,7 @@ def create_pre_2_7_defaults() -> ConfigParser:
 
 
 def write_default_airflow_configuration_if_needed() -> AirflowConfigParser:
+    global FERNET_KEY
     airflow_config = pathlib.Path(AIRFLOW_CONFIG)
     if airflow_config.is_dir():
         msg = (
@@ -2023,13 +2024,16 @@ def write_default_airflow_configuration_if_needed() -> 
AirflowConfigParser:
                 raise FileNotFoundError(msg) from None
             log.debug("Create directory %r for Airflow config", 
config_directory.__fspath__())
             config_directory.mkdir(parents=True, exist_ok=True)
-        if conf.get("core", "fernet_key", fallback=None) is None:
+        if conf.get("core", "fernet_key", fallback=None) in (None, ""):
             # We know that FERNET_KEY is not set, so we can generate it, set 
as global key
             # and also write it to the config file so that same key will be 
used next time
-            global FERNET_KEY
             FERNET_KEY = _generate_fernet_key()
             conf.remove_option("core", "fernet_key")
+            if not conf.has_section("core"):
+                conf.add_section("core")
             conf.set("core", "fernet_key", FERNET_KEY)
+            
conf.configuration_description["core"]["options"]["fernet_key"]["default"] = 
FERNET_KEY
+
         pathlib.Path(airflow_config.__fspath__()).touch()
         make_group_other_inaccessible(airflow_config.__fspath__())
         with open(airflow_config, "w") as file:
diff --git a/airflow/utils/jwt_signer.py b/airflow/utils/jwt_signer.py
index fe4811eb827..5d8e82965b3 100644
--- a/airflow/utils/jwt_signer.py
+++ b/airflow/utils/jwt_signer.py
@@ -16,6 +16,9 @@
 # under the License.
 from __future__ import annotations
 
+import logging
+import os
+from base64 import b64encode
 from datetime import timedelta
 from typing import Any
 
@@ -24,6 +27,21 @@ import jwt
 from airflow.utils import timezone
 
 
+def get_signing_key(section: str, key: str) -> str:
+    from airflow.configuration import conf
+
+    secret_key = conf.get(section, key, fallback="")
+
+    if secret_key == "":
+        logging.getLogger(__name__).warning(
+            "`%s/%s` was empty, using a generated one for now. Please set this 
in your config", section, key
+        )
+        secret_key = b64encode(os.urandom(16)).decode("utf-8")
+        # Set it back so any other callers get the same value for the duration 
of this process
+        conf.set(section, key, secret_key)
+    return secret_key
+
+
 class JWTSigner:
     """
     Signs and verifies JWT Token. Used to authorise and verify requests.
diff --git a/airflow/utils/log/file_task_handler.py 
b/airflow/utils/log/file_task_handler.py
index 2df73ef4ffa..a33ab73afc7 100644
--- a/airflow/utils/log/file_task_handler.py
+++ b/airflow/utils/log/file_task_handler.py
@@ -90,11 +90,11 @@ def _fetch_logs_from_service(url, log_relative_path):
     # Import occurs in function scope for perf. Ref: 
https://github.com/apache/airflow/pull/21438
     import requests
 
-    from airflow.utils.jwt_signer import JWTSigner
+    from airflow.utils.jwt_signer import JWTSigner, get_signing_key
 
     timeout = conf.getint("webserver", "log_fetch_timeout_sec", fallback=None)
     signer = JWTSigner(
-        secret_key=conf.get("webserver", "secret_key"),
+        secret_key=get_signing_key("webserver", "secret_key"),
         expiration_time_in_seconds=conf.getint("webserver", 
"log_request_clock_grace", fallback=30),
         audience="task-instance-logs",
     )
diff --git a/airflow/utils/serve_logs.py b/airflow/utils/serve_logs.py
index 31ef86600da..2cdb0bc5851 100644
--- a/airflow/utils/serve_logs.py
+++ b/airflow/utils/serve_logs.py
@@ -37,7 +37,7 @@ from werkzeug.exceptions import HTTPException
 
 from airflow.configuration import conf
 from airflow.utils.docs import get_docs_url
-from airflow.utils.jwt_signer import JWTSigner
+from airflow.utils.jwt_signer import JWTSigner, get_signing_key
 from airflow.utils.module_loading import import_string
 
 logger = logging.getLogger(__name__)
@@ -71,7 +71,7 @@ def create_app():
         except Exception as e:
             raise ImportError(f"Unable to load {log_config_class} due to 
error: {e}")
     signer = JWTSigner(
-        secret_key=conf.get("webserver", "secret_key"),
+        secret_key=get_signing_key("webserver", "secret_key"),
         expiration_time_in_seconds=expiration_time_in_seconds,
         audience="task-instance-logs",
     )
diff --git a/airflow/www/app.py b/airflow/www/app.py
index 23d79b01861..927d3c9b693 100644
--- a/airflow/www/app.py
+++ b/airflow/www/app.py
@@ -35,6 +35,7 @@ from airflow.logging_config import configure_logging
 from airflow.models import import_all_models
 from airflow.settings import _ENABLE_AIP_44
 from airflow.utils.json import AirflowJsonProvider
+from airflow.utils.jwt_signer import get_signing_key
 from airflow.www.extensions.init_appbuilder import init_appbuilder
 from airflow.www.extensions.init_appbuilder_links import init_appbuilder_links
 from airflow.www.extensions.init_auth_manager import get_auth_manager
@@ -73,7 +74,7 @@ csrf = CSRFProtect()
 def create_app(config=None, testing=False):
     """Create a new instance of Airflow WWW app."""
     flask_app = Flask(__name__)
-    flask_app.secret_key = conf.get("webserver", "SECRET_KEY")
+    flask_app.secret_key = get_signing_key("webserver", "SECRET_KEY")
 
     flask_app.config["PERMANENT_SESSION_LIFETIME"] = 
timedelta(minutes=settings.get_session_lifetime_config())
 
diff --git a/tests/core/test_configuration.py b/tests/core/test_configuration.py
index 58e1e029af8..d48c4665310 100644
--- a/tests/core/test_configuration.py
+++ b/tests/core/test_configuration.py
@@ -1800,3 +1800,29 @@ class TestWriteDefaultAirflowConfigurationIfNeeded:
         mock_mask_secret.assert_any_call("supersecret2")
 
         assert mock_mask_secret.call_count == 2
+
+
+@conf_vars({("core", "unit_test_mode"): "False"})
+def test_write_default_config_contains_generated_secrets(tmp_path, 
monkeypatch):
+    import airflow.configuration
+
+    cfgpath = tmp_path / "airflow-gneerated.cfg"
+    # Patch these globals so it gets reverted by monkeypath after this test is 
over.
+    monkeypatch.setattr(airflow.configuration, "FERNET_KEY", "")
+    monkeypatch.setattr(airflow.configuration, "AIRFLOW_CONFIG", str(cfgpath))
+
+    # Create a new global conf object so our changes don't persist
+    localconf: AirflowConfigParser = airflow.configuration.initialize_config()
+    monkeypatch.setattr(airflow.configuration, "conf", localconf)
+
+    airflow.configuration.write_default_airflow_configuration_if_needed()
+
+    assert cfgpath.is_file()
+
+    lines = cfgpath.read_text().splitlines()
+
+    assert airflow.configuration.FERNET_KEY
+
+    fernet_line = next(line for line in lines if line.startswith("fernet_key = 
"))
+
+    assert fernet_line == f"fernet_key = {airflow.configuration.FERNET_KEY}"

Reply via email to