mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-25 01:02:15 +00:00
fix(proxy): point users who must rotate at the rotation guide before they save a new key
This commit is contained in:
parent
0415382f9e
commit
186ba50bce
3 changed files with 35 additions and 8 deletions
|
|
@ -17,7 +17,9 @@ MASTER_KEY_ENV_VAR: Final = "LITELLM_MASTER_KEY"
|
|||
SALT_KEY_ENV_VAR: Final = "LITELLM_SALT_KEY"
|
||||
PUBLICLY_KNOWN_MASTER_KEYS: Final = frozenset({"sk-1234"})
|
||||
ROTATION_DOCS_URL: Final = "https://docs.litellm.ai/docs/proxy/master_key_rotations#proxy-refuses-to-start"
|
||||
GENERATE_MASTER_KEY_COMMAND: Final = f'echo "{MASTER_KEY_ENV_VAR}=sk-$(openssl rand -hex 32)" | tee -a .env'
|
||||
_NEW_MASTER_KEY: Final = "sk-$(openssl rand -hex 32)"
|
||||
GENERATE_MASTER_KEY_COMMAND: Final = f'echo "{MASTER_KEY_ENV_VAR}={_NEW_MASTER_KEY}" | tee -a .env'
|
||||
PRINT_NEW_MASTER_KEY_COMMAND: Final = f'echo "{_NEW_MASTER_KEY}"'
|
||||
|
||||
|
||||
class UnsafeMasterKeyReason(Enum):
|
||||
|
|
@ -129,8 +131,7 @@ def render_refusal(refusal: UnsafeMasterKeyRefused) -> str:
|
|||
return "\n\n".join(
|
||||
(
|
||||
f"LiteLLM proxy refused to start: {_REFUSAL_HEADLINE[refusal.reason]}\n{_source_line(refusal)}",
|
||||
_fix_steps(refusal.source),
|
||||
*((_ROTATION_WARNING,) if refusal.stored_credentials_need_rotation else ()),
|
||||
_ROTATE_INSTEAD_OF_REPLACING if refusal.stored_credentials_need_rotation else _fix_steps(refusal.source),
|
||||
_OVERRIDE_HINT,
|
||||
)
|
||||
)
|
||||
|
|
@ -161,9 +162,12 @@ _SAVE_KEY_STEP: Final = (
|
|||
f" {MASTER_KEY_ENV_VAR} environment variable instead."
|
||||
)
|
||||
|
||||
_ROTATION_WARNING: Final = (
|
||||
f"Credentials stored in your database are encrypted with the current master key because {SALT_KEY_ENV_VAR}\n"
|
||||
f"is not set. Rotate the key before changing it, or they become undecryptable:\n{ROTATION_DOCS_URL}"
|
||||
_ROTATE_INSTEAD_OF_REPLACING: Final = (
|
||||
f"Credentials stored in your database are encrypted with this master key because {SALT_KEY_ENV_VAR} is not\n"
|
||||
"set, so replacing the key makes them undecryptable. Rotate it by following this guide, which re-encrypts them:\n"
|
||||
f" {ROTATION_DOCS_URL}\n"
|
||||
"Generate the new key for it with (save it only once the guide says to):\n"
|
||||
f" {PRINT_NEW_MASTER_KEY_COMMAND}"
|
||||
)
|
||||
|
||||
_OVERRIDE_HINT: Final = (
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import pytest
|
|||
from litellm.proxy.auth.master_key_boot_check import (
|
||||
GENERATE_MASTER_KEY_COMMAND,
|
||||
MASTER_KEY_ENV_VAR,
|
||||
PRINT_NEW_MASTER_KEY_COMMAND,
|
||||
ROTATION_DOCS_URL,
|
||||
UNSAFE_PROXY_OVERRIDE_ENV_VAR,
|
||||
UNSAFE_PROXY_OVERRIDE_SETTING,
|
||||
|
|
@ -172,7 +173,7 @@ def test_unset_key_refusal_says_nothing_supplied_one():
|
|||
assert "Neither general_settings.master_key nor" in text
|
||||
|
||||
|
||||
def test_rotation_warning_appears_only_when_needed():
|
||||
def test_rotation_guide_appears_only_when_needed():
|
||||
with_rotation = render_refusal(_refusal(stored_credentials_need_rotation=True))
|
||||
without_rotation = render_refusal(_refusal(stored_credentials_need_rotation=False))
|
||||
|
||||
|
|
@ -180,6 +181,17 @@ def test_rotation_warning_appears_only_when_needed():
|
|||
assert ROTATION_DOCS_URL not in without_rotation
|
||||
|
||||
|
||||
@pytest.mark.parametrize("source", [EnvironmentSource(), ConfigFileSource(config_file_path="/app/config.yaml")])
|
||||
def test_refusal_never_tells_a_user_who_must_rotate_to_save_the_new_key_first(
|
||||
source: ConfigFileSource | EnvironmentSource,
|
||||
):
|
||||
text = render_refusal(_refusal(source=source, stored_credentials_need_rotation=True))
|
||||
|
||||
assert PRINT_NEW_MASTER_KEY_COMMAND in text
|
||||
assert ".env" not in text
|
||||
assert "os.environ/" not in text
|
||||
|
||||
|
||||
@pytest.mark.parametrize("stored_credentials_need_rotation", [True, False])
|
||||
def test_override_hint_is_the_last_paragraph(stored_credentials_need_rotation: bool):
|
||||
text = render_refusal(_refusal(stored_credentials_need_rotation=stored_credentials_need_rotation))
|
||||
|
|
@ -202,6 +214,17 @@ def test_printed_command_saves_a_key_the_boot_check_accepts(tmp_path: Path):
|
|||
assert _verdict(match.group(1)) == SafeMasterKey()
|
||||
|
||||
|
||||
@pytest.mark.skipif(shutil.which("openssl") is None, reason="the printed command shells out to openssl")
|
||||
def test_rotation_command_prints_a_key_the_boot_check_accepts_and_saves_nothing(tmp_path: Path):
|
||||
completed = subprocess.run(
|
||||
["bash", "-c", PRINT_NEW_MASTER_KEY_COMMAND], cwd=tmp_path, capture_output=True, text=True, check=True
|
||||
)
|
||||
|
||||
assert re.fullmatch(r"sk-[0-9a-f]{64}\n", completed.stdout) is not None
|
||||
assert _verdict(completed.stdout.strip()) == SafeMasterKey()
|
||||
assert list(tmp_path.iterdir()) == []
|
||||
|
||||
|
||||
def test_refusal_announces_the_fix_and_aborts_the_boot():
|
||||
announced: list[str] = []
|
||||
refusal = _refusal(source=ConfigFileSource(config_file_path="/app/config.yaml"))
|
||||
|
|
|
|||
|
|
@ -1663,7 +1663,7 @@ async def test_proxy_startup_refuses_an_unsafe_master_key_before_connecting_to_t
|
|||
pass
|
||||
|
||||
assert len(announced) == 1
|
||||
assert "LITELLM_MASTER_KEY=sk-$(openssl rand -hex 32)" in announced[0]
|
||||
assert "sk-$(openssl rand -hex 32)" in announced[0]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue