Skip to content

Commit 39653d3

Browse files
committed
docs(tcp): add a forward-proxy guide for pomerium-cli tcp
Document the --forward-proxy flag and proxy env-var support for the TCP tunnel, with a runnable docker-compose example (Pomerium Core + Postgres + Squid + a SOCKS5 proxy + a pomerium-cli client) that tunnels to Postgres through the proxy. Includes a policy-gated route that proves Pomerium denies an unauthenticated tunnel. ENG-4082
1 parent b49b70b commit 39653d3

10 files changed

Lines changed: 587 additions & 0 deletions

File tree

content/docs/capabilities/non-http/tcp.mdx

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,55 @@ routes:
150150
to: http://second-proxy.example.corp.com:10003
151151
```
152152

153+
### Connect through a client-side forward proxy {#forward-proxy}
154+
155+
When the machine running `pomerium-cli` can only reach the internet through an HTTP or SOCKS5 forward proxy, route the TCP tunnel through it with `--forward-proxy`:
156+
157+
```bash
158+
pomerium-cli tcp db.internal:5432 \
159+
--pomerium-url https://pomerium.example.com \
160+
--forward-proxy http://proxy.internal:3128
161+
```
162+
163+
The proxy carries the connection from `pomerium-cli` to the **Pomerium edge**, not to the upstream TCP service. Write proxy allowlists and `NO_PROXY` entries for the Pomerium edge hostname, not for the database, SSH server, or other upstream. This is different from [Proxy chaining support](#proxy-chaining-support), which places a second proxy behind Pomerium.
164+
165+
:::info Requires pomerium-cli vX.Y or later
166+
167+
Proxy-aware tunneling was added to `pomerium-cli` in vX.Y. Check your client with `pomerium-cli tcp --help | grep forward-proxy` and upgrade if `--forward-proxy` is not listed.
168+
169+
:::
170+
171+
`pomerium-cli` selects a proxy in this order:
172+
173+
| Order | Source | `NO_PROXY` | Notes |
174+
| --- | --- | --- | --- |
175+
| 1 | `--forward-proxy` | Ignored | Authoritative; used as-is when set. |
176+
| 2 | `HTTPS_PROXY` / `HTTP_PROXY` | Honored | Scheme-aware, based on the Pomerium edge URL. Lowercase variants also work. |
177+
| 3 | `ALL_PROXY` | Honored | Fallback when no scheme-specific proxy applies. |
178+
| 4 | none | n/a | Direct connection; unchanged from earlier behavior. |
179+
180+
Both the tunnel and `pomerium-cli`'s own authentication and token requests use the selected proxy. If the command opens a system browser to log in, the browser uses its own network and proxy settings, not `--forward-proxy`.
181+
182+
The proxy URL takes any of these forms (and must not include a path, query, or fragment):
183+
184+
| Form | Meaning |
185+
| --- | --- |
186+
| `proxy.internal:3128` | Same as `http://proxy.internal:3128`. |
187+
| `http://proxy.internal:3128` | HTTP CONNECT proxy. |
188+
| `https://proxy.internal:8443` | TLS to the proxy, then HTTP CONNECT. |
189+
| `socks5://proxy.internal:1080` | SOCKS5 proxy. |
190+
| `socks5h://proxy.internal:1080` | SOCKS5 proxy (`socks5h` scheme). |
191+
| `http://user:pass@proxy.internal:3128` | HTTP proxy with Basic authentication; URL-encode special characters. |
192+
193+
A few more things to know:
194+
195+
- TCP tunnels use the proxy; `pomerium-cli udp` (MASQUE) always dials directly.
196+
- When a proxy is selected, `pomerium-cli tcp` skips HTTP/3 and uses the HTTP/1 tunnel, since QUIC can't traverse a CONNECT or SOCKS proxy.
197+
- `--alternate-ca-path` configures trust for the Pomerium edge certificate. It does not configure trust for an `https://` forward proxy; a proxy that terminates TLS with a private CA must be trusted by the operating system running `pomerium-cli`.
198+
- Credentials in an `http://user:pass@proxy` URL are sent to the proxy in cleartext.
199+
200+
For a runnable walkthrough with Pomerium, an upstream service, and a Squid proxy, see [Connect through a client-side forward proxy](/docs/guides/tcp-forward-proxy).
201+
153202
:::info TCP examples
154203

