Skip to content

docs: stop advertising sk-1234 as the master key in shipped configs and examples - #42011

Merged
mateo-berri merged 1 commit into
mainfrom
litellm_scrub_default_master_key
Sep 20, 2026
Merged

mateo-berri merged 1 commit into
mainfrom
litellm_scrub_default_master_key

Conversation

@ryan-crabbe-berri

@ryan-crabbe-berri ryan-crabbe-berri commented Sep 19, 2026 •

Copy link
Copy Markdown
Contributor

TLDR

Problem this solves:

  • Shipped configs and examples hardcode sk-1234 as the master key
  • Users copy them, so a publicly known key reaches real deployments

How it solves it:

  • Shipped configs read the master key from LITELLM_MASTER_KEY
  • .env examples ship a blank value plus an openssl generate command
  • Docs, the missing env vars page and Admin UI snippets show <your-master-key>

This is the behaviour-neutral companion to the upcoming change that makes the proxy refuse to boot when the master key is unset, empty or sk-1234. No auth code changes here

What changed, by area

Shipped proxy configs now set master_key: os.environ/LITELLM_MASTER_KEY instead of master_key: sk-1234: proxy_server_config.yaml, litellm/proxy/proxy_config.yaml, dev_config.yaml, _new_secret_config.yaml, wildcard_config.yaml, and under litellm/proxy/example_config_yaml/ the adaptive_router_example, oai_misc_config, pass_through_config, reject_clientside_metadata_tags_config and tool_permission_example files. Runnable files carry no literal key at all, because a placeholder literal would itself become a publicly known working key if copied unedited

CI: the two CircleCI docker run steps that mount proxy_server_config.yaml and oai_misc_config.yaml now pass -e LITELLM_MASTER_KEY="sk-1234", so the key those jobs run with is unchanged and the tests that send sk-1234 as the bearer token keep passing. The pass_through_config.yaml job already passed it. No other CI block is touched

.env.example and docker/.env.example ship LITELLM_MASTER_KEY blank with # Generate one with: echo "LITELLM_MASTER_KEY=sk-$(openssl rand -hex 32)" on the line above

Docs: README.md, CONTRIBUTING.md, the in-package READMEs (anthropic_interface, containers, bitbucket, gitlab, workflows), the curl comments in the generic guardrail example_config.yaml, and scripts/adaptive_router_demo/README.md use <your-master-key> in curl and code examples. Where an example boots a proxy it now exports a generated key first and reuses it in the later step

The "missing env vars" HTML page rendered by admin_ui_utils.py shows the generate command and a blank value instead of LITELLM_MASTER_KEY="sk-1234"

Admin UI: the code snippets in the agent builder Connect tab, the prompt editor "Get code" dialog, the public model hub, the AI Hub table, the API reference page, the cost tracking "how it works" panel and the MCP semantic filter test fall back to <your-master-key>. I checked the two fallbacks that looked like runtime keys (AgentBuilderView.tsx and PromptCodeSnippets.tsx): both only feed displayed snippets, neither is sent on a request, so no request guard was needed

User Flow

Before: an operator who copies a shipped config and exports their own strong master key still ends up with a proxy that only accepts the public sk-1234

  1. They run export LITELLM_MASTER_KEY="sk-$(openssl rand -hex 32)" and start the proxy with litellm/proxy/wildcard_config.yaml
  2. They send GET http://localhost:4731/key/list with Authorization: Bearer $LITELLM_MASTER_KEY and get HTTP 401 token_not_found_in_db
  3. They send the same GET with Authorization: Bearer sk-1234 and get HTTP 200 with the key list
  4. Anyone who knows the default key can list and manage keys on that proxy

After: the same steps give a proxy that accepts only the key the operator generated

  1. They run export LITELLM_MASTER_KEY="sk-$(openssl rand -hex 32)" and start the proxy with litellm/proxy/wildcard_config.yaml
  2. They send GET http://localhost:4731/key/list with Authorization: Bearer $LITELLM_MASTER_KEY and get HTTP 200 with the key list
  3. They send the same GET with Authorization: Bearer sk-1234 and get HTTP 401 token_not_found_in_db
  4. Someone who only knows the default key can no longer list or manage keys on that proxy

Relevant issues

Companion to #42019, which makes the proxy refuse to start on an unset, empty, or sk-1234 master key. Merge #42019 first, so a shipped config started without LITELLM_MASTER_KEY refuses to boot and never runs unauthenticated

Affected release

Linear ticket

Pre-Submission checklist

