fix(#3961): scrub Azure managed-identity (MSI/IMDS) credential-source env vars

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 <rodboev@users.noreply.github.com>
This commit is contained in:
nesquena-hermes 2026-06-22 19:58:44 +00:00
parent 51dd69b4f2
commit d60ad140b1
2 changed files with 14 additions and 0 deletions

View file

@ -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",

View file

@ -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):