From 5b6cf688eed8043a85084ac68cf92afb923c22e0 Mon Sep 17 00:00:00 2001 From: Qingchuan Hao Date: Wed, 25 Mar 2026 08:18:52 +0000 Subject: [PATCH 1/3] [aks-agent] Add Microsoft Entra ID authentication support for Azure OpenAI This commit adds support for keyless authentication using Microsoft Entra ID (formerly Azure AD) for Azure OpenAI. Users can now skip providing an API key during initialization to enable workload identity-based authentication. Changes: - Bump version to 1.0.0b22 and aks-agent to v0.7.0 - Allow empty API key during Azure OpenAI configuration - Configure aks-agent pod to use workload identity with the same service account as aks-mcp - Add helm values: workloadIdentity.enabled=true, serviceAccount.create=false - Skip creating secrets and environment variables when API key is empty - Enable azureADTokenAuth flag in helm when API key is not provided --- src/aks-agent/HISTORY.rst | 4 ++++ src/aks-agent/azext_aks_agent/_consts.py | 2 +- .../agent/k8s/aks_agent_manager.py | 16 ++++++++++++++++ .../azext_aks_agent/agent/llm_config_manager.py | 6 ++++-- .../agent/llm_providers/azure_provider.py | 9 ++++++--- .../azext_aks_agent/agent/llm_providers/base.py | 10 ++++++++-- src/aks-agent/setup.py | 2 +- 7 files changed, 40 insertions(+), 9 deletions(-) diff --git a/src/aks-agent/HISTORY.rst b/src/aks-agent/HISTORY.rst index d6de8f0fc5d..15f36e3f668 100644 --- a/src/aks-agent/HISTORY.rst +++ b/src/aks-agent/HISTORY.rst @@ -12,6 +12,10 @@ To release a new version, please select a new version number (usually plus 1 to Pending +++++++ +1.0.0b22 +++++++++ +* Feature: Add Microsoft Entra ID (formerly Azure AD) authentication support for Azure OpenAI + 1.0.0b21 ++++++++ * Bump aks-agent to v0.6.0 diff --git a/src/aks-agent/azext_aks_agent/_consts.py b/src/aks-agent/azext_aks_agent/_consts.py index 4d121032dd5..c91a0171af8 100644 --- a/src/aks-agent/azext_aks_agent/_consts.py +++ b/src/aks-agent/azext_aks_agent/_consts.py @@ -52,7 +52,7 @@ AKS_MCP_LABEL_SELECTOR = "app.kubernetes.io/name=aks-mcp" # AKS Agent Version (shared by helm chart and docker image) -AKS_AGENT_VERSION = "0.6.0" +AKS_AGENT_VERSION = "0.7.0" # Helm Configuration HELM_VERSION = "3.16.0" diff --git a/src/aks-agent/azext_aks_agent/agent/k8s/aks_agent_manager.py b/src/aks-agent/azext_aks_agent/agent/k8s/aks_agent_manager.py index 5f2deb7e7fe..53580186325 100644 --- a/src/aks-agent/azext_aks_agent/agent/k8s/aks_agent_manager.py +++ b/src/aks-agent/azext_aks_agent/agent/k8s/aks_agent_manager.py @@ -927,6 +927,22 @@ def _create_helm_values(self): "create": False, } + # Configure aks-agent pod to use the same service account as aks-mcp for workload identity + helm_values["workloadIdentity"] = { + "enabled": True, + } + helm_values["serviceAccount"] = { + "create": False, + "name": self.aks_mcp_service_account_name, + } + + has_empty_api_key = any( + not model_config.get("api_key") or not model_config.get("api_key").strip() + for model_config in self.llm_config_manager.model_list.values() + ) + if has_empty_api_key: + helm_values["azureADTokenAuth"] = True + return helm_values def save_llm_config(self, provider: LLMProvider, params: dict) -> None: diff --git a/src/aks-agent/azext_aks_agent/agent/llm_config_manager.py b/src/aks-agent/azext_aks_agent/agent/llm_config_manager.py index 8461ac931b1..375cd299c1b 100644 --- a/src/aks-agent/azext_aks_agent/agent/llm_config_manager.py +++ b/src/aks-agent/azext_aks_agent/agent/llm_config_manager.py @@ -50,8 +50,10 @@ def get_env_vars(self, secret_name: str) -> List[Dict[str, str]]: """ env_vars_list = [] for _, model_config in self.model_list.items(): - env_var = LLMProvider.to_env_vars(secret_name, model_config) - env_vars_list.append(env_var) + api_key = model_config.get("api_key") + if api_key and api_key.strip(): + env_var = LLMProvider.to_env_vars(secret_name, model_config) + env_vars_list.append(env_var) return env_vars_list diff --git a/src/aks-agent/azext_aks_agent/agent/llm_providers/azure_provider.py b/src/aks-agent/azext_aks_agent/agent/llm_providers/azure_provider.py index 658b8441e99..5f5c0ddafc8 100644 --- a/src/aks-agent/azext_aks_agent/agent/llm_providers/azure_provider.py +++ b/src/aks-agent/azext_aks_agent/agent/llm_providers/azure_provider.py @@ -43,8 +43,8 @@ def parameter_schema(self): "api_key": { "secret": True, "default": None, - "hint": None, - "validator": non_empty + "hint": "press enter to enable keyless authentication with Microsoft Entra ID", + "validator": lambda v: True }, "api_base": { "secret": False, @@ -65,9 +65,12 @@ def validate_connection(self, params: dict) -> Tuple[str, str]: api_version = params.get("api_version") deployment_name = params.get("model") - if not all([api_key, api_base, api_version, deployment_name]): + if not all([api_base, api_version, deployment_name]): return "Missing required Azure parameters.", "retry_input" + if not api_key or not api_key.strip(): + return None, "save" + # REST API reference: https://learn.microsoft.com/en-us/azure/ai-foundry/openai/api-version-lifecycle?tabs=rest url = urljoin(api_base, f"openai/deployments/{deployment_name}/chat/completions") diff --git a/src/aks-agent/azext_aks_agent/agent/llm_providers/base.py b/src/aks-agent/azext_aks_agent/agent/llm_providers/base.py index 3e4c1135c26..9b35fca1a39 100644 --- a/src/aks-agent/azext_aks_agent/agent/llm_providers/base.py +++ b/src/aks-agent/azext_aks_agent/agent/llm_providers/base.py @@ -175,6 +175,8 @@ def to_k8s_secret_data(cls, params: dict): """ secret_key = cls.sanitize_k8s_secret_key(params) secret_value = params.get("api_key") + if not secret_value or not secret_value.strip(): + return {} secret_data = { secret_key: base64.b64encode(secret_value.encode("utf-8")).decode("utf-8"), } @@ -206,9 +208,13 @@ def to_secured_model_list_config(cls, params: dict) -> Dict[str, dict]: """Create a model config dictionary for the model list from the provider parameters. Returns a copy of params with the api_key replaced by environment variable reference. """ - secret_key = cls.sanitize_k8s_secret_key(params) secured_params = params.copy() - secured_params.update({"api_key": f"{{{{ env.{secret_key} }}}}"}) + api_key = params.get("api_key") + if api_key and api_key.strip(): + secret_key = cls.sanitize_k8s_secret_key(params) + secured_params.update({"api_key": f"{{{{ env.{secret_key} }}}}"}) + else: + secured_params.pop("api_key", None) return secured_params @classmethod diff --git a/src/aks-agent/setup.py b/src/aks-agent/setup.py index db249ebd4e5..da915508c4f 100644 --- a/src/aks-agent/setup.py +++ b/src/aks-agent/setup.py @@ -9,7 +9,7 @@ from setuptools import find_packages, setup -VERSION = "1.0.0b21" +VERSION = "1.0.0b22" CLASSIFIERS = [ "Development Status :: 4 - Beta", From 643eb7d02c6c5b6561a3bb644280879bcaa05fca Mon Sep 17 00:00:00 2001 From: Qingchuan Hao Date: Thu, 26 Mar 2026 04:10:34 +0000 Subject: [PATCH 2/3] [aks-agent] Separate Azure OpenAI providers and add --yes flag - Split Azure OpenAI provider into two separate providers: - Azure OpenAI (API Key): Requires API key authentication - Azure OpenAI (Microsoft Entra ID): Supports keyless authentication - Add role assignment reminders in helm deployment for keyless auth - Add --yes/-y flag to agent-cleanup command to skip confirmation - Change secret empty data message from warning to debug level - Bump to v0.7.1 (suppress litellm debug logs) --- src/aks-agent/HISTORY.rst | 5 +- src/aks-agent/azext_aks_agent/_consts.py | 2 +- src/aks-agent/azext_aks_agent/_params.py | 6 ++ .../agent/k8s/aks_agent_manager.py | 2 +- .../agent/llm_providers/__init__.py | 10 ++-- .../llm_providers/azure_entraid_provider.py | 58 +++++++++++++++++++ .../agent/llm_providers/azure_provider.py | 11 ++-- src/aks-agent/azext_aks_agent/custom.py | 45 +++++++++++--- 8 files changed, 117 insertions(+), 22 deletions(-) create mode 100644 src/aks-agent/azext_aks_agent/agent/llm_providers/azure_entraid_provider.py diff --git a/src/aks-agent/HISTORY.rst b/src/aks-agent/HISTORY.rst index 15f36e3f668..e8e77545933 100644 --- a/src/aks-agent/HISTORY.rst +++ b/src/aks-agent/HISTORY.rst @@ -14,7 +14,10 @@ Pending 1.0.0b22 ++++++++ -* Feature: Add Microsoft Entra ID (formerly Azure AD) authentication support for Azure OpenAI +* Bump aks-agent to v0.7.1 + * Suppress litellm debug logs +* Feature: Separate Azure OpenAI provider into API Key and Microsoft Entra ID (keyless) providers +* Feature: Add --yes/-y flag to agent-cleanup command to skip confirmation prompt 1.0.0b21 ++++++++ diff --git a/src/aks-agent/azext_aks_agent/_consts.py b/src/aks-agent/azext_aks_agent/_consts.py index c91a0171af8..a60a4368074 100644 --- a/src/aks-agent/azext_aks_agent/_consts.py +++ b/src/aks-agent/azext_aks_agent/_consts.py @@ -52,7 +52,7 @@ AKS_MCP_LABEL_SELECTOR = "app.kubernetes.io/name=aks-mcp" # AKS Agent Version (shared by helm chart and docker image) -AKS_AGENT_VERSION = "0.7.0" +AKS_AGENT_VERSION = "0.7.1" # Helm Configuration HELM_VERSION = "3.16.0" diff --git a/src/aks-agent/azext_aks_agent/_params.py b/src/aks-agent/azext_aks_agent/_params.py index c5d34d75613..b96c193ea49 100644 --- a/src/aks-agent/azext_aks_agent/_params.py +++ b/src/aks-agent/azext_aks_agent/_params.py @@ -111,3 +111,9 @@ def load_arguments(self, _): help="The mode decides how the agent is deployed.", default="cluster", ) + c.argument( + "yes", + options_list=["--yes", "-y"], + action="store_true", + help="Do not prompt for confirmation.", + ) diff --git a/src/aks-agent/azext_aks_agent/agent/k8s/aks_agent_manager.py b/src/aks-agent/azext_aks_agent/agent/k8s/aks_agent_manager.py index 53580186325..79e54a4afe8 100644 --- a/src/aks-agent/azext_aks_agent/agent/k8s/aks_agent_manager.py +++ b/src/aks-agent/azext_aks_agent/agent/k8s/aks_agent_manager.py @@ -232,7 +232,7 @@ def _populate_api_keys_from_secret(self): ) if not secret.data: - logger.warning("Secret '%s' exists but has no data", self.llm_secret_name) + logger.debug("Secret '%s' exists but has no data", self.llm_secret_name) return # Decode secret data (base64 encoded) diff --git a/src/aks-agent/azext_aks_agent/agent/llm_providers/__init__.py b/src/aks-agent/azext_aks_agent/agent/llm_providers/__init__.py index c98dc8e52db..8cbd498638d 100644 --- a/src/aks-agent/azext_aks_agent/agent/llm_providers/__init__.py +++ b/src/aks-agent/azext_aks_agent/agent/llm_providers/__init__.py @@ -5,11 +5,12 @@ from typing import List, Tuple -from azext_aks_agent.agent.console import ERROR_COLOR, HELP_COLOR +from azext_aks_agent.agent.console import ERROR_COLOR, HELP_COLOR, INFO_COLOR from rich.console import Console from .anthropic_provider import AnthropicProvider from .azure_provider import AzureProvider +from .azure_entraid_provider import AzureEntraIDProvider from .base import LLMProvider from .gemini_provider import GeminiProvider from .openai_compatible_provider import OpenAICompatibleProvider @@ -19,11 +20,11 @@ _PROVIDER_CLASSES: List[LLMProvider] = [ AzureProvider, + AzureEntraIDProvider, OpenAIProvider, AnthropicProvider, GeminiProvider, OpenAICompatibleProvider, - # Add new providers here ] PROVIDER_REGISTRY = {} @@ -49,8 +50,9 @@ def _get_provider_by_index(idx: int) -> LLMProvider: Raises ValueError if index is out of range. """ if 1 <= idx <= len(_PROVIDER_CLASSES): - console.print("You selected provider:", _PROVIDER_CLASSES[idx - 1]().readable_name, style=f"bold {HELP_COLOR}") - return _PROVIDER_CLASSES[idx - 1]() + provider = _PROVIDER_CLASSES[idx - 1]() + console.print("You selected provider:", provider.readable_name, style=f"bold {HELP_COLOR}") + return provider raise ValueError(f"Invalid provider index: {idx}") diff --git a/src/aks-agent/azext_aks_agent/agent/llm_providers/azure_entraid_provider.py b/src/aks-agent/azext_aks_agent/agent/llm_providers/azure_entraid_provider.py new file mode 100644 index 00000000000..117b5e989fe --- /dev/null +++ b/src/aks-agent/azext_aks_agent/agent/llm_providers/azure_entraid_provider.py @@ -0,0 +1,58 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + + +from typing import Tuple + +from .base import LLMProvider, is_valid_url, non_empty + + +def is_valid_api_base(v: str) -> bool: + if not v.startswith("https://"): + return False + return is_valid_url(v) + + +class AzureEntraIDProvider(LLMProvider): + @property + def readable_name(self) -> str: + return "Azure OpenAI (Microsoft Entra ID)" + + @property + def model_route(self) -> str: + return "azure" + + @property + def parameter_schema(self): + return { + "model": { + "secret": False, + "default": None, + "hint": "ensure your deployment name is the same as the model name, e.g., gpt-5", + "validator": non_empty, + "alias": "deployment_name" + }, + "api_base": { + "secret": False, + "default": None, + "validator": is_valid_api_base + }, + "api_version": { + "secret": False, + "default": "2025-04-01-preview", + "hint": None, + "validator": non_empty + } + } + + def validate_connection(self, params: dict) -> Tuple[str, str]: + api_base = params.get("api_base") + api_version = params.get("api_version") + deployment_name = params.get("model") + + if not all([api_base, api_version, deployment_name]): + return "Missing required Azure parameters.", "retry_input" + + return None, "save" diff --git a/src/aks-agent/azext_aks_agent/agent/llm_providers/azure_provider.py b/src/aks-agent/azext_aks_agent/agent/llm_providers/azure_provider.py index 5f5c0ddafc8..60f7a44cd13 100644 --- a/src/aks-agent/azext_aks_agent/agent/llm_providers/azure_provider.py +++ b/src/aks-agent/azext_aks_agent/agent/llm_providers/azure_provider.py @@ -24,7 +24,7 @@ def is_valid_api_base(v: str) -> bool: class AzureProvider(LLMProvider): @property def readable_name(self) -> str: - return "Azure OpenAI" + return "Azure OpenAI (API Key)" @property def model_route(self) -> str: @@ -43,8 +43,8 @@ def parameter_schema(self): "api_key": { "secret": True, "default": None, - "hint": "press enter to enable keyless authentication with Microsoft Entra ID", - "validator": lambda v: True + "hint": None, + "validator": non_empty }, "api_base": { "secret": False, @@ -65,12 +65,9 @@ def validate_connection(self, params: dict) -> Tuple[str, str]: api_version = params.get("api_version") deployment_name = params.get("model") - if not all([api_base, api_version, deployment_name]): + if not all([api_key, api_base, api_version, deployment_name]): return "Missing required Azure parameters.", "retry_input" - if not api_key or not api_key.strip(): - return None, "save" - # REST API reference: https://learn.microsoft.com/en-us/azure/ai-foundry/openai/api-version-lifecycle?tabs=rest url = urljoin(api_base, f"openai/deployments/{deployment_name}/chat/completions") diff --git a/src/aks-agent/azext_aks_agent/custom.py b/src/aks-agent/azext_aks_agent/custom.py index f7e15e9b7ad..f4abf803f70 100644 --- a/src/aks-agent/azext_aks_agent/custom.py +++ b/src/aks-agent/azext_aks_agent/custom.py @@ -19,6 +19,7 @@ from azext_aks_agent.agent.k8s import AKSAgentManager, AKSAgentManagerClient from azext_aks_agent.agent.k8s.aks_agent_manager import AKSAgentManagerLLMConfigBase from azext_aks_agent.agent.llm_providers import prompt_provider_choice +from azext_aks_agent.agent.llm_providers.azure_entraid_provider import AzureEntraIDProvider from azext_aks_agent.agent.telemetry import CLITelemetryClient from azure.cli.core.azclierror import AzCLIError from azure.cli.core.commands.client_factory import get_subscription_id @@ -178,6 +179,19 @@ def _setup_helm_deployment(console, aks_agent_manager: AKSAgentManager): console.print( f"\n👤 Current service account in namespace '{aks_agent_manager.namespace}': {service_account_name}", style="cyan") + + # Check if using Azure Entra ID provider and show role assignment reminder + model_list = aks_agent_manager.get_llm_config() + if model_list and any("azure/" in model_name and not model_config.get("api_key") for model_name, model_config in model_list.items()): + console.print( + f"\n⚠️ IMPORTANT: If using keyless authentication with Azure OpenAI, ensure the 'Cognitive Services OpenAI User' or 'Azure AI Developer' role " + f"is assigned to the workload identity (service account: {service_account_name}).", + style=f"bold {INFO_COLOR}" + ) + console.print( + "Learn more: https://learn.microsoft.com/en-us/azure/ai-services/openai/how-to/managed-identity\n", + style=INFO_COLOR + ) elif helm_status == "not_found": console.print( @@ -196,6 +210,19 @@ def _setup_helm_deployment(console, aks_agent_manager: AKSAgentManager): "To have access to Azure resources, the service account should be annotated with " "'azure.workload.identity/client-id: '.", style=WARNING_COLOR) + + # Check if using Azure Entra ID provider and show role assignment note + model_list = aks_agent_manager.get_llm_config() + if model_list and any("azure/" in model_name and not model_config.get("api_key") for model_name, model_config in model_list.items()): + console.print( + "\n⚠️ NOTE: You are using keyless authentication with Azure OpenAI. " + "Ensure the 'Cognitive Services OpenAI User' or 'Azure AI Developer' role is assigned to the workload identity.", + style=f"bold {INFO_COLOR}" + ) + console.print( + "Learn more: https://learn.microsoft.com/en-us/azure/ai-services/openai/how-to/managed-identity", + style=INFO_COLOR + ) # Prompt user for service account name (required) while True: @@ -422,6 +449,7 @@ def aks_agent_cleanup( cluster_name, namespace, mode=None, + yes=False, ): """Cleanup and uninstall the AKS agent.""" with CLITelemetryClient(event_type="cleanup") as telemetry_client: @@ -442,16 +470,17 @@ def aks_agent_cleanup( f"⚠️ Warning: --namespace '{namespace}' is specified but will be ignored in client mode.", style=WARNING_COLOR) - console.print( - "\n⚠️ Warning: This will uninstall the AKS agent and delete all associated resources.", - style=WARNING_COLOR) + if not yes: + console.print( + "\n⚠️ Warning: This will uninstall the AKS agent and delete all associated resources.", + style=WARNING_COLOR) - user_confirmation = console.input( - f"\n[{WARNING_COLOR}]Are you sure you want to proceed with cleanup? (y/N): [/]").strip().lower() + user_confirmation = console.input( + f"\n[{WARNING_COLOR}]Are you sure you want to proceed with cleanup? (y/N): [/]").strip().lower() - if user_confirmation not in ['y', 'yes']: - console.print("❌ Cleanup cancelled.", style=INFO_COLOR) - return + if user_confirmation not in ['y', 'yes']: + console.print("❌ Cleanup cancelled.", style=INFO_COLOR) + return console.print("\n🗑️ Starting cleanup (this typically takes a few seconds)...", style=INFO_COLOR) From a4c144799eaa9cf4d6e3cd454c658669b2490e2d Mon Sep 17 00:00:00 2001 From: Qingchuan Hao Date: Thu, 26 Mar 2026 04:42:18 +0000 Subject: [PATCH 3/3] fix style check errors --- .../azext_aks_agent/agent/llm_providers/__init__.py | 2 +- src/aks-agent/azext_aks_agent/custom.py | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/aks-agent/azext_aks_agent/agent/llm_providers/__init__.py b/src/aks-agent/azext_aks_agent/agent/llm_providers/__init__.py index 8cbd498638d..04d932f8322 100644 --- a/src/aks-agent/azext_aks_agent/agent/llm_providers/__init__.py +++ b/src/aks-agent/azext_aks_agent/agent/llm_providers/__init__.py @@ -5,7 +5,7 @@ from typing import List, Tuple -from azext_aks_agent.agent.console import ERROR_COLOR, HELP_COLOR, INFO_COLOR +from azext_aks_agent.agent.console import ERROR_COLOR, HELP_COLOR from rich.console import Console from .anthropic_provider import AnthropicProvider diff --git a/src/aks-agent/azext_aks_agent/custom.py b/src/aks-agent/azext_aks_agent/custom.py index f4abf803f70..e3e6e670e1a 100644 --- a/src/aks-agent/azext_aks_agent/custom.py +++ b/src/aks-agent/azext_aks_agent/custom.py @@ -19,7 +19,6 @@ from azext_aks_agent.agent.k8s import AKSAgentManager, AKSAgentManagerClient from azext_aks_agent.agent.k8s.aks_agent_manager import AKSAgentManagerLLMConfigBase from azext_aks_agent.agent.llm_providers import prompt_provider_choice -from azext_aks_agent.agent.llm_providers.azure_entraid_provider import AzureEntraIDProvider from azext_aks_agent.agent.telemetry import CLITelemetryClient from azure.cli.core.azclierror import AzCLIError from azure.cli.core.commands.client_factory import get_subscription_id @@ -179,7 +178,7 @@ def _setup_helm_deployment(console, aks_agent_manager: AKSAgentManager): console.print( f"\n👤 Current service account in namespace '{aks_agent_manager.namespace}': {service_account_name}", style="cyan") - + # Check if using Azure Entra ID provider and show role assignment reminder model_list = aks_agent_manager.get_llm_config() if model_list and any("azure/" in model_name and not model_config.get("api_key") for model_name, model_config in model_list.items()): @@ -210,7 +209,7 @@ def _setup_helm_deployment(console, aks_agent_manager: AKSAgentManager): "To have access to Azure resources, the service account should be annotated with " "'azure.workload.identity/client-id: '.", style=WARNING_COLOR) - + # Check if using Azure Entra ID provider and show role assignment note model_list = aks_agent_manager.get_llm_config() if model_list and any("azure/" in model_name and not model_config.get("api_key") for model_name, model_config in model_list.items()):