Skip to content

fix: add Zscaler SSL certificate support for ChromaDB vector search#884

Closed
RClark4958 wants to merge 2 commits intothedotmack:mainfrom
RClark4958:fix/zscaler-ssl-certificates
Closed

fix: add Zscaler SSL certificate support for ChromaDB vector search#884
RClark4958 wants to merge 2 commits intothedotmack:mainfrom
RClark4958:fix/zscaler-ssl-certificates

Conversation

@RClark4958
Copy link
Contributor

Summary

  • Adds automatic detection and handling of Zscaler enterprise security certificates on macOS
  • Fixes SSL certificate verification failures when using ChromaDB in corporate environments with Zscaler

Problem

In enterprise environments with Zscaler, HTTPS traffic is intercepted and re-signed with Zscaler's certificate. Python's SSL verification fails because it doesn't trust the Zscaler certificate, causing all ChromaDB operations (add/query) to fail with SSL: CERTIFICATE_VERIFY_FAILED.

Solution

  • Added getCombinedCertPath() method that automatically detects Zscaler certificates in the macOS system keychain
  • Combines standard certifi CA certificates with Zscaler certificates into a single bundle
  • Passes SSL environment variables (SSL_CERT_FILE, REQUESTS_CA_BUNDLE, CURL_CA_BUNDLE) to the chroma-mcp process
  • Certificate bundle is cached for 24 hours

Test plan

  • Tested on macOS with Zscaler installed
  • Verified ChromaDB add and query operations work after fix
  • Confirmed no private/device-specific information is included

Detects and combines enterprise security certificates (Zscaler) with standard certifi certificates on macOS. Passes SSL environment variables to chroma-mcp process.
@greptile-apps
Copy link
Contributor

greptile-apps bot commented Feb 2, 2026

Greptile Overview

Greptile Summary

Added automatic Zscaler SSL certificate detection and configuration for ChromaDB vector search on macOS to fix SSL verification failures in corporate environments with SSL interception.

The implementation:

  • Added getCombinedCertPath() method that scans the uv cache for certifi certificates and extracts Zscaler certificates from the macOS system keychain
  • Combines both certificate sources into a single PEM bundle cached for 24 hours at ~/.claude-mem/combined_certs.pem
  • Passes SSL environment variables (SSL_CERT_FILE, REQUESTS_CA_BUNDLE, CURL_CA_BUNDLE) to the chroma-mcp Python subprocess
  • Only activates on macOS (Zscaler detection uses macOS security command)
  • Gracefully falls back to standard behavior if Zscaler certificates aren't found

Potential improvements:

  • The certifi path discovery is brittle - it scans the uv cache directory structure with nested loops and takes the first match. If uv changes its cache layout or multiple Python versions exist, this could fail or pick the wrong version
  • Certificate file writing lacks atomic write protection (write-then-rename pattern) which could leave a corrupted cert bundle if the process crashes mid-write
  • The execSync call to extract certificates has no timeout, which could hang if the keychain is locked
  • Certificate validation only checks for the string 'BEGIN CERTIFICATE' without validating proper PEM format

Confidence Score: 3/5

  • Safe to merge with minor improvements recommended - solves the stated problem but has fragile edge cases
  • The fix addresses the core problem (SSL verification with Zscaler) and includes appropriate error handling and caching. However, the certifi path discovery is brittle and could break with uv cache structure changes, the certificate file writing lacks atomicity protection, and the execSync has no timeout. These are quality-of-life improvements rather than blockers.
  • Pay close attention to src/services/sync/ChromaSync.ts - specifically the uv cache scanning logic (lines 138-154) and file writing (lines 177-178)

Important Files Changed

Filename Overview
src/services/sync/ChromaSync.ts Added Zscaler certificate detection and SSL environment variable configuration; has cache invalidation logic and error handling but certifi path discovery is brittle
plugin/scripts/worker-service.cjs Minified build output containing the ChromaSync changes - appears to be correctly compiled
plugin/scripts/mcp-server.cjs Minified build output - appears to be correctly compiled

Sequence Diagram

sequenceDiagram
    participant App as ChromaSync
    participant FS as File System
    participant KeyChain as macOS Keychain
    participant UV as UV Cache
    participant ChromaMCP as chroma-mcp (Python)
    
    Note over App: ensureConnection() called
    App->>App: getCombinedCertPath()
    
    alt Cached cert exists and < 24h old
        App->>FS: Check combined_certs.pem age
        FS-->>App: Use cached cert
    else Need to create cert bundle
        App->>App: Check if platform is darwin
        
        alt On macOS
            App->>UV: Scan ~/.cache/uv/archive-v0
            UV-->>App: Find certifi/cacert.pem path
            
            App->>KeyChain: execSync("security find-certificate -a -c Zscaler...")
            KeyChain-->>App: Return Zscaler certificate
            
            App->>FS: Read certifi cacert.pem
            FS-->>App: Standard CA certs content
            
            App->>FS: writeFileSync(combined_certs.pem, certifi + zscaler)
            FS-->>App: Combined bundle created
            
            App-->>App: Return combinedCertPath
        else Not macOS or Zscaler not found
            App-->>App: Return undefined
        end
    end
    
    App->>App: Configure transportOptions
    alt combinedCertPath exists
        App->>App: Set env.SSL_CERT_FILE = combinedCertPath
        App->>App: Set env.REQUESTS_CA_BUNDLE = combinedCertPath
        App->>App: Set env.CURL_CA_BUNDLE = combinedCertPath
    end
    
    App->>ChromaMCP: StdioClientTransport.spawn(uvx chroma-mcp) with env vars
    Note over ChromaMCP: Python process inherits<br/>SSL environment variables
    ChromaMCP-->>App: Connection established
    
    Note over ChromaMCP: HTTPS requests now trust<br/>both standard CAs and Zscaler cert
Loading

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3 files reviewed, 4 comments

Edit Code Review Agent Settings | Greptile

Comment on lines 138 to 154
const cacheEntries = fs.readdirSync(uvCacheDir);
for (const entry of cacheEntries) {
const candidatePath = path.join(uvCacheDir, entry, 'lib');
if (fs.existsSync(candidatePath)) {
const libEntries = fs.readdirSync(candidatePath);
for (const libEntry of libEntries) {
if (libEntry.startsWith('python')) {
const cacertPath = path.join(candidatePath, libEntry, 'site-packages', 'certifi', 'cacert.pem');
if (fs.existsSync(cacertPath)) {
certifiPath = cacertPath;
break;
}
}
}
}
if (certifiPath) break;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

brittle certifi path discovery relies on specific uv cache structure

The nested loops scanning ~/.cache/uv/archive-v0 assume a specific directory layout. If uv changes its cache structure, or if multiple Python versions exist, this could pick the wrong certifi or fail silently. The code breaks on the first match without verifying it's the correct version being used by uvx for this specific invocation.

Consider using uvx --with certifi python -c "import certifi; print(certifi.where())" to let uvx resolve the correct certifi path for the exact Python environment it will use.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/services/sync/ChromaSync.ts
Line: 138:154

Comment:
brittle certifi path discovery relies on specific uv cache structure

The nested loops scanning `~/.cache/uv/archive-v0` assume a specific directory layout. If uv changes its cache structure, or if multiple Python versions exist, this could pick the wrong certifi or fail silently. The code breaks on the first match without verifying it's the correct version being used by `uvx` for this specific invocation.

Consider using `uvx --with certifi python -c "import certifi; print(certifi.where())"` to let uvx resolve the correct certifi path for the exact Python environment it will use.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines 177 to 178
const certifiContent = fs.readFileSync(certifiPath, 'utf8');
fs.writeFileSync(combinedCertPath, certifiContent + '\n' + zscalerCert);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

combined certificate file lacks atomic write protection

Writing directly with writeFileSync means if the process crashes mid-write, the cert bundle could be corrupted. Since this file is cached for 24 hours (line 120), a corrupted file would cause SSL failures until the cache expires or is manually deleted.

Suggested change
const certifiContent = fs.readFileSync(certifiPath, 'utf8');
fs.writeFileSync(combinedCertPath, certifiContent + '\n' + zscalerCert);
const tempPath = combinedCertPath + '.tmp';
fs.writeFileSync(tempPath, certifiContent + '\n' + zscalerCert);
fs.renameSync(tempPath, combinedCertPath);
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/services/sync/ChromaSync.ts
Line: 177:178

Comment:
combined certificate file lacks atomic write protection

Writing directly with `writeFileSync` means if the process crashes mid-write, the cert bundle could be corrupted. Since this file is cached for 24 hours (line 120), a corrupted file would cause SSL failures until the cache expires or is manually deleted.

```suggestion
      const tempPath = combinedCertPath + '.tmp';
      fs.writeFileSync(tempPath, certifiContent + '\n' + zscalerCert);
      fs.renameSync(tempPath, combinedCertPath);
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines 163 to 166
zscalerCert = execSync(
'security find-certificate -a -c "Zscaler" -p /Library/Keychains/System.keychain',
{ encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

consider timeout for security command execution

execSync without a timeout could hang indefinitely if the macOS keychain is locked or unresponsive. This would block the entire ChromaSync initialization.

Suggested change
zscalerCert = execSync(
'security find-certificate -a -c "Zscaler" -p /Library/Keychains/System.keychain',
{ encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }
);
zscalerCert = execSync(
'security find-certificate -a -c "Zscaler" -p /Library/Keychains/System.keychain',
{ encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'], timeout: 5000 }
);
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/services/sync/ChromaSync.ts
Line: 163:166

Comment:
consider timeout for security command execution

`execSync` without a timeout could hang indefinitely if the macOS keychain is locked or unresponsive. This would block the entire ChromaSync initialization.

```suggestion
        zscalerCert = execSync(
          'security find-certificate -a -c "Zscaler" -p /Library/Keychains/System.keychain',
          { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'], timeout: 5000 }
        );
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines 172 to 174
if (!zscalerCert || !zscalerCert.includes('BEGIN CERTIFICATE')) {
return undefined;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

weak certificate validation

Checking only for 'BEGIN CERTIFICATE' substring doesn't validate certificate format or integrity. Malformed certificates would pass this check and potentially cause SSL errors later.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/services/sync/ChromaSync.ts
Line: 172:174

Comment:
weak certificate validation

Checking only for `'BEGIN CERTIFICATE'` substring doesn't validate certificate format or integrity. Malformed certificates would pass this check and potentially cause SSL errors later.

How can I resolve this? If you propose a fix, please make it concise.

- Use uvx to resolve certifi path instead of scanning cache
- Add timeouts to execSync calls (5s/10s)
- Validate full PEM format (BEGIN + END markers)
- Atomic file write (temp file + rename)
@thedotmack
Copy link
Owner

Manually merged into main with conflict resolution (compiled output conflicts from PR #769 merge). Source changes applied cleanly. All 39 Chroma tests pass. Build clean. Thank you @RClark4958 for the enterprise SSL support!

@thedotmack thedotmack closed this Feb 6, 2026
thedotmack added a commit that referenced this pull request Feb 6, 2026
…omaDB vector search

Adds automatic detection and handling of Zscaler enterprise security certificates
on macOS. Combines standard certifi CA certificates with Zscaler certificates into
a single bundle, passed via SSL_CERT_FILE/REQUESTS_CA_BUNDLE/CURL_CA_BUNDLE env vars
to the chroma-mcp subprocess. Certificate bundle is cached for 24 hours. Falls back
gracefully when Zscaler is not present, with no impact on non-Zscaler environments.

Co-Authored-By: RClark4958 <rickdclark48@gmail.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
thedotmack added a commit that referenced this pull request Feb 6, 2026
…ge notes

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
thedotmack added a commit that referenced this pull request Feb 6, 2026
…rge conflicts with #769/#884

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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.

2 participants