-
Notifications
You must be signed in to change notification settings - Fork 122
[CLI] Require explicit CA for remote Python REST connections #166
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
qiluo-msft
merged 3 commits into
sonic-net:master
from
ashutosh-agrawal:fix/cli-rest-server-verification
Sep 8, 2026
+213
−10
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,166 @@ | ||
| import os | ||
| import sys | ||
| import unittest | ||
| import warnings | ||
| from unittest import mock | ||
|
|
||
| import requests | ||
| from urllib3.exceptions import InsecureRequestWarning | ||
|
|
||
|
|
||
| ACTIONER_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'actioner')) | ||
| sys.path.insert(0, ACTIONER_DIR) | ||
|
|
||
| import cli_client | ||
|
|
||
|
|
||
| def _response(): | ||
| response = requests.Response() | ||
| response.status_code = 200 | ||
| response._content = b'' | ||
| return response | ||
|
|
||
|
|
||
| class ApiClientCertificateVerificationTest(unittest.TestCase): | ||
|
|
||
| def test_loopback_endpoint_detection(self): | ||
| loopback_urls = ( | ||
| 'https://localhost', | ||
| 'https://localhost:443', | ||
| 'https://127.0.0.1', | ||
| 'https://127.0.0.1:8443', | ||
| 'https://127.0.0.2', | ||
| 'https://127.255.255.254', | ||
| 'https://[::1]', | ||
| 'https://[::1]:8443', | ||
| ) | ||
| remote_urls = ( | ||
| 'https://rest.example.com', | ||
| 'https://localhost.example.com', | ||
| 'https://192.0.2.1', | ||
| 'https://[::1', | ||
| ) | ||
|
|
||
| for url in loopback_urls: | ||
| with self.subTest(url=url): | ||
| self.assertTrue(cli_client._is_loopback_endpoint(url)) | ||
|
|
||
| for url in remote_urls: | ||
| with self.subTest(url=url): | ||
| self.assertFalse(cli_client._is_loopback_endpoint(url)) | ||
|
|
||
| def test_loopback_request_skips_certificate_verification(self): | ||
| with mock.patch.object( | ||
| cli_client.ApiClient, | ||
| '_ApiClient__api_root', | ||
| 'https://127.0.0.1'): | ||
| with mock.patch.object( | ||
| cli_client.ApiClient, | ||
| '_ApiClient__session') as session: | ||
| session.request.return_value = _response() | ||
|
|
||
| cli_client.ApiClient().get('/restconf/data') | ||
|
|
||
| self.assertFalse(session.request.call_args.kwargs['verify']) | ||
|
|
||
| def test_remote_request_uses_configured_ca_certificate(self): | ||
| with mock.patch.object( | ||
| cli_client.ApiClient, | ||
| '_ApiClient__api_root', | ||
| 'https://rest.example.com'): | ||
| with mock.patch.dict( | ||
| os.environ, | ||
| {cli_client.REST_API_CA_CERT: '/etc/sonic/rest-ca.pem'}): | ||
| with mock.patch('cli_client.os.path.isfile', return_value=True): | ||
| with mock.patch('cli_client.os.access', return_value=True): | ||
| with mock.patch.object( | ||
| cli_client.ApiClient, | ||
| '_ApiClient__session') as session: | ||
| session.request.return_value = _response() | ||
|
|
||
| cli_client.ApiClient().get('/restconf/data') | ||
|
|
||
| self.assertEqual( | ||
| '/etc/sonic/rest-ca.pem', | ||
| session.request.call_args.kwargs['verify']) | ||
|
|
||
| def test_remote_request_requires_ca_certificate(self): | ||
| with mock.patch.object( | ||
| cli_client.ApiClient, | ||
| '_ApiClient__api_root', | ||
| 'https://rest.example.com'): | ||
| with mock.patch.dict(os.environ, {}, clear=True): | ||
| with mock.patch.object( | ||
| cli_client.ApiClient, | ||
| '_ApiClient__session') as session: | ||
|
|
||
| response = cli_client.ApiClient().get('/restconf/data') | ||
|
|
||
| session.request.assert_not_called() | ||
| self.assertEqual( | ||
| '%Error: REST_API_CA_CERT must be set for a remote Management REST Server', | ||
| response.content['ietf-restconf:errors']['error'][0]['error-message']) | ||
|
|
||
| def test_remote_request_does_not_use_requests_ca_bundle(self): | ||
| with mock.patch.object( | ||
| cli_client.ApiClient, | ||
| '_ApiClient__api_root', | ||
| 'https://rest.example.com'): | ||
| with mock.patch.dict( | ||
| os.environ, | ||
| {'REQUESTS_CA_BUNDLE': '/etc/ssl/certs/ca-certificates.crt'}, | ||
| clear=True): | ||
| with mock.patch.object( | ||
| cli_client.ApiClient, | ||
| '_ApiClient__session') as session: | ||
|
|
||
| response = cli_client.ApiClient().get('/restconf/data') | ||
|
|
||
| session.request.assert_not_called() | ||
| self.assertEqual( | ||
| '%Error: REST_API_CA_CERT must be set for a remote Management REST Server', | ||
| response.content['ietf-restconf:errors']['error'][0]['error-message']) | ||
|
|
||
| def test_remote_request_rejects_unreadable_ca_certificate(self): | ||
| with mock.patch.object( | ||
| cli_client.ApiClient, | ||
| '_ApiClient__api_root', | ||
| 'https://rest.example.com'): | ||
| with mock.patch.dict( | ||
| os.environ, | ||
| {cli_client.REST_API_CA_CERT: '/etc/sonic/missing.pem'}): | ||
| with mock.patch('cli_client.os.path.isfile', return_value=False): | ||
| with mock.patch.object( | ||
| cli_client.ApiClient, | ||
| '_ApiClient__session') as session: | ||
|
|
||
| response = cli_client.ApiClient().get('/restconf/data') | ||
|
|
||
| session.request.assert_not_called() | ||
| self.assertEqual( | ||
| '%Error: REST_API_CA_CERT must identify a readable CA certificate file', | ||
| response.content['ietf-restconf:errors']['error'][0]['error-message']) | ||
|
|
||
| def test_loopback_warning_suppression_is_scoped_to_request(self): | ||
| def request_with_warning(*args, **kwargs): | ||
| warnings.warn('loopback request', InsecureRequestWarning) | ||
| return _response() | ||
|
|
||
| with mock.patch.object( | ||
| cli_client.ApiClient, | ||
| '_ApiClient__api_root', | ||
| 'https://localhost'): | ||
| with mock.patch.object( | ||
| cli_client.ApiClient, | ||
| '_ApiClient__session') as session: | ||
| session.request.side_effect = request_with_warning | ||
|
|
||
| with warnings.catch_warnings(): | ||
| warnings.simplefilter('error', InsecureRequestWarning) | ||
| cli_client.ApiClient().get('/restconf/data') | ||
| with self.assertRaises(InsecureRequestWarning): | ||
| warnings.warn('outside request', InsecureRequestWarning) | ||
|
|
||
|
|
||
| if __name__ == '__main__': | ||
| unittest.main() |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The fix correctly moves away from verify=False for remote endpoints, but using verify=True
relies on whatever CA certificates happen to be present in the container's system store.
For a managed SONiC deployment this is fragile — the container may not carry the right
enterprise CA, and there's no explicit operator control over which CA is trusted.
Suggest adding a --rest-ca-cert CLI option (or a counterpart in the existing REST
client config) that accepts a PEM-format CA file, and using that path as the verify=
argument for remote requests:
where ca_cert_path is the value of --rest-ca-cert. If a remote endpoint is configured
and no --rest-ca-cert is supplied, the client should fail with a clear error rather than
falling back to the system store.
This gives operators an explicit, visible CA provisioning step instead of implicit
system-bundle trust.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@qiluo-msft Thanks. After looking further, I agree that falling back to the general system trust store does not provide REST-specific control over the trusted CA.
I propose splitting the work into focused changes. In this PR, I will update the Python actioner client to require an explicitly configured CA file for non-loopback
REST_API_ROOTvalues, without falling back to the system trust store.The CLI also has a C++ libcurl REST client used for authentication and token refresh. I will submit a follow-up PR applying the same explicit-CA behavior there. The operator-facing
--rest-ca-certoption, including host-to-container certificate provisioning, can then be added separately insonic-buildimage.Would this staged approach work for you?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated in
f0e11c8. The Python actioner now requiresREST_API_CA_CERTfor non-loopback endpoints and passes that PEM path directly to Requests. It does not fall back toREQUESTS_CA_BUNDLEor the system CA store, and missing or unreadable configuration returns a clear error before making the request.I added focused unit coverage and repeated the VS validation. The configured explicit CA succeeded, while the Requests bundle fallback and an invalid explicit path were rejected. The C++ client and operator-facing CLI plumbing remain scoped to the follow-up changes described above.