Skip to content

Commit

Permalink
(Feat) - Hashicorp secret manager, use TLS cert authentication (Berri…
Browse files Browse the repository at this point in the history
…AI#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
  • Loading branch information
ishaan-jaff authored and rajatvig committed Jan 15, 2025
1 parent e871a8b commit 2a88538
Show file tree
Hide file tree
Showing 4 changed files with 113 additions and 4 deletions.
2 changes: 2 additions & 0 deletions docs/my-website/docs/proxy/config_settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,8 @@ router_settings:
| GOOGLE_KMS_RESOURCE_NAME | Name of the resource in Google KMS
| HF_API_BASE | Base URL for Hugging Face API
| 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_TOKEN | Token for [Hashicorp Vault Secret Manager](../secret.md#hashicorp-vault)
| HELICONE_API_KEY | API key for Helicone service
Expand Down
12 changes: 12 additions & 0 deletions docs/my-website/docs/secret.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

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
HCP_VAULT_ADDR="https://test-cluster-public-vault-0f98180c.e98296b2.z1.hashicorp.cloud:8200"
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*****"


# OPTIONAL
HCP_VAULT_REFRESH_INTERVAL="86400" # defaults to 86400, frequency of cache refresh for Hashicorp Vault
```
Expand Down
68 changes: 64 additions & 4 deletions litellm/secret_managers/hashicorp_secret_manager.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import os
from typing import Optional

import httpx

import litellm
from litellm._logging import verbose_logger
from litellm.caching import InMemoryCache
Expand All @@ -22,6 +24,10 @@ def __init__(self):
# If your KV engine is mounted somewhere other than "secret", adjust here:
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
if not self.vault_token:
raise ValueError(
Expand All @@ -41,13 +47,69 @@ def __init__(self):
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:
_url = f"{self.vault_addr}/v1/"
if self.vault_namespace:
_url += f"{self.vault_namespace}/"
_url += f"secret/data/{secret_name}"
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]:
"""
Reads a secret from Vault KV v2 using an async HTTPX client.
Expand All @@ -65,9 +127,7 @@ async def async_read_secret(self, secret_name: str) -> Optional[str]:
_url = self.get_url(secret_name)
url = _url

response = await async_client.get(
url, headers={"X-Vault-Token": self.vault_token}
)
response = await async_client.get(url, headers=self._get_request_headers())
response.raise_for_status()

# For KV v2, the secret is in response.json()["data"]["data"]
Expand All @@ -93,7 +153,7 @@ def read_secret(self, secret_name: str) -> Optional[str]:
# For KV v2: /v1/<mount>/data/<path>
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()

# For KV v2, the secret is in response.json()["data"]["data"]
Expand Down
35 changes: 35 additions & 0 deletions tests/secret_manager_tests/test_hashicorp.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
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"
)

0 comments on commit 2a88538

Please sign in to comment.