{"record":{"id":"174ebf2e5fd727ef","repo":"BerriAI/litellm","slug":"cannot-migrate-covered-tables-no-salt-key-maste","errorCode":null,"errorMessage":"Cannot migrate covered tables: no salt key / master key is set. Set LITELLM_SALT_KEY before migrating.","messagePattern":"Cannot migrate covered tables: no salt key / master key is set\\. Set LITELLM_SALT_KEY before migrating\\.","errorType":"exception","errorClass":"RuntimeError","httpStatus":null,"severity":"error","filePath":"litellm/proxy/management_endpoints/credential_migration.py","lineNumber":578,"sourceCode":"    \"\"\"Re-encrypt the tables already covered by ``_rotate_master_key`` (model\n    table, credentials, MCP credential/env tables, config environment_variables)\n    by running that orchestrator in *same-key* mode. With the AES gate on, the\n    re-encrypt writes land in ``v2:`` format.\n\n    ``_rotate_master_key`` returns no counts, so we bracket it with read-only\n    scans: the pre-scan's legacy total minus the post-scan's gives the number\n    actually migrated per location, and the post-scan supplies the residual /\n    already-v2 / scanned figures. Returns one report per covered location.\n    \"\"\"\n    from litellm.proxy.management_endpoints.key_management_endpoints import (\n        _rotate_master_key,\n    )\n\n    pre: Final = {r.location: r for r in await _scan_covered_tables(prisma_client)}\n\n    current_key: Final = _get_salt_key()\n    if current_key is None:\n        raise RuntimeError(\n            \"Cannot migrate covered tables: no salt key / master key is set. Set LITELLM_SALT_KEY before migrating.\"\n        )\n    await _rotate_master_key(\n        prisma_client=cast(\"PrismaClient\", prisma_client),\n        user_api_key_dict=cast(\"UserAPIKeyAuth\", user_api_key_dict),\n        current_master_key=current_key,\n        new_master_key=current_key,  # same key, algorithm-only switch\n    )\n\n    post: Final = await _scan_covered_tables(prisma_client)\n    for post_report in post:\n        pre_report = pre.get(post_report.location)\n        pre_legacy = pre_report.legacy if pre_report else 0\n        # Everything that was legacy before and is no longer legacy now was\n        # converted this run.\n        post_report.migrated = max(0, pre_legacy - post_report.legacy)\n    return post\n","sourceCodeStart":560,"sourceCodeEnd":596,"githubUrl":"https://github.com/BerriAI/litellm/blob/77b7c6c40c0c5aa5fbcb1d6a1825ac39ca8829b8/litellm/proxy/management_endpoints/credential_migration.py#L560-L596","documentation":"Raised by LiteLLM's credential-migration flow (litellm/proxy/management_endpoints/credential_migration.py) when the pre-flight _get_salt_key() returns None: the proxy process has neither LITELLM_SALT_KEY set nor a master key to fall back to (encrypt_decrypt_utils._get_salt_key checks the env var, then master_key). Migration re-encrypts stored provider credentials by calling _rotate_master_key with the same key (algorithm-only switch), so a key must exist before any rotation runs. The RuntimeError aborts before a single table is touched.","triggerScenarios":"Invoking the covered-tables credential migration endpoint while the proxy process has no LITELLM_SALT_KEY exported and no master key configured; running the migration from a new pod, CI job, or shell that did not inherit the proxy's secret environment.","commonSituations":"Ops schedules a credentials re-encryption after upgrading LiteLLM, but the Kubernetes deployment / Docker env is missing LITELLM_SALT_KEY; the key lives in a .env file the proxy never loads; the migration is invoked from a different container than the one holding the secret.","solutions":["Export LITELLM_SALT_KEY in the proxy process environment (deployment env vars, docker -e, or systemd Environment=) and restart the proxy","Alternatively ensure a master key (LITELLM_MASTER_KEY / general_settings.master_key) is configured, since _get_salt_key() falls back to it","Verify inside the same container/process that runs the migration: printenv LITELLM_SALT_KEY must be non-empty","Re-run the migration endpoint and confirm the pre/post scan reports are returned"],"exampleFix":"# before\npodman run -p 4000:4000 litellm/litellm --config /app/config.yaml   # no salt key -> migration raises RuntimeError\n\n# after\npodman run -p 4000:4000 -e LITELLM_SALT_KEY=sk-salt-... litellm/litellm --config /app/config.yaml","handlingStrategy":"validation","validationCode":"import os\n\n\ndef credential_migration_ready() -> bool:\n    return bool(os.getenv(\"LITELLM_SALT_KEY\") or os.getenv(\"LITELLM_MASTER_KEY\"))\n\n\nif not credential_migration_ready():\n    raise SystemExit(\"Set LITELLM_SALT_KEY before running credential migration\")","typeGuard":null,"tryCatchPattern":"try:\n    report = await migrate_covered_tables(prisma_client, user_api_key_dict)\nexcept RuntimeError as e:\n    if \"no salt key\" in str(e):\n        # environment problem, not a data problem: fix env and re-run once\n        raise SystemExit(\"LITELLM_SALT_KEY missing in this process\") from e\n    raise","preventionTips":["Pass LITELLM_SALT_KEY explicitly in deployment manifests instead of relying on interactive shell exports","Add a startup readiness check that fails fast when the salt key is missing, before any migration job is scheduled","Run migration jobs with the same secret-injection mechanism as the proxy container"],"tags":["litellm","credentials","encryption","salt-key","migration","environment-variable"],"backgroundTag":"missing-env-var","analyzedSha":"77b7c6c40c0c5aa5fbcb1d6a1825ac39ca8829b8","analyzedAt":"2026-08-18T11:44:31.656Z","schemaVersion":2},"datasetVersion":"2026-08-21T08:17:14.275Z"}