Skip to content

Commit 8365b0e

Browse files
committed
Add TLS profiles
1 parent 0c6e51c commit 8365b0e

11 files changed

Lines changed: 1004 additions & 3 deletions

File tree

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
@@ -150,3 +150,7 @@
150150
# quota limiters constants
151151
USER_QUOTA_LIMITER = "user_limiter"
152152
CLUSTER_QUOTA_LIMITER = "cluster_limiter"
153+
154+
# TLS security profile constants
155+
DEFAULT_SSL_VERSION = "TLSv1_2"
156+
DEFAULT_SSL_CIPHERS = "DEFAULT"

src/models/config.py

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import constants
2828

2929
from utils import checks
30+
from utils import tls
3031

3132

3233
class ConfigurationBase(BaseModel):
@@ -75,6 +76,98 @@ def check_tls_configuration(self) -> Self:
7576
return self
7677

7778

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

329427
@model_validator(mode="after")
330428
def check_llama_stack_model(self) -> Self:

0 commit comments

Comments
 (0)