[Feat] Adds IAM role assumption support for AWS Secret Manager (#16887)

* add AWS fields for KeyManagementSettings

* docs IAM roles

* use aws iam auth on secret manager v2

* fix: load_aws_secret_manager

* test_secret_manager_with_iam_role_settings
This commit is contained in:
Ishaan Jaff 2025-11-20 12:38:48 -08:00 committed by GitHub
parent 9120a02474
commit 57544f1662
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 362 additions and 8 deletions

View File

@ -110,3 +110,57 @@ The `primary_secret_name` allows you to read multiple keys from a single AWS Sec
This reduces the number of AWS Secrets you need to manage.
## IAM Role Assumption
Use IAM roles instead of static AWS credentials for better security.
### Basic IAM Role
```yaml
general_settings:
key_management_system: "aws_secret_manager"
key_management_settings:
store_virtual_keys: true
aws_region_name: "us-east-1"
aws_role_name: "arn:aws:iam::123456789012:role/LiteLLMSecretManagerRole"
aws_session_name: "litellm-session"
```
### Cross-Account Access
```yaml
general_settings:
key_management_system: "aws_secret_manager"
key_management_settings:
store_virtual_keys: true
aws_region_name: "us-east-1"
aws_role_name: "arn:aws:iam::999999999999:role/CrossAccountRole"
aws_external_id: "unique-external-id"
```
### EKS with IRSA
```yaml
general_settings:
key_management_system: "aws_secret_manager"
key_management_settings:
store_virtual_keys: true
aws_region_name: "us-east-1"
aws_role_name: "arn:aws:iam::123456789012:role/LiteLLMServiceAccountRole"
aws_web_identity_token: "os.environ/AWS_WEB_IDENTITY_TOKEN_FILE"
```
### Configuration Parameters
| Parameter | Description |
|-----------|-------------|
| `aws_region_name` | AWS region |
| `aws_role_name` | IAM role ARN to assume |
| `aws_session_name` | Session name (optional) |
| `aws_external_id` | External ID for cross-account |
| `aws_profile_name` | AWS profile from `~/.aws/credentials` |
| `aws_web_identity_token` | OIDC token path for IRSA |
| `aws_sts_endpoint` | Custom STS endpoint for VPC |

View File

@ -2692,7 +2692,10 @@ class ProxyConfig:
AWSSecretsManagerV2,
)
AWSSecretsManagerV2.load_aws_secret_manager(use_aws_secret_manager=True)
AWSSecretsManagerV2.load_aws_secret_manager(
use_aws_secret_manager=True,
key_management_settings=litellm._key_management_settings,
)
elif key_management_system == KeyManagementSystem.AWS_KMS.value:
load_aws_kms(use_aws_kms=True)
elif (

View File

@ -33,25 +33,73 @@ from .base_secret_manager import BaseSecretManager
class AWSSecretsManagerV2(BaseAWSLLM, BaseSecretManager):
def __init__(self, **kwargs):
def __init__(
self,
aws_region_name: Optional[str] = None,
aws_role_name: Optional[str] = None,
aws_session_name: Optional[str] = None,
aws_external_id: Optional[str] = None,
aws_profile_name: Optional[str] = None,
aws_web_identity_token: Optional[str] = None,
aws_sts_endpoint: Optional[str] = None,
**kwargs
):
BaseSecretManager.__init__(self, **kwargs)
BaseAWSLLM.__init__(self, **kwargs)
# Store AWS authentication settings
self.aws_region_name = aws_region_name
self.aws_role_name = aws_role_name
self.aws_session_name = aws_session_name
self.aws_external_id = aws_external_id
self.aws_profile_name = aws_profile_name
self.aws_web_identity_token = aws_web_identity_token
self.aws_sts_endpoint = aws_sts_endpoint
@classmethod
def validate_environment(cls):
if "AWS_REGION_NAME" not in os.environ:
raise ValueError("Missing required environment variable - AWS_REGION_NAME")
# AWS_REGION_NAME is only strictly required if not using a profile or role
# When using IAM roles, the region can come from multiple sources
if (
"AWS_REGION_NAME" not in os.environ
and "AWS_REGION" not in os.environ
and "AWS_DEFAULT_REGION" not in os.environ
):
verbose_logger.warning(
"No AWS region found in environment. Ensure aws_region_name is set in key_management_settings "
"or AWS_REGION_NAME/AWS_REGION/AWS_DEFAULT_REGION is set in environment."
)
@classmethod
def load_aws_secret_manager(cls, use_aws_secret_manager: Optional[bool]):
def load_aws_secret_manager(
cls,
use_aws_secret_manager: Optional[bool],
key_management_settings: Optional[Any] = None,
):
"""
Initialize AWSSecretsManagerV2 and sets litellm.secret_manager_client = AWSSecretsManagerV2() and litellm._key_management_system = KeyManagementSystem.AWS_SECRET_MANAGER
Initialize AWSSecretsManagerV2 with settings from key_management_settings
"""
if use_aws_secret_manager is None or use_aws_secret_manager is False:
return
try:
cls.validate_environment()
litellm.secret_manager_client = cls()
# Extract AWS settings from key_management_settings if provided
aws_kwargs = {}
if key_management_settings is not None:
aws_kwargs = {
"aws_region_name": getattr(key_management_settings, "aws_region_name", None),
"aws_role_name": getattr(key_management_settings, "aws_role_name", None),
"aws_session_name": getattr(key_management_settings, "aws_session_name", None),
"aws_external_id": getattr(key_management_settings, "aws_external_id", None),
"aws_profile_name": getattr(key_management_settings, "aws_profile_name", None),
"aws_web_identity_token": getattr(key_management_settings, "aws_web_identity_token", None),
"aws_sts_endpoint": getattr(key_management_settings, "aws_sts_endpoint", None),
}
# Remove None values
aws_kwargs = {k: v for k, v in aws_kwargs.items() if v is not None}
litellm.secret_manager_client = cls(**aws_kwargs)
litellm._key_management_system = KeyManagementSystem.AWS_SECRET_MANAGER
except Exception as e:
@ -327,6 +375,24 @@ class AWSSecretsManagerV2(BaseAWSLLM, BaseSecretManager):
except ImportError:
raise ImportError("Missing boto3 to call bedrock. Run 'pip install boto3'.")
optional_params = optional_params or {}
# Build optional_params from instance settings if not provided
# This allows the IAM role settings to be used for Secret Manager calls
if not optional_params.get("aws_role_name") and self.aws_role_name:
optional_params["aws_role_name"] = self.aws_role_name
if not optional_params.get("aws_session_name") and self.aws_session_name:
optional_params["aws_session_name"] = self.aws_session_name
if not optional_params.get("aws_region_name") and self.aws_region_name:
optional_params["aws_region_name"] = self.aws_region_name
if not optional_params.get("aws_external_id") and self.aws_external_id:
optional_params["aws_external_id"] = self.aws_external_id
if not optional_params.get("aws_profile_name") and self.aws_profile_name:
optional_params["aws_profile_name"] = self.aws_profile_name
if not optional_params.get("aws_web_identity_token") and self.aws_web_identity_token:
optional_params["aws_web_identity_token"] = self.aws_web_identity_token
if not optional_params.get("aws_sts_endpoint") and self.aws_sts_endpoint:
optional_params["aws_sts_endpoint"] = self.aws_sts_endpoint
boto3_credentials_info = self._get_boto_credentials_from_optional_params(
optional_params
)

View File

@ -49,4 +49,26 @@ class KeyManagementSettings(LiteLLMPydanticObjectBase):
"""
Path to custom secret manager class (e.g. "my_secret_manager.InMemorySecretManager")
Required when key_management_system is "custom"
"""
"""
# AWS IAM Role Assumption Settings (for AWS Secret Manager)
aws_region_name: Optional[str] = None
"""AWS region for Secret Manager operations (e.g., 'us-east-1')"""
aws_role_name: Optional[str] = None
"""ARN of IAM role to assume for Secret Manager access (e.g., 'arn:aws:iam::123456789012:role/MyRole')"""
aws_session_name: Optional[str] = None
"""Session name for the assumed role session (optional, auto-generated if not provided)"""
aws_external_id: Optional[str] = None
"""External ID for role assumption (required for cross-account access)"""
aws_profile_name: Optional[str] = None
"""AWS profile name to use from ~/.aws/credentials"""
aws_web_identity_token: Optional[str] = None
"""Web identity token for OIDC/IRSA authentication"""
aws_sts_endpoint: Optional[str] = None
"""Custom STS endpoint URL (useful for VPC endpoints or testing)"""

View File

@ -31,6 +31,7 @@ import pytest
from litellm._uuid import uuid
import json
from litellm.secret_managers.aws_secret_manager_v2 import AWSSecretsManagerV2
from litellm.types.secret_managers.main import KeyManagementSettings
def check_aws_credentials():
@ -253,3 +254,211 @@ async def test_write_secret_with_description_and_tags():
delete_response = await secret_manager.async_delete_secret(secret_name=test_secret_name)
print("Delete Response:", delete_response)
assert delete_response is not None
def test_secret_manager_with_iam_role_settings():
"""
Test AWS Secret Manager initialization with IAM role settings
"""
settings = KeyManagementSettings(
aws_region_name="us-east-1",
aws_role_name="arn:aws:iam::123456789012:role/TestRole",
aws_session_name="test-session",
)
secret_manager = AWSSecretsManagerV2(
aws_region_name=settings.aws_region_name,
aws_role_name=settings.aws_role_name,
aws_session_name=settings.aws_session_name,
)
# Verify settings are stored
assert secret_manager.aws_role_name == settings.aws_role_name
assert secret_manager.aws_region_name == settings.aws_region_name
assert secret_manager.aws_session_name == settings.aws_session_name
def test_secret_manager_with_cross_account_settings():
"""
Test AWS Secret Manager initialization with cross-account IAM role settings
"""
settings = KeyManagementSettings(
aws_region_name="us-west-2",
aws_role_name="arn:aws:iam::999999999999:role/CrossAccountRole",
aws_session_name="cross-account-session",
aws_external_id="unique-external-id",
)
secret_manager = AWSSecretsManagerV2(
aws_region_name=settings.aws_region_name,
aws_role_name=settings.aws_role_name,
aws_session_name=settings.aws_session_name,
aws_external_id=settings.aws_external_id,
)
# Verify settings are stored
assert secret_manager.aws_role_name == settings.aws_role_name
assert secret_manager.aws_region_name == settings.aws_region_name
assert secret_manager.aws_external_id == settings.aws_external_id
def test_secret_manager_with_irsa_settings():
"""
Test AWS Secret Manager initialization with IRSA (EKS) settings
"""
settings = KeyManagementSettings(
aws_region_name="us-east-1",
aws_role_name="arn:aws:iam::123456789012:role/EKSServiceAccountRole",
aws_session_name="eks-session",
aws_web_identity_token="os.environ/AWS_WEB_IDENTITY_TOKEN_FILE",
)
secret_manager = AWSSecretsManagerV2(
aws_region_name=settings.aws_region_name,
aws_role_name=settings.aws_role_name,
aws_session_name=settings.aws_session_name,
aws_web_identity_token=settings.aws_web_identity_token,
)
# Verify settings are stored
assert secret_manager.aws_role_name == settings.aws_role_name
assert secret_manager.aws_web_identity_token == settings.aws_web_identity_token
def test_secret_manager_with_custom_sts_endpoint():
"""
Test AWS Secret Manager initialization with custom STS endpoint (VPC endpoint)
"""
settings = KeyManagementSettings(
aws_region_name="us-east-1",
aws_role_name="arn:aws:iam::123456789012:role/VPCRole",
aws_session_name="vpc-session",
aws_sts_endpoint="https://sts.us-east-1.vpce-0123456789abcdef.amazonaws.com",
)
secret_manager = AWSSecretsManagerV2(
aws_region_name=settings.aws_region_name,
aws_role_name=settings.aws_role_name,
aws_session_name=settings.aws_session_name,
aws_sts_endpoint=settings.aws_sts_endpoint,
)
# Verify settings are stored
assert secret_manager.aws_role_name == settings.aws_role_name
assert secret_manager.aws_sts_endpoint == settings.aws_sts_endpoint
def test_secret_manager_with_aws_profile():
"""
Test AWS Secret Manager initialization with AWS profile
"""
settings = KeyManagementSettings(
aws_region_name="us-east-1",
aws_profile_name="litellm-dev",
)
secret_manager = AWSSecretsManagerV2(
aws_region_name=settings.aws_region_name,
aws_profile_name=settings.aws_profile_name,
)
# Verify settings are stored
assert secret_manager.aws_profile_name == settings.aws_profile_name
def test_load_aws_secret_manager_with_settings():
"""
Test loading AWS Secret Manager with key_management_settings
"""
import litellm
settings = KeyManagementSettings(
store_virtual_keys=True,
aws_region_name="us-east-1",
aws_role_name="arn:aws:iam::123456789012:role/TestRole",
aws_session_name="test-session",
)
# Set environment variable for validation to pass
os.environ["AWS_REGION_NAME"] = "us-east-1"
try:
AWSSecretsManagerV2.load_aws_secret_manager(
use_aws_secret_manager=True,
key_management_settings=settings,
)
# Verify the client was created
assert litellm.secret_manager_client is not None
assert isinstance(litellm.secret_manager_client, AWSSecretsManagerV2)
# Verify settings were passed through
assert litellm.secret_manager_client.aws_role_name == settings.aws_role_name
assert litellm.secret_manager_client.aws_region_name == settings.aws_region_name
assert litellm.secret_manager_client.aws_session_name == settings.aws_session_name
finally:
# Cleanup
litellm.secret_manager_client = None
@pytest.mark.asyncio
async def test_end_to_end_iam_role_secret_write():
"""
Test writing a secret using IAM role assumption (integration test)
Requires:
- AWS_REGION_NAME environment variable
- TEST_IAM_ROLE_ARN environment variable with ARN of a role that can be assumed
- Proper AWS credentials configured (via instance profile, IAM role, or environment)
"""
# Skip if TEST_IAM_ROLE_ARN is not set
test_role_arn = os.getenv("TEST_IAM_ROLE_ARN")
if not test_role_arn:
pytest.skip("TEST_IAM_ROLE_ARN environment variable not set")
aws_region = os.getenv("AWS_REGION_NAME", "us-east-1")
settings = KeyManagementSettings(
store_virtual_keys=True,
aws_region_name=aws_region,
aws_role_name=test_role_arn,
aws_session_name="integration-test-session",
)
secret_manager = AWSSecretsManagerV2(
aws_region_name=settings.aws_region_name,
aws_role_name=settings.aws_role_name,
aws_session_name=settings.aws_session_name,
)
test_secret_name = f"litellm_test_iam_{uuid.uuid4().hex[:8]}"
test_secret_value = "test_value_iam_role"
try:
# Test write operation using IAM role
response = await secret_manager.async_write_secret(
secret_name=test_secret_name,
secret_value=test_secret_value,
)
print("Write Response with IAM Role:", response)
assert response is not None
assert "ARN" in response
# Test read operation using IAM role
read_value = await secret_manager.async_read_secret(
secret_name=test_secret_name
)
print("Read Value with IAM Role:", read_value)
assert read_value == test_secret_value
finally:
# Cleanup: Delete the secret
try:
delete_response = await secret_manager.async_delete_secret(
secret_name=test_secret_name
)
print("Delete Response:", delete_response)
except Exception as e:
print(f"Cleanup failed: {e}")