Skip to content

Encrypt callback_vars in key/team metadata in DB - #27141

Merged
yuneng-berri merged 4 commits into
litellm_internal_stagingfrom
litellm_langfuseMetadataRedact
May 23, 2026
Merged

Encrypt callback_vars in key/team metadata in DB#27141
yuneng-berri merged 4 commits into
litellm_internal_stagingfrom
litellm_langfuseMetadataRedact

Conversation

@Michael-RZ-Berri

@Michael-RZ-Berri Michael-RZ-Berri commented May 4, 2026

Copy link
Copy Markdown
Contributor

Relevant issues

Several of the API keys stored in the DB are being kept in plain-text rather than encrypted. This allows anyone with auth access to the key to retrieve it through endpoints like key/info, when this should not be the case. For example, a team member with access to team keys can find team API keys for third-party services by hitting that endpoint. This PR encrypts them when stored in the DB and can decrypt them if needed with the existing salting pattern.

This change encrypts keys in the DB for the following:

  • Langfuse
  • Langsmith
  • Arize
  • Braintrust
  • Lago
  • DataDog
  • Openmeter
  • AWS
  • GCS

Linear ticket

Resolves LIT-1958.

Pre-Submission checklist

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

  • I have Added testing in the tests/test_litellm/ directory, Adding at least 1 test is a hard requirement - see details
  • My PR passes all unit tests on make test-unit
  • My PR's scope is as isolated as possible, it only solves 1 specific problem
  • I have requested a Greptile review by commenting @greptileai and received a Confidence Score of at least 4/5 before requesting a maintainer review

Screenshots / Proof of Fix

Example with Langfuse:
Screenshot 2026-05-04 at 3 59 25 PM

Type

🐛 Bug Fix
✅ Test

Changes

callback_utils, litellm_pre_call_utils, key_management_endpoints, team_endpoints


Note

Medium Risk
Touches proxy key/team metadata persistence and dynamic logging callback resolution; incorrect encryption/decryption or key-rotation edge cases could break callbacks or make stored metadata unreadable.

Overview
Encrypts credential-bearing callback_vars fields before they are stored in DB-backed key/team metadata, and transparently decrypts them when used at runtime.

Adds encrypt_callback_vars/decrypt_callback_vars utilities with a sentinel prefix and sensitive-key detection, and wires them into key creation/update flows and team callback/team metadata writes; pre-call logging settings now read decrypted values. Updates and adds unit tests to validate round-trip behavior, idempotency, non-mutation, plaintext passthrough, and that management endpoints persist encrypted values.

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

@CLAassistant

CLAassistant commented May 4, 2026

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you all sign our Contributor License Agreement before we can accept your contribution.
1 out of 2 committers have signed the CLA.

✅ yuneng-berri
❌ Michael Riad Zaky


Michael Riad Zaky seems not to be a GitHub user. You need a GitHub account to be able to sign the CLA. If you have already a GitHub account, please add the email address used for this commit to your account.
You have signed the CLA already but the status is still pending? Let us recheck it.

@greptile-apps

greptile-apps Bot commented May 4, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR encrypts credential-bearing callback_vars fields (Langfuse, Langsmith, Arize, etc.) before they are written to the DB for both keys and teams, and transparently decrypts them at runtime in the pre-call logging path. A sentinel prefix (litellm_enc::) is used for cheap idempotency without needing a decrypt round-trip.

  • encrypt_callback_vars / decrypt_callback_vars utilities are added to callback_utils.py and wired into key create/update, team create/update, team callback add/disable, and the pre-call logging read path.
  • /key/info and /v2/key/info now return litellm_enc:: blobs instead of plaintext — the handlers do not strip or redact callback_vars from the response, and when LITELLM_SALT_KEY is absent the encryption silently falls back to plaintext with no log warning, meaning those endpoints can still expose raw secrets in misconfigured environments.

Confidence Score: 3/5

Not safe to merge without addressing the silent encryption failure: a missing LITELLM_SALT_KEY causes credentials to be stored as plaintext with no log, and /key/info then returns those secrets unmasked.

