Skip to content

docker secret manager - #23519

Closed
mubashir1osmani wants to merge 6 commits into
BerriAI:mainfrom
mubashir1osmani:feat/docker-secret-manager
Closed

docker secret manager#23519
mubashir1osmani wants to merge 6 commits into
BerriAI:mainfrom
mubashir1osmani:feat/docker-secret-manager

Conversation

@mubashir1osmani

Copy link
Copy Markdown
Collaborator

Relevant issues

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

CI (LiteLLM team)

CI status guideline:

  • 50-55 passing tests: main is stable with minor issues.
  • 45-49 passing tests: acceptable but needs attention
  • <= 40 passing tests: unstable; be careful with your merges and assess the risk.
  • Branch creation CI run
    Link:

  • CI run for the last commit
    Link:

  • Merge / cherry-pick CI run
    Links:

Type

🆕 New Feature
🐛 Bug Fix
🧹 Refactoring
📖 Documentation
🚄 Infrastructure
✅ Test

Changes

Reads secrets from Docker secrets mounted at /run/secrets (configurable).
Supports Docker Swarm (encrypted at rest + in transit) and Docker Compose.

- DockerSecretManager class with sync + async read, read-only by design
- Path traversal protection via realpath() + prefix check (blocks ../, absolute paths, symlinks outside secrets dir)
- Case-folding: tries exact name then lowercase fallback (bridges UPPER_CASE env style and lower_case Docker secret naming)
- Trailing whitespace stripping (handles echo footgun)
- 27 unit + e2e tests
- Docs page + sidebar + overview entry

Config:
  general_settings:
    key_management_system: "docker"
    key_management_settings:
      secrets_path: "/run/secrets"  # optional, this is the default

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@vercel

vercel Bot commented Mar 13, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
litellm Ready Ready Preview, Comment Mar 24, 2026 3:59am

Request Review

@greptile-apps

greptile-apps Bot commented Mar 13, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds a new DockerSecretManager that reads secrets from Docker-mounted secret files (default: /run/secrets/), supporting both Docker Swarm and Docker Compose deployments. The implementation is well-structured and consistent with existing secret manager patterns (CyberArk, Hashicorp Vault), and the documented env-var fallback behaviour is now correctly implemented via the ValueError-based propagation in get_secret_from_manager.

Key changes:

  • litellm/secret_managers/docker_secret_manager.py — New read-only manager with path traversal protection (path separator rejection + realpath prefix check + symlink blocking), case-insensitive name resolution (exact → lowercase fallback), and Latin-1 fallback for non-UTF-8 secret files.
  • litellm/secret_managers/secret_manager_handler.py — Docker branch follows the same None → ValueError → env-var fallback pattern used by CyberArk and Hashicorp Vault; minor redundancy in the error message text.
  • litellm/types/secret_managers/main.py — Correctly adds DOCKER enum value and secrets_path: Optional[str] = None (non-polluting for other managers).
  • litellm/proxy/proxy_server.py — Clean integration using the factory classmethod, mirroring the existing CyberArk branch.
  • Tests — Comprehensive unit and E2E tests with no real network calls. One test (test_load_factory_no_settings) leaves litellm._key_management_system unreset, risking test-order-dependent failures.
  • Docs — Accurately describes behaviour including env-var fallback and security properties.

Confidence Score: 4/5

  • Safe to merge with two minor fixes: incomplete test cleanup and a redundant error message string.
  • The core implementation is solid — path traversal protection is thorough, the Latin-1 fallback addresses the previous feedback, and the env-var fallback mechanic is now correctly wired. The two remaining issues are low-risk: stale global state in one test (can cause non-deterministic test failures in CI but not a production bug) and a redundant phrase in an error message (cosmetic). No real network calls in tests, consistent with the repo rule.
  • tests/test_litellm/secret_managers/test_docker_secret_manager.py — test_load_factory_no_settings cleanup; litellm/secret_managers/secret_manager_handler.py — error message wording

Important Files Changed

Filename Overview
litellm/secret_managers/docker_secret_manager.py New read-only Docker secret manager with path traversal protection, Latin-1 fallback, and correct None-to-ValueError conversion for env-var fallback. Minor: httpx is imported at runtime solely for type hints that are never exercised.
litellm/secret_managers/secret_manager_handler.py Docker branch follows the same pattern as CyberArk/Hashicorp: raises ValueError for None so get_secret() falls back to env vars. Minor: the ValueError message contains redundant "Falling back to environment variable." text that duplicates the log line emitted by get_secret().
tests/test_litellm/secret_managers/test_docker_secret_manager.py Comprehensive unit and E2E test suite with no real network calls. One gap: test_load_factory_no_settings resets secret_manager_client but not _key_management_system, leaving stale global state that can affect subsequent tests.
litellm/types/secret_managers/main.py Adds DOCKER to KeyManagementSystem enum and secrets_path: Optional[str] = None to KeyManagementSettings. Field is correctly Optional so it does not pollute dumps for other managers.
litellm/proxy/proxy_server.py Clean integration point mirroring the existing CyberArk pattern; delegates to the factory classmethod with the current key_management_settings.
docs/my-website/docs/secret_managers/docker_secret_manager.md Documentation accurately describes the implemented behaviour including env-var fallback (now correct with the ValueError-based fallback in the handler), path traversal protection, and configuration options.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[get_secret called] --> B{Should use secret manager?}
    B -- No --> C[Read from process environment]
    B -- Yes --> D[get_secret_from_manager with docker key_manager]
    D --> E{Correct client type?}
    E -- No --> F[raise ValueError wrong client]
    E -- Yes --> G[sync_read_secret called]
    G --> H{Path separator in name?}
    H -- Yes --> I[raise ValueError path traversal]
    H -- No --> J{Resolved path inside secrets dir?}
    J -- No --> K[raise ValueError path traversal]
    J -- Yes --> L{Exact file exists?}
    L -- No --> M{Lowercase file exists?}
    M -- No --> N[return None]
    M -- Yes --> O[Read file UTF-8 with Latin-1 fallback]
    L -- Yes --> O
    O --> P[return stripped secret value to caller]
    N --> Q[handler raises ValueError not found]
    Q --> R[get_secret catches exception and logs]
    R --> S[Fallback to process environment lookup]
    I --> R
    K --> R
Loading

Comments Outside Diff (1)

  1. tests/test_litellm/secret_managers/test_docker_secret_manager.py, line 737-740 (link)

    P1 Incomplete cleanup leaves _key_management_system set after test

    DockerSecretManager.__init__ sets both litellm.secret_manager_client and litellm._key_management_system, but this test only resets secret_manager_client. Any test that runs after this one and inspects litellm._key_management_system will see a stale KeyManagementSystem.DOCKER value, potentially causing spurious failures.

    Every other test that calls the constructor (including test_init_sets_litellm_globals and test_load_factory_secrets_path_none_uses_default) correctly resets both globals. This cleanup block should be consistent:

Reviews (4): Last reviewed commit: "fix(docker-secret-manager): address PR r..." | Re-trigger Greptile

Comment thread litellm/types/secret_managers/main.py Outdated
Comment thread litellm/secret_managers/docker_secret_manager.py Outdated
Comment thread litellm/secret_managers/docker_secret_manager.py
Comment thread litellm/secret_managers/secret_manager_handler.py
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
- Latin-1 fallback: _read_file now tries UTF-8 then falls back to
  Latin-1 so secrets with é, ñ, ü, ß (ISO-8859-1/Windows-1252) are
  decoded correctly instead of silently returning None
- Path separator guard: reject secret names containing '/' or os.sep
  before realpath resolution to prevent unintended subdirectory reads
- Consistent exception logging: wrap Docker branch in
  secret_manager_handler with try/except + print_verbose, matching the
  pattern used by CyberArk and HashiCorp branches

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Comment thread litellm/secret_managers/docker_secret_manager.py
Comment thread litellm/secret_managers/docker_secret_manager.py
- Factory None crash: `getattr(settings, "secrets_path") or _DEFAULT`
  guard so operators who omit secrets_path from their config (the typical
  case) no longer hit TypeError from os.path.realpath(None) on startup
- Misleading warning: reword "fall back to environment variables" to
  "lookups will return None" — env-var fallback is caller-controlled,
  not a DockerSecretManager responsibility
- Root-safe test: add skipif(os.getuid() == 0) to
  test_unreadable_file_returns_none so chmod 000 tests don't silently
  pass/fail in Docker CI runners that run as root
- New regression test: test_load_factory_secrets_path_none_uses_default
  explicitly covers the None factory path that was crashing

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Comment on lines +95 to +108
- model_name: gpt-4o
litellm_params:
model: openai/gpt-4o
api_key: os.environ/OPENAI_API_KEY # reads $OPENAI_API_KEY from env
```

Without the `os.environ/` prefix, a value that isn't found as a Docker secret file is returned as-is (the literal string), so use literal values only when you intend to pass them directly.

## Configuration options

| Setting | Required | Default | Description |
|---------|----------|---------|-------------|
| `key_management_system` | Yes | — | Must be `"docker"` |
| `key_management_settings.secrets_path` | No | `/run/secrets` | Directory where Docker secrets are mounted |

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.

os.environ/ prefix does not fall back to the environment when a Docker secret is missing

This section tells operators that adding the os.environ/ prefix causes the system to read the corresponding environment variable when the Docker secret is not present. This is incorrect.

In litellm/secret_managers/main.py, the environment fallback inside get_secret() only triggers when get_secret_from_manager() raises an exception. When DockerSecretManager.sync_read_secret() returns None for a missing secret, get_secret propagates None directly — it does not read from the process environment as a fallback.

The test test_e2e_get_secret_returns_none_when_not_in_docker documents this explicitly: get_secret() only falls back to environment variables on exceptions, not on None returns.

An operator who follows this section and expects silent fallback behaviour will instead receive None, causing silent authentication failures.

This section should be removed or corrected to clarify that the os.environ/ prefix is the standard LiteLLM mechanism for naming a secret (no special fallback is added when the Docker manager is active).

Comment on lines +161 to +162
secret = client.sync_read_secret(secret_name=secret_name)
# None is valid — secret not found, caller falls back to env vars

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.

Misleading comment about fallback behaviour

The comment states that the caller falls back to environment variables when None is returned, but this is not what happens. In litellm/secret_managers/main.py, get_secret() only performs an environment lookup when get_secret_from_manager() raises an exception — a None return propagates directly to the caller with no substitution. The test test_e2e_get_secret_returns_none_when_not_in_docker explicitly verifies this behaviour.

The comment should be corrected to accurately describe the None semantics, e.g. # None indicates the secret file was not found; the caller receives None.

- Raise ValueError in handler when Docker secret is missing so get_secret()
  falls back to os.environ (consistent with Hashicorp/CyberArk branches)
- Remove misleading "caller falls back to env vars" comment on None return
- Fix sync_read_secret docstring that incorrectly described the fallback mechanism
- Fix docker_secret_manager.md: the os.environ/ section previously stated
  that a missing Docker secret triggers env-var fallback, which was wrong
  (fallback only triggered on exceptions). Now correctly documents that the
  handler raises for missing secrets, causing get_secret() to fall back.
- Update test_e2e_get_secret_returns_none_when_not_in_docker → now asserts
  env-var fallback actually works when Docker secret is absent
- Remove duplicate litellm._key_management_system = None in teardown

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@codspeed-hq

codspeed-hq Bot commented Mar 24, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 16 untouched benchmarks


Comparing mubashir1osmani:feat/docker-secret-manager (929503d) with main (3292d02)

Open in CodSpeed

)
return None

async def async_write_secret(

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.

P2 httpx imported only for type annotations but pulled in at import time

httpx is imported at module level solely to provide the httpx.Timeout type hint in async_read_secret and sync_read_secret. However, DockerSecretManager never performs any network I/O — timeout is accepted purely to satisfy the BaseSecretManager interface and is never used. The unconditional top-level import means every environment that imports this module must have httpx installed, even though the module never uses it at runtime.

Other pure-filesystem secret managers (e.g. the local-env helper) avoid this by using TYPE_CHECKING guards. Consider:

from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
    import httpx

This keeps the type hints intact for static analysis while removing the hard runtime dependency.

Comment on lines +163 to +166
raise ValueError(
f"No secret found in Docker secrets for {secret_name!r}. "
f"Falling back to environment variable."
)

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.

P2 Redundant fallback text in the exception message

When this ValueError propagates to get_secret(), it is already logged with a "defaulting to env" message. The phrase "Falling back to environment variable." embedded in the exception body therefore produces a confusing duplicate — operators reading the log see two different phrasings of the same fact, and print_verbose at line 168 prepends yet another "An error occurred - " prefix.

The Hashicorp Vault and CyberArk branches immediately above use only "No secret found in … for {secret_name}" without any fallback commentary. Aligning the Docker branch with that style removes the redundancy and keeps the handler consistent.

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.

1 participant