From d60ad140b14ac26869e8c51a05d3bcc5d2c4b1bb Mon Sep 17 00:00:00 2001 From: nesquena-hermes Date: Mon, 22 Jun 2026 19:58:44 +0000 Subject: [PATCH] fix(#3961): scrub Azure managed-identity (MSI/IMDS) credential-source env vars MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Final credential-surface item: agent/azure_identity_adapter.py treats IDENTITY_ENDPOINT / MSI_ENDPOINT as ManagedIdentityCredential sources, so an empty named profile could inherit the host's managed identity on the quota + detached-worker paths. Add IDENTITY_ENDPOINT / IDENTITY_HEADER / MSI_ENDPOINT / MSI_SECRET to the non-registry scrub set (config var AZURE_FOUNDRY_BASE_URL kept). Detached-worker regression extended. This completes the agent's enumerable credential-env read surface (verified from source). Decision (with maintainer): stop the Codex deep-gate here — the remaining theoretical surface is cloud-SDK-internal credential-source endpoints on pre-existing infra, not vectors introduced by this PR; validated via Opus + full suite. The PR's #3961 scope (os.environ mirror race on /api/providers + /api/models) is fully closed plus the broader same-class hardening. Co-authored-by: rodboev --- api/profiles.py | 8 ++++++++ tests/test_issue3957_profile_providers_models.py | 6 ++++++ 2 files changed, 14 insertions(+) diff --git a/api/profiles.py b/api/profiles.py index 09e611d6..e6aebaad 100644 --- a/api/profiles.py +++ b/api/profiles.py @@ -910,6 +910,14 @@ _NON_REGISTRY_AGENT_CREDENTIAL_ENV_NAMES: tuple[str, ...] = ( "AZURE_CLIENT_SECRET", "AZURE_TENANT_ID", "AZURE_FEDERATED_TOKEN_FILE", + # Azure managed-identity (App Service MSI / IMDS) credential-source vars — + # agent/azure_identity_adapter.py treats these as ManagedIdentityCredential + # sources, so an empty named profile must not inherit the host's managed + # identity. (NOT AZURE_FOUNDRY_BASE_URL — that's config, kept.) + "IDENTITY_ENDPOINT", + "IDENTITY_HEADER", + "MSI_ENDPOINT", + "MSI_SECRET", "AWS_BEARER_TOKEN_BEDROCK", "AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", diff --git a/tests/test_issue3957_profile_providers_models.py b/tests/test_issue3957_profile_providers_models.py index d5ac485d..d77a76c3 100644 --- a/tests/test_issue3957_profile_providers_models.py +++ b/tests/test_issue3957_profile_providers_models.py @@ -839,6 +839,8 @@ def test_detached_worker_scope_scrubs_non_registry_agent_creds(monkeypatch, tmp_ monkeypatch.setenv("AWS_SECRET_ACCESS_KEY", "process-default-aws-secret") monkeypatch.setenv("AZURE_CLIENT_SECRET", "process-default-azure-secret") monkeypatch.setenv("AZURE_FOUNDRY_API_KEY", "process-default-foundry-key") + monkeypatch.setenv("IDENTITY_ENDPOINT", "http://169.254.169.254/msi") + monkeypatch.setenv("MSI_ENDPOINT", "http://169.254.169.254/msi") with profiles.profile_scope_for_detached_worker("work", "test-worker"): assert os.environ.get("CUSTOM_API_KEY") is None @@ -846,6 +848,8 @@ def test_detached_worker_scope_scrubs_non_registry_agent_creds(monkeypatch, tmp_ assert os.environ.get("AWS_SECRET_ACCESS_KEY") is None assert os.environ.get("AZURE_CLIENT_SECRET") is None assert os.environ.get("AZURE_FOUNDRY_API_KEY") is None + assert os.environ.get("IDENTITY_ENDPOINT") is None + assert os.environ.get("MSI_ENDPOINT") is None assert config._thread_local_env_value("CUSTOM_API_KEY") == "" assert os.environ.get("CUSTOM_API_KEY") == "process-default-custom-key" @@ -853,6 +857,8 @@ def test_detached_worker_scope_scrubs_non_registry_agent_creds(monkeypatch, tmp_ assert os.environ.get("AWS_SECRET_ACCESS_KEY") == "process-default-aws-secret" assert os.environ.get("AZURE_CLIENT_SECRET") == "process-default-azure-secret" assert os.environ.get("AZURE_FOUNDRY_API_KEY") == "process-default-foundry-key" + assert os.environ.get("IDENTITY_ENDPOINT") == "http://169.254.169.254/msi" + assert os.environ.get("MSI_ENDPOINT") == "http://169.254.169.254/msi" def test_expand_env_vars_does_not_leak_process_env_under_block_scope(monkeypatch):