# Cache certificate setup

## Choose a cache-mode TLS option

> **No `_cache-` password suffix? Change nothing.** Ordinary proxy mode keeps end-to-end TLS and
> requires no certificate installation or client TLS changes for the plaintext `http://` proxy
> endpoint. The `https://` proxy endpoint is the one exception: its hub certificate chains to this
> same root, so trust it for the proxy connection as described in
> [HTTP vs HTTPS proxy endpoint](/docs/proxy-concepts). That trust alone never enables cache mode.

With cache mode active, choose one:

| Option | Certificate verification | Best for |
| --- | --- | --- |
| **Trust the Litport root (Recommended)** | On | Ongoing cache clients and dedicated automation profiles |
| **Skip certificate verification** | Off | Isolated public-data automation where you accept the security tradeoff |

Skipping verification means the client cannot authenticate target certificates. **Never use that
option for logins, payments, private APIs, cookies, or sensitive traffic.**

The proxy URL may begin with `http://` because it identifies the HTTP proxy endpoint. The target
still remains `https://example.com/...`; proxy endpoint scheme and target scheme are different.

## Skip verification for isolated public-data automation

Use this option only in a dedicated cache client that fetches public static data. It disables the
client's target-certificate authentication; it does not make private traffic safe to intercept.
Keep the proxy credentials in a secret store and remove the cache suffix when the isolated workload
ends.

### curl

~~~bash
curl --insecure --proxy 'http://hub-us-7.litport.net:PORT' \
  --proxy-user 'USER:PASSWORD_cache-scripts' \
  'https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js'
~~~

### Python Requests

~~~python
import requests

requests.get(
    'https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js',
    proxies={'https': 'http://USER:PASSWORD_cache-scripts@hub-us-7.litport.net:PORT'},
    verify=False,
    timeout=30,
)
~~~

### Node

~~~bash
NODE_TLS_REJECT_UNAUTHORIZED=0 node scraper.mjs
~~~

Configure the HTTP proxy in that isolated Node process. This environment setting disables certificate
verification for that process; do not set it for a general-purpose service.

### Browser automation

~~~js
import { chromium } from 'playwright'

const browser = await chromium.launch({ proxy: {
  server: 'http://hub-us-7.litport.net:PORT',
  username: 'USER',
  password: 'PASSWORD_cache-scripts',
}})
const context = await browser.newContext({ ignoreHTTPSErrors: true })
await (await context.newPage()).goto('https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js')
~~~

Use a dedicated browser profile for this workload. Do not browse signed-in or sensitive sites with it.

### Go

~~~go
proxyURL, err := url.Parse("http://USER:PASSWORD_cache-scripts@hub-us-7.litport.net:PORT")
if err != nil { log.Fatal(err) }
client := &http.Client{Transport: &http.Transport{
  Proxy: http.ProxyURL(proxyURL),
  TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, // public-data-only isolated client
}}
response, err := client.Get("https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js")
~~~

## Trust Litport root (Recommended)

### Two verified TLS legs

Cache mode terminates the client-facing TLS connection at the selected Litport hub so it can read an
eligible response and serve a later local copy. The client verifies the issued target certificate
against the Litport cache root. Separately, the hub verifies the real origin certificate and hostname
before reading the origin response. Keep verification enabled at both trust boundaries.

~~~docs-figure
cache-trust-boundary
The client verifies the Litport-issued target certificate on TLS leg 1. The hub separately verifies the public origin certificate and hostname on TLS leg 2.
~~~

An HTTP target has no authenticated origin identity and stays on the ordinary proxy path. Use an HTTPS
target for cache mode.

## Published root identity

Download: <https://litport.net/certificates/litport-cache-root.crt>

Machine-readable identity (optional): [litport-cache-root.json](/certificates/litport-cache-root.json)

| Field | Value |
| --- | --- |
| Subject | `O=Litport`, `CN=Litport cache root CA` |
| Issuer | `O=Litport`, `CN=Litport cache root CA` |
| PEM format | PEM-encoded X.509 certificate |
| Valid from (UTC) | `2026-09-08T18:54:30.000Z` |
| Valid to (UTC) | `2036-09-08T18:59:30.000Z` |
| X.509 SHA-256 fingerprint | `6E:0D:49:97:67:B0:D5:E3:41:E3:C7:B8:CC:65:85:C2:FD:04:EE:64:E7:64:E8:5A:79:18:27:F6:DD:1E:FD:38` |
| Downloaded PEM file SHA-256 | `b5a687754f15ab2fe7a00fc77738bc8f9e608e890159e4eccf5cbd48a46a9b7a` |

The fingerprint hashes the certificate's DER representation. The file checksum hashes the downloaded
PEM bytes, including textual encoding and line endings, so these SHA-256 values differ.

~~~bash
curl --fail --remote-name https://litport.net/certificates/litport-cache-root.crt
openssl x509 -in litport-cache-root.crt -noout -subject -issuer -dates -fingerprint -sha256
sha256sum litport-cache-root.crt
~~~

The download contains public certificate material, never a private key. One root works for every
selected hub. A hub switch can cause a cache miss because entries are hub-local; it does not require
a new root.

## What installing the root permits

The public root allows your client to validate target certificates issued by Litport's cache CA
hierarchy. The same root also validates the certificate a hub presents on its `https://` proxy
endpoint; see [HTTP vs HTTPS proxy endpoint](/docs/proxy-concepts). Litport keeps the root private key offline; it is not deployed to hubs. Each hub receives
its own intermediate certificate and private key, so the root key is not exposed if one hub is
compromised.

For a cache-enabled connection, the selected hub can read the HTTPS request and response. Installing
the root system-wide also makes every application using that system store accept certificates issued
by the Litport cache hierarchy. Scope trust to the cache client or its dedicated container, use cache
mode only for public static data, and remove the root when the workload no longer needs it. Without a
valid `_cache-` suffix, Litport keeps the HTTPS tunnel opaque even if the root remains installed.

## Client setup

Prefer per-process, per-profile, or dedicated-container trust. System trust is broad: all applications
using that store can trust certificates issued by this root. Replace `USER`, `PASSWORD`, and
`PORT`; percent-encode reserved URL characters and never commit proxy secrets.

~~~docs-tabs
cache-client-setup
Copyable verified cache-mode examples for curl, Python, Node, browser automation, Go, Java, Android, and iPhone or iPad.
~~~

### curl

`--cacert` replaces curl's default CA bundle for this request. Combine required roots in a dedicated
bundle if the target needs another private CA.

~~~bash
curl --proxy 'http://hub-us-7.litport.net:PORT' --proxy-user 'USER:PASSWORD_cache-scripts' \
  --cacert ./litport-cache-root.crt --verbose \
  'https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js'
~~~

### Python Requests, httpx, and aiohttp

Requests' `verify` file replaces its default bundle. For httpx and aiohttp, create the default
context first, then call `load_verify_locations()` to append this root to the system roots.

~~~python
import asyncio, ssl
import aiohttp, httpx, requests
proxy = 'http://USER:PASSWORD_cache-scripts@hub-us-7.litport.net:PORT'
url = 'https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js'
requests.get(url, proxies={'https': proxy}, verify='litport-cache-root.crt', timeout=30)
context = ssl.create_default_context()
context.load_verify_locations(cafile='litport-cache-root.crt')
httpx.get(url, proxy=proxy, verify=context, timeout=30)
async def fetch():
    async with aiohttp.ClientSession(connector=aiohttp.TCPConnector(ssl=context)) as session:
        async with session.get(url, proxy=proxy) as response: return await response.read()
asyncio.run(fetch())
~~~

### Node HTTPS clients

`NODE_EXTRA_CA_CERTS` appends roots to Node. A real HTTPS proxy agent routes traffic; keep
`rejectUnauthorized` at its default `true`.

~~~bash
npm install https-proxy-agent
NODE_EXTRA_CA_CERTS=./litport-cache-root.crt node scraper.mjs
~~~

~~~js
import https from 'node:https'
import { HttpsProxyAgent } from 'https-proxy-agent'
const agent = new HttpsProxyAgent('http://USER:PASSWORD_cache-scripts@hub-us-7.litport.net:PORT')
https.get('https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js', { agent }, response => response.resume())
~~~

This setting does not configure Chromium, Playwright, or Puppeteer traffic.

### Browser automation

Install the root in the isolated browser profile's OS or NSS store. Keep
`ignoreHTTPSErrors: false` and do not use `--ignore-certificate-errors`.

~~~js
import { chromium } from 'playwright'
const browser = await chromium.launch({ proxy: {
  server: 'http://hub-us-7.litport.net:PORT', username: 'USER', password: 'PASSWORD_cache-scripts',
}})
const context = await browser.newContext({ ignoreHTTPSErrors: false })
await (await context.newPage()).goto('https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js')
~~~

### Go

~~~go
pool, err := x509.SystemCertPool()
if err != nil || pool == nil { pool = x509.NewCertPool() }
pem, err := os.ReadFile("litport-cache-root.crt")
if err != nil || !pool.AppendCertsFromPEM(pem) { log.Fatal("could not add Litport root") }
proxyURL, err := url.Parse("http://USER:PASSWORD_cache-scripts@hub-us-7.litport.net:PORT")
if err != nil { log.Fatal(err) }
client := &http.Client{Transport: &http.Transport{
  Proxy: http.ProxyURL(proxyURL),
  TLSClientConfig: &tls.Config{RootCAs: pool, MinVersion: tls.VersionTLS12},
}}
response, err := client.Get("https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js")
~~~

`SystemCertPool` plus `AppendCertsFromPEM` preserves system roots and appends this root.

### Java

A dedicated truststore is process-scoped, but replaces Java's default bundle. Import other required
roots into that dedicated store.

~~~bash
keytool -importcert -noprompt -alias litport-cache -file litport-cache-root.crt \
  -keystore ./litport-cache-truststore.p12 -storetype PKCS12 -storepass changeit
java -Djavax.net.ssl.trustStore=./litport-cache-truststore.p12 \
  -Djavax.net.ssl.trustStoreType=PKCS12 -Djavax.net.ssl.trustStorePassword=changeit App
~~~

## Install and remove from broad trust stores

Use these only when process/profile trust is unavailable; they affect other applications too.

### Debian and Ubuntu

~~~bash
sudo install -m 0644 litport-cache-root.crt /usr/local/share/ca-certificates/litport-cache-root.crt
sudo update-ca-certificates
# Remove: sudo rm /usr/local/share/ca-certificates/litport-cache-root.crt && sudo update-ca-certificates
~~~

### RHEL and Fedora

~~~bash
sudo install -m 0644 litport-cache-root.crt /etc/pki/ca-trust/source/anchors/litport-cache-root.crt
sudo update-ca-trust extract
# Remove: sudo rm /etc/pki/ca-trust/source/anchors/litport-cache-root.crt && sudo update-ca-trust extract
~~~

### macOS

~~~bash
sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain litport-cache-root.crt
# Remove: sudo security delete-certificate -c 'Litport cache root CA' /Library/Keychains/System.keychain
~~~

### Windows

Run in elevated PowerShell:

~~~powershell
certutil -addstore -f Root .\litport-cache-root.crt
# Remove: certutil -delstore Root "Litport cache root CA"
~~~

### Firefox and NSS

Set `FIREFOX_PROFILE` to the profile directory with `cert9.db`; on Debian/Ubuntu install
`libnss3-tools` first.

~~~bash
certutil -A -n 'Litport cache root' -t 'C,,' -i litport-cache-root.crt -d "sql:$FIREFOX_PROFILE"
# Remove: certutil -D -n 'Litport cache root' -d "sql:$FIREFOX_PROFILE"
~~~

## Containers and Kubernetes

Use a dedicated workload image or read-only runtime mount instead of host trust. For Node:

~~~dockerfile
COPY litport-cache-root.crt /opt/litport/litport-cache-root.crt
ENV NODE_EXTRA_CA_CERTS=/opt/litport/litport-cache-root.crt
~~~

A distro system-store update trusts every process in the container. For Kubernetes, create a
ConfigMap from the downloaded public PEM, then mount it only into the workload requiring cache mode:

~~~bash
kubectl create configmap litport-cache-root --from-file=litport-cache-root.crt
~~~

~~~yaml
# Pod template fragment
spec:
  containers:
    - name: client
      env:
        - name: NODE_EXTRA_CA_CERTS
          value: /var/run/litport-ca/litport-cache-root.crt
      volumeMounts:
        - { name: litport-cache-root, mountPath: /var/run/litport-ca, readOnly: true }
  volumes:
    - name: litport-cache-root
      configMap: { name: litport-cache-root }
~~~

## Android

Android 7.0+ apps normally do not trust user-added CAs. A bundled CA is narrow: include this PEM in
the debug build and trust only it. `src="user"` trusts every user-installed CA and is broader than
most production apps should accept. Android documents these trust sources and debug overrides in
[Network Security Configuration](https://developer.android.com/privacy-and-security/security-config).

~~~xml
<!-- AndroidManifest.xml -->
<application android:networkSecurityConfig="@xml/network_security_config" />
~~~

The release build keeps its normal system roots:

~~~xml
<!-- src/main/res/xml/network_security_config.xml -->
<network-security-config><base-config><trust-anchors><certificates src="system" /></trust-anchors></base-config></network-security-config>
~~~

The debug build appends the bundled Litport root:

~~~xml
<!-- src/debug/res/xml/network_security_config.xml -->
<network-security-config>
  <base-config><trust-anchors><certificates src="system" /></trust-anchors></base-config>
  <debug-overrides><trust-anchors><certificates src="@raw/litport_cache_root" /></trust-anchors></debug-overrides>
</network-security-config>
~~~

Place the PEM at `src/debug/res/raw/litport_cache_root.pem`.
A third-party app whose trust policy cannot change is incompatible with verified cache mode; remove
the `_cache-` suffix.

## iPhone and iPad

Install the downloaded root profile, then complete installation in **Settings → General → VPN & Device
Management**. In **Settings → General → About → Certificate Trust Settings**, enable full trust for
the Litport root. Remove it through **VPN & Device Management → profile → Remove Profile**. Managed
devices should install and remove the same payload through MDM. An app with its own policy or
certificate pinning can still reject the connection.
Apple documents the separate full-trust step in [Trust manually installed certificate profiles](https://support.apple.com/102390).

## Verify the configuration

1. Make a no-suffix control request: it needs no root and uses ordinary proxy behavior.
2. Make the same HTTPS request with a valid cache-enabled password and verification enabled.
3. Repeat that identical request to the same hub. Inspect cache statistics for a miss then a hit;
   do not assume proprietary cache headers.
4. Add `--verbose` to the verified curl request and confirm that certificate verification succeeds
   and the displayed issuer belongs to the Litport cache hierarchy.
5. A hub switch can miss because entries are hub-local; it does not require a new root.

~~~bash
curl --proxy 'http://hub-us-7.litport.net:PORT' --proxy-user 'USER:PASSWORD_cache-scripts' \
  --cacert ./litport-cache-root.crt --verbose --output /dev/null \
  'https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js'
~~~

## Focused certificate symptoms

| Symptom | Cause | Fix |
| --- | --- | --- |
| `unknown authority`, `unable to get local issuer`, or `CERTIFICATE_VERIFY_FAILED` | Caller does not trust the root. | Add it to that process, profile, container, or store; keep verification enabled. |
| curl works, application fails | curl's CA setting is scoped to curl. | Configure the application's own trust bundle, preserving needed roots. |
| Browser error | Browser/profile uses another store. | Install in its actual OS/NSS profile store; retain browser verification. |
| Pinned app fails | Pinning rejects the issued target certificate. | Only its owner can alter pins; use ordinary mode. |
| HTTP target | No authenticated origin TLS leg. | Use an HTTPS target, not merely an HTTP proxy endpoint. |

## Rotation and credential safety

During rotation, verify the new PEM's published fingerprint and file checksum, trust both roots for
the stated overlap, test cache mode, then remove the old root from every dedicated bundle, browser
profile, image, and managed-device payload after overlap. Use quoted proxy URLs and `--proxy-user`
where available; use a secret store or injected environment variable, percent-encode reserved URL
characters, and never commit proxy secrets or password-bearing command history.

Return to the [cache quickstart](/docs/proxy-cache) or [options and troubleshooting](/docs/proxy-cache/reference).
