Docs
Static egress quickstart
Send a job's traffic out through your dedicated IPv4 address, in proxy mode or with WireGuard, and check that the other side sees the address you expect.
Before you start
When your order is confirmed we send you what you need. Nothing here works until the destination has your address on its allowlist, so hand the address over first.
- Your dedicated IPv4 address. Give it to the owner of the database, partner API or firewall you want to reach. On Pro you have two addresses, one in France (GRA) and one in Germany (LIM): give them both.
- For the proxy: the node's DNS name, an HTTPS proxy port, a username and a password.
- For WireGuard: a configuration file that holds your private key, the tunnel address we assigned to you and the endpoint of our egress node.
- An allowlist of the destinations your jobs may reach, agreed with us. Everything else is refused. See Allowlist.
Credentials are shown once
The proxy password and the WireGuard private key are shown once, when they are created. We store a hash of the password and only the public half of the key, so we cannot show them again. If you lose them, ask us to rotate: the old ones stop working and you get new ones. Store them in your secret manager straight away. See Security notes.
The examples use placeholders and addresses from documentation ranges. Replace them with your own values.
| Placeholder | Meaning |
|---|---|
PROXY_HOST | The node's DNS name from your welcome email, for example 198-51-100-10.sslip.io. Use the name, not the bare address: the proxy's certificate is issued for the name |
HTTP_PORT | The proxy port from your welcome email. It speaks TLS |
USER, PASSWORD | Your proxy login. Every dedicated address has its own |
203.0.113.25 | Example of your dedicated IPv4 address |
api.partner.example | A destination on your allowlist |
ip-echo.example.com | A service on your allowlist that answers with the address it sees you connect from |
Proxy or WireGuard
| Proxy | WireGuard | |
|---|---|---|
| Good for | Tools that honour an HTTPS proxy setting: curl, package managers, SDKs, HTTP clients | Tools that ignore proxies, such as database clients, and jobs where you want no proxy settings |
| Allowlist entries | Host names (exact match) or address ranges, each with ports | Address ranges with ports. Host names are not used |
| What you need | Nothing installed, no privileges | WireGuard tools and root, or the NET_ADMIN capability in a container |
| Path to our egress node | The connection to the proxy is TLS, so the login and the host names you ask for are encrypted on the way, and HTTPS content stays encrypted end to end | The whole path is encrypted by WireGuard |
| Only some traffic | Set the proxy only on the steps or processes that need it | Route only the allowlisted destinations into the tunnel |
You can use both: the proxy for the HTTP calls, WireGuard for the database. Both leave from the same dedicated address.
Proxy mode
Each dedicated address has an HTTPS proxy port on the proxy host. You connect to it with TLS and your client checks the node's certificate against the host name, then sends your username and password inside the encrypted connection. The port accepts CONNECT only, so it carries HTTPS and other TCP tunnels, and it refuses a plain http:// request with 403. Use HTTPS destinations. There is no SOCKS5 port on the Internet: SOCKS5 sends the password in clear text, so it is not offered.
The proxy URL starts with https://. Your client needs support for an HTTPS proxy: curl 7.52 or later, Node 20 or later with undici, Python requests and Go do. If your client only knows http:// proxies, use WireGuard mode.
Environment variables
Most tools read the standard variables. Export them in the shell, the container or the job, and only for the processes that need the dedicated address.
export HTTPS_PROXY="https://USER:PASSWORD@PROXY_HOST:HTTP_PORT"
# Keep local traffic off the proxy
export NO_PROXY="localhost"
# Some tools only read the lower-case names
export https_proxy="$HTTPS_PROXY" no_proxy="$NO_PROXY"
HTTPS_PROXYsends HTTPS requests through the proxy port withCONNECT, inside TLS. The proxy resolves host names, which is what an allowlist entry that names a host needs.- The certificate is checked against the host name in the URL. If a client reports a certificate error, you used the bare IP address instead of the name, or the node's certificate has expired: see Troubleshooting. Do not switch certificate checking off.
- A username or password with characters such as
@,:or/must be percent-encoded inside a URL. The logins we generate use letters, digits,-and_, so they need no encoding. - Do not export these in a shared shell profile, and do not commit them. In CI, store the values as secrets, or use the GitHub Action.
Verify the source IP
Run this first. It asks a service on your allowlist which address it sees.
curl -sS --max-time 15 https://ip-echo.example.com/
# prints your dedicated IP, for example 203.0.113.25
If it prints a different address, the request did not go through the proxy: the client ignored the variable, or NO_PROXY matched the host. If it fails, see Troubleshooting. A service that is not on your allowlist is refused, so ask us to add the echo service you use, or use an endpoint of your own that logs the source address.
curl
With the variables exported, curl needs no options. Without them you can pass the proxy on the command line, but that puts the password in the process list. Reading the settings from standard input keeps the password off the command line and out of the shell history:
# EGRESS_USER and EGRESS_PASSWORD come from your secret manager
curl --config - https://api.partner.example/v1/status <<EOF
proxy = "https://PROXY_HOST:HTTP_PORT"
proxy-user = "$EGRESS_USER:$EGRESS_PASSWORD"
proxytunnel
EOF
Do not run set -x or curl -v in a job whose log others can read: they can print the login.
Python
requests reads HTTPS_PROXY from the environment.
import os
import requests
# For example https://USER:PASSWORD@PROXY_HOST:HTTP_PORT, kept in a secret
proxy = os.environ["EGRESS_PROXY_URL"]
response = requests.get(
"https://api.partner.example/v1/status",
proxies={"https": proxy},
timeout=15,
)
response.raise_for_status()
print(response.json())
Node.js
Node 22.21 and later, and Node 24, read the standard variables for fetch and for the http and https modules when you turn it on. Use an https:// proxy URL.
export HTTPS_PROXY="https://USER:PASSWORD@PROXY_HOST:HTTP_PORT"
NODE_USE_ENV_PROXY=1 node sync.js
On an older Node, use the undici package:
import { EnvHttpProxyAgent, setGlobalDispatcher } from "undici";
// Reads HTTPS_PROXY and NO_PROXY
setGlobalDispatcher(new EnvHttpProxyAgent());
const response = await fetch("https://api.partner.example/v1/status");
console.log(await response.json());
PostgreSQL and other clients without proxy support
psql and most database drivers do not speak HTTP proxies. Two ways to reach a database from your dedicated address:
- Use WireGuard mode and route only the database address into the tunnel. This is the simplest and needs no change to the client.
- Put a small local forwarder in front of the database that opens the
CONNECTtunnel over TLS for you, and pointpsqlat its local port. Keep the login in a file that only you can read.
Your allowlist needs the database host (or its address range) and port 5432. TLS to the database stays end to end. The proxy does not offer SOCKS5 on the Internet, so a SOCKS5 wrapper such as proxychains-ng has nothing to connect to.
WireGuard mode
In WireGuard mode your host holds a tunnel to our egress node, and the traffic you route into it leaves from your dedicated address. It works at the IP layer, so any client works, with no proxy settings.
The configuration
Save the file we sent you as penduses-egress.conf, readable by root only. Then set AllowedIPs to the destinations you want to send through the tunnel, and nothing else.
[Interface]
PrivateKey = <your-private-key>
# The tunnel address we assigned to you
Address = 100.64.12.7/32
[Peer]
PublicKey = <penduses-public-key>
Endpoint = <endpoint-from-your-welcome-email>:51820
# Only the allowlisted destinations go through the tunnel
AllowedIPs = 203.0.113.10/32, 198.51.100.64/28
PersistentKeepalive = 25
- Split routing. With
AllowedIPslimited to your destinations, everything else your host does (package downloads, DNS, the connection you administer it over) keeps its normal route. Do not use0.0.0.0/0for static egress: the allowlist would refuse every other destination. - Addresses, not names. WireGuard routes IP addresses. Look up the address of each destination and use it in
AllowedIPs. When a provider changes its addresses, update the list, or use proxy mode with a host name entry. - No
DNSline. Leave it out so the tunnel does not replace your resolver. - Match the allowlist. What you route must also be on the allowlist we apply, with the ports you use. Other traffic that reaches the tunnel is dropped, and so is ping.
Bring it up and verify
sudo wg-quick up ./penduses-egress.conf
# A recent handshake means the tunnel works
sudo wg show
# Ask a destination on your allowlist which address it sees
curl -sS --max-time 15 https://ip-echo.example.com/
# prints your dedicated IP, for example 203.0.113.25
psql "host=203.0.113.10 dbname=app user=app sslmode=require"
sudo wg-quick down ./penduses-egress.conf
For the check to work, the address of the echo service must be inside AllowedIPs and on your allowlist. No handshake after a few seconds usually means the endpoint is not reachable from your network or the keys do not match. See Troubleshooting.
In CI, the GitHub Action does these steps for you and takes the tunnel down when the job ends.
Next steps
- Allowlist: what to list and how changes work.
- GitHub Action: the same setup for your workflows.
- Pro failover: use the second address when the first does not answer.
- Troubleshooting: what an error means.
The pilot is best effort, with no SLA. Questions: support@penduses.com.