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