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

shahar1 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 fc845cf64c0 Write Cloud SQL keyfile_dict credentials with 0600 
permissions (#67507)
fc845cf64c0 is described below

commit fc845cf64c0ab20e08890b3e0470d6c50368a260
Author: Jarek Potiuk <[email protected]>
AuthorDate: Thu May 28 18:54:11 2026 +0200

    Write Cloud SQL keyfile_dict credentials with 0600 permissions (#67507)
    
    When the Google connection supplies credentials via ``keyfile_dict``,
    ``CloudSqlProxyRunner._get_credential_parameters`` wrote the credentials
    file with ``open(path, "w")``. That inherits the process umask
    (typically ``0o644`` on most distributions), leaving the service-account
    private key world-readable on shared worker hosts — including any
    other process on the same machine that can read the worker's temp
    directory.
    
    Use ``os.open(..., O_WRONLY | O_CREAT | O_TRUNC, 0o600)`` followed by
    ``os.fdopen`` so the file is created with restrictive permissions
    atomically. Matches the explicit-mode handling already used for the
    SSL temp files in the same module.
---
 .../providers/google/cloud/hooks/cloud_sql.py      |  6 ++++-
 .../unit/google/cloud/hooks/test_cloud_sql.py      | 30 ++++++++++++++++++++++
 2 files changed, 35 insertions(+), 1 deletion(-)

diff --git 
a/providers/google/src/airflow/providers/google/cloud/hooks/cloud_sql.py 
b/providers/google/src/airflow/providers/google/cloud/hooks/cloud_sql.py
index 51868610223..d4d5f18134f 100644
--- a/providers/google/src/airflow/providers/google/cloud/hooks/cloud_sql.py
+++ b/providers/google/src/airflow/providers/google/cloud/hooks/cloud_sql.py
@@ -626,7 +626,11 @@ class CloudSqlProxyRunner(LoggingMixin):
         elif keyfile_dict:
             keyfile_content = keyfile_dict if isinstance(keyfile_dict, dict) 
else json.loads(keyfile_dict)
             self.log.info("Saving credentials to %s", self.credentials_path)
-            with open(self.credentials_path, "w") as file:
+            # Explicit 0o600 — the file holds a service-account private key. 
The plain
+            # ``open()`` form inherits the process umask (typically 0o644), 
which leaves the
+            # key world-readable on shared worker hosts.
+            fd = os.open(self.credentials_path, os.O_WRONLY | os.O_CREAT | 
os.O_TRUNC, 0o600)
+            with os.fdopen(fd, "w") as file:
                 json.dump(keyfile_content, file)
             credential_params = ["-credential_file", self.credentials_path]
         else:
diff --git a/providers/google/tests/unit/google/cloud/hooks/test_cloud_sql.py 
b/providers/google/tests/unit/google/cloud/hooks/test_cloud_sql.py
index b2a82c2a781..710f4793fa8 100644
--- a/providers/google/tests/unit/google/cloud/hooks/test_cloud_sql.py
+++ b/providers/google/tests/unit/google/cloud/hooks/test_cloud_sql.py
@@ -21,7 +21,9 @@ import base64
 import json
 import os
 import platform
+import stat
 import tempfile
+from pathlib import Path
 from unittest import mock
 from unittest.mock import PropertyMock, call, mock_open
 from urllib.parse import parse_qsl, unquote, urlsplit
@@ -1934,6 +1936,34 @@ class TestCloudSqlProxyRunner:
         assert runner._get_credential_parameters() == ["-credential_file", 
"/tmp/key.json"]
         assert "-enable_iam_login" in runner.command_line_parameters
 
+    
@mock.patch("airflow.providers.google.cloud.hooks.cloud_sql.GoogleBaseHook.get_connection")
+    def test_credentials_file_from_keyfile_dict_is_chmod_0600(self, 
get_connection, tmp_path):
+        """The keyfile_dict credentials file must be written with explicit 
0600 permissions.
+
+        Plain ``open(...)`` inherits the process umask (typically 0644), 
leaving the
+        service-account private key world-readable on shared worker hosts.
+        """
+        keyfile_dict = {"type": "service_account", "private_key": "PRIVATE"}
+        connection = Connection(conn_id="google_conn", 
conn_type="google_cloud_platform")
+        extra = json.dumps({"keyfile_dict": json.dumps(keyfile_dict)})
+        if AIRFLOW_V_3_1_PLUS:
+            connection.extra = extra
+        else:
+            connection.set_extra(extra)
+        get_connection.return_value = connection
+
+        runner = CloudSqlProxyRunner(
+            path_prefix=str(tmp_path / "creds"),
+            instance_specification="project:us-east-1:instance",
+            gcp_conn_id="google_conn",
+        )
+        runner._get_credential_parameters()
+
+        creds_path = Path(runner.credentials_path)
+        assert creds_path.exists()
+        # Mask off the file-type bits, keep only the permission bits.
+        assert stat.S_IMODE(creds_path.stat().st_mode) == 0o600
+
 
 class TestCloudSQLAsyncHook:
     @pytest.mark.asyncio

Reply via email to