Skip to content

Commit e90b04b

Browse files
committed
dont expose api key in configmap
1 parent c12deac commit e90b04b

File tree

4 files changed

+143
-13
lines changed

4 files changed

+143
-13
lines changed

src/aks-agent/azext_aks_agent/agent/k8s/aks_agent_manager.py

Lines changed: 62 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
# Licensed under the MIT License. See License.txt in the project root for license information.
44
# --------------------------------------------------------------------------------------------
55

6+
import base64
67
import json
78
import os
89
import time
@@ -80,14 +81,13 @@ def __init__(self, namespace: str = AGENT_NAMESPACE, kubeconfig_path: Optional[s
8081

8182
self.kubeconfig_path = kubeconfig_path
8283

84+
# Initialize Kubernetes client
85+
self._init_k8s_client()
8386
# Use provided helm manager or create a new one with kubeconfig
8487
self.helm_manager = helm_manager or HelmManager(kubeconfig_path=self.kubeconfig_path)
8588

8689
self._load_existing_helm_release_config()
8790

88-
# Initialize Kubernetes client
89-
self._init_k8s_client()
90-
9191
def set_aks_context(self, resource_group_name: Optional[str] = None,
9292
cluster_name: Optional[str] = None,
9393
subscription_id: Optional[str] = None):
@@ -159,7 +159,11 @@ def _load_existing_helm_release_config(self):
159159
self.llm_config_manager.model_list = model_list
160160
if not model_list:
161161
logger.warning("No modelList found in Helm values")
162-
logger.debug("LLM configuration loaded from Helm values: %d models found", len(model_list))
162+
else:
163+
logger.debug("LLM configuration loaded from Helm values: %d models found", len(model_list))
164+
165+
# Read API keys from Kubernetes secret and populate model_list
166+
self._populate_api_keys_from_secret()
163167

164168
# Load managed identity client ID if present
165169
mcp_addons = helm_values.get("mcpAddons", {})
@@ -183,6 +187,59 @@ def _load_existing_helm_release_config(self):
183187
logger.error("Failed to load LLM config from Helm values: %s", e)
184188
raise AzCLIError(f"Failed to load LLM config from Helm values: {e}")
185189

190+
def _populate_api_keys_from_secret(self):
191+
"""
192+
Read API keys from Kubernetes secret and populate them into model_list.
193+
194+
The model_list from Helm values contains environment variable references like
195+
'{{ env.AZURE_GPT_4_API_KEY }}'. This method reads the actual API keys from
196+
the Kubernetes secret and replaces those references with actual values.
197+
"""
198+
try:
199+
# Try to read the secret
200+
secret = self.core_v1.read_namespaced_secret(
201+
name=self.llm_secret_name,
202+
namespace=self.namespace
203+
)
204+
205+
if not secret.data:
206+
logger.warning("Secret '%s' exists but has no data", self.llm_secret_name)
207+
return
208+
209+
# Decode secret data (base64 encoded)
210+
secret_data = {}
211+
for key, value in secret.data.items():
212+
decoded_value = base64.b64decode(value).decode("utf-8")
213+
secret_data[key] = decoded_value
214+
215+
logger.debug("Read %d API keys from secret '%s'", len(secret_data), self.llm_secret_name)
216+
217+
from azext_aks_agent.agent.llm_providers.base import LLMProvider
218+
219+
# Populate API keys into model_list
220+
221+
for model_name, model_config in self.llm_config_manager.model_list.items():
222+
# Get the expected secret key for this model
223+
secret_key = LLMProvider.sanitize_k8s_secret_key(model_config)
224+
225+
# If the secret contains this key, populate it
226+
if secret_key in secret_data:
227+
model_config["api_key"] = secret_data[secret_key]
228+
logger.debug("Populated API key for model '%s' from secret key '%s'",
229+
model_name, secret_key)
230+
else:
231+
logger.warning("API key is not found for model '%s', please update the model '%s' API key.",
232+
model_name, model_name)
233+
234+
except ApiException as e:
235+
if e.status == 404:
236+
logger.debug("Secret '%s' not found in namespace '%s', skipping API key population",
237+
self.llm_secret_name, self.namespace)
238+
else:
239+
logger.warning("Failed to read secret '%s': %s", self.llm_secret_name, e)
240+
except Exception as e: # pylint: disable=broad-exception-caught
241+
logger.warning("Unexpected error reading API keys from secret: %s", e)
242+
186243
def get_agent_pods(self) -> Tuple[bool, Union[List[str], str]]:
187244
"""
188245
Get running AKS agent pods from the Kubernetes cluster.
@@ -738,7 +795,7 @@ def _create_helm_values(self):
738795
env_vars = self.llm_config_manager.get_env_vars(self.llm_secret_name)
739796

740797
helm_values = {
741-
"modelList": self.llm_config_manager.model_list,
798+
"modelList": self.llm_config_manager.secured_model_list(),
742799
"additionalEnvVars": env_vars,
743800
"nodeSelector": {"kubernetes.io/os": "linux"},
744801
}

src/aks-agent/azext_aks_agent/agent/llm_config_manager.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,12 @@ def save(self, provider: LLMProvider, params: dict):
2424
params["model"] = model_name
2525
self.model_list[model_name] = params
2626

27+
def secured_model_list(self) -> Dict[str, dict]:
28+
secured_config = {}
29+
for model_name, model_config in self.model_list.items():
30+
secured_config[model_name] = LLMProvider.to_secured_model_list_config(model_config)
31+
return secured_config
32+
2733
def get_llm_model_secret_data(self) -> Dict[str, str]:
2834
"""
2935
Get Kubernetes secret data for all LLM models in the configuration.

src/aks-agent/azext_aks_agent/agent/llm_providers/base.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -202,14 +202,14 @@ def sanitize_key_part(part):
202202
return secret_key
203203

204204
@classmethod
205-
def to_model_list_config(cls, params: dict) -> Dict[str, dict]:
205+
def to_secured_model_list_config(cls, params: dict) -> Dict[str, dict]:
206206
"""Create a model config dictionary for the model list from the provider parameters.
207+
Returns a copy of params with the api_key replaced by environment variable reference.
207208
"""
208209
secret_key = cls.sanitize_k8s_secret_key(params)
209-
model_name = params.get("model")
210-
model_list_config = {model_name: params}
211-
model_list_config[model_name].update({"api_key": f"{{{{ env.{secret_key} }}}}"})
212-
return model_list_config
210+
secured_params = params.copy()
211+
secured_params.update({"api_key": f"{{{{ env.{secret_key} }}}}"})
212+
return secured_params
213213

214214
@classmethod
215215
def to_env_vars(cls, secret_name, params: dict) -> Dict[str, str]:

src/aks-agent/azext_aks_agent/tests/latest/test_llm_config_manager.py

Lines changed: 70 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,10 @@ def setUp(self):
2121
"""Set up test fixtures."""
2222
self.manager = LLMConfigManager()
2323
self.test_model_config = {
24-
"provider": "azure",
2524
"model": "gpt-4",
2625
"api_key": "test-key",
2726
"api_base": "https://test.openai.azure.com",
2827
"api_version": "2023-05-15",
29-
"DEPLOYMENT_NAME": "gpt-4-deployment",
30-
"MODEL_NAME": "gpt-4"
3128
}
3229

3330
def test_init_empty(self):
@@ -85,6 +82,76 @@ def test_get_env_vars(self, mock_to_env):
8582
self.assertIsNotNone(result)
8683
mock_to_env.assert_called_once()
8784

85+
@patch('azext_aks_agent.agent.llm_config_manager.LLMProvider.to_secured_model_list_config')
86+
def test_secured_model_list_multiple_models(self, mock_to_secured):
87+
"""Test secured_model_list with multiple models."""
88+
gpt4_secured = {
89+
"model": "azure/gpt-4",
90+
"api_key": "{{ env.AZURE_GPT_4_API_KEY }}"
91+
}
92+
gpt35_secured = {
93+
"model": "azure/gpt-35-turbo",
94+
"api_key": "{{ env.AZURE_GPT_35_TURBO_API_KEY }}"
95+
}
96+
97+
mock_to_secured.side_effect = [gpt4_secured, gpt35_secured]
98+
99+
self.manager.model_list = {
100+
"azure/gpt-4": {"model": "azure/gpt-4", "api_key": "key1"},
101+
"azure/gpt-35-turbo": {"model": "azure/gpt-35-turbo", "api_key": "key2"}
102+
}
103+
104+
result = self.manager.secured_model_list()
105+
106+
self.assertEqual(len(result), 2)
107+
self.assertIn("azure/gpt-4", result)
108+
self.assertIn("azure/gpt-35-turbo", result)
109+
self.assertEqual(result["azure/gpt-4"], gpt4_secured)
110+
self.assertEqual(result["azure/gpt-35-turbo"], gpt35_secured)
111+
self.assertEqual(mock_to_secured.call_count, 2)
112+
113+
@patch('azext_aks_agent.agent.llm_config_manager.LLMProvider.to_secured_model_list_config')
114+
def test_secured_model_list_does_not_modify_original(self, mock_to_secured):
115+
"""Test that secured_model_list doesn't modify the original model_list."""
116+
original_api_key = "test-key"
117+
original_config = {
118+
"model": "azure/gpt-4",
119+
"api_key": original_api_key
120+
}
121+
122+
secured_config = {
123+
"model": "azure/gpt-4",
124+
"api_key": "{{ env.AZURE_GPT_4_API_KEY }}"
125+
}
126+
mock_to_secured.return_value = secured_config
127+
128+
self.manager.model_list = {
129+
"azure/gpt-4": original_config
130+
}
131+
132+
result = self.manager.secured_model_list()
133+
134+
# Verify original model_list is unchanged
135+
self.assertEqual(self.manager.model_list["azure/gpt-4"]["api_key"], original_api_key)
136+
137+
# Verify result has secured api_key
138+
self.assertEqual(result["azure/gpt-4"]["api_key"], "{{ env.AZURE_GPT_4_API_KEY }}")
139+
140+
@patch('azext_aks_agent.agent.llm_config_manager.LLMProvider.to_secured_model_list_config')
141+
def test_secured_model_list_preserves_model_names(self, mock_to_secured):
142+
"""Test that secured_model_list preserves model names as keys."""
143+
mock_to_secured.return_value = {"secured": "config"}
144+
145+
model_names = ["azure/gpt-4", "openai/gpt-4-turbo", "anthropic/claude-3"]
146+
for model_name in model_names:
147+
self.manager.model_list[model_name] = {"model": model_name}
148+
149+
result = self.manager.secured_model_list()
150+
151+
# Verify all model names are preserved as keys
152+
for model_name in model_names:
153+
self.assertIn(model_name, result)
154+
88155

89156
if __name__ == '__main__':
90157
unittest.main()

0 commit comments

Comments
 (0)