# HTTPS proxy endpoint

Every Litport HTTP proxy endpoint also accepts TLS on the same host and port. Change the scheme in
the proxy URL from `http://` to `https://` and the connection between your client and the hub is
encrypted. Nothing else changes: same credentials, same port, same targets, same error codes.

| Scheme | Client to hub | Example |
| --- | --- | --- |
| `http://` | Plaintext | `http://USER:PASSWORD@hub-eu-1.litport.net:PORT` |
| `https://` | TLS | `https://USER:PASSWORD@hub-eu-1.litport.net:PORT` |

`PORT` is the HTTP proxy port shown for your token in the dashboard (`1337` and `31337` are the
defaults; see [Proxy ingress](/docs/proxy-concepts#proxy-ingress)). SOCKS5 endpoints have no TLS
variant.

## Choose a security level

There are three ways to talk to the proxy. Pick the one that matches where your client runs; the
rest of this page shows how to set each one up.

| Level | Proxy URL and setup | Passive observer (Wi-Fi, ISP, shared network) | Active attacker (redirects your connection) | Client support |
| --- | --- | --- | --- | --- |
| **0. Plaintext** | `http://`, nothing else | Reads your credentials, every `CONNECT` hostname, and plain-HTTP bodies | Same | Every client |
| **1. Encrypted, unverified** | `https://` plus the client's "skip proxy verification" option | Sees nothing | Can impersonate the hub and capture your credentials | Most libraries; not browsers, not Requests without also unverifying the target |
| **2. Encrypted and verified (Recommended)** | `https://` plus the Litport root file | Sees nothing | Fails closed: the client refuses anything that is not a Litport hub | Every client that supports an `https://` proxy |

**Recommendation: use level 2.** It costs one public file and one option, and it is the only level
that protects a spendable credential against both kinds of attacker. Download the root once and keep
it next to your code:

~~~bash
curl --fail --remote-name https://litport.net/certificates/litport-cache-root.crt
~~~

Level 1 is a reasonable step up from plaintext for a quick manual test or a client you cannot give a
file to. Level 0 is the default only because it is what every client supports; move off it wherever
the network between you and the hub is not yours.

## Two different things called "HTTPS proxy"

- **Proxying an HTTPS target** is what an HTTP proxy has always done: your client sends `CONNECT
  example.com:443`, the hub opens a tunnel, and your TLS session runs end to end inside it. The
  `http://` proxy endpoint supports this; the tunnel contents are opaque to Litport unless you opt
  into [cache mode](/docs/proxy-cache).
- **An HTTPS proxy endpoint** encrypts the hop between your client and the hub itself. Everything
  you send to the proxy, including the `CONNECT` line and your credentials, travels inside that TLS
  session.

This page is about the second one. The two are independent: you can use either scheme for any target.

## What the plaintext endpoint exposes (level 0)

With `http://`, anything on the path between your client and the hub can read:

| Exposed on the wire | Consequence |
| --- | --- |
| `Proxy-Authorization` header | Your proxy username and password, on every request. A pay-per-GB credential is spendable by whoever captures it. |
| `CONNECT host:port` lines | Every hostname you connect to, even when the tunnel itself is encrypted. |
| Plain `http://` target requests | Full request and response bodies. |

"On the path" includes the local network you are on, your ISP, a shared cloud network, a corporate
egress proxy, and any hop that logs traffic. The target site sees the same thing in both cases: the
selected exit IP, never yours.

## When to use `https://`

Use the HTTPS endpoint when:

- **The client is on a network you do not control.** Laptops on public Wi-Fi, shared offices, hotel
  and airport networks, and any environment with a transparent inspection device.
- **The client runs on shared or third-party infrastructure.** CI runners, serverless functions,
  shared cloud VPCs, and containers on hosts you do not administer. Credential capture there is quiet
  and durable.
- **The destination list itself is sensitive.** `CONNECT` lines reveal which hosts you research even
  when the content is encrypted.
- **You send plain `http://` targets.** Those bodies are otherwise readable end to end on the first
  hop.
- **Plain proxy traffic is blocked or throttled.** Some networks flag `CONNECT` on non-standard ports;
  TLS to a hostname looks like ordinary HTTPS.

Stay on `http://` when:

- The client and hub already share a private, encrypted path, for example a VPN into a network you
  control, and you want the smallest possible per-connection overhead.
- The client library cannot negotiate TLS to a proxy at all (see the client notes below).
- You open very many short connections and the extra handshake per connection matters more than the
  exposure.

## What it costs

- **One extra TLS handshake per connection.** Keep-alive clients pay it once per connection, not per
  request.
- **TLS overhead counts as traffic.** Record framing and the handshake are bytes through the hub, so
  ordinary (non-cache) requests are metered slightly higher, typically a few percent on small requests
  and negligible on large transfers. Cache-mode accounting is unchanged.
- **Your client must trust the Litport root** for the proxy connection. That is the subject of the
  next section, and it is the reason `curl -k` does not help.

## The hub certificate and why `-k` does not work

Level 2 means verifying the hub's certificate. The hub presents a certificate for its own hostname (for example `hub-eu-1.litport.net`) issued by
the [Litport cache root](/docs/proxy-cache/certificate). That root is not in operating-system trust
stores, so a client that verifies the proxy certificate against its default bundle fails:

~~~text
curl: (60) SSL certificate problem: unable to get local issuer certificate
~~~

`curl -k` does **not** fix this. Clients keep two separate TLS legs with separate settings:

| TLS leg | What it protects | curl options |
| --- | --- | --- |
| Client to proxy | Your credentials and `CONNECT` lines | `--proxy-cacert`, `--proxy-insecure` |
| Client to target (inside the tunnel) | The site you are fetching | `--cacert`, `-k` / `--insecure` |

`-k` only relaxes the target leg. The proxy leg still verifies against the system bundle and aborts.
Point the proxy leg at the Litport root instead:

~~~bash
curl --fail --remote-name https://litport.net/certificates/litport-cache-root.crt
curl --proxy-cacert litport-cache-root.crt \
  -x 'https://USER:PASSWORD@hub-eu-1.litport.net:PORT' \
  'https://example.com/'
~~~

Two things to know before you install that root more broadly:

- Trusting the root for the proxy hop **does not enable cache mode**. Cache mode needs a `_cache-`
  password suffix on the credential; without it the tunnel stays opaque.
- The same root can also issue certificates for cache-mode interception. If you would rather not
  add it to a system-wide store, scope it to the process or container that uses the proxy, as every
  example below does.

Always use the hub **hostname**, never its IP address. The certificate is issued for the hostname.

## Level 2: verified client examples

Replace `USER`, `PASSWORD`, and `PORT`; percent-encode reserved characters in credentials. All
examples verify the proxy certificate against `litport-cache-root.crt` and keep normal public-CA
verification for the target. Each section ends with the level 1 switch that skips verification of
the proxy leg only; read [Level 1](#level-1-encrypted-without-verification) for what that costs
before using it.

### curl

Verified (recommended):

~~~bash
curl --proxy-cacert litport-cache-root.crt \
  -x 'https://hub-eu-1.litport.net:PORT' --proxy-user 'USER:PASSWORD' \
  'https://example.com/'
~~~

Add `--verbose` to see two handshakes: the first against the proxy (`hub-eu-1.litport.net`), then
`CONNECT` inside it, then the target's.

Unverified, for a throwaway test only (credentials are still encrypted, but the client cannot tell a
real hub from an impostor):

~~~bash
curl --proxy-insecure -x 'https://USER:PASSWORD@hub-eu-1.litport.net:PORT' 'https://example.com/'
~~~

`--proxy-cacert` needs curl 7.52 or later, which every current distribution ships.

### Python Requests

Requests uses one CA bundle for both legs, and `verify=` replaces its default bundle. Build a
combined bundle once so the target keeps public-CA verification:

~~~bash
cat "$(python -c 'import certifi; print(certifi.where())')" litport-cache-root.crt > litport-bundle.pem
~~~

~~~python
import requests

proxy = 'https://USER:PASSWORD@hub-eu-1.litport.net:PORT'
response = requests.get(
    'https://example.com/',
    proxies={'http': proxy, 'https': proxy},
    verify='litport-bundle.pem',
    timeout=30,
)
~~~

List the proxy under both `http` and `https` keys; Requests picks the entry by the target's scheme.
TLS-in-TLS through an `https://` proxy needs urllib3 1.26 or later, which Requests 2.26 and later
depend on.

Requests has no proxy-only switch: `verify=False` disables verification of **both** legs, the hub
and the target, and prints a warning on every request. Use it only for throwaway tests.

~~~python
response = requests.get('https://example.com/', proxies={'http': proxy, 'https': proxy}, verify=False, timeout=30)
~~~

### Python httpx

httpx separates the proxy's TLS settings from the target's:

~~~python
import ssl
import httpx

context = ssl.create_default_context()
context.load_verify_locations(cafile='litport-cache-root.crt')
proxy = httpx.Proxy('https://USER:PASSWORD@hub-eu-1.litport.net:PORT', ssl_context=context)
with httpx.Client(proxy=proxy, timeout=30) as client:
    response = client.get('https://example.com/')
~~~

The `ssl_context` on `Proxy` verifies only the hub; the client's default context still verifies
the target. To skip the proxy leg while keeping the target verified, give `Proxy` a context with
verification turned off:

~~~python
insecure = ssl.create_default_context()
insecure.check_hostname = False
insecure.verify_mode = ssl.CERT_NONE
proxy = httpx.Proxy('https://USER:PASSWORD@hub-eu-1.litport.net:PORT', ssl_context=insecure)
~~~

### Node

`https-proxy-agent` accepts an `https://` proxy URL and verifies the hub with Node's trust store, so
append the root through `NODE_EXTRA_CA_CERTS`. It does not disable verification anywhere.

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

~~~js
import https from 'node:https'
import { HttpsProxyAgent } from 'https-proxy-agent'

const agent = new HttpsProxyAgent('https://USER:PASSWORD@hub-eu-1.litport.net:PORT')
https.get('https://example.com/', { agent }, response => response.resume())
~~~

With `undici` (also the engine behind global `fetch` in Node 18 and later) the proxy leg has its
own TLS options, so no environment variable is needed:

~~~js
import { readFileSync } from 'node:fs'
import { ProxyAgent, fetch } from 'undici'

const dispatcher = new ProxyAgent({
  uri: 'https://hub-eu-1.litport.net:PORT',
  token: 'Basic ' + Buffer.from('USER:PASSWORD').toString('base64'),
  proxyTls: { ca: [readFileSync('litport-cache-root.crt')] },
})
const response = await fetch('https://example.com/', { dispatcher })
~~~

Both libraries can skip the proxy leg alone. For `https-proxy-agent`, the second argument is passed
to the TLS connection to the hub; for undici it is `proxyTls`. Target verification stays on. Do not
use `NODE_TLS_REJECT_UNAUTHORIZED=0`, which disables verification everywhere in the process.

~~~js
const agent = new HttpsProxyAgent('https://USER:PASSWORD@hub-eu-1.litport.net:PORT', { rejectUnauthorized: false })
const dispatcher = new ProxyAgent({ uri: 'https://hub-eu-1.litport.net:PORT', token, proxyTls: { rejectUnauthorized: false } })
~~~

### Go

Go's `http.Transport` dials an `https://` proxy with the same `TLSClientConfig` it uses for
targets, so append the root to the system pool rather than replacing it:

~~~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("https://USER:PASSWORD@hub-eu-1.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://example.com/")
~~~

Go applies `TLSClientConfig` to both legs, so `InsecureSkipVerify: true` would also stop verifying
the target. There is no proxy-only switch; keep the root in the pool instead.

### PHP

The curl extension exposes the same proxy-leg options as the command line:

~~~php
$ch = curl_init('https://example.com/');
curl_setopt_array($ch, [
    CURLOPT_PROXY => 'https://hub-eu-1.litport.net:PORT',
    CURLOPT_PROXYUSERPWD => 'USER:PASSWORD',
    CURLOPT_PROXY_CAINFO => __DIR__ . '/litport-cache-root.crt',
    CURLOPT_RETURNTRANSFER => true,
]);
$body = curl_exec($ch);
~~~

To skip the proxy leg only, replace `CURLOPT_PROXY_CAINFO` with
`CURLOPT_PROXY_SSL_VERIFYPEER => false` and `CURLOPT_PROXY_SSL_VERIFYHOST => 0`; the target's
`CURLOPT_SSL_VERIFYPEER` stays at its default.

### Java

The JDK's built-in `java.net.http.HttpClient` only supports plaintext proxies. Use Apache HttpClient
5, which accepts an `https` proxy host, and give the JVM a truststore that contains the Litport root
alongside the default roots:

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

~~~java
HttpHost proxy = new HttpHost("https", "hub-eu-1.litport.net", PORT);
BasicCredentialsProvider credentials = new BasicCredentialsProvider();
credentials.setCredentials(new AuthScope(proxy), new UsernamePasswordCredentials("USER", "PASSWORD".toCharArray()));
try (CloseableHttpClient client = HttpClients.custom()
        .setProxy(proxy)
        .setDefaultCredentialsProvider(credentials)
        .build()) {
    client.execute(new HttpGet("https://example.com/"), response -> EntityUtils.toString(response.getEntity()));
}
~~~

A dedicated truststore replaces the JVM default bundle, so import the public roots you need into it
as well, or import the Litport root into a copy of the JVM's `cacerts`. Java has no proxy-only
switch: a trust-all `TrustManager` would also stop verifying targets, so use the truststore.

### Browsers and browser automation

Browsers verify the proxy certificate with the operating-system store (Firefox with its own store).
Install the root there for the profile you use, then point the browser at the HTTPS proxy:

- **Chrome and Edge:** start with `--proxy-server="https://hub-eu-1.litport.net:PORT"`, or use a
  PAC file returning `HTTPS hub-eu-1.litport.net:PORT`. The browser prompts for the proxy
  credentials.
- **Firefox:** use a PAC file with the same `HTTPS host:port` directive, or set the proxy type to
  HTTPS in the connection settings.
- **Playwright and Puppeteer:** pass `server: 'https://hub-eu-1.litport.net:PORT'` with the
  username and password. The browser still needs the root in its store; `ignoreHTTPSErrors` and
  `--ignore-certificate-errors` affect only target certificates. Browsers never skip verification
  of a proxy certificate, so installing the root is the only option.

Instructions for each operating-system and Firefox store are on the
[certificate page](/docs/proxy-cache/certificate#install-and-remove-from-broad-trust-stores).

### Clients without TLS-to-proxy support

Scrapy's default downloader, the JDK `HttpClient`, and many older HTTP libraries can only talk to a
plaintext proxy. For those, either keep `http://` or run a small local forwarder on `127.0.0.1` that
accepts plaintext and connects to the hub over TLS; the sensitive hop then stays on your machine.

## Level 1: encrypted without verification

Every verified example above has an unverified variant. Skipping verification keeps the hop
encrypted, so a passive observer on the network still sees nothing. It removes the guarantee that
you are talking to a Litport hub: an active attacker who can redirect your connection could present
any certificate and capture your credentials. That is the same exposure level 0 has against an
active attacker, so treat level 1 as a step up from `http://`, not as equivalent to level 2.

| Client | Proxy leg only | Both legs |
| --- | --- | --- |
| curl | `--proxy-insecure` | `--proxy-insecure -k` |
| Python Requests | not available | `verify=False` |
| Python httpx | `Proxy(..., ssl_context=insecure)` | add `verify=False` on the client |
| Node `https-proxy-agent` | `{ rejectUnauthorized: false }` agent option | `NODE_TLS_REJECT_UNAUTHORIZED=0` (avoid) |
| Node undici | `proxyTls: { rejectUnauthorized: false }` | add `requestTls: { rejectUnauthorized: false }` |
| Go | not available | `InsecureSkipVerify: true` |
| PHP curl | `CURLOPT_PROXY_SSL_VERIFYPEER` / `CURLOPT_PROXY_SSL_VERIFYHOST` | add `CURLOPT_SSL_VERIFYPEER => false` |
| Java | not available | trust-all `TrustManager` (avoid) |
| Browsers | not available | not available; install the root |

Prefer level 2 for anything that runs unattended. Verification costs one file and one option; the
credential it protects is spendable.

## Checking that TLS is really in use

- `curl --verbose` prints the proxy handshake before the `CONNECT` line, including the certificate
  subject `CN=hub-eu-1.litport.net`.
- A wrong password over `https://` still returns `407` with `X-Proxy-Error-Code: 4`, exactly as over
  `http://`; the error headers travel inside TLS.
- If the connection closes immediately with no HTTP response, the hub could not complete the TLS
  handshake. Check that you used the hostname rather than an IP address, that the scheme is
  `https://` on an HTTP port and not a SOCKS5 port, and that your client trusts the Litport root for
  the proxy leg. See [Proxy error reference](/docs/proxy-errors#http-proxy-errors).

## Related pages

- [Tokens, proxy products, and hubs](/docs/proxy-concepts) for ports and credentials.
- [Cache certificate setup](/docs/proxy-cache/certificate) for the root's identity, fingerprints, and
  store-by-store installation.
- [Static response cache](/docs/proxy-cache) if you also want repeat static responses served free.