The write path correctly encrypts when LITELLM_SALT_KEY is present, and the pre-call decryption path is properly wired. However, the bare except Exception in _encrypt_if_plaintext swallows any encryption failure silently, leaving credentials as plaintext in the DB and exposed via /key/info without any operator-visible signal. Additionally, neither /key/info, /v2/key/info, nor /team/info strip or redact callback_vars from the response, so a misconfigured environment would still surface plaintext secrets through those endpoints.

litellm/proxy/common_utils/callback_utils.py (_encrypt_if_plaintext silent fallback) and litellm/proxy/management_endpoints/key_management_endpoints.py (info_key_fn / info_key_fn_v2 response handling)

Security Review

  • Silent plaintext fallback (callback_utils.py _encrypt_if_plaintext): when LITELLM_SALT_KEY is absent or encrypt_value_helper raises, the bare except Exception block returns the credential as plaintext with no log warning. A misconfigured production deployment silently stores secrets unencrypted, and /key/info//v2/key/info then expose them as plaintext.
  • /key/info and /v2/key/info expose callback_vars without field-level redaction (key_management_endpoints.py): neither endpoint strips or redacts callback_vars before returning the response. In the encryption-failed case this results in plaintext credential exposure; even in the success case the litellm_enc:: prefix leaks the encryption scheme.
  • /team/info has the same gap (team_endpoints.py): team metadata containing callback_vars is returned verbatim, with the same plaintext-on-failure risk.

Important Files Changed

Filename Overview
litellm/proxy/common_utils/callback_utils.py Adds encrypt/decrypt utilities for callback_vars; the silent exception fallback in _encrypt_if_plaintext stores plaintext without any log warning when LITELLM_SALT_KEY is missing.
litellm/proxy/management_endpoints/key_management_endpoints.py Wires encrypt_callback_vars into key creation/update and decrypt into key_health; /key/info and /v2/key/info still return callback_vars (as encrypted blobs or plaintext on failure) without stripping sensitive fields.
litellm/proxy/litellm_pre_call_utils.py Correctly wraps key and team metadata reads with decrypt_callback_vars before use in the hot path; changes are minimal and safe.
litellm/proxy/management_endpoints/team_callback_endpoints.py Adds encrypt_callback_vars before the DB write in add_team_callbacks and disable_team_logging; encryption is applied correctly on the write path.
litellm/proxy/management_endpoints/team_endpoints.py Encrypts metadata before new_team and update_team DB writes; /team/info also returns metadata with encrypted blobs or plaintext without stripping, the same gap as /key/info.

Comments Outside Diff (1)

  1. litellm/proxy/management_endpoints/key_management_endpoints.py, line 3244-3249 (link)

    P1 security /v2/key/info and /key/info expose callback_vars without field-level redaction

    After this PR, callback_vars values written by the encryption path carry a visible litellm_enc:: prefix followed by the ciphertext and are returned verbatim by both info_key_fn_v2 and info_key_fn. In environments where LITELLM_SALT_KEY is not set the fallback is silent plaintext (see companion comment), and in configured environments callers receive an opaque blob that leaks field names and the encryption scheme prefix. Stripping or redacting callback_vars entirely before the response is returned is the more principled fix: it prevents both plaintext leakage when encryption is misconfigured and ciphertext exposure.

Reviews (4): Last reviewed commit: "Merge remote-tracking branch 'origin/lit..." | Re-trigger Greptile

Comment thread litellm/proxy/common_utils/callback_utils.py Outdated
@Michael-RZ-Berri
Michael-RZ-Berri force-pushed the litellm_langfuseMetadataRedact branch from 24d4d6a to 99a124e Compare May 5, 2026 00:17
@Michael-RZ-Berri

Copy link
Copy Markdown
Contributor Author

@greptile-ai

@Michael-RZ-Berri

Michael-RZ-Berri commented May 5, 2026

Copy link
Copy Markdown
Contributor Author

Greptile Summary

This PR introduces at-rest encryption of callback_vars (API keys for Langfuse, Langsmith, Arize, etc.) stored in key and team metadata, using a litellm_enc:: sentinel prefix for idempotent encrypt/decrypt and SensitiveDataMasker to limit encryption to credential-bearing fields while leaving routing fields (host, base_url, project) in plaintext. Decryption is applied in the pre-call path before credentials are used; tests cover round-trips, idempotency, mutation safety, and selective-field behaviour. The unresolved backwards-incompatible change noted in a previous review — GET /key/info, GET /v2/key/info, and GET /team/info now return raw litellm_enc:: ciphertext blobs in metadata.logging[*].callback_vars instead of plaintext — remains unaddressed in this revision.

Confidence Score: 3/5

Not safe to merge until /key/info and /team/info decrypt callback_vars before returning them — encrypted blobs are a breaking change for operator tooling and UIs that read those fields.

This is purposefully a breaking, backwards-incompatible change for security reasons. Users should not be relying on third-party plain-text keys being accessible from the DB / endpoints, and the codebase generally does not allow users to see complete keys after initial generation or input.

@Michael-RZ-Berri

Copy link
Copy Markdown
Contributor Author

@greptile-ai

@Michael-RZ-Berri
Michael-RZ-Berri force-pushed the litellm_langfuseMetadataRedact branch from 99a124e to 52cc3ef Compare May 6, 2026 00:15
@codecov

codecov Bot commented May 6, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.36066% with 1 line in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
litellm/proxy/litellm_pre_call_utils.py 66.66% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@Michael-RZ-Berri
Michael-RZ-Berri force-pushed the litellm_langfuseMetadataRedact branch 2 times, most recently from e412d0d to 7887bfb Compare May 6, 2026 16:30
@Michael-RZ-Berri
Michael-RZ-Berri force-pushed the litellm_langfuseMetadataRedact branch from 7887bfb to f05da4f Compare May 6, 2026 16:54
…itellm_langfuseMetadataRedact

# Conflicts:
#	litellm/proxy/litellm_pre_call_utils.py
@yuneng-berri

Copy link
Copy Markdown
Collaborator

@greptile review again. Pushing back on returning plaintext secrets inside of /key/info and /v2/key/info, this is bad for security

Comment on lines +630 to +636
try:
return _CALLBACK_VAR_ENCRYPTED_PREFIX + encrypt_value_helper(value)
except Exception:
# No salt key / master key configured — leave the value as-is rather
# than crash the write. Dev environments without LITELLM_SALT_KEY hit
# this path; production always has a master key so encryption proceeds.
return value

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.

P1 security Silent encryption failure leaves secrets in plaintext with no operator signal

When LITELLM_SALT_KEY is absent or encrypt_value_helper raises for any other reason (key derivation error, misconfigured secret store, etc.), the bare except Exception block silently returns the original plaintext value and writes it to the DB — with no log at any level. The PR's stated security goal (preventing plaintext credentials in the DB) is then not achieved, but neither the operator nor any monitoring system receives any indication. Critically, a /key/info or /v2/key/info call against such a row would then return the plaintext secret — the exact scenario the reviewer is pushing back on.

At minimum a verbose_proxy_logger.warning(...) should be emitted so operators know encryption was skipped.

Suggested change
try:
return _CALLBACK_VAR_ENCRYPTED_PREFIX + encrypt_value_helper(value)
except Exception:
# No salt key / master key configured — leave the value as-is rather
# than crash the write. Dev environments without LITELLM_SALT_KEY hit
# this path; production always has a master key so encryption proceeds.
return value
try:
return _CALLBACK_VAR_ENCRYPTED_PREFIX + encrypt_value_helper(value)
except Exception:
# No salt key / master key configured — leave the value as-is rather
# than crash the write. Dev environments without LITELLM_SALT_KEY hit
# this path; production always has a master key so encryption proceeds.
verbose_proxy_logger.warning(
"callback_utils: failed to encrypt callback_var '%s' — "
"storing as plaintext. Set LITELLM_SALT_KEY to enable encryption.",
key,
)
return value

@yuneng-berri
yuneng-berri enabled auto-merge (squash) May 21, 2026 00:45
@yuneng-berri
yuneng-berri merged commit 3b2ce20 into litellm_internal_staging May 23, 2026
107 of 116 checks passed
fzowl pushed a commit to fzowl/litellm that referenced this pull request Jun 24, 2026
Co-authored-by: Michael Riad Zaky <michaelr@Michaels-MacBook-Air.local>
Co-authored-by: Yuneng Jiang <yuneng@berri.ai>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants