Skip to content

Commit 1ab87af

Browse files
committed
Add TLS profiles
1 parent feb565b commit 1ab87af

File tree

11 files changed

+1005
-3
lines changed

11 files changed

+1005
-3
lines changed

docs/tls-security-profile.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# TLS Security Profile Configuration
2+
3+
This document describes how to configure and test the TLS security profile for outgoing connections to the Llama Stack provider.
4+
5+
## Overview
6+
7+
The TLS security profile allows you to enforce specific TLS security settings for connections from Lightspeed Stack to the Llama Stack server. This includes:
8+
9+
- **Profile Type**: Predefined security profiles (OldType, IntermediateType, ModernType, Custom)
10+
- **Minimum TLS Version**: Enforce minimum TLS protocol version (TLS 1.0 - 1.3)
11+
- **Cipher Suites**: Specify allowed cipher suites
12+
- **CA Certificate**: Custom CA certificate for server verification
13+
- **Skip Verification**: Option to skip TLS verification (testing only)
14+
15+
## Configuration
16+
17+
Add the `tls_security_profile` section under `llama_stack` in your configuration file:
18+
19+
```yaml
20+
llama_stack:
21+
url: https://llama-stack-server:8321
22+
use_as_library_client: false
23+
tls_security_profile:
24+
type: ModernType
25+
minTLSVersion: VersionTLS13
26+
caCertPath: /path/to/ca-certificate.crt
27+
```
28+
29+
### Configuration Options
30+
31+
| Field | Type | Description |
32+
|-------|------|-------------|
33+
| `type` | string | Profile type: `OldType`, `IntermediateType`, `ModernType`, or `Custom` |
34+
| `minTLSVersion` | string | Minimum TLS version: `VersionTLS10`, `VersionTLS11`, `VersionTLS12`, `VersionTLS13` |
35+
| `ciphers` | list[string] | List of allowed cipher suites (optional, uses profile defaults) |
36+
| `caCertPath` | string | Path to CA certificate file for server verification |
37+
| `skipTLSVerification` | boolean | Skip TLS certificate verification (default: false, **testing only**) |
38+
39+
### Profile Types
40+
41+
| Profile | Min TLS Version | Description |
42+
|---------|-----------------|-------------|
43+
| `OldType` | TLS 1.0 | Legacy compatibility, wide cipher support |
44+
| `IntermediateType` | TLS 1.2 | Balanced security and compatibility |
45+
| `ModernType` | TLS 1.3 | Maximum security, TLS 1.3 only |
46+
| `Custom` | Configurable | User-defined settings |

src/client.py

Lines changed: 81 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
"""Llama Stack client retrieval class."""
22

33
import logging
4-
4+
import ssl
55
from typing import Optional
66

7+
import httpx
78
from llama_stack import (
89
AsyncLlamaStackAsLibraryClient, # type: ignore
910
)
1011
from llama_stack_client import AsyncLlamaStackClient # type: ignore
11-
from models.config import LlamaStackConfiguration
12+
from models.config import LlamaStackConfiguration, TLSSecurityProfile
1213
from utils.types import Singleton
14+
from utils import tls
1315

1416

1517
logger = logging.getLogger(__name__)
@@ -20,6 +22,76 @@ class AsyncLlamaStackClientHolder(metaclass=Singleton):
2022

2123
_lsc: Optional[AsyncLlamaStackClient] = None
2224

25+
def _construct_httpx_client(
26+
self, tls_security_profile: Optional[TLSSecurityProfile]
27+
) -> Optional[httpx.AsyncClient]:
28+
"""Construct HTTPX client with TLS security profile configuration.
29+
30+
Args:
31+
tls_security_profile: TLS security profile configuration.
32+
33+
Returns:
34+
Configured httpx.AsyncClient if TLS profile is set, None otherwise.
35+
"""
36+
# if security profile is not set, return None to use default httpx client
37+
if tls_security_profile is None or tls_security_profile.profile_type is None:
38+
logger.info("No TLS security profile configured, using default settings")
39+
return None
40+
41+
logger.info("TLS security profile: %s", tls_security_profile.profile_type)
42+
43+
# get the TLS profile type
44+
profile_type = tls.TLSProfiles(tls_security_profile.profile_type)
45+
46+
# retrieve ciphers - custom list or profile-based
47+
ciphers = tls.ciphers_as_string(tls_security_profile.ciphers, profile_type)
48+
logger.info("TLS ciphers: %s", ciphers)
49+
50+
# retrieve minimum TLS version
51+
min_tls_ver = tls.min_tls_version(
52+
tls_security_profile.min_tls_version, profile_type
53+
)
54+
logger.info("Minimum TLS version: %s", min_tls_ver)
55+
56+
ssl_version = tls.ssl_tls_version(min_tls_ver)
57+
logger.info("SSL version: %s", ssl_version)
58+
59+
# check if TLS verification should be skipped (for testing only)
60+
if tls_security_profile.skip_tls_verification:
61+
logger.warning(
62+
"TLS verification is disabled. This is insecure and should "
63+
"only be used for testing purposes."
64+
)
65+
return httpx.AsyncClient(verify=False)
66+
67+
# create SSL context with the configured settings
68+
context = ssl.create_default_context()
69+
70+
# load CA certificate if specified
71+
if tls_security_profile.ca_cert_path is not None:
72+
logger.info("Loading CA certificate from: %s", tls_security_profile.ca_cert_path)
73+
context.load_verify_locations(cafile=str(tls_security_profile.ca_cert_path))
74+
75+
if ssl_version is not None:
76+
context.minimum_version = ssl_version
77+
78+
if ciphers is not None:
79+
# Note: TLS 1.3 ciphers cannot be set via set_ciphers() - they are
80+
# automatically negotiated when TLS 1.3 is used. The set_ciphers()
81+
# method only affects TLS 1.2 and below cipher selection.
82+
try:
83+
context.set_ciphers(ciphers)
84+
except ssl.SSLError as e:
85+
logger.warning(
86+
"Could not set ciphers '%s': %s. "
87+
"TLS 1.3 ciphers are automatically negotiated.",
88+
ciphers,
89+
e,
90+
)
91+
92+
logger.info("Creating httpx.AsyncClient with TLS security profile")
93+
return httpx.AsyncClient(verify=context)
94+
2395
async def load(self, llama_stack_config: LlamaStackConfiguration) -> None:
2496
"""Retrieve Async Llama stack client according to configuration."""
2597
if llama_stack_config.use_as_library_client is True:
@@ -37,13 +109,20 @@ async def load(self, llama_stack_config: LlamaStackConfiguration) -> None:
37109
raise ValueError(msg)
38110
else:
39111
logger.info("Using Llama stack running as a service")
112+
113+
# construct httpx client with TLS security profile if configured
114+
http_client = self._construct_httpx_client(
115+
llama_stack_config.tls_security_profile
116+
)
117+
40118
self._lsc = AsyncLlamaStackClient(
41119
base_url=llama_stack_config.url,
42120
api_key=(
43121
llama_stack_config.api_key.get_secret_value()
44122
if llama_stack_config.api_key is not None
45123
else None
46124
),
125+
http_client=http_client,
47126
)
48127

49128
def get_client(self) -> AsyncLlamaStackClient:

src/constants.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,3 +152,7 @@
152152
# quota limiters constants
153153
USER_QUOTA_LIMITER = "user_limiter"
154154
CLUSTER_QUOTA_LIMITER = "cluster_limiter"
155+
156+
# TLS security profile constants
157+
DEFAULT_SSL_VERSION = "TLSv1_2"
158+
DEFAULT_SSL_CIPHERS = "DEFAULT"

src/models/config.py

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import constants
2929

3030
from utils import checks
31+
from utils import tls
3132

3233

3334
class ConfigurationBase(BaseModel):
@@ -76,6 +77,98 @@ def check_tls_configuration(self) -> Self:
7677
return self
7778

7879

80+
class TLSSecurityProfile(ConfigurationBase):
81+
"""TLS security profile for outgoing connections.
82+
83+
This configuration allows customizing the TLS security settings for
84+
outgoing connections to LM providers. Users can specify:
85+
- A predefined profile type (OldType, IntermediateType, ModernType, Custom)
86+
- Minimum TLS version (VersionTLS10, VersionTLS11, VersionTLS12, VersionTLS13)
87+
- List of allowed cipher suites
88+
- CA certificate path for custom certificate authorities
89+
- Option to skip TLS verification (for testing only)
90+
91+
Example configuration:
92+
tls_security_profile:
93+
type: Custom
94+
minTLSVersion: VersionTLS13
95+
ciphers:
96+
- TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
97+
- TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
98+
caCertPath: /path/to/ca.crt
99+
"""
100+
101+
profile_type: Optional[str] = Field(
102+
None,
103+
alias="type",
104+
title="Profile type",
105+
description="TLS profile type: OldType, IntermediateType, ModernType, or Custom",
106+
)
107+
min_tls_version: Optional[str] = Field(
108+
None,
109+
alias="minTLSVersion",
110+
title="Minimum TLS version",
111+
description="Minimum TLS version: VersionTLS10, VersionTLS11, VersionTLS12, VersionTLS13",
112+
)
113+
ciphers: Optional[list[str]] = Field(
114+
None,
115+
title="Ciphers",
116+
description="List of allowed cipher suites",
117+
)
118+
ca_cert_path: Optional[FilePath] = Field(
119+
None,
120+
alias="caCertPath",
121+
title="CA certificate path",
122+
description="Path to CA certificate file for verifying server certificates",
123+
)
124+
skip_tls_verification: bool = Field(
125+
False,
126+
alias="skipTLSVerification",
127+
title="Skip TLS verification",
128+
description="Skip TLS certificate verification (for testing only, not recommended for production)",
129+
)
130+
131+
model_config = ConfigDict(extra="forbid", populate_by_name=True)
132+
133+
@model_validator(mode="after")
134+
def check_tls_security_profile(self) -> Self:
135+
"""Validate TLS security profile configuration."""
136+
# check the TLS profile type
137+
if self.profile_type is not None:
138+
try:
139+
tls.TLSProfiles(self.profile_type)
140+
except ValueError as e:
141+
valid_profiles = [p.value for p in tls.TLSProfiles]
142+
raise ValueError(
143+
f"Invalid TLS profile type '{self.profile_type}'. "
144+
f"Valid types: {valid_profiles}"
145+
) from e
146+
147+
# check the TLS protocol version
148+
if self.min_tls_version is not None:
149+
try:
150+
tls.TLSProtocolVersion(self.min_tls_version)
151+
except ValueError as e:
152+
valid_versions = [v.value for v in tls.TLSProtocolVersion]
153+
raise ValueError(
154+
f"Invalid minimal TLS version '{self.min_tls_version}'. "
155+
f"Valid versions: {valid_versions}"
156+
) from e
157+
158+
# check ciphers - validate against profile if not Custom
159+
if self.ciphers is not None and self.profile_type is not None:
160+
if self.profile_type != tls.TLSProfiles.CUSTOM_TYPE:
161+
profile = tls.TLSProfiles(self.profile_type)
162+
supported_ciphers = tls.TLS_CIPHERS.get(profile, [])
163+
for cipher in self.ciphers:
164+
if cipher not in supported_ciphers:
165+
raise ValueError(
166+
f"Unsupported cipher '{cipher}' for profile '{self.profile_type}'"
167+
)
168+
169+
return self
170+
171+
79172
class CORSConfiguration(ConfigurationBase):
80173
"""CORS configuration.
81174
@@ -431,6 +524,12 @@ class LlamaStackConfiguration(ConfigurationBase):
431524
description="Path to configuration file used when Llama Stack is run in library mode",
432525
)
433526

527+
tls_security_profile: Optional[TLSSecurityProfile] = Field(
528+
None,
529+
title="TLS security profile",
530+
description="TLS security profile for outgoing connections to Llama Stack",
531+
)
532+
434533
@model_validator(mode="after")
435534
def check_llama_stack_model(self) -> Self:
436535
"""

0 commit comments

Comments
 (0)