Please complete all items before asking a LiteLLM maintainer to review your PR

  • I have added meaningful tests
  • The handful of test files covering my change pass locally, e.g. uv run pytest tests/test_litellm/<your_test_file>.py -v. Leave the suites (make test-unit-*, make test-unit) to CI: it finishes in ~15 minutes where a laptop takes an hour or more
  • My PR passes all required CI/CD checks (e.g., lint, schema.d.ts sync check, etc.)
  • My PR's scope is as isolated as possible; it only solves 1 specific problem
  • I have received a Greptile Confidence Score of at least 4/5 before requesting a maintainer review (Greptile reviews automatically once the PR is opened; only comment @greptileai to re-request a review after pushing changes)

Delays in PR merge?

If you're seeing a delay in your PR being merged, ping the LiteLLM Team on Slack (#pr-review).

Screenshots / Proof of Fix

Shared setup: each leg runs from its own worktree and .venv at the named commit with the developer .env moved out of the tree. The proxy boots litellm/proxy/wildcard_config.yaml with --num_workers 2 on a random free port against a local Postgres database created for that leg (mat627_before, mat627_after), with export LITELLM_MASTER_KEY="sk-$(openssl rand -hex 32)" in the same shell (redacted to its first 6 characters below). The Admin UI dev server (npm run dev in ui/litellm-dashboard) points at that proxy. Every curl was repeated so both workers answered (4 times before, 6 times after) with identical output each time, and one copy is shown. No LLM calls are involved, since this change only touches which master key the proxy accepts, so the unified endpoints are out of scope

Before (b946d12)

Shipped config with a generated LITELLM_MASTER_KEY (proxy on 22261)

  1. echo "${LITELLM_MASTER_KEY:0:6}..."
    sk-a5a...
    
  2. curl -s -w '\nHTTP %{http_code}\n' http://localhost:22261/key/list -H "Authorization: Bearer $LITELLM_MASTER_KEY"
    {"error":{"message":"Authentication Error, Invalid proxy server token passed. Received API Key = sk-...4a7a, Key Hash (Token) =522a3d980c8458c89b6cfd4dcce010e43a174ce3fa11b072e862cac88ebda211. Unable to find token in cache or `LiteLLM_VerificationTokenTable`","type":"token_not_found_in_db","param":"key","code":"401"}}
    HTTP 401
    
  3. curl -s -w '\nHTTP %{http_code}\n' http://localhost:22261/key/list -H 'Authorization: Bearer sk-1234'
    {"keys":[],"total_count":0,"current_page":1,"total_pages":0}
    HTTP 200
    

Missing env vars page and keyless boot (no LITELLM_MASTER_KEY, no DATABASE_URL, proxy on 41977)

  1. curl -s http://localhost:41977/sso/key/generate | grep MASTER_KEY
    <span class="env-var">LITELLM_MASTER_KEY="sk-1234"</span> <span class="comment"># Your master key for the proxy server. Can use this to send /chat/completion requests etc</span>
    
  2. curl -s -w '\nHTTP %{http_code}\n' http://localhost:41977/key/list -H 'Authorization: Bearer sk-1234'
    {"error":{"message":"Authentication Error, Database not connected","type":"internal_server_error","param":"None","code":"500"}}
    HTTP 500
    
  3. curl -s -w '\nHTTP %{http_code}\n' http://localhost:41977/v1/models
    {"error":{"message":"Authentication Error, No api key passed in.","type":"auth_error","param":"None","code":"401"}}
    HTTP 401
    

Admin UI API reference page (dev server on 28455 against the proxy on 22261)

  1. Go to http://localhost:28455/?page=api_ref (it lands on http://localhost:28455/api-reference/) and log in as admin with the generated key. The page shows the 401 below instead of the API reference
    {"error":{"message":"Invalid credentials used to access UI. Check 'UI_USERNAME' and 'UI_PASSWORD', or the password set for your user","type":"auth_error","param":"invalid_credentials","code":"401"}}
    
    pr42011-c8e0f2ddb4-before-login-generated-key-401.png
  2. Log in as admin with sk-1234 instead: redirected to /ui/?login=success
  3. Open the LlamaIndex tab: api_key="sk-1234" on lines 11 and 18 of the snippet
    pr42011-c8e0f2ddb4-before-api-reference.png

After (c8e0f2d)

Shipped config with a generated LITELLM_MASTER_KEY (proxy on 50267)

  1. echo "${LITELLM_MASTER_KEY:0:6}..."
    sk-10e...
    
  2. curl -s -w '\nHTTP %{http_code}\n' http://localhost:50267/key/list -H "Authorization: Bearer $LITELLM_MASTER_KEY"
    {"keys":[],"total_count":0,"current_page":1,"total_pages":0}
    HTTP 200
    
  3. curl -s -w '\nHTTP %{http_code}\n' http://localhost:50267/key/list -H 'Authorization: Bearer sk-1234'
    {"error":{"message":"Authentication Error, Invalid proxy server token passed. Received API Key = sk-..., Key Hash (Token) =88dc28d0f030c55ed4ab77ed8faf098196cb1c05df778539800c9f1243fe6b4b. Unable to find token in cache or `LiteLLM_VerificationTokenTable`","type":"token_not_found_in_db","param":"key","code":"401"}}
    HTTP 401
    

Missing env vars page and keyless boot (no LITELLM_MASTER_KEY, no DATABASE_URL, proxy on 34628)

  1. curl -s http://localhost:34628/sso/key/generate | grep MASTER_KEY
    <span class="comment"># Generate one with: echo "LITELLM_MASTER_KEY=sk-$(openssl rand -hex 32)"</span>
    <span class="env-var">LITELLM_MASTER_KEY=""</span> <span class="comment"># Your master key for the proxy server. Can use this to send /chat/completion requests etc</span>
    <p>DATABASE_URL, LITELLM_MASTER_KEY</p>
    
  2. curl -s -w '\nHTTP %{http_code}\n' http://localhost:34628/key/list -H 'Authorization: Bearer sk-1234'
    {"error":{"message":"Authentication Error, Database not connected","type":"internal_server_error","param":"None","code":"500"}}
    HTTP 500
    
  3. curl -s -w '\nHTTP %{http_code}\n' http://localhost:34628/v1/models, with no Authorization header at all. This is the keyless boot the Medium caveat below describes, and the reason feat(proxy)!: refuse to start with an unset, empty, or publicly known master key #42019 merges first
    {"data":[{"id":"anthropic/*","object":"model","created":1677610602,"owned_by":"openai"},{"id":"gpt-5.5","object":"model","created":1677610602,"owned_by":"openai"},{"id":"gpt-5.5-mini","object":"model","created":1677610602,"owned_by":"openai"},{"id":"gpt-5.5-nano","object":"model","created":1677610602,"owned_by":"openai"},{"id":"openai/*","object":"model","created":1677610602,"owned_by":"openai"},{"id":"my-general-model","object":"model","created":1677610602,"owned_by":"openai"}],"object":"list"}
    HTTP 200
    

Admin UI API reference page (dev server on 37102 against the proxy on 50267)

  1. Go to http://localhost:37102/?page=api_ref (it lands on http://localhost:37102/api-reference/) and log in as admin with the generated key: redirected to /ui/?login=success, and the page titled "OpenAI Compatible Proxy: API Reference" loads
  2. Open the LlamaIndex tab: line 11 reads api_key="<your-master-key>", # litellm proxy API Key and line 18 reads api_key="<your-master-key>",. No sk-1234 on either changed tab
    pr42011-c8e0f2ddb4-after-api-reference.png

Blank key from the .env examples (LITELLM_MASTER_KEY="", proxy on 41977, database connected)

  1. curl -s -w '\nHTTP %{http_code}\n' http://localhost:41977/v1/models
    {"error":{"message":"Authentication Error, No api key passed in.","type":"auth_error","param":"None","code":"401"}}
    HTTP 401
    
  2. curl -s -w '\nHTTP %{http_code}\n' http://localhost:41977/key/list -H 'Authorization: Bearer sk-1234'
    {"error":{"message":"Authentication Error, Invalid proxy server token passed. Received API Key = sk-..., Key Hash (Token) =88dc28d0f030c55ed4ab77ed8faf098196cb1c05df778539800c9f1243fe6b4b. Unable to find token in cache or `LiteLLM_VerificationTokenTable`","type":"token_not_found_in_db","param":"key","code":"401"}}
    HTTP 401
    
  3. curl -s -w '\nHTTP %{http_code}\n' http://localhost:41977/key/list -H 'Authorization: Bearer '
    {"error":{"message":"Authentication Error, Malformed API Key passed in. Ensure Key has `Bearer ` prefix.","type":"auth_error","param":"None","code":"401"}}
    HTTP 401
    
  4. curl -s -w '\nHTTP %{http_code}\n' -X POST http://localhost:41977/v2/login -H 'Content-Type: application/json' -d '{"username":"admin","password":""}'
    {"error":{"message":"HMAC key must not be empty.","type":"auth_error","param":"None","code":"500"}}
    HTTP 500
    

A blank key fails closed on every route tried, and #42019 refuses to boot on it

