This is an automated email from the ASF dual-hosted git repository.
eladkal 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 6e6d0af05e5 Add cloud-based auth (aws_iam, gcp, azure_ad) to Akeyless
secrets backend (#69772)
6e6d0af05e5 is described below
commit 6e6d0af05e5575d19ca46f0fbfd1098b39793d60
Author: baraka-akeyless <[email protected]>
AuthorDate: Sun Jul 12 14:06:03 2026 +0300
Add cloud-based auth (aws_iam, gcp, azure_ad) to Akeyless secrets backend
(#69772)
* Add cloud-based auth (aws_iam, gcp, azure_ad) to AkeylessBackend
The secrets backend previously only supported api_key and uid auth.
This adds aws_iam, gcp, and azure_ad authentication using the
akeyless_cloud_id package, enabling seamless integration with managed
Airflow services like Amazon MWAA, GCP Cloud Composer, and Azure
workloads without requiring static API keys.
Co-authored-by: Cursor <[email protected]>
* Update Cloud Composer references to Managed Service for Apache Airflow
Cloud Composer was rebranded to Managed Service for Apache Airflow
(Managed Airflow) as of April 2026.
Co-authored-by: Cursor <[email protected]>
---------
Co-authored-by: Cursor <[email protected]>
---
providers/akeyless/docs/secrets-backend.rst | 86 ++++++++++++++-
.../airflow/providers/akeyless/secrets/akeyless.py | 51 ++++++++-
.../tests/unit/akeyless/secrets/test_akeyless.py | 117 ++++++++++++++++++++-
3 files changed, 247 insertions(+), 7 deletions(-)
diff --git a/providers/akeyless/docs/secrets-backend.rst
b/providers/akeyless/docs/secrets-backend.rst
index ea970705e4a..8fa04e1c20a 100644
--- a/providers/akeyless/docs/secrets-backend.rst
+++ b/providers/akeyless/docs/secrets-backend.rst
@@ -94,6 +94,81 @@ Connections can be stored in three formats:
"port": 5432
}
+Authentication Methods
+----------------------
+
+The secrets backend supports the following authentication types:
+
+.. list-table::
+ :header-rows: 1
+ :widths: 15 85
+
+ * - ``access_type``
+ - Description
+ * - ``api_key``
+ - Authenticate with Access ID + Access Key. The default method.
+ * - ``uid``
+ - Use a pre-existing Universal Identity token.
+ * - ``aws_iam``
+ - Authenticate using the host's AWS IAM role. Ideal for **Amazon MWAA**,
+ EC2, ECS, and EKS workloads. No static credentials required.
+ * - ``gcp``
+ - Authenticate using GCP workload identity. Ideal for **Google Managed
+ Service for Apache Airflow** (formerly Cloud Composer) and GCE/GKE
+ workloads.
+ * - ``azure_ad``
+ - Authenticate using Azure AD identity. Ideal for Azure-hosted workloads.
+
+Cloud-based auth types (``aws_iam``, ``gcp``, ``azure_ad``) require the
+optional ``akeyless_cloud_id`` package:
+
+.. code-block:: bash
+
+ pip install apache-airflow-providers-akeyless[cloud_id]
+
+Using with Amazon MWAA
+""""""""""""""""""""""
+
+On `Amazon MWAA
<https://aws.amazon.com/managed-workflows-for-apache-airflow/>`__
+you can leverage the environment's IAM execution role to authenticate with
+Akeyless -- no static API keys needed.
+
+1. Add to your ``requirements.txt`` (uploaded to S3):
+
+ .. code-block:: text
+
+ apache-airflow-providers-akeyless[cloud_id]
+
+2. In the MWAA console, add these **Airflow configuration options**:
+
+ .. list-table::
+ :widths: 30 70
+
+ * - ``secrets.backend``
+ - ``airflow.providers.akeyless.secrets.akeyless.AkeylessBackend``
+ * - ``secrets.backend_kwargs``
+ - ``{"api_url": "https://api.akeyless.io", "access_id": "p-xxxxxxxxx",
"access_type": "aws_iam"}``
+
+3. Ensure the MWAA VPC has outbound HTTPS access to your Akeyless API
+ endpoint (``api.akeyless.io`` or your Akeyless Gateway).
+
+4. Create an Akeyless ``aws_iam`` Auth Method associated with the MWAA
+ execution role ARN.
+
+Using with Google Managed Service for Apache Airflow
+""""""""""""""""""""""""""""""""""""""""""""""""""""
+
+.. code-block:: ini
+
+ [secrets]
+ backend = airflow.providers.akeyless.secrets.akeyless.AkeylessBackend
+ backend_kwargs = {
+ "api_url": "https://api.akeyless.io",
+ "access_id": "p-xxxxxxxxx",
+ "access_type": "gcp",
+ "gcp_audience": "akeyless.io"
+ }
+
Parameters
----------
@@ -127,4 +202,13 @@ Parameters
- Akeyless Access Key (for ``api_key`` auth).
* - ``access_type``
- ``api_key``
- - Authentication method.
+ - Authentication method (``api_key``, ``uid``, ``aws_iam``, ``gcp``, or
``azure_ad``).
+ * - ``gcp_audience``
+ -
+ - GCP audience string (only for ``gcp`` auth).
+ * - ``azure_object_id``
+ -
+ - Azure AD Object ID (only for ``azure_ad`` auth).
+ * - ``token_ttl``
+ - ``600``
+ - Seconds to cache the API token before re-authenticating.
diff --git
a/providers/akeyless/src/airflow/providers/akeyless/secrets/akeyless.py
b/providers/akeyless/src/airflow/providers/akeyless/secrets/akeyless.py
index 2e2dc265a3c..eedc6794810 100644
--- a/providers/akeyless/src/airflow/providers/akeyless/secrets/akeyless.py
+++ b/providers/akeyless/src/airflow/providers/akeyless/secrets/akeyless.py
@@ -32,7 +32,8 @@ from airflow.utils.log.logging_mixin import LoggingMixin
if TYPE_CHECKING:
from airflow.models.connection import Connection
-_SUPPORTED_BACKEND_AUTH_TYPES = ("api_key", "uid")
+_SUPPORTED_BACKEND_AUTH_TYPES = ("api_key", "uid", "aws_iam", "gcp",
"azure_ad")
+_CLOUD_AUTH_TYPES = ("aws_iam", "gcp", "azure_ad")
_DEFAULT_TOKEN_TTL = 600 # 10 minutes
@@ -62,9 +63,21 @@ class AkeylessBackend(BaseSecretsBackend, LoggingMixin):
(when ``global_secrets_path`` is set) or ``{base_path}/{key}`` (default).
Team-scoped lookup can be disabled with ``use_team_secrets_path = False``.
- Only ``api_key`` and ``uid`` authentication types are supported in the
- secrets backend. For cloud-based authentication (``aws_iam``, ``gcp``,
- ``azure_ad``) or other advanced methods, use ``AkeylessHook`` directly.
+ Supported authentication types:
+
+ * ``api_key`` -- authenticate with Access ID + Access Key.
+ * ``uid`` -- use a pre-existing Universal Identity token.
+ * ``aws_iam`` -- authenticate using the host's AWS IAM role (ideal for
+ Amazon MWAA and EC2/ECS/EKS workloads).
+ * ``gcp`` -- authenticate using GCP workload identity (ideal for Google
+ Managed Service for Apache Airflow and GCE/GKE workloads).
+ * ``azure_ad`` -- authenticate using Azure AD identity (ideal for Azure
+ workloads).
+
+ Cloud-based auth types (``aws_iam``, ``gcp``, ``azure_ad``) require the
+ optional ``akeyless_cloud_id`` package::
+
+ pip install apache-airflow-providers-akeyless[cloud_id]
:param connections_path: Akeyless path prefix for Connections (None to
disable).
:param variables_path: Akeyless path prefix for Variables (None to
disable).
@@ -77,7 +90,10 @@ class AkeylessBackend(BaseSecretsBackend, LoggingMixin):
:param api_url: Akeyless API endpoint.
:param access_id: Access ID.
:param access_key: Access Key (for ``api_key`` auth).
- :param access_type: Auth type (``api_key`` or ``uid``).
+ :param access_type: Auth type (``api_key``, ``uid``, ``aws_iam``, ``gcp``,
+ or ``azure_ad``).
+ :param gcp_audience: GCP audience for ``gcp`` auth (optional).
+ :param azure_object_id: Azure AD Object ID for ``azure_ad`` auth
(optional).
:param token_ttl: Seconds to cache the API token before refreshing
(default 600).
"""
@@ -132,6 +148,13 @@ class AkeylessBackend(BaseSecretsBackend, LoggingMixin):
if self._access_type == "uid":
token = self._extra["uid_token"]
+ elif self._access_type in _CLOUD_AUTH_TYPES:
+ body = akeyless.Auth(
+ access_id=self._access_id,
+ access_type=self._access_type,
+ cloud_id=self._get_cloud_id(),
+ )
+ token = self._client.auth(body).token
else:
body = akeyless.Auth(access_id=self._access_id,
access_key=self._access_key)
token = self._client.auth(body).token
@@ -140,6 +163,24 @@ class AkeylessBackend(BaseSecretsBackend, LoggingMixin):
self._token_expiry = now + self._token_ttl
return token
+ def _get_cloud_id(self) -> str:
+ """Generate a cloud identity token for AWS IAM / GCP / Azure AD
auth."""
+ try:
+ from akeyless_cloud_id import CloudId
+ except ImportError:
+ raise ImportError(
+ f"`akeyless_cloud_id` is required for {self._access_type}
authentication. "
+ "Install it with: pip install
apache-airflow-providers-akeyless[cloud_id]"
+ )
+ cid = CloudId()
+ if self._access_type == "aws_iam":
+ return cid.generate()
+ if self._access_type == "gcp":
+ return cid.generateGcp(self._extra.get("gcp_audience"))
+ if self._access_type == "azure_ad":
+ return cid.generateAzure(self._extra.get("azure_object_id"))
+ raise ValueError(f"No cloud-id generator for {self._access_type!r}")
+
def _get_secret(self, base_path: str | None, key: str) -> str | None:
if base_path is None:
return None
diff --git a/providers/akeyless/tests/unit/akeyless/secrets/test_akeyless.py
b/providers/akeyless/tests/unit/akeyless/secrets/test_akeyless.py
index aa400db3561..eb97bfecb38 100644
--- a/providers/akeyless/tests/unit/akeyless/secrets/test_akeyless.py
+++ b/providers/akeyless/tests/unit/akeyless/secrets/test_akeyless.py
@@ -185,7 +185,7 @@ class TestAkeylessBackend:
def test_unsupported_access_type_raises(self):
with pytest.raises(ValueError, match="Unsupported access_type"):
- _backend(access_type="aws_iam")
+ _backend(access_type="ldap")
@patch(f"{BACKEND_MODULE}.akeyless")
def test_token_caching(self, mock_sdk):
@@ -277,3 +277,118 @@ class TestAkeylessBackend:
val = backend.get_variable("my_var", team_name="analytics")
assert val == "plain"
mock_sdk.GetSecretValue.assert_called_with(names=["/airflow/variables/my_var"],
token="t")
+
+ # ------------------------------------------------------------------
+ # Cloud-based authentication tests
+ # ------------------------------------------------------------------
+
+ @patch(f"{BACKEND_MODULE}.akeyless")
+ def test_aws_iam_auth(self, mock_sdk):
+ """aws_iam auth generates a cloud ID and authenticates."""
+ api = mock_sdk.V2Api.return_value
+ api.auth.return_value = MagicMock(token="aws-token")
+ api.get_secret_value.return_value = {"/airflow/variables/v": "aws-val"}
+
+ mock_cloud_id = MagicMock()
+ mock_cloud_id.generate.return_value = "fake-aws-cloud-id"
+
+ with patch(f"{BACKEND_MODULE}.AkeylessBackend._get_cloud_id",
return_value="fake-aws-cloud-id"):
+ backend = _backend(access_type="aws_iam", access_key=None)
+ val = backend.get_variable("v")
+
+ assert val == "aws-val"
+ mock_sdk.Auth.assert_called_once_with(
+ access_id="p-test123", access_type="aws_iam",
cloud_id="fake-aws-cloud-id"
+ )
+
+ @patch(f"{BACKEND_MODULE}.akeyless")
+ def test_gcp_auth(self, mock_sdk):
+ """gcp auth generates a GCP cloud ID and authenticates."""
+ api = mock_sdk.V2Api.return_value
+ api.auth.return_value = MagicMock(token="gcp-token")
+ api.get_secret_value.return_value = {"/airflow/variables/v": "gcp-val"}
+
+ with patch(f"{BACKEND_MODULE}.AkeylessBackend._get_cloud_id",
return_value="fake-gcp-cloud-id"):
+ backend = _backend(access_type="gcp", access_key=None,
gcp_audience="my-audience")
+ val = backend.get_variable("v")
+
+ assert val == "gcp-val"
+ mock_sdk.Auth.assert_called_once_with(
+ access_id="p-test123", access_type="gcp",
cloud_id="fake-gcp-cloud-id"
+ )
+
+ @patch(f"{BACKEND_MODULE}.akeyless")
+ def test_azure_ad_auth(self, mock_sdk):
+ """azure_ad auth generates an Azure cloud ID and authenticates."""
+ api = mock_sdk.V2Api.return_value
+ api.auth.return_value = MagicMock(token="azure-token")
+ api.get_secret_value.return_value = {"/airflow/variables/v":
"azure-val"}
+
+ with patch(f"{BACKEND_MODULE}.AkeylessBackend._get_cloud_id",
return_value="fake-azure-id"):
+ backend = _backend(access_type="azure_ad", access_key=None,
azure_object_id="obj-123")
+ val = backend.get_variable("v")
+
+ assert val == "azure-val"
+ mock_sdk.Auth.assert_called_once_with(
+ access_id="p-test123", access_type="azure_ad",
cloud_id="fake-azure-id"
+ )
+
+ @patch(f"{BACKEND_MODULE}.akeyless")
+ def test_aws_iam_cloud_id_integration(self, mock_sdk):
+ """aws_iam calls CloudId.generate() to produce the cloud identity."""
+ api = mock_sdk.V2Api.return_value
+ api.auth.return_value = MagicMock(token="t")
+ api.get_secret_value.return_value = {"/airflow/variables/v": "val"}
+
+ mock_cid_instance = MagicMock()
+ mock_cid_instance.generate.return_value = "real-aws-cloud-id"
+ mock_cloud_id_cls = MagicMock(return_value=mock_cid_instance)
+
+ with patch.dict("sys.modules", {"akeyless_cloud_id":
MagicMock(CloudId=mock_cloud_id_cls)}):
+ backend = _backend(access_type="aws_iam", access_key=None)
+ backend.get_variable("v")
+
+ mock_cid_instance.generate.assert_called_once()
+
+ @patch(f"{BACKEND_MODULE}.akeyless")
+ def test_gcp_cloud_id_passes_audience(self, mock_sdk):
+ """gcp auth passes gcp_audience to CloudId.generateGcp()."""
+ api = mock_sdk.V2Api.return_value
+ api.auth.return_value = MagicMock(token="t")
+ api.get_secret_value.return_value = {"/airflow/variables/v": "val"}
+
+ mock_cid_instance = MagicMock()
+ mock_cid_instance.generateGcp.return_value = "gcp-id"
+ mock_cloud_id_cls = MagicMock(return_value=mock_cid_instance)
+
+ with patch.dict("sys.modules", {"akeyless_cloud_id":
MagicMock(CloudId=mock_cloud_id_cls)}):
+ backend = _backend(access_type="gcp", access_key=None,
gcp_audience="my-aud")
+ backend.get_variable("v")
+
+ mock_cid_instance.generateGcp.assert_called_once_with("my-aud")
+
+ @patch(f"{BACKEND_MODULE}.akeyless")
+ def test_azure_cloud_id_passes_object_id(self, mock_sdk):
+ """azure_ad auth passes azure_object_id to CloudId.generateAzure()."""
+ api = mock_sdk.V2Api.return_value
+ api.auth.return_value = MagicMock(token="t")
+ api.get_secret_value.return_value = {"/airflow/variables/v": "val"}
+
+ mock_cid_instance = MagicMock()
+ mock_cid_instance.generateAzure.return_value = "azure-id"
+ mock_cloud_id_cls = MagicMock(return_value=mock_cid_instance)
+
+ with patch.dict("sys.modules", {"akeyless_cloud_id":
MagicMock(CloudId=mock_cloud_id_cls)}):
+ backend = _backend(access_type="azure_ad", access_key=None,
azure_object_id="obj-456")
+ backend.get_variable("v")
+
+ mock_cid_instance.generateAzure.assert_called_once_with("obj-456")
+
+ def test_cloud_auth_missing_package_raises(self):
+ """Cloud auth raises ImportError when akeyless_cloud_id is not
installed."""
+ import sys
+
+ backend = _backend(access_type="aws_iam", access_key=None)
+ with patch.dict(sys.modules, {"akeyless_cloud_id": None}):
+ with pytest.raises(ImportError, match="akeyless_cloud_id"):
+ backend._get_cloud_id()