Skip to content

feat(tls): support custom CA via OS trust store + SSL_CERT_FILE#56

Merged
voska merged 1 commit into
masterfrom
feat/custom-ca-trust
May 17, 2026
Merged

feat(tls): support custom CA via OS trust store + SSL_CERT_FILE#56
voska merged 1 commit into
masterfrom
feat/custom-ca-trust

Conversation

@voska

@voska voska commented May 17, 2026

Copy link
Copy Markdown
Owner

Summary

Users running their own PKI (step-ca, smallstep, homelab OpenSSL) need hass-mcp to verify their HA's certificate without disabling TLS. Supersedes #15 with a portable, defaults-safe mechanism that works across local installs, Docker containers, and standalone deployments.

Two layered mechanisms

  1. OS native trust store via truststore — the same library pip has used by default since 24.2 (mid-2024). Bridges to macOS Keychain, Windows Cert Store, and Linux ca-certificates. Users who installed their CA at the OS level get it for free with no config.

  2. SSL_CERT_FILE explicit override — the canonical OpenSSL env var, honored by every TLS-aware tool. Primary mechanism for Docker (bind-mount + env var). Also the override path on any platform.

# app/hass.py
def _build_ssl_context() -> ssl.SSLContext:
    cert_file = os.environ.get("SSL_CERT_FILE")
    if cert_file:
        return ssl.create_default_context(cafile=cert_file)
    return truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT)

_client = httpx.AsyncClient(timeout=10.0, verify=_build_ssl_context())

Deployment coverage

Mode How it works
Laptop (any OS) with CA installed at OS level truststore picks it up automatically
Docker bind-mount the CA, set SSL_CERT_FILE — explicit override path
Standalone server / Smithery same as Docker: env var + mounted file
Anyone with SSL_CERT_FILE already set honored on every platform

Documented in README with the canonical Docker example.

Explicitly declined

  • REQUESTS_CA_BUNDLErequests-specific alias, not a standard. We're not a requests codebase; don't propagate it.
  • verify=False — silent downgrade is worse than a hard failure. Users who genuinely want unencrypted local LAN can use HA_URL=http://... which is already documented.

Why this is better than #15

#15 reads REQUESTS_CA_BUNDLE/SSL_CERT_FILE and passes to verify=. Two problems:

  1. SSL_CERT_FILE is already honored by httpx natively when trust_env=True (default). The env-var read is partially redundant.
  2. Doesn't help macOS users with CAs in Keychain — OpenSSL doesn't read the Keychain. And doesn't help Linux users who installed via update-ca-certificates if hass-mcp runs in a Docker container without that file mounted in.

This PR fixes both: truststore handles every OS-native store, SSL_CERT_FILE handles every explicit-path scenario.

Dependency

Adds truststore>=0.10. ~30 KB, no transitive runtime deps. Used by pip, uv, and recommended in the httpx SSL docs for OS-store integration. Python 3.13 (our minimum) satisfies its 3.10+ floor.

Tests

$ uv run pytest tests/
= 46 passed in 9.11s =

Two new tests in tests/test_hass.py::TestSSLContext:

  • test_default_uses_truststore_os_native_store — confirms truststore is selected when no env var
  • test_ssl_cert_file_takes_precedence — generates a self-signed PEM, points SSL_CERT_FILE at it, confirms a stdlib ssl.SSLContext is built from the file (not truststore)

Credit

@rmoriz flagged the right problem in #15. The cleaner mechanism preserves the spirit (don't weaken TLS) with a more portable implementation. Their PR can be closed as superseded; Co-Authored-By on the merge commit.

Users running their own CA (step-ca, smallstep, homelab OpenSSL) need
hass-mcp to trust the certificate without disabling verification.
Existing approaches in the ecosystem either silently downgrade to
verify=False or duplicate work httpx already does. This PR layers two
mechanisms so the right thing happens regardless of how the user
deploys hass-mcp:

1. OS native trust store via truststore (the same library pip uses by
   default since 24.2). Bridges to macOS Keychain, Windows Cert Store,
   and Linux ca-certificates. Users who installed their CA at the OS
   level get it for free with zero config.

2. Explicit SSL_CERT_FILE override. The canonical OpenSSL env var,
   honored by every TLS-aware tool. This is the primary Docker
   mechanism — bind-mount the CA file, set the env var, done. Also
   serves as the override path on any platform.

Explicitly declined:
- REQUESTS_CA_BUNDLE — that's a requests-specific alias, not a standard
- verify=False — silent downgrade is worse than a hard failure;
  HA_URL=http://... is the documented path for unencrypted LAN

Adds `truststore>=0.10` (~30 KB, no runtime deps; Python 3.13 satisfies
the 3.10+ floor).

Tests cover both code paths:
- Default returns truststore.SSLContext
- SSL_CERT_FILE takes precedence and yields a stdlib ssl.SSLContext

README documents the canonical Docker pattern (bind-mount + env var).

Supersedes #15.

Co-Authored-By: rmoriz <rmoriz@users.noreply.github.com>
@voska voska merged commit bdf2039 into master May 17, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant