Skip to content

Commit 7092618

Browse files
authored
To introduce env based cert discovery (#1754)
* fix to introduce env based cert discovery and also eliminates conditional ssl logic complexity * fix review * fix UT failure * fix pre-commit
1 parent cf0d0d7 commit 7092618

File tree

6 files changed

+869
-64
lines changed

6 files changed

+869
-64
lines changed

README.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ ENABLE_ARI_POSTPROCESS="False"
2727
WCA_SECRET_BACKEND_TYPE="dummy"
2828
# configure model server
2929
ANSIBLE_AI_MODEL_MESH_CONFIG="..."
30+
# configure SSL certificate path (optional)
31+
# ANSIBLE_AI_SERVICE_CA_PATH="/etc/ssl/certs/ca-certificates.crt"
3032
```
3133
See the example [ANSIBLE_AI_MODEL_MESH_CONFIG](./docs/config/examples/README-ANSIBLE_AI_MODEL_MESH_CONFIG.md).
3234

@@ -61,6 +63,29 @@ podman compose -f tools/docker-compose/compose.yaml down
6163

6264
### Service configuration
6365

66+
### SSL/TLS Certificate Configuration
67+
68+
For SSL communication with model servers and external services, you can configure the certificate authority (CA) certificate path:
69+
70+
```bash
71+
# Default: Uses OpenShift/Kubernetes service account certificate
72+
# Path: /var/run/secrets/kubernetes.io/serviceaccount/service-ca.crt
73+
74+
# Custom certificate path (for containerized installations outside OpenShift)
75+
export ANSIBLE_AI_SERVICE_CA_PATH="/etc/ssl/certs/ca-certificates.crt"
76+
77+
# Disable service CA certificate discovery (rely on system certificates only)
78+
export ANSIBLE_AI_SERVICE_CA_PATH=""
79+
```
80+
81+
**ANSIBLE_AI_SERVICE_CA_PATH**
82+
- **Default**: `/var/run/secrets/kubernetes.io/serviceaccount/service-ca.crt` (OpenShift/Kubernetes)
83+
- **Purpose**: Specifies the path to the CA certificate file for SSL verification
84+
- **Use Cases**:
85+
- **OpenShift/Kubernetes**: Default path works automatically
86+
- **Containerized deployments**: Set to `/etc/ssl/certs/ca-certificates.crt` or your custom path
87+
- **Development/Testing**: Set to empty string to disable custom CA and use system certificates
88+
6489
### Secret storage
6590
For most development usages, you can skip the call to AWS Secrets Manager
6691
and always use the dummy `WCA_SECRET_BACKEND_TYPE` by setting the following in your

ansible_ai_connect/ai/api/model_pipelines/http/pipelines.py

Lines changed: 31 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import copy
1616
import json
1717
import logging
18+
import os
1819
import ssl
1920
from json import JSONDecodeError
2021
from typing import Any, AsyncGenerator
@@ -70,10 +71,34 @@ def __init__(self, config: HttpConfiguration):
7071
self.headers = {"Content-Type": "application/json"}
7172
i = self.config.timeout
7273
self._timeout = int(i) if i is not None else None
74+
# Help ssl.create_default_context() find mounted certificates
75+
self._setup_ssl_context()
7376

7477
def task_gen_timeout(self, task_count=1):
7578
return self._timeout * task_count if self._timeout else None
7679

80+
def _setup_ssl_context(self):
81+
"""Let ssl.create_default_context() discover certs.
82+
Following container best practices - use environment variables to help
83+
Python's default SSL context find mounted certificates automatically.
84+
This avoids explicit certificate path management in application code.
85+
"""
86+
if self.config.verify_ssl:
87+
# Check for mounted service-ca certificate (container/K8s pattern)
88+
service_ca = settings.SERVICE_CA_PATH
89+
if os.path.exists(service_ca):
90+
os.environ.setdefault("REQUESTS_CA_BUNDLE", service_ca)
91+
os.environ.setdefault("SSL_CERT_FILE", service_ca)
92+
logger.info("Configured SSL context to use mounted service-ca certificate")
93+
94+
def get_ssl_verification(self):
95+
"""Just return verify_ssl boolean.
96+
ssl.create_default_context() will automatically discover certificates
97+
via environment variables set in _setup_ssl_context().
98+
No explicit certificate path management needed.
99+
"""
100+
return self.config.verify_ssl
101+
77102

78103
@Register(api_type="http")
79104
class HttpCompletionsPipeline(HttpMetaData, ModelPipelineCompletions[HttpConfiguration]):
@@ -97,9 +122,7 @@ def invoke(self, params: CompletionsParameters) -> CompletionsResponse:
97122
headers=self.headers,
98123
json=model_input,
99124
timeout=self.task_gen_timeout(task_count),
100-
verify=(
101-
self.config.ca_cert_file if self.config.ca_cert_file else self.config.verify_ssl
102-
),
125+
verify=self.get_ssl_verification(),
103126
)
104127
result.raise_for_status()
105128
response = json.loads(result.text)
@@ -119,9 +142,7 @@ def self_test(self) -> HealthCheckSummary:
119142
try:
120143
res = requests.get(
121144
url,
122-
verify=(
123-
self.config.ca_cert_file if self.config.ca_cert_file else self.config.verify_ssl
124-
),
145+
verify=self.get_ssl_verification(),
125146
timeout=1,
126147
)
127148
res.raise_for_status()
@@ -155,9 +176,7 @@ def self_test(self) -> HealthCheckSummary:
155176
self.config.inference_url + "/readiness",
156177
headers=headers,
157178
timeout=1,
158-
verify=(
159-
self.config.ca_cert_file if self.config.ca_cert_file else self.config.verify_ssl
160-
),
179+
verify=self.get_ssl_verification(),
161180
)
162181
r.raise_for_status()
163182

@@ -214,7 +233,7 @@ def invoke(self, params: ChatBotParameters) -> ChatBotResponse:
214233
headers=self.headers,
215234
json=data,
216235
timeout=self.task_gen_timeout(1),
217-
verify=self.config.ca_cert_file if self.config.ca_cert_file else self.config.verify_ssl,
236+
verify=self.get_ssl_verification(),
218237
)
219238

220239
if response.status_code == 200:
@@ -277,9 +296,8 @@ def send_schema1_event(self, ev):
277296

278297
async def async_invoke(self, params: StreamingChatBotParameters) -> AsyncGenerator:
279298

280-
# Configure SSL context based on verify_ssl setting
281-
if self.config.ca_cert_file:
282-
ssl_context = ssl.create_default_context(cafile=self.config.ca_cert_file)
299+
if self.config.verify_ssl:
300+
ssl_context = ssl.create_default_context()
283301
connector = aiohttp.TCPConnector(ssl=ssl_context)
284302
else:
285303
connector = aiohttp.TCPConnector(ssl=self.config.verify_ssl)

ansible_ai_connect/ai/api/model_pipelines/http/tests/test_pipelines.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
# limitations under the License.
1515
import json
1616
import logging
17+
import ssl
1718
from typing import cast
1819
from unittest import IsolatedAsyncioTestCase
1920
from unittest.mock import MagicMock, patch
@@ -246,8 +247,11 @@ async def test_ssl_context_verify_ssl_true(self, mock_tcp_connector, mock_post):
246247
params = self.get_params()
247248
async for _ in pipeline.async_invoke(params):
248249
pass
249-
# Verify TCPConnector was created with ssl=True
250-
mock_tcp_connector.assert_called_once_with(ssl=True)
250+
# Verify TCPConnector was created with SSL context when verify_ssl=True
251+
mock_tcp_connector.assert_called_once()
252+
call_args = mock_tcp_connector.call_args[1]["ssl"]
253+
# Should be an SSLContext object when verify_ssl=True
254+
self.assertIsInstance(call_args, ssl.SSLContext)
251255

252256
@patch("aiohttp.ClientSession.post")
253257
@patch("aiohttp.TCPConnector")
@@ -294,7 +298,13 @@ async def test_ssl_context_integration_with_existing_flow(self, mock_tcp_connect
294298
# Verify that streaming still works
295299
self.assertGreater(result_count, 0, "Streaming should return data")
296300
# Verify SSL configuration is correct
297-
mock_tcp_connector.assert_called_with(ssl=verify_ssl_value)
301+
call_args = mock_tcp_connector.call_args[1]["ssl"]
302+
if verify_ssl_value:
303+
# When verify_ssl=True, should get an SSLContext object
304+
self.assertIsInstance(call_args, ssl.SSLContext)
305+
else:
306+
# When verify_ssl=False, should get False
307+
self.assertEqual(call_args, False)
298308
# Reset mocks for next iteration
299309
mock_tcp_connector.reset_mock()
300310
mock_post.reset_mock()

0 commit comments

Comments
 (0)