155204
The guides below demonstrate how to proxy TCP tunnels with Pomerium to well-known services:
@@ -161,5 +210,6 @@ The guides below demonstrate how to proxy TCP tunnels with Pomerium to well-know
161210
- [**Redis**](./examples/redis)
162211
- [**SSH**](./examples/ssh)
163212
- [**SSH over port 443 through an L4 edge**](/docs/guides/ssh-tcp-l4-passthrough) - for environments restricted to outbound port 443
213+
- [**Connect through a client-side forward proxy**](/docs/guides/tcp-forward-proxy) - for clients whose only egress is an HTTP or SOCKS5 proxy
164214

165215
:::
Lines changed: 282 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,282 @@
1+
---
2+
title: 'Connect through a client-side forward proxy'
3+
sidebar_label: 'TCP through a forward proxy'
4+
lang: en-US
5+
description: 'Tunnel a TCP route with pomerium-cli when the client can only reach the internet through an HTTP or SOCKS5 forward proxy.'
6+
keywords:
7+
[
8+
pomerium,
9+
pomerium-cli,
10+
tcp,
11+
tcp routes,
12+
forward proxy,
13+
http connect,
14+
socks5,
15+
squid,
16+
postgres,
17+
egress proxy,
18+
]
19+
---
20+
21+
import ComposeFile from '!!raw-loader!@site/content/examples/tcp-forward-proxy/docker-compose.yml';
22+
import SquidConf from '!!raw-loader!@site/content/examples/tcp-forward-proxy/squid/squid.conf';
23+
24+
import CodeBlock from '@theme/CodeBlock';
25+
26+
# Connect through a client-side forward proxy
27+
28+
This guide shows how to use `pomerium-cli tcp` from a network whose only outbound path is an HTTP or SOCKS5 forward proxy, such as a corporate egress proxy. The `--forward-proxy` flag routes the tunnel through that proxy to reach Pomerium. The local example tunnels to a Postgres database and runs `psql` over the tunnel.
29+
30+
This is about the client's egress path. It is different from [Proxy chaining support](/docs/capabilities/non-http/tcp#proxy-chaining-support), which puts a second proxy behind Pomerium.
31+
32+
## How it works
33+
34+
```mermaid
35+
flowchart LR
36+
A[psql] --> B[pomerium-cli tcp<br/>local listener]
37+
B --> C[Client-side forward proxy<br/>Squid or SOCKS5]
38+
C --> D[Pomerium edge<br/>terminates TLS]
39+
D --> E[Postgres]
40+
```
41+
42+
The forward proxy connects `pomerium-cli` to the Pomerium edge, not to the upstream service. Write proxy allowlists and `NO_PROXY` entries for the Pomerium edge hostname, not for the database. For flag and environment-variable precedence and the supported proxy URL forms, see [Connect through a client-side forward proxy](/docs/capabilities/non-http/tcp#forward-proxy) in the TCP reference.
43+
44+
:::info Requires pomerium-cli vX.Y or later
45+
46+
Proxy-aware tunneling was added to `pomerium-cli` in vX.Y. Check your client with `pomerium-cli tcp --help | grep forward-proxy` and upgrade if `--forward-proxy` is not listed.
47+
48+
:::
49+
50+
## Prerequisites
51+
52+
- Docker Compose v2
53+
- OpenSSL
54+
- Git
55+
56+
## Run the local example
57+
58+
The runnable example lives in [`content/examples/tcp-forward-proxy`](https://github.com/pomerium/documentation/tree/main/content/examples/tcp-forward-proxy). It runs Postgres, Pomerium, a [Squid](http://www.squid-cache.org/) HTTP proxy, a SOCKS5 proxy, and a client container with `pomerium-cli` and `psql`. The client reaches a proxy by name, and the proxy reaches the Pomerium edge by its Docker network alias.
59+
60+
1. Clone the docs repository and enter the example directory.
61+
62+
```bash
63+
git clone https://github.com/pomerium/documentation
64+
cd documentation/content/examples/tcp-forward-proxy
65+
```
66+
67+
1. Generate a demo CA and server certificate.
68+
69+
```bash
70+
./gen-certs.sh
71+
```
72+
73+
The script writes a local demo CA and a server certificate for the `*.localhost.pomerium.io` hostnames, then deletes the CA private key. In production, use a publicly trusted certificate or your organization's managed trust chain.
74+
75+
1. Review the routes in `config/pomerium.yaml`. One public route demonstrates the tunnel; one policy-gated route demonstrates enforcement.
76+
77+
```yaml
78+
routes:
79+
- from: tcp+https://pgsql.localhost.pomerium.io:5432
80+
to: tcp://postgres:5432
81+
allow_public_unauthenticated_access: true
82+
- from: tcp+https://secure.localhost.pomerium.io:5432
83+
to: tcp://postgres:5432
84+
policy:
85+
- allow:
86+
and:
87+
- authenticated_user: true
88+
```
89+
90+
:::caution Lab-only public route
91+
92+
The first route is public so the lab needs no identity provider. Do not use `allow_public_unauthenticated_access` for production TCP services. See [Require authentication in production](#require-authentication-in-production).
93+
94+
:::
95+
96+
1. Start the stack.
97+
98+
```bash
99+
docker compose up -d --build
100+
docker compose ps
101+
```
102+
103+
1. Open the tunnel through the HTTP proxy.
104+
105+
```bash
106+
docker compose exec -d client sh -lc \
107+
'pomerium-cli tcp pgsql.localhost.pomerium.io:5432 \
108+
--pomerium-url https://proxy.localhost.pomerium.io \
109+
--alternate-ca-path /certs/ca.crt \
110+
--forward-proxy http://squid:3128 \
111+
--browser-cmd /bin/true \
112+
--listen 127.0.0.1:5432 \
113+
>/tmp/pomerium-cli.log 2>&1'
114+
```
115+
116+
`--browser-cmd /bin/true` suppresses the browser launch in the headless client container; drop it for interactive use. The public lab route never triggers a login.
117+
118+
1. Run a query through the tunnel. `PGPASSWORD` is fine for this throwaway lab; prefer `~/.pgpass` or a prompt elsewhere.
119+
120+
```bash
121+
docker compose exec client sh -lc \
122+
'PGPASSWORD=postgres psql -h 127.0.0.1 -p 5432 -U postgres <<SQL
123+
create table demo (id int primary key, note text);
124+
insert into demo values (1, '\''through pomerium-cli and a forward proxy'\'');
125+
select * from demo;
126+
drop table demo;
127+
SQL'
128+
```
129+
130+
```text
131+
CREATE TABLE
132+
INSERT 0 1
133+
id | note
134+
----+------------------------------------------
135+
1 | through pomerium-cli and a forward proxy
136+
(1 row)
137+
138+
DROP TABLE
139+
```
140+
141+
The round-trip confirms the tunnel reached Postgres through the proxy.
142+
143+
### Use a SOCKS5 proxy instead
144+
145+
The same tunnel works through a SOCKS5 proxy. Point `--forward-proxy` at the `socks5://` URL (or set `ALL_PROXY`):
146+
147+
```bash
148+
docker compose exec -d client sh -lc \
149+
'pomerium-cli tcp pgsql.localhost.pomerium.io:5432 \
150+
--pomerium-url https://proxy.localhost.pomerium.io \
151+
--alternate-ca-path /certs/ca.crt \
152+
--forward-proxy socks5://socks5:1080 \
153+
--browser-cmd /bin/true \
154+
--listen 127.0.0.1:5433 \
155+
>/tmp/pomerium-cli-socks5.log 2>&1'
156+
157+
docker compose exec client sh -lc \
158+
'PGPASSWORD=postgres psql -h 127.0.0.1 -p 5433 -U postgres -c "select 1;"'
159+
```
160+
161+
## Verify the path
162+
163+
Confirm the tunnel traversed Squid. The CONNECT target is the Pomerium edge, not the upstream:
164+
165+
```bash
166+
docker compose exec squid grep CONNECT /var/log/squid/access.log
167+
```
168+
169+
```text
170+
... CONNECT proxy.localhost.pomerium.io:443 ...
171+
```
172+
173+
Check Pomerium's access log for the tunnel:
174+
175+
```bash
176+
docker compose logs pomerium | grep '"method":"CONNECT"'
177+
```
178+
179+
Look for the route-match host and a successful response:
180+
181+
```json
182+
"host":"pgsql.localhost.pomerium.io:5432"
183+
"response-code":200
184+
```
185+
186+
For log field definitions, see [Access Log Fields](/docs/reference/access-log-fields).
187+
188+
## Confirm Pomerium enforces access
189+
190+
The proxy gets you to Pomerium, but Pomerium still decides who reaches the upstream. Open a tunnel to the policy-gated route with no credentials:
191+
192+
```bash
193+
docker compose exec -d client sh -lc \
194+
'pomerium-cli tcp secure.localhost.pomerium.io:5432 \
195+
--pomerium-url https://proxy.localhost.pomerium.io \
196+
--alternate-ca-path /certs/ca.crt \
197+
--forward-proxy http://squid:3128 \
198+
--browser-cmd /bin/true \
199+
--listen 127.0.0.1:5499 \
200+
>/tmp/pomerium-cli-secure.log 2>&1'
201+
202+
docker compose exec client sh -lc \
203+
'PGCONNECT_TIMEOUT=8 PGPASSWORD=postgres psql -h 127.0.0.1 -p 5499 -U postgres -c "select 1;"'
204+
```
205+
206+
The connection fails: Pomerium requires authentication before it will open the tunnel, so `psql` never reaches Postgres. The authorize log shows the denial:
207+
208+
```bash
209+
docker compose logs pomerium | grep secure.localhost.pomerium.io
210+
```
211+
212+
```json
213+
"host":"secure.localhost.pomerium.io:5432"
214+
"allow":false
215+
"allow-why-false":["user-unauthenticated"]
216+
```
217+
218+
## Require authentication in production
219+
220+
The first lab route is public to keep the example self-contained. In production, drop `allow_public_unauthenticated_access` and attach a policy, as the `secure` route above does:
221+
222+
```yaml
223+
routes:
224+
- from: tcp+https://pgsql.example.com:5432
225+
to: tcp://postgres.internal:5432
226+
policy:
227+
- allow:
228+
and:
229+
- email:
230+
is: data-team@example.com
231+
```
232+
233+
You also need an identity provider. To avoid running your own, point `authenticate_service_url` at Pomerium's hosted authenticate service (`https://authenticate.pomerium.app`), as the [Get Started guide](/docs/get-started/fundamentals/core/get-started) does, or use [Pomerium Zero](/docs/get-started/fundamentals/zero/zero-build-routes). The first time `pomerium-cli` opens a protected tunnel it runs the OIDC login flow, and that login uses the same `--forward-proxy` as the tunnel. For non-interactive access, [Pomerium Zero and Enterprise](/docs/capabilities/service-accounts) issue service-account tokens you pass with `--service-account-file`.
234+
235+
## Tunnel SSH instead
236+
237+
The same pattern works for SSH. Use `pomerium-cli` as an SSH `ProxyCommand` and add the proxy with `--forward-proxy` (or `HTTPS_PROXY`):
238+
239+
```ssh-config
240+
Host db-bastion
241+
HostName ssh.example.com
242+
Port 22
243+
ProxyCommand pomerium-cli tcp --listen - %h:%p --pomerium-url https://pomerium.example.com --forward-proxy http://proxy.internal:3128
244+
```
245+
246+
For SSH-specific networking, including running Pomerium behind an L4 edge, see [SSH over port 443 through an L4 edge](/docs/guides/ssh-tcp-l4-passthrough).
247+
248+
## Troubleshoot
249+
250+
| Symptom | Likely cause | Fix |
251+
| --- | --- | --- |
252+
| No `CONNECT` line in the Squid log | No proxy was selected, or `NO_PROXY` matched the edge host | Check `--forward-proxy`, `HTTPS_PROXY`, and `NO_PROXY`. |
253+
| `proxy CONNECT failed: 407` | The proxy requires authentication | Add credentials to the proxy URL, URL-encoding special characters. |
254+
| TLS error reaching Pomerium | The edge certificate is not trusted | Pass `--alternate-ca-path`, or install the CA. |
255+
| TLS error reaching an `https://` proxy | The proxy certificate is not trusted by the OS | Install the proxy CA into the system trust store; `--alternate-ca-path` does not affect the proxy hop. |
256+
| `pomerium-cli udp` ignores the proxy | Expected | This feature applies to `pomerium-cli tcp` only. |
257+
258+
## Clean up
259+
260+
```bash
261+
docker compose down -v
262+
rm -f certs/ca.crt certs/ca.key certs/ca.srl certs/pomerium.crt certs/pomerium.csr certs/pomerium.key
263+
```
264+
265+
## Reference
266+
267+
The full Compose file and Squid config:
268+
269+
<CodeBlock language="yaml" title="docker-compose.yml">
270+
{ComposeFile}
271+
</CodeBlock>
272+
273+
<CodeBlock language="squid" title="squid/squid.conf">
274+
{SquidConf}
275+
</CodeBlock>
276+
277+
## More resources
278+
279+
- [TCP support](/docs/capabilities/non-http/tcp)
280+
- [PostgreSQL over TCP](/docs/capabilities/non-http/examples/postgres)
281+
- [Pomerium Policy Language](/docs/internals/ppl)
282+
- [Access Log Fields](/docs/reference/access-log-fields)
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
certs/ca.crt
2+
certs/ca.key
3+
certs/ca.srl
4+
certs/pomerium.crt
5+
certs/pomerium.csr
6+
certs/pomerium.key

0 commit comments

Comments
 (0)