[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:
Ishaan Jaff 2025-11-07 14:39:33 -08:00 • committed by GitHub
parent ea4048324b
commit a6b0993405
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 235 additions and 9 deletions

View file

@ -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**

View file

@ -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(

View file

@ -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