Repository navigation
feat(ENG-15617): add an option to trust the operating system trust store - #603
Draft
cloudsmith-iduffy wants to merge 4 commits into
Draft
cloudsmith-iduffy wants to merge 4 commits into
cloudsmith-iduffy wants to merge 4 commits into
Conversation
Add --ssl-system-trust-store/--without-ssl-system-trust-store, the CLOUDSMITH_SSL_SYSTEM_TRUST_STORE environment variable and the ssl_system_trust_store config.ini key. The option is off by default. When the option is on, the requests sessions, the cloudsmith_api SDK and the MCP server verify TLS with a truststore context. That context trusts the operating system trust store in addition to the certifi bundle or REQUESTS_CA_BUNDLE. --without-ssl-verify-strict and --without-ssl-verify apply as before. The hint for a certificate from an untrusted CA now names the option. The frozen binary selftest checks that truststore is bundled. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
🟡 Changes recommended
Credential-helper custom-domain lookups reset the resolved TLS option to false during SDK initialization, causing discovery and runtime lookups to fail behind proxies whose CA is trusted only by the operating system.
1 open issue
What changed in this PR
Adds opt-in operating-system CA trust alongside the existing CA bundle, allowing the CLI to work behind TLS-intercepting proxies without changing default behavior.
Changes:
- Adds flag, environment-variable, and configuration controls for system trust.
- Integrates truststore contexts into requests and MCP clients and propagates the setting through shared connection setup.
- Updates certificate-error guidance, TLS tests, and frozen-binary selftests.
| File | Description |
|---|---|
uv.lock |
Records truststore as a direct dependency. |
pyproject.toml |
Declares the truststore dependency. |
packaging/pyinstaller/entry.py |
Adds a system-trust context selftest. |
cloudsmith_cli/data/config.ini |
Documents the system-trust setting. |
cloudsmith_cli/core/update_check.py |
Passes the setting to update-check sessions. |
cloudsmith_cli/core/tests/test_update_check.py |
Checks update-session propagation. |
cloudsmith_cli/core/tests/test_rest.py |
Tests context selection and local TLS handshakes. |
cloudsmith_cli/core/tests/test_mcp_http_client.py |
Tests MCP trust contexts and option combinations. |
cloudsmith_cli/core/tests/test_certificate_errors.py |
Checks the new troubleshooting hint. |
cloudsmith_cli/core/session.py |
Creates and selects truststore contexts. |
cloudsmith_cli/core/mcp/server.py |
Combines system trust with the resolved CA bundle. |
cloudsmith_cli/core/certificate_errors.py |
Suggests system trust for untrusted-CA failures. |
cloudsmith_cli/core/api/init.py |
Stores the setting in SDK configuration. |
cloudsmith_cli/cli/webserver.py |
Preserves the setting after SAML authentication. |
cloudsmith_cli/cli/tests/test_webserver.py |
Checks authentication-refresh propagation. |
cloudsmith_cli/cli/tests/test_decorators.py |
Tests option resolution and transport propagation. |
cloudsmith_cli/cli/tests/test_credential_helper_maven_tools.py |
Checks helper-download session propagation. |
cloudsmith_cli/cli/decorators.py |
Exposes CLI controls and forwards the setting. |
cloudsmith_cli/cli/config.py |
Adds the configuration schema and accessor. |
cloudsmith_cli/cli/commands/credential_helper/manage.py |
Passes the setting to helper-download sessions. |
Review effort: Balanced
Merged
6 of 10 tasks
…kups A credential helper initialises the SDK again for an uncached custom domain lookup. That reset --ssl-verify, --ssl-verify-strict and --ssl-system-trust-store to their defaults, so the lookup failed behind a TLS-intercepting proxy. initialise_session now sets the TLS settings as process defaults, as it does for the proxy, and initialise_api uses them when a caller leaves a setting out. Add the CHANGELOG entries for the option and the fix. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
REQUESTS_CA_BUNDLE replaces the included bundle, so the CHANGELOG must not say that the CLI trusts both. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
cloudsmith-iduffy
added a commit
to cloudsmith-labs/cloudsmith-maven-tools
that referenced
this pull request
Nov 6, 2026
# Description A customer used the Gradle plugin behind a TLS-intercepting proxy. `cloudsmith domains list` failed, the plugin added no credentials, and the build got 401 responses with no clue to the cause. The CLI printed the fix on stderr, but the plugin discarded stderr and logged the failure at debug level. This change: - Logs a warning when the CLI is missing, when `CLOUDSMITH_CLI_PATH` is not executable, and when a CLI command fails, times out, or prints output that is not JSON. The warning includes the stderr of the CLI, for example `Hint: ... set REQUESTS_CA_BUNDLE to a CA bundle that contains the proxy CA.` The CLI never prints the credential on stderr, and the warning never includes stdout. - Adds `Warnings`, because sbt does not show SLF4J output. Maven and Gradle log through SLF4J as before. The sbt plugin sends the warnings to its task logger. - Adds the TLS and proxy variables (`REQUESTS_CA_BUNDLE`, `SSL_CERT_FILE`, `HTTPS_PROXY`, `NO_PROXY` and others) to the key of the CLI cache in the Gradle daemon. Before, a fix to these variables applied only after the five-minute TTL. - Documents the init script in `~/.gradle/init.d`. Gradle resolves the `plugins` block of `settings.gradle.kts` before it applies the plugin, so the plugin cannot cover other settings plugins from a private repository. `cloudsmith credential-helper install gradle` writes the same init script. A failed answer is still cached for the TTL, so a second build in the same Gradle daemon or sbt session within five minutes does not repeat the warning. ## Type of change - [x] Bug fix - [x] New feature - [ ] Breaking change - [x] Documentation update - [ ] Refactoring - [ ] Other (describe below) ## Checklist - [x] The title is a [Conventional Commit](https://www.conventionalcommits.org/), for example `fix: ...` - [x] Tests cover the change in behavior - [ ] `mise run lint` and `mise run verify` pass - [x] No new runtime dependency (see CONTRIBUTING.md) ## Additional notes Tests were written first and seen failing: - Gradle integration tests: a failing CLI with a TLS hint, a missing `CLOUDSMITH_CLI_PATH`, no CLI on `PATH`, and a second build after `REQUESTS_CA_BUNDLE` is set. 11 of 11 pass. - Maven: a new integration test for the failing CLI. 4 of 4 integration tests and all unit tests pass. - sbt 1: a new integration test for the failing CLI. 6 of 6 pass. The sbt 2 tests could not run locally, because a mirror in the local `~/.m2/settings.xml` returns 401. CI runs them. The pre-commit hooks pass. `mise run verify` did not run as one command locally, for the same mirror reason. Related: cloudsmith-io/cloudsmith-docs#714 documents the settings plugin limit. cloudsmith-io/cloudsmith-cli#603 adds `CLOUDSMITH_SSL_SYSTEM_TRUST_STORE`, which is an alternative to `REQUESTS_CA_BUNDLE`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This branch has not been deployed
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.

