mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-09 03:18:44 +00:00
[Feat] Secret Manager - Hashicorp, add auth via approle (#16374)
* add _verify_required_credentials_exist and _auth_via_approle * test_hashicorp_secret_manager_approle_auth * docs hcorp auth
This commit is contained in:
parent
ea4048324b
commit
a6b0993405
3 changed files with 235 additions and 9 deletions
|
|
@ -16,21 +16,28 @@ import Image from '@theme/IdealImage';
|
|||
|---------|----------|-------------|
|
||||
| Reading Secrets | ✅ | Read secrets e.g `OPENAI_API_KEY` |
|
||||
| Writing Secrets | ✅ | Store secrets e.g `Virtual Keys` |
|
||||
| Authentication Methods to Hashicorp Vault | ✅ | AppRole, TLS Certificate, Token |
|
||||
|
||||
Read secrets from [Hashicorp Vault](https://developer.hashicorp.com/vault/docs/secrets/kv/kv-v2)
|
||||
|
||||
**Step 1.** Add Hashicorp Vault details in your environment
|
||||
|
||||
LiteLLM supports two methods of authentication:
|
||||
LiteLLM supports three methods of authentication:
|
||||
|
||||
1. TLS cert authentication - `HCP_VAULT_CLIENT_CERT` and `HCP_VAULT_CLIENT_KEY`
|
||||
2. Token authentication - `HCP_VAULT_TOKEN`
|
||||
1. AppRole authentication (recommended) - `HCP_VAULT_APPROLE_ROLE_ID` and `HCP_VAULT_APPROLE_SECRET_ID`
|
||||
2. TLS cert authentication - `HCP_VAULT_CLIENT_CERT` and `HCP_VAULT_CLIENT_KEY`
|
||||
3. Token authentication - `HCP_VAULT_TOKEN`
|
||||
|
||||
```bash
|
||||
HCP_VAULT_ADDR="https://test-cluster-public-vault-0f98180c.e98296b2.z1.hashicorp.cloud:8200"
|
||||
HCP_VAULT_NAMESPACE="admin"
|
||||
|
||||
# Authentication via TLS cert
|
||||
# Authentication via AppRole (recommended)
|
||||
HCP_VAULT_APPROLE_ROLE_ID="your-role-id"
|
||||
HCP_VAULT_APPROLE_SECRET_ID="your-secret-id"
|
||||
HCP_VAULT_APPROLE_MOUNT_PATH="approle" # OPTIONAL. defaults to "approle"
|
||||
|
||||
# OR - Authentication via TLS cert
|
||||
HCP_VAULT_CLIENT_CERT="path/to/client.pem"
|
||||
HCP_VAULT_CLIENT_KEY="path/to/client.key"
|
||||
|
||||
|
|
@ -64,6 +71,80 @@ $ litellm --config /path/to/config.yaml
|
|||
[Quick Test Proxy](../proxy/user_keys)
|
||||
|
||||
|
||||
## Authentication Methods
|
||||
|
||||
LiteLLM supports three authentication methods for Hashicorp Vault, with the following priority:
|
||||
|
||||
1. **AppRole** - Recommended for production applications
|
||||
2. **TLS Certificate** - For certificate-based authentication
|
||||
3. **Token** - Direct token authentication
|
||||
|
||||
### 1. AppRole Authentication
|
||||
|
||||
To set up AppRole authentication:
|
||||
|
||||
1. Enable AppRole auth in Vault:
|
||||
```bash
|
||||
vault auth enable approle
|
||||
```
|
||||
|
||||
2. Create a policy and role for LiteLLM:
|
||||
```bash
|
||||
# Create a policy file (litellm-policy.hcl)
|
||||
path "secret/data/*" {
|
||||
capabilities = ["create", "read", "update", "delete", "list"]
|
||||
}
|
||||
|
||||
# Apply the policy
|
||||
vault policy write litellm-policy litellm-policy.hcl
|
||||
|
||||
# Create an AppRole
|
||||
vault write auth/approle/role/litellm \
|
||||
token_policies="litellm-policy" \
|
||||
token_ttl=32d \
|
||||
token_max_ttl=32d
|
||||
```
|
||||
|
||||
3. Get your Role ID and Secret ID:
|
||||
```bash
|
||||
# Get Role ID
|
||||
vault read auth/approle/role/litellm/role-id
|
||||
|
||||
# Generate Secret ID
|
||||
vault write -f auth/approle/role/litellm/secret-id
|
||||
```
|
||||
|
||||
4. Set the environment variables:
|
||||
```bash
|
||||
export HCP_VAULT_APPROLE_ROLE_ID="your-role-id"
|
||||
export HCP_VAULT_APPROLE_SECRET_ID="your-secret-id"
|
||||
```
|
||||
|
||||
### 2. TLS Certificate Authentication
|
||||
|
||||
TLS Certificate authentication uses client certificates for mutual TLS authentication with Vault.
|
||||
|
||||
**Environment Variables:**
|
||||
```bash
|
||||
export HCP_VAULT_CLIENT_CERT="path/to/client.pem"
|
||||
export HCP_VAULT_CLIENT_KEY="path/to/client.key"
|
||||
export HCP_VAULT_CERT_ROLE="your-cert-role" # Optional
|
||||
```
|
||||
|
||||
**How it works:**
|
||||
- LiteLLM uses the client certificate and key for mutual TLS authentication
|
||||
- Vault validates the certificate and issues a temporary token
|
||||
- The token is cached for the duration of its lease
|
||||
|
||||
### 3. Token Authentication
|
||||
|
||||
Direct token authentication uses a static Vault token.
|
||||
|
||||
**Environment Variables:**
|
||||
```bash
|
||||
export HCP_VAULT_TOKEN="hvs.CAESIG52gL6ljBSdmq*****"
|
||||
```
|
||||
|
||||
## How it works
|
||||
|
||||
**Reading Secrets**
|
||||
|
|
|
|||
|
|
@ -37,11 +37,12 @@ class HashicorpSecretManager(BaseSecretManager):
|
|||
self.tls_key_path = os.getenv("HCP_VAULT_CLIENT_KEY", "")
|
||||
self.vault_cert_role = os.getenv("HCP_VAULT_CERT_ROLE", None)
|
||||
|
||||
# Validate environment
|
||||
if not self.vault_token:
|
||||
raise ValueError(
|
||||
"Missing Vault token. Please set HCP_VAULT_TOKEN in your environment."
|
||||
)
|
||||
# Optional config for AppRole auth
|
||||
self.approle_role_id = os.getenv("HCP_VAULT_APPROLE_ROLE_ID", "")
|
||||
self.approle_secret_id = os.getenv("HCP_VAULT_APPROLE_SECRET_ID", "")
|
||||
self.approle_mount_path = os.getenv("HCP_VAULT_APPROLE_MOUNT_PATH", "approle")
|
||||
|
||||
self._verify_required_credentials_exist()
|
||||
|
||||
litellm.secret_manager_client = self
|
||||
litellm._key_management_system = KeyManagementSystem.HASHICORP_VAULT
|
||||
|
|
@ -62,6 +63,92 @@ class HashicorpSecretManager(BaseSecretManager):
|
|||
f"Hashicorp secret manager is only available for premium users. {CommonProxyErrors.not_premium_user.value}"
|
||||
)
|
||||
|
||||
def _verify_required_credentials_exist(self) -> None:
|
||||
"""
|
||||
Validate that at least one authentication method is configured.
|
||||
|
||||
Raises:
|
||||
ValueError: If no valid authentication credentials are provided
|
||||
"""
|
||||
if not self.vault_token and not (self.approle_role_id and self.approle_secret_id):
|
||||
raise ValueError(
|
||||
"Missing Vault authentication credentials. Please set either:\n"
|
||||
" - HCP_VAULT_TOKEN for token-based auth, or\n"
|
||||
" - HCP_VAULT_APPROLE_ROLE_ID and HCP_VAULT_APPROLE_SECRET_ID for AppRole auth"
|
||||
)
|
||||
|
||||
def _auth_via_approle(self) -> str:
|
||||
"""
|
||||
Authenticate to Vault using AppRole auth method.
|
||||
|
||||
Ref: https://developer.hashicorp.com/vault/api-docs/auth/approle
|
||||
|
||||
Request:
|
||||
```
|
||||
curl \
|
||||
--request POST \
|
||||
--header "X-Vault-Namespace: mynamespace/" \
|
||||
--data '{"role_id": "...", "secret_id": "..."}' \
|
||||
http://127.0.0.1:8200/v1/auth/approle/login
|
||||
```
|
||||
|
||||
Response:
|
||||
```
|
||||
{
|
||||
"auth": {
|
||||
"client_token": "hvs.CAESI...",
|
||||
"accessor": "hmac-sha256...",
|
||||
"policies": ["default", "dev-policy"],
|
||||
"token_policies": ["default", "dev-policy"],
|
||||
"lease_duration": 2764800,
|
||||
"renewable": true
|
||||
}
|
||||
}
|
||||
```
|
||||
"""
|
||||
verbose_logger.debug("Using AppRole auth for Hashicorp Vault")
|
||||
|
||||
# Check cache first
|
||||
cached_token = self.cache.get_cache(key="hcp_vault_approle_token")
|
||||
if cached_token:
|
||||
verbose_logger.debug("Using cached Vault token from AppRole auth")
|
||||
return cached_token
|
||||
|
||||
# Vault endpoint for AppRole login
|
||||
login_url = f"{self.vault_addr}/v1/auth/{self.approle_mount_path}/login"
|
||||
|
||||
headers = {}
|
||||
if hasattr(self, "vault_namespace") and self.vault_namespace:
|
||||
headers["X-Vault-Namespace"] = self.vault_namespace
|
||||
|
||||
try:
|
||||
client = _get_httpx_client()
|
||||
resp = client.post(
|
||||
url=login_url,
|
||||
headers=headers,
|
||||
json={
|
||||
"role_id": self.approle_role_id,
|
||||
"secret_id": self.approle_secret_id,
|
||||
},
|
||||
)
|
||||
resp.raise_for_status()
|
||||
|
||||
auth_data = resp.json()["auth"]
|
||||
token = auth_data["client_token"]
|
||||
_lease_duration = auth_data["lease_duration"]
|
||||
|
||||
verbose_logger.debug(
|
||||
f"Successfully obtained Vault token via AppRole auth. Lease duration: {_lease_duration}s"
|
||||
)
|
||||
|
||||
# Cache the token with its lease duration
|
||||
self.cache.set_cache(
|
||||
key="hcp_vault_approle_token", value=token, ttl=_lease_duration
|
||||
)
|
||||
return token
|
||||
except Exception as e:
|
||||
raise RuntimeError(f"Could not authenticate to Vault via AppRole: {e}")
|
||||
|
||||
def _auth_via_tls_cert(self) -> str:
|
||||
"""
|
||||
Ref: https://developer.hashicorp.com/vault/api-docs/auth/cert
|
||||
|
|
@ -144,8 +231,23 @@ class HashicorpSecretManager(BaseSecretManager):
|
|||
return _url
|
||||
|
||||
def _get_request_headers(self) -> dict:
|
||||
"""
|
||||
Get the headers for Vault API requests.
|
||||
|
||||
Authentication priority:
|
||||
1. AppRole (if role_id and secret_id are configured)
|
||||
2. TLS Certificate (if cert paths are configured)
|
||||
3. Direct token (if HCP_VAULT_TOKEN is set)
|
||||
"""
|
||||
# Priority 1: AppRole auth
|
||||
if self.approle_role_id and self.approle_secret_id:
|
||||
return {"X-Vault-Token": self._auth_via_approle()}
|
||||
|
||||
# Priority 2: TLS cert auth
|
||||
if self.tls_cert_path and self.tls_key_path:
|
||||
return {"X-Vault-Token": self._auth_via_tls_cert()}
|
||||
|
||||
# Priority 3: Direct token
|
||||
return {"X-Vault-Token": self.vault_token}
|
||||
|
||||
async def async_read_secret(
|
||||
|
|
|
|||
|
|
@ -217,6 +217,49 @@ def test_hashicorp_secret_manager_tls_cert_auth(monkeypatch):
|
|||
assert test_manager.cache.get_cache("hcp_vault_token") == "test-client-token-12345"
|
||||
|
||||
|
||||
def test_hashicorp_secret_manager_approle_auth(monkeypatch):
|
||||
"""
|
||||
Test AppRole authentication makes the expected POST request to the correct URL.
|
||||
"""
|
||||
monkeypatch.setenv("HCP_VAULT_TOKEN", "test-token-12345")
|
||||
|
||||
with patch("litellm.llms.custom_httpx.http_handler.HTTPHandler.post") as mock_post:
|
||||
mock_response = MagicMock()
|
||||
mock_response.json.return_value = {
|
||||
"auth": {
|
||||
"client_token": "hvs.approle-token-67890",
|
||||
"lease_duration": 2764800,
|
||||
"renewable": True,
|
||||
}
|
||||
}
|
||||
mock_response.raise_for_status.return_value = None
|
||||
mock_post.return_value = mock_response
|
||||
|
||||
test_manager = HashicorpSecretManager()
|
||||
test_manager.approle_role_id = "test-role-id-123"
|
||||
test_manager.approle_secret_id = "test-secret-id-456"
|
||||
test_manager.approle_mount_path = "approle"
|
||||
|
||||
token = test_manager._auth_via_approle()
|
||||
|
||||
assert token == "hvs.approle-token-67890"
|
||||
|
||||
expected_headers = {}
|
||||
if test_manager.vault_namespace:
|
||||
expected_headers["X-Vault-Namespace"] = test_manager.vault_namespace
|
||||
|
||||
mock_post.assert_called_once_with(
|
||||
url=f"{test_manager.vault_addr}/v1/auth/approle/login",
|
||||
headers=expected_headers,
|
||||
json={
|
||||
"role_id": "test-role-id-123",
|
||||
"secret_id": "test-secret-id-456",
|
||||
},
|
||||
)
|
||||
|
||||
assert test_manager.cache.get_cache("hcp_vault_approle_token") == "hvs.approle-token-67890"
|
||||
|
||||
|
||||
def test_hashicorp_custom_mount_and_prefix():
|
||||
"""Test URL construction with custom mount name and path prefix using get_url method."""
|
||||
# Save original values
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue