Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions docs/tls-security-profile.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# TLS Security Profile Configuration

This document describes how to configure and test the TLS security profile for outgoing connections to the Llama Stack provider.

## Overview

The TLS security profile allows you to enforce specific TLS security settings for connections from Lightspeed Stack to the Llama Stack server. This includes:

- **Profile Type**: Predefined security profiles (OldType, IntermediateType, ModernType, Custom)
- **Minimum TLS Version**: Enforce minimum TLS protocol version (TLS 1.0 - 1.3)
- **Cipher Suites**: Specify allowed cipher suites
- **CA Certificate**: Custom CA certificate for server verification
- **Skip Verification**: Option to skip TLS verification (testing only)

## Configuration

Add the `tls_security_profile` section under `llama_stack` in your configuration file:

```yaml
llama_stack:
url: https://llama-stack-server:8321
use_as_library_client: false
tls_security_profile:
type: ModernType
minTLSVersion: VersionTLS13
caCertPath: /path/to/ca-certificate.crt
```

### Configuration Options

| Field | Type | Description |
|-------|------|-------------|
| `type` | string | Profile type: `OldType`, `IntermediateType`, `ModernType`, or `Custom` |
| `minTLSVersion` | string | Minimum TLS version: `VersionTLS10`, `VersionTLS11`, `VersionTLS12`, `VersionTLS13` |
| `ciphers` | list[string] | List of allowed cipher suites (optional, uses profile defaults) |
| `caCertPath` | string | Path to CA certificate file for server verification |
| `skipTLSVerification` | boolean | Skip TLS certificate verification (default: false, **testing only**) |

### Profile Types

| Profile | Min TLS Version | Description |
|---------|-----------------|-------------|
| `OldType` | TLS 1.0 | Legacy compatibility, wide cipher support |
| `IntermediateType` | TLS 1.2 | Balanced security and compatibility |
| `ModernType` | TLS 1.3 | Maximum security, TLS 1.3 only |
| `Custom` | Configurable | User-defined settings |
83 changes: 81 additions & 2 deletions src/client.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
"""Llama Stack client retrieval class."""

import logging

import ssl
from typing import Optional

import httpx
from llama_stack import (
AsyncLlamaStackAsLibraryClient, # type: ignore
)
from llama_stack_client import AsyncLlamaStackClient # type: ignore
from models.config import LlamaStackConfiguration
from models.config import LlamaStackConfiguration, TLSSecurityProfile
from utils.types import Singleton
from utils import tls


logger = logging.getLogger(__name__)
Expand All @@ -20,6 +22,76 @@

_lsc: Optional[AsyncLlamaStackClient] = None

def _construct_httpx_client(
self, tls_security_profile: Optional[TLSSecurityProfile]
) -> Optional[httpx.AsyncClient]:
"""Construct HTTPX client with TLS security profile configuration.
Args:
tls_security_profile: TLS security profile configuration.
Returns:
Configured httpx.AsyncClient if TLS profile is set, None otherwise.
"""
# if security profile is not set, return None to use default httpx client
if tls_security_profile is None or tls_security_profile.profile_type is None:
logger.info("No TLS security profile configured, using default settings")
return None

logger.info("TLS security profile: %s", tls_security_profile.profile_type)

# get the TLS profile type
profile_type = tls.TLSProfiles(tls_security_profile.profile_type)

# retrieve ciphers - custom list or profile-based
ciphers = tls.ciphers_as_string(tls_security_profile.ciphers, profile_type)
logger.info("TLS ciphers: %s", ciphers)

# retrieve minimum TLS version
min_tls_ver = tls.min_tls_version(
tls_security_profile.min_tls_version, profile_type
)
logger.info("Minimum TLS version: %s", min_tls_ver)

ssl_version = tls.ssl_tls_version(min_tls_ver)
logger.info("SSL version: %s", ssl_version)

# check if TLS verification should be skipped (for testing only)
if tls_security_profile.skip_tls_verification:
logger.warning(
"TLS verification is disabled. This is insecure and should "
"only be used for testing purposes."
)
return httpx.AsyncClient(verify=False)

Check failure

Code scanning / Bandit

Call to httpx with verify=False disabling SSL certificate checks, security issue. Error

Call to httpx with verify=False disabling SSL certificate checks, security issue.

Check warning

Code scanning / Bandit

Call to httpx without timeout Warning

Call to httpx without timeout

# create SSL context with the configured settings
context = ssl.create_default_context()

# load CA certificate if specified
if tls_security_profile.ca_cert_path is not None:
logger.info("Loading CA certificate from: %s", tls_security_profile.ca_cert_path)
context.load_verify_locations(cafile=str(tls_security_profile.ca_cert_path))

if ssl_version is not None:
context.minimum_version = ssl_version

if ciphers is not None:
# Note: TLS 1.3 ciphers cannot be set via set_ciphers() - they are
# automatically negotiated when TLS 1.3 is used. The set_ciphers()
# method only affects TLS 1.2 and below cipher selection.
try:
context.set_ciphers(ciphers)
except ssl.SSLError as e:
logger.warning(
"Could not set ciphers '%s': %s. "
"TLS 1.3 ciphers are automatically negotiated.",
ciphers,
e,
)

logger.info("Creating httpx.AsyncClient with TLS security profile")
return httpx.AsyncClient(verify=context)

Check warning

Code scanning / Bandit

Call to httpx without timeout Warning

Call to httpx without timeout

async def load(self, llama_stack_config: LlamaStackConfiguration) -> None:
"""Retrieve Async Llama stack client according to configuration."""
if llama_stack_config.use_as_library_client is True:
Expand All @@ -37,13 +109,20 @@
raise ValueError(msg)
else:
logger.info("Using Llama stack running as a service")

# construct httpx client with TLS security profile if configured
http_client = self._construct_httpx_client(
llama_stack_config.tls_security_profile
)

self._lsc = AsyncLlamaStackClient(
base_url=llama_stack_config.url,
api_key=(
llama_stack_config.api_key.get_secret_value()
if llama_stack_config.api_key is not None
else None
),
http_client=http_client,
)

def get_client(self) -> AsyncLlamaStackClient:
Expand Down
4 changes: 4 additions & 0 deletions src/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,3 +152,7 @@
# quota limiters constants
USER_QUOTA_LIMITER = "user_limiter"
CLUSTER_QUOTA_LIMITER = "cluster_limiter"

# TLS security profile constants
DEFAULT_SSL_VERSION = "TLSv1_2"
DEFAULT_SSL_CIPHERS = "DEFAULT"
99 changes: 99 additions & 0 deletions src/models/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import constants

from utils import checks
from utils import tls


class ConfigurationBase(BaseModel):
Expand Down Expand Up @@ -76,6 +77,98 @@ def check_tls_configuration(self) -> Self:
return self


class TLSSecurityProfile(ConfigurationBase):
"""TLS security profile for outgoing connections.

This configuration allows customizing the TLS security settings for
outgoing connections to LM providers. Users can specify:
- A predefined profile type (OldType, IntermediateType, ModernType, Custom)
- Minimum TLS version (VersionTLS10, VersionTLS11, VersionTLS12, VersionTLS13)
- List of allowed cipher suites
- CA certificate path for custom certificate authorities
- Option to skip TLS verification (for testing only)

Example configuration:
tls_security_profile:
type: Custom
minTLSVersion: VersionTLS13
ciphers:
- TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
- TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
caCertPath: /path/to/ca.crt
"""

profile_type: Optional[str] = Field(
None,
alias="type",
title="Profile type",
description="TLS profile type: OldType, IntermediateType, ModernType, or Custom",
)
min_tls_version: Optional[str] = Field(
None,
alias="minTLSVersion",
title="Minimum TLS version",
description="Minimum TLS version: VersionTLS10, VersionTLS11, VersionTLS12, VersionTLS13",
)
ciphers: Optional[list[str]] = Field(
None,
title="Ciphers",
description="List of allowed cipher suites",
)
ca_cert_path: Optional[FilePath] = Field(
None,
alias="caCertPath",
title="CA certificate path",
description="Path to CA certificate file for verifying server certificates",
)
skip_tls_verification: bool = Field(
False,
alias="skipTLSVerification",
title="Skip TLS verification",
description="Skip TLS certificate verification (for testing only, not recommended for production)",
)

model_config = ConfigDict(extra="forbid", populate_by_name=True)

@model_validator(mode="after")
def check_tls_security_profile(self) -> Self:
"""Validate TLS security profile configuration."""
# check the TLS profile type
if self.profile_type is not None:
try:
tls.TLSProfiles(self.profile_type)
except ValueError as e:
valid_profiles = [p.value for p in tls.TLSProfiles]
raise ValueError(
f"Invalid TLS profile type '{self.profile_type}'. "
f"Valid types: {valid_profiles}"
) from e

# check the TLS protocol version
if self.min_tls_version is not None:
try:
tls.TLSProtocolVersion(self.min_tls_version)
except ValueError as e:
valid_versions = [v.value for v in tls.TLSProtocolVersion]
raise ValueError(
f"Invalid minimal TLS version '{self.min_tls_version}'. "
f"Valid versions: {valid_versions}"
) from e

# check ciphers - validate against profile if not Custom
if self.ciphers is not None and self.profile_type is not None:
if self.profile_type != tls.TLSProfiles.CUSTOM_TYPE:
profile = tls.TLSProfiles(self.profile_type)
supported_ciphers = tls.TLS_CIPHERS.get(profile, [])
for cipher in self.ciphers:
if cipher not in supported_ciphers:
raise ValueError(
f"Unsupported cipher '{cipher}' for profile '{self.profile_type}'"
)

return self


class CORSConfiguration(ConfigurationBase):
"""CORS configuration.

Expand Down Expand Up @@ -431,6 +524,12 @@ class LlamaStackConfiguration(ConfigurationBase):
description="Path to configuration file used when Llama Stack is run in library mode",
)

tls_security_profile: Optional[TLSSecurityProfile] = Field(
None,
title="TLS security profile",
description="TLS security profile for outgoing connections to Llama Stack",
)

@model_validator(mode="after")
def check_llama_stack_model(self) -> Self:
"""
Expand Down
Loading
Loading