Description
Resolves ENG-15617.
A customer ran the Cloudsmith Gradle plugin behind a TLS-intercepting proxy.
cloudsmith domains listfailed with a certificate error. The CLI verifies TLS with thecertifibundle, not with the trust store of the operating system. The proxy CA was in the operating system trust store, but the user had to setREQUESTS_CA_BUNDLEto make the CLI work.This PR adds an opt-in setting that also trusts the operating system trust store. It uses the
truststorepackage. The default behaviour does not change.--ssl-system-trust-store/--without-ssl-system-trust-storeCLOUDSMITH_SSL_SYSTEM_TRUST_STORE=1config.inikeyssl_system_trust_store = true(in[default]or a[profile:NAME]section)Precedence is the same as for
--ssl-verify-strict: the flag, then the environment variable, thenconfig.ini, then the default (off). Tools that run the CLI as a subprocess, such as the Maven tools, can set only the environment variable.Design decision: in addition to certifi, not instead of it
When the setting is on, the CLI trusts the operating system trust store in addition to the CA bundle that it uses today (
certifi, orREQUESTS_CA_BUNDLE/CURL_CA_BUNDLEwhen set; for the MCP server alsoSSL_CERT_FILE/SSL_CERT_DIR). A user who already setREQUESTS_CA_BUNDLEcan turn the setting on and nothing breaks.This works because
truststore0.10 adds the CA certificates that are loaded into its context to the trust anchors of the platform verifier:SecTrustSetAnchorCertificateswithSecTrustSetAnchorCertificatesOnly(false), so the Keychain stays trusted.SSL_CERT_FILE/SSL_CERT_DIR, the compiled-in path, or a list of well-known distribution paths) and the loaded bundle into one store.requestsand urllib3 already loadcertifiorREQUESTS_CA_BUNDLEinto the context, so the sessions get this behaviour with no extra code. The MCP server loads the bundle that_resolve_ca_locationgives into the context.What changes
core/session.py:create_system_trust_store_context()makes atruststore.SSLContextwith the urllib3 default verify flags.create_requests_session()takesssl_system_trust_storeand falls back to the SDK configuration, asssl_verify_strictdoes.SSLContextAdaptergets the factory, so the context is not used when--without-ssl-verifyis set.core/api/init.py:initialise_api()storesssl_system_trust_storeon thecloudsmith_apiconfiguration. The SDKRestClientusescreate_requests_session(), so the SDK calls get the setting.core/mcp/server.py:create_ssl_context()uses the truststore context when the setting is on, and loads the resolved CA bundle into it.cli/decorators.py,cli/config.py,data/config.ini: the flag, the environment variable and the config key. The setting goes to every place that getsssl_verify_stricttoday: the shared session, the SDK, the update check, the Maven tools downloads and the SAML refresh.core/certificate_errors.py: theE_TLS_UNTRUSTED_CAhint names the new setting beforeREQUESTS_CA_BUNDLE.E_TLS_STRICTdoes not change, because the strict checks are a different failure.pyproject.toml,uv.lock:truststore>=0.10.4is now a direct dependency. It was already in the lock throughhttpx2/httpcore2, so the locked version does not change.packaging/pyinstaller/entry.py: the selftest makes a truststore context. The CLI importstruststoreonly when the setting is on, so the import sweep does not load it.How it composes with the other TLS options
certifiorREQUESTS_CA_BUNDLE, with strict X.509 checks on Python 3.13+--ssl-system-trust-storecertifiorREQUESTS_CA_BUNDLE--ssl-system-trust-store --without-ssl-verify-strictVERIFY_X509_STRICT. This has an effect on Linux only, see the risks--ssl-system-trust-store --without-ssl-verifyCredential helper domain lookups
A credential helper initialises the SDK again for an uncached custom domain lookup (
credential_helpers/domains.py). That reset--ssl-verify,--ssl-verify-strictand--ssl-system-trust-storeto their defaults, so the lookup failed behind a TLS-intercepting proxy even with the setting on.--without-ssl-verify-stricthad the same problem before this PR.initialise_sessionnow sets the TLS settings as process defaults, as it does for the proxy, andinitialise_apiuses them when a caller leaves a setting out.Frozen binary
truststoreis pure Python. On macOS and Windows it calls the Security framework andcrypt32.dllthroughctypes, and these are part of the operating system. PyInstaller finds the function-levelimport truststoreincore/session.py, andhttpcore2imports it too. I built the onedir binary on macOS arm64 with thebinariesworkflow command.truststore,truststore._macos,truststore._windowsandtruststore._opensslare in the PYZ, andCLOUDSMITH_SELFTEST=1reportsSELFTEST: OK.Risks
serverAuthextended key usage, or with a validity longer than 825 days. A proxy certificate that fails these rules fails with the setting on, and--without-ssl-verify-strictdoes not change that. The user can turn the setting off and useREQUESTS_CA_BUNDLE.certifi, so the CLI works as it does today. I checked this in apython:3.13-slimcontainer with/etc/ssl/certsremoved./etc/ssl/cert.pem,/etc/pki/...,/etc/ssl/certs/ca-certificates.crt,/etc/ssl/ca-bundle.pem). A distribution with its bundle in another place needsSSL_CERT_FILE. I did not build a Linux or Windows binary for this PR. The binaries workflow selftest covers the import on all targets.Tests
TDD: the new tests failed before the implementation.
core/tests/test_rest.py: the session uses a truststore context, composes with--without-ssl-verify-strictand--without-ssl-verify, and falls back to the SDK configuration. Two tests do a real TLS handshake with a local server that has a certificate from a test CA. With the CA in the bundle, the request succeeds, which shows that the bundle is trusted in addition to the system store. With no bundle, the request fails.core/tests/test_mcp_http_client.py: the MCP context is a truststore context, loads the CA file or directory, and composes with the strict and verify settings.cli/tests/test_decorators.py: the flag, the environment variable andconfig.ini. Another test sets only the environment variable and checks the SDK configuration, the requests session and the MCP context.test_update_check.py,test_webserver.py,test_credential_helper_maven_tools.py,test_certificate_errors.py: the setting reaches each caller, and the hint names it.Results:
pytest -m "not integration": 2151 passed. Run with a cleanHOMEand noCLOUDSMITH_WORKSPACE. With my realHOME, five workspace tests fail because my~/.cloudsmith/config.inisets a workspace. This change does not touch them.python:3.13-slim.pre-commit run --files <changed files>: passed.REQUESTS_CA_BUNDLEset to a bundle that does not contain the CA ofapi.cloudsmith.io:cloudsmith check servicefails with the new hint by default. It succeeds withCLOUDSMITH_SSL_SYSTEM_TRUST_STORE=1, from source and from the frozen binary. The MCPhttpx2client gives the same results.Type of Change
Additional Notes
CHANGELOG.mdhas an entry under[Unreleased]for the option and one for the fix below. The option ships in v1.32.0 or later.🤖 Generated with Claude Code