Merged tree (origin/main 75f4c11 with #42019 at fdd614d and this PR, proxy on 43841, database connected)

  1. LITELLM_MASTER_KEY unset: the proxy refuses to start with UnsafeMasterKeyError: LiteLLM proxy refused to start: no master key is set, so every request would be accepted without authentication
  2. LITELLM_MASTER_KEY="": refused with the master key is empty
  3. LITELLM_MASTER_KEY=sk-1234: refused with the master key is a publicly known default
  4. A generated key: boots, and GET /key/list with Bearer sk-1234 gets the same 401 token_not_found_in_db as the After leg

Observations from the run, none caused by this PR:

Type

📖 Documentation
🚄 Infrastructure

Caveats (if any)

Medium

  • Shipped config with LITELLM_MASTER_KEY unset now boots with no master key

Low

  • Admin UI snippet changes have unit tests only, except the API reference page, which has before and after screenshots above
  • <your-master-key> also shows on pages aimed at virtual key holders
  • docker-compose.hardened.yml mounts proxy_server_config.yaml and gets its key from .env
  • Out of scope and unchanged: tests/, cookbook/, Python docstring curl examples, ci_cd/, and the two operator scripts that default a client key to sk-1234 (qa_sticky_session.sh, scripts/health_check/run_parallel_health_checks.sh)
  • The committed Admin UI bundle under litellm/proxy/_experimental/out still carries the old sk-1234 snippet fallbacks until the next build_release_ui.sh run picks up the source changes; rebuilding it here would add a generated bundle diff to a docs change
  • The helm chart already reads os.environ/PROXY_MASTER_KEY and generates a random key when none is given, so it needed no change
  • The merged tree check ran with feat(proxy)!: refuse to start with an unset, empty, or publicly known master key #42019 at fdd614d, not its current tip 99f99cf. The commits between them change the key migration (a failed migration now stops the boot unless allow_requests_on_db_unavailable tolerates a connection outage) and the refusal text, not whether an unset, empty, or sk-1234 key is refused
  • Three CircleCI jobs stay red at this tip after the from-failed reruns on pipeline 89900, and each failure is matched to main after the merge base rather than to this diff, which touches none of the files those tests read
    • proxy_e2e_anthropic_messages_tests (job 2192664): the two bedrock test_bedrock_invoke_messages_with_all_beta_headers cases fail with invalid beta flag, fixed on main by 7966f50
    • integration-cost (job 2192672): the fireworks fallback cache-read case expects a stale price, repinned on main by 02736e2
    • integration-extensions (job 2192673): all four modules fail collection with No module named 'mcp.server.fastmcp', which is red on main's own pipelines 89898, 89899, and 89901 since 2026-09-19 23:48Z and green on 89877
    • The OpenAI 429 credit failures from the first CircleCI run cleared on the reruns

Live PR risk

Breaking

None found. No auth code changes; the shipped configs resolve master_key through the existing os.environ/ reader, and the two CircleCI docker runs that mount the changed configs receive LITELLM_MASTER_KEY="sk-1234", so the tests that send sk-1234 as the bearer token keep passing

Backward incompatible

Regression risk

  • CircleCI build_and_test and e2e_openai_endpoints mount the changed configs and pass the key explicitly, so their sk-1234 bearer tests still hold; proxy_pass_through_endpoint_tests already passed it
  • The seven Admin UI snippet fallbacks only feed displayed code. AgentBuilderView.tsx and PromptCodeSnippets.tsx send nothing on a request, so no request guard was needed
  • The missing env vars page from admin_ui_utils.py renders the generate comment and a blank value (After proof, keyless boot step 1)

Dependency graph

  • litellm/proxy/wildcard_config.yaml: verified live on both legs
  • proxy_server_config.yaml, oai_misc_config.yaml, pass_through_config.yaml: tested by the CircleCI jobs build_and_test, e2e_openai_endpoints, proxy_pass_through_endpoint_tests
  • litellm/proxy/proxy_config.yaml, dev_config.yaml, _new_secret_config.yaml, and the other example_config_yaml files: untested, the same one-line master_key change as the verified config
  • admin_ui_utils.py missing env vars page: verified live
  • Admin UI API reference page: verified live with screenshots; the other six snippet sites are covered by the diff's unit tests only
  • .env.example and docker/.env.example blank key: verified live at the tip
  • README.md, CONTRIBUTING.md, the package READMEs, and the adaptive router demo README: docs only, nothing to drive
  • Merged tree (origin/main 75f4c11 with feat(proxy)!: refuse to start with an unset, empty, or publicly known master key #42019 at fdd614d and this PR): unset, empty, and sk-1234 keys refuse to boot with the UnsafeMasterKeyError headlines quoted above, a generated key boots and rejects sk-1234

Not verified

Final Attestation

  • The tests check the right things, including the edge cases, and regressions in the respective real-world customer use-cases are not possible after this PR

Note

Medium Risk
Changes default proxy bootstrap and operator docs around authentication secrets; until the companion boot guard lands, starting shipped configs without LITELLM_MASTER_KEY may leave auth misconfigured.

Overview
Stops treating sk-1234 as the default LiteLLM proxy master key in anything operators are likely to copy. Shipped proxy YAML now sets general_settings.master_key to os.environ/LITELLM_MASTER_KEY instead of a literal sk-1234, so a generated env key is what the proxy actually honors when users follow the repo configs.

.env.example and docker/.env.example ship a blank LITELLM_MASTER_KEY plus an openssl rand -hex 32 generate hint. README, CONTRIBUTING, package READMEs, and dashboard code snippets use <your-master-key> (or $LITELLM_MASTER_KEY in runnable examples). The missing env vars HTML from admin_ui_utils matches that pattern.

CircleCI passes LITELLM_MASTER_KEY="sk-1234" into the two Docker jobs that mount updated configs (build_and_test, e2e_openai_endpoints) so CI behavior stays the same. Unit tests lock in the admin UI and snippet placeholder behavior.

No proxy auth logic changes here; this pairs with a follow-up that refuses boot on unset/empty/sk-1234 keys.

Reviewed by Cursor Bugbot for commit c8e0f2d. Bugbot is set up for automated code reviews on this repo. Configure here.

…nd examples

Shipped proxy configs now read general_settings.master_key from
os.environ/LITELLM_MASTER_KEY, the .env examples ship a blank value with
the openssl generate command above it, and READMEs, the missing env vars
page and Admin UI code snippets show a generate command or the
<your-master-key> placeholder instead of the literal sk-1234

The two CircleCI docker runs that mount proxy_server_config.yaml and
oai_misc_config.yaml now pass LITELLM_MASTER_KEY so their runtime key is
unchanged
@codspeed

codspeed Bot commented Sep 19, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 31 untouched benchmarks


Comparing litellm_scrub_default_master_key (c8e0f2d) with main (b946d12)1

Open in CodSpeed

Footnotes

  1. No successful run was found on main (a93bfdc) during the generation of this report, so b946d12 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report. ↩

@greptile-apps

greptile-apps Bot commented Sep 19, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 5/5

The PR appears safe to merge after its stated companion startup validation dependency is satisfied

Summary

This PR hardens shipped configuration and documentation by replacing a known example credential with environment-backed configuration, generated-key instructions, and nonfunctional placeholders. It also updates CI to preserve its existing test credential and adds focused checks for the revised Admin UI guidance

  • Runnable proxy configurations now resolve LITELM_MASTER_KEY from the environment
  • Environment templates provide key-generation guidance instead of a shared default
  • Documentation and dashboard snippets use placeholders or generated values
  • CircleCI explicitly supplies the credential expected by affected proxy tests
  • No changes were made after the previous review

Reviews (2) · Last reviewed commit: "docs: stop advertising sk-1234 as the ma..."

Comment thread proxy_server_config.yaml
Comment thread litellm/anthropic_interface/readme.md
@codecov

codecov Bot commented Sep 19, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@mateo-berri

Copy link
Copy Markdown
Contributor

@greptileai

@mateo-berri

Copy link
Copy Markdown
Contributor

bugbot run

@cursor cursor Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread .env.example
# Development Configs
LITELLM_MASTER_KEY = "sk-1234"
# Generate one with: echo "LITELLM_MASTER_KEY=sk-$(openssl rand -hex 32)"
LITELLM_MASTER_KEY = ""

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Empty master key locks out operators

Medium Severity

Copying the shipped .env examples sets LITELLM_MASTER_KEY to an empty string, which get_secret keeps as "" rather than None. Auth then requires a key, the empty master key never matches, and show_missing_vars_in_env only treats master_key is None as missing, so the setup page never appears.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit c8e0f2d. Configure here.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An empty key fails closed at this tip, verified live with 401s. #42019 merges first and refuses to boot on it, printing the generate command

@mateo-berri

Copy link
Copy Markdown
Contributor

bugbot run

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

1 issue from previous review remains unresolved.

Fix All in Cursor

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit c8e0f2d. Configure here.

@mateo-berri mateo-berri left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. Thanks!

@mateo-berri
mateo-berri merged commit 5417abd into main Sep 20, 2026
159 of 162 checks passed
@mateo-berri
mateo-berri deleted the litellm_scrub_default_master_key branch September 20, 2026 02:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants