mirror of
https://github.com/BerriAI/litellm.git
synced 2025-04-27 11:43:54 +00:00
(Feat) - Hashicorp secret manager, use TLS cert authentication (#7532)
* fix - don't print hcorp secrets in debug logs * hcorp - tls auth fixes * fix tls_ca_cert_path * test_hashicorp_secret_manager_tls_cert_auth * hcp secret docs
This commit is contained in:
parent
d3a3e45e5b
commit
fb59f20979
4 changed files with 113 additions and 4 deletions
|
@ -391,6 +391,8 @@ router_settings:
|
||||||
| GOOGLE_KMS_RESOURCE_NAME | Name of the resource in Google KMS
|
| GOOGLE_KMS_RESOURCE_NAME | Name of the resource in Google KMS
|
||||||
| HF_API_BASE | Base URL for Hugging Face API
|
| HF_API_BASE | Base URL for Hugging Face API
|
||||||
| HCP_VAULT_ADDR | Address for [Hashicorp Vault Secret Manager](../secret.md#hashicorp-vault)
|
| HCP_VAULT_ADDR | Address for [Hashicorp Vault Secret Manager](../secret.md#hashicorp-vault)
|
||||||
|
| HCP_VAULT_CLIENT_CERT | Path to client certificate for [Hashicorp Vault Secret Manager](../secret.md#hashicorp-vault)
|
||||||
|
| HCP_VAULT_CLIENT_KEY | Path to client key for [Hashicorp Vault Secret Manager](../secret.md#hashicorp-vault)
|
||||||
| HCP_VAULT_NAMESPACE | Namespace for [Hashicorp Vault Secret Manager](../secret.md#hashicorp-vault)
|
| HCP_VAULT_NAMESPACE | Namespace for [Hashicorp Vault Secret Manager](../secret.md#hashicorp-vault)
|
||||||
| HCP_VAULT_TOKEN | Token for [Hashicorp Vault Secret Manager](../secret.md#hashicorp-vault)
|
| HCP_VAULT_TOKEN | Token for [Hashicorp Vault Secret Manager](../secret.md#hashicorp-vault)
|
||||||
| HELICONE_API_KEY | API key for Helicone service
|
| HELICONE_API_KEY | API key for Helicone service
|
||||||
|
|
|
@ -246,11 +246,23 @@ Read secrets from [Hashicorp Vault](https://developer.hashicorp.com/vault/docs/s
|
||||||
|
|
||||||
Step 1. Add Hashicorp Vault details in your environment
|
Step 1. Add Hashicorp Vault details in your environment
|
||||||
|
|
||||||
|
LiteLLM supports two methods of authentication:
|
||||||
|
|
||||||
|
1. TLS cert authentication - `HCP_VAULT_CLIENT_CERT` and `HCP_VAULT_CLIENT_KEY`
|
||||||
|
2. Token authentication - `HCP_VAULT_TOKEN`
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
HCP_VAULT_ADDR="https://test-cluster-public-vault-0f98180c.e98296b2.z1.hashicorp.cloud:8200"
|
HCP_VAULT_ADDR="https://test-cluster-public-vault-0f98180c.e98296b2.z1.hashicorp.cloud:8200"
|
||||||
HCP_VAULT_NAMESPACE="admin"
|
HCP_VAULT_NAMESPACE="admin"
|
||||||
|
|
||||||
|
# Authentication via TLS cert
|
||||||
|
HCP_VAULT_CLIENT_CERT="path/to/client.pem"
|
||||||
|
HCP_VAULT_CLIENT_KEY="path/to/client.key"
|
||||||
|
|
||||||
|
# OR - Authentication via token
|
||||||
HCP_VAULT_TOKEN="hvs.CAESIG52gL6ljBSdmq*****"
|
HCP_VAULT_TOKEN="hvs.CAESIG52gL6ljBSdmq*****"
|
||||||
|
|
||||||
|
|
||||||
# OPTIONAL
|
# OPTIONAL
|
||||||
HCP_VAULT_REFRESH_INTERVAL="86400" # defaults to 86400, frequency of cache refresh for Hashicorp Vault
|
HCP_VAULT_REFRESH_INTERVAL="86400" # defaults to 86400, frequency of cache refresh for Hashicorp Vault
|
||||||
```
|
```
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import os
|
import os
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
|
||||||
import litellm
|
import litellm
|
||||||
from litellm._logging import verbose_logger
|
from litellm._logging import verbose_logger
|
||||||
from litellm.caching import InMemoryCache
|
from litellm.caching import InMemoryCache
|
||||||
|
@ -22,6 +24,10 @@ class HashicorpSecretManager:
|
||||||
# If your KV engine is mounted somewhere other than "secret", adjust here:
|
# If your KV engine is mounted somewhere other than "secret", adjust here:
|
||||||
self.vault_namespace = os.getenv("HCP_VAULT_NAMESPACE", None)
|
self.vault_namespace = os.getenv("HCP_VAULT_NAMESPACE", None)
|
||||||
|
|
||||||
|
# Optional config for TLS cert auth
|
||||||
|
self.tls_cert_path = os.getenv("HCP_VAULT_CLIENT_CERT", "")
|
||||||
|
self.tls_key_path = os.getenv("HCP_VAULT_CLIENT_KEY", "")
|
||||||
|
|
||||||
# Validate environment
|
# Validate environment
|
||||||
if not self.vault_token:
|
if not self.vault_token:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
|
@ -41,6 +47,57 @@ class HashicorpSecretManager:
|
||||||
f"Hashicorp secret manager is only available for premium users. {CommonProxyErrors.not_premium_user.value}"
|
f"Hashicorp secret manager is only available for premium users. {CommonProxyErrors.not_premium_user.value}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _auth_via_tls_cert(self) -> str:
|
||||||
|
"""
|
||||||
|
Ref: https://developer.hashicorp.com/vault/api-docs/auth/cert
|
||||||
|
|
||||||
|
Request:
|
||||||
|
```
|
||||||
|
curl \
|
||||||
|
--request POST \
|
||||||
|
--cacert vault-ca.pem \
|
||||||
|
--cert cert.pem \
|
||||||
|
--key key.pem \
|
||||||
|
--data @payload.json \
|
||||||
|
https://127.0.0.1:8200/v1/auth/cert/login
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
Response:
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"auth": {
|
||||||
|
"client_token": "cf95f87d-f95b-47ff-b1f5-ba7bff850425",
|
||||||
|
"policies": ["web", "stage"],
|
||||||
|
"lease_duration": 3600,
|
||||||
|
"renewable": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
verbose_logger.debug("Using TLS cert auth for Hashicorp Vault")
|
||||||
|
|
||||||
|
# Vault endpoint for cert-based login, e.g. '/v1/auth/cert/login'
|
||||||
|
login_url = f"{self.vault_addr}/v1/auth/cert/login"
|
||||||
|
|
||||||
|
try:
|
||||||
|
# We use the client cert and key for mutual TLS
|
||||||
|
resp = httpx.post(
|
||||||
|
login_url,
|
||||||
|
cert=(self.tls_cert_path, self.tls_key_path),
|
||||||
|
)
|
||||||
|
resp.raise_for_status()
|
||||||
|
token = resp.json()["auth"]["client_token"]
|
||||||
|
_lease_duration = resp.json()["auth"]["lease_duration"]
|
||||||
|
verbose_logger.info("Successfully obtained Vault token via TLS cert auth.")
|
||||||
|
self.cache.set_cache(
|
||||||
|
key="hcp_vault_token", value=token, ttl=_lease_duration
|
||||||
|
)
|
||||||
|
return token
|
||||||
|
except Exception as e:
|
||||||
|
raise RuntimeError(f"Could not authenticate to Vault via TLS cert: {e}")
|
||||||
|
|
||||||
def get_url(self, secret_name: str) -> str:
|
def get_url(self, secret_name: str) -> str:
|
||||||
_url = f"{self.vault_addr}/v1/"
|
_url = f"{self.vault_addr}/v1/"
|
||||||
if self.vault_namespace:
|
if self.vault_namespace:
|
||||||
|
@ -48,6 +105,11 @@ class HashicorpSecretManager:
|
||||||
_url += f"secret/data/{secret_name}"
|
_url += f"secret/data/{secret_name}"
|
||||||
return _url
|
return _url
|
||||||
|
|
||||||
|
def _get_request_headers(self) -> dict:
|
||||||
|
if self.tls_cert_path and self.tls_key_path:
|
||||||
|
return {"X-Vault-Token": self._auth_via_tls_cert()}
|
||||||
|
return {"X-Vault-Token": self.vault_token}
|
||||||
|
|
||||||
async def async_read_secret(self, secret_name: str) -> Optional[str]:
|
async def async_read_secret(self, secret_name: str) -> Optional[str]:
|
||||||
"""
|
"""
|
||||||
Reads a secret from Vault KV v2 using an async HTTPX client.
|
Reads a secret from Vault KV v2 using an async HTTPX client.
|
||||||
|
@ -65,9 +127,7 @@ class HashicorpSecretManager:
|
||||||
_url = self.get_url(secret_name)
|
_url = self.get_url(secret_name)
|
||||||
url = _url
|
url = _url
|
||||||
|
|
||||||
response = await async_client.get(
|
response = await async_client.get(url, headers=self._get_request_headers())
|
||||||
url, headers={"X-Vault-Token": self.vault_token}
|
|
||||||
)
|
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
|
|
||||||
# For KV v2, the secret is in response.json()["data"]["data"]
|
# For KV v2, the secret is in response.json()["data"]["data"]
|
||||||
|
@ -93,7 +153,7 @@ class HashicorpSecretManager:
|
||||||
# For KV v2: /v1/<mount>/data/<path>
|
# For KV v2: /v1/<mount>/data/<path>
|
||||||
url = self.get_url(secret_name)
|
url = self.get_url(secret_name)
|
||||||
|
|
||||||
response = sync_client.get(url, headers={"X-Vault-Token": self.vault_token})
|
response = sync_client.get(url, headers=self._get_request_headers())
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
|
|
||||||
# For KV v2, the secret is in response.json()["data"]["data"]
|
# For KV v2, the secret is in response.json()["data"]["data"]
|
||||||
|
|
|
@ -65,3 +65,38 @@ def test_hashicorp_secret_manager_get_secret():
|
||||||
== "https://test-cluster-public-vault-0f98180c.e98296b2.z1.hashicorp.cloud:8200/v1/admin/secret/data/sample-secret-mock"
|
== "https://test-cluster-public-vault-0f98180c.e98296b2.z1.hashicorp.cloud:8200/v1/admin/secret/data/sample-secret-mock"
|
||||||
)
|
)
|
||||||
assert "X-Vault-Token" in mock_get.call_args.kwargs["headers"]
|
assert "X-Vault-Token" in mock_get.call_args.kwargs["headers"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_hashicorp_secret_manager_tls_cert_auth():
|
||||||
|
with patch("httpx.post") as mock_post:
|
||||||
|
# Configure the mock response for TLS auth
|
||||||
|
mock_auth_response = MagicMock()
|
||||||
|
mock_auth_response.json.return_value = {
|
||||||
|
"auth": {
|
||||||
|
"client_token": "test-client-token-12345",
|
||||||
|
"lease_duration": 3600,
|
||||||
|
"renewable": True,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mock_auth_response.raise_for_status.return_value = None
|
||||||
|
mock_post.return_value = mock_auth_response
|
||||||
|
|
||||||
|
# Create a new instance with TLS cert config
|
||||||
|
test_manager = HashicorpSecretManager()
|
||||||
|
test_manager.tls_cert_path = "cert.pem"
|
||||||
|
test_manager.tls_key_path = "key.pem"
|
||||||
|
|
||||||
|
# Test the TLS auth method
|
||||||
|
token = test_manager._auth_via_tls_cert()
|
||||||
|
|
||||||
|
# Verify the token and request parameters
|
||||||
|
assert token == "test-client-token-12345"
|
||||||
|
mock_post.assert_called_once_with(
|
||||||
|
f"{test_manager.vault_addr}/v1/auth/cert/login",
|
||||||
|
cert=("cert.pem", "key.pem"),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verify the token was cached
|
||||||
|
assert (
|
||||||
|
test_manager.cache.get_cache("hcp_vault_token") == "test-client-token-12345"
|
||||||
|
)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue