From f2240df6daad23f546c73ac5a95f07c0b533b01e Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Sat, 29 Aug 2026 14:40:22 -0700 Subject: [PATCH] fix(proxy): keep WIF secret pointers unresolved when expanding os.environ references --- litellm/proxy/proxy_server.py | 13 +++-- litellm/router.py | 3 +- litellm/types/router.py | 11 ++++ tests/test_litellm/proxy/test_proxy_server.py | 51 +++++++++++++++++++ tests/test_litellm/test_router.py | 25 +++++++++ tests/test_litellm/types/test_router.py | 11 ++++ 6 files changed, 109 insertions(+), 5 deletions(-) diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index 597acdf661f..3a237eba765 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -698,6 +698,7 @@ from litellm.types.router import ( RouterGeneralSettings, RoutingPlugin, SearchToolTypedDict, + holds_secret_pointer, updateDeployment, ) from litellm.types.router import ModelInfo as RouterModelInfo @@ -4497,7 +4498,7 @@ class ProxyConfig: if isinstance(item, dict): item = self._check_for_os_environ_vars(config=item, depth=depth + 1, max_depth=max_depth) # if the value is a string and starts with "os.environ/" - then it's an environment variable - elif isinstance(value, str) and value.startswith("os.environ/"): + elif isinstance(value, str) and value.startswith("os.environ/") and not holds_secret_pointer(key): resolved = get_secret(value) if resolved is None and secret_manager_would_be_consulted(value): verbose_proxy_logger.warning("%s is absent from the configured secret manager", value) @@ -5530,7 +5531,7 @@ class ProxyConfig: for model in model_list: ### LOAD FROM os.environ/ ### for k, v in model["litellm_params"].items(): - if isinstance(v, str) and v.startswith("os.environ/"): + if isinstance(v, str) and v.startswith("os.environ/") and not holds_secret_pointer(k): model["litellm_params"][k] = get_secret(v) validate_deployment_max_agentic_loops(model) validate_deployment_complexity_router_placement(model) @@ -5926,7 +5927,7 @@ class ProxyConfig: for model in model_list: ### LOAD FROM os.environ/ ### for k, v in model["litellm_params"].items(): - if isinstance(v, str) and v.startswith("os.environ/"): + if isinstance(v, str) and v.startswith("os.environ/") and not holds_secret_pointer(k): model["litellm_params"][k] = get_secret(v) ## check if they have model-id's ## @@ -5954,7 +5955,11 @@ class ProxyConfig: return value decrypted_value: Final = decrypt_value_helper(value=value, key=key, return_original_value=True) - if isinstance(decrypted_value, str) and decrypted_value.startswith("os.environ/"): + if ( + isinstance(decrypted_value, str) + and decrypted_value.startswith("os.environ/") + and not holds_secret_pointer(key) + ): return get_secret(decrypted_value) return decrypted_value diff --git a/litellm/router.py b/litellm/router.py index 76275a248c8..0286cd7aa50 100644 --- a/litellm/router.py +++ b/litellm/router.py @@ -217,6 +217,7 @@ from litellm.types.router import ( RoutingStrategy, SearchToolTypedDict, TaggedPreRoutingStrategy, + holds_secret_pointer, ) from litellm.types.services import ServiceTypes from litellm.types.utils import ( @@ -8757,7 +8758,7 @@ class Router: ## check if litellm params in os.environ if isinstance(_litellm_params, dict): for k, v in _litellm_params.items(): - if isinstance(v, str) and v.startswith("os.environ/"): + if isinstance(v, str) and v.startswith("os.environ/") and not holds_secret_pointer(k): _litellm_params[k] = get_secret(v) _model_info: dict = model.pop("model_info", {}) diff --git a/litellm/types/router.py b/litellm/types/router.py index 5a5b9c74db8..d891416f37c 100644 --- a/litellm/types/router.py +++ b/litellm/types/router.py @@ -329,6 +329,17 @@ def anthropic_wif_fields_named(keys: Container[str]) -> tuple[str, ...]: return tuple(name for name in _anthropic_wif_litellm_params if name in keys) +_ANTHROPIC_WIF_POINTER_FIELDS: Final = frozenset( + name for name in _anthropic_wif_litellm_params if name.endswith("_ref") +) + + +def holds_secret_pointer(param_name: str) -> bool: + """A ``*_ref`` federation field is a secret POINTER the identity source dereferences at use + time, so a loader expanding ``os.environ/`` values must leave it as written.""" + return param_name in _ANTHROPIC_WIF_POINTER_FIELDS + + _RESERVED_INIT_KEYS: Final = frozenset({"self", "params", "__class__"}) diff --git a/tests/test_litellm/proxy/test_proxy_server.py b/tests/test_litellm/proxy/test_proxy_server.py index 8e470cc663b..f222b1107ab 100644 --- a/tests/test_litellm/proxy/test_proxy_server.py +++ b/tests/test_litellm/proxy/test_proxy_server.py @@ -11756,3 +11756,54 @@ async def test_load_config_router_authorizes_fallback_targets_against_the_callin router, _, _ = await ProxyConfig().load_config(router=None, config_file_path=str(config_file)) assert router.fallback_access_check is router_fallback_access_check + + +def test_resolve_db_litellm_param_keeps_wif_secret_pointers(monkeypatch): + from litellm.proxy.proxy_server import ProxyConfig + + monkeypatch.setenv("WIF_TEST_KC_SECRET", "kc-secret") + proxy_config = ProxyConfig() + + pointer = proxy_config._resolve_db_litellm_param( + "anthropic_keycloak_client_secret_ref", "os.environ/WIF_TEST_KC_SECRET" + ) + dereferenced = proxy_config._resolve_db_litellm_param("api_key", "os.environ/WIF_TEST_KC_SECRET") + + assert pointer == "os.environ/WIF_TEST_KC_SECRET" + assert dereferenced == "kc-secret" + + +@pytest.mark.asyncio +async def test_load_config_keeps_wif_secret_pointers_on_config_models(tmp_path, monkeypatch): + from litellm.proxy.proxy_server import ProxyConfig + + monkeypatch.setenv("WIF_TEST_SIGNING_KEY", "-----BEGIN PRIVATE KEY-----") + monkeypatch.setenv("WIF_TEST_FDRL", "fdrl_from_env") + config_file = tmp_path / "config.yaml" + config_file.write_text( + yaml.dump( + { + "model_list": [ + { + "model_name": "claude-wif", + "litellm_params": { + "model": "anthropic/claude-haiku-4-5", + "anthropic_federation_rule_id": "os.environ/WIF_TEST_FDRL", + "anthropic_identity_source": "internal_issuer", + "anthropic_issuer_url": "https://litellm.example", + "anthropic_issuer_audience": "https://api.anthropic.com", + "anthropic_issuer_signing_key_ref": "os.environ/WIF_TEST_SIGNING_KEY", + }, + } + ] + } + ) + ) + + _router, model_list, _general_settings = await ProxyConfig().load_config( + router=None, config_file_path=str(config_file) + ) + + litellm_params = model_list[0]["litellm_params"] + assert litellm_params["anthropic_federation_rule_id"] == "fdrl_from_env" + assert litellm_params["anthropic_issuer_signing_key_ref"] == "os.environ/WIF_TEST_SIGNING_KEY" diff --git a/tests/test_litellm/test_router.py b/tests/test_litellm/test_router.py index f14a74a1056..a882ec5583b 100644 --- a/tests/test_litellm/test_router.py +++ b/tests/test_litellm/test_router.py @@ -11564,3 +11564,28 @@ class TestTierParamsTheTargetAccepts: accepted = router._tier_params_the_target_accepts("no-such-group", {"reasoning_effort": "max"}, {}) assert accepted == {"reasoning_effort": "max"} + + +def test_router_keeps_wif_secret_pointers_unresolved(monkeypatch): + monkeypatch.setenv("WIF_TEST_KC_SECRET", "kc-secret") + monkeypatch.setenv("WIF_TEST_FDRL", "fdrl_from_env") + router = Router( + model_list=[ + { + "model_name": "claude-wif", + "litellm_params": { + "model": "anthropic/claude-haiku-4-5", + "anthropic_federation_rule_id": "os.environ/WIF_TEST_FDRL", + "anthropic_identity_source": "keycloak", + "anthropic_keycloak_token_url": "https://keycloak.example/token", + "anthropic_keycloak_client_id": "litellm", + "anthropic_keycloak_client_secret_ref": "os.environ/WIF_TEST_KC_SECRET", + }, + } + ] + ) + + litellm_params = router.get_model_list()[0]["litellm_params"] + + assert litellm_params["anthropic_federation_rule_id"] == "fdrl_from_env" + assert litellm_params["anthropic_keycloak_client_secret_ref"] == "os.environ/WIF_TEST_KC_SECRET" diff --git a/tests/test_litellm/types/test_router.py b/tests/test_litellm/types/test_router.py index f377a59f21c..f8c822971cd 100644 --- a/tests/test_litellm/types/test_router.py +++ b/tests/test_litellm/types/test_router.py @@ -8,6 +8,7 @@ from litellm.types.router import ( ModelInfo, anthropic_wif_fields_named, anthropic_wif_fields_present, + holds_secret_pointer, ) from litellm.types.utils import ( CustomPricingLiteLLMParams, @@ -146,3 +147,13 @@ def test_anthropic_wif_fields_named_reports_keys_whatever_their_value(): def test_anthropic_wif_fields_named_is_derived_from_the_shared_list(): assert set(anthropic_wif_fields_named(frozenset(anthropic_wif_litellm_params))) == set(anthropic_wif_litellm_params) + + +@pytest.mark.parametrize("param_name", ["anthropic_issuer_signing_key_ref", "anthropic_keycloak_client_secret_ref"]) +def test_wif_ref_fields_hold_secret_pointers(param_name: str): + assert holds_secret_pointer(param_name) + + +@pytest.mark.parametrize("param_name", ["api_key", "anthropic_federation_rule_id", "anthropic_identity_token"]) +def test_dereferenced_fields_do_not_hold_secret_pointers(param_name: str): + assert not holds_secret_pointer(param_name)