Skip to content
ProxyForge

How to set up a Selenium proxy with authentication

ProxyForge engineeringUpdated 7 min read

A Selenium proxy is configured through browser options: --proxy-server=http://host:port for Chrome, or network.proxy.* preferences for Firefox. Neither accepts a username and password, so the clean way to use an authenticated proxy with Selenium is to allowlist the IP your browsers connect from and pass only the host and port. When allowlisting is not possible, run a small local forwarder that adds the credentials, or, in Firefox, answer the proxy's challenge with Selenium's BiDi authentication handler.

This guide covers each route with Selenium 4 Python examples, then the parts that differ on Selenium Grid, and the timeouts, retries and exit-IP checks every proxied browser job needs. The examples read connection details from the environment: PROXY_SERVER holds a host and port with no credentials, like http://gateway.proxyforge.io:PORT, and PROXY_URL holds the full connection string, like http://USERNAME:[email protected]:PORT. Your dashboard generates both for the country and session mode you choose.

Why Chrome's --proxy-server has no place for credentials

Chrome's proxy switch takes a scheme, host and port. There is no field for a username or password, and in our tests a value containing credentials made every navigation fail with net::ERR_NO_SUPPORTED_PROXIES. When an authenticating proxy challenges Chrome with a 407, Chrome expects someone to answer: a human at a login prompt, or a tool that intercepts the challenge. A plain Selenium proxy setup with ChromeDriver has neither.

That leaves three workable designs:

Approach Browsers Credentials live Best for
IP allowlist Chrome, Firefox, Edge Nowhere on the client Fixed egress: servers, CI with static NAT, Grid nodes
Local authenticating forwarder Any On the machine running the browser Changing egress, laptops, shared runners
Firefox BiDi authentication handler Firefox In the test process Firefox-only suites that cannot allowlist

Playwright and Puppeteer handle the challenge in the automation library itself, which is why their proxy setup is shorter; see the Playwright proxy guide and the Puppeteer proxy guide if you have a choice of tool.

The clean path: allowlist the IP and pass only host and port

With your egress address on the allowlist, the gateway accepts connections without credentials, and the Selenium proxy configuration shrinks to the address of the gateway. For Chrome:

import os

from selenium import webdriver
from selenium.webdriver.common.by import By

options = webdriver.ChromeOptions()
options.add_argument("--headless=new")
options.add_argument(f"--proxy-server={os.environ['PROXY_SERVER']}")

driver = webdriver.Chrome(options=options)
try:
    driver.set_page_load_timeout(30)
    driver.get("https://api.ipify.org?format=json")
    print(driver.find_element(By.TAG_NAME, "body").text)
finally:
    driver.quit()

For Firefox, set the manual proxy preferences on the options object, which Selenium writes into the temporary profile it creates:

import os
from urllib.parse import urlsplit

from selenium import webdriver
from selenium.webdriver.common.by import By

proxy = urlsplit(os.environ["PROXY_SERVER"])

options = webdriver.FirefoxOptions()
options.add_argument("-headless")
options.set_preference("network.proxy.type", 1)
options.set_preference("network.proxy.http", proxy.hostname)
options.set_preference("network.proxy.http_port", proxy.port)
options.set_preference("network.proxy.ssl", proxy.hostname)
options.set_preference("network.proxy.ssl_port", proxy.port)
options.set_preference("network.proxy.no_proxies_on", "localhost,127.0.0.1")

driver = webdriver.Firefox(options=options)
try:
    driver.set_page_load_timeout(30)
    driver.get("https://api.ipify.org?format=json")
    print(driver.find_element(By.TAG_NAME, "body").text)
finally:
    driver.quit()

network.proxy.type set to 1 means manual configuration. The ssl pair covers HTTPS pages, which is most of the web; forgetting it is the Firefox equivalent of setting only http in a Python proxies dictionary. For SOCKS5, set network.proxy.socks, network.proxy.socks_port, network.proxy.socks_version to 5 and network.proxy.socks_remote_dns to True instead, so hostnames are resolved at the exit rather than locally. SOCKS5 in either browser also depends on allowlisting, since neither will send SOCKS credentials from Selenium.

To find the address to allowlist, request an IP echo service from the machine that runs the browser, without a proxy. Behind NAT, that is the NAT gateway's public address, not the interface address. IP allowlist vs username and password covers the trade-offs of each mode in more depth, including what happens when egress addresses change.

When you cannot allowlist: a local authenticating forwarder

If the browser's egress IP changes, as it does on laptops and shared CI runners, a forwarder on the same machine can hold the credentials instead. The browser talks to 127.0.0.1 without authentication, and the forwarder relays each connection to the gateway with a Proxy-Authorization header added. This is enough for HTTP and HTTPS through CONNECT:

import asyncio
import base64
import os
from urllib.parse import unquote, urlsplit

upstream = urlsplit(os.environ["PROXY_URL"])
credentials = f"{unquote(upstream.username)}:{unquote(upstream.password)}"
auth_header = b"Proxy-Authorization: Basic " + base64.b64encode(credentials.encode())


async def pipe(reader, writer):
    try:
        while data := await reader.read(65536):
            writer.write(data)
            await writer.drain()
    finally:
        writer.close()


async def handle(client_reader, client_writer):
    head = await client_reader.readuntil(b"\r\n\r\n")
    lines = [
        line for line in head.split(b"\r\n")
        if line and not line.lower().startswith((b"proxy-authorization:", b"proxy-connection:", b"connection:"))
    ]
    lines[1:1] = [auth_header, b"Connection: close"]
    upstream_reader, upstream_writer = await asyncio.open_connection(upstream.hostname, upstream.port)
    upstream_writer.write(b"\r\n".join(lines) + b"\r\n\r\n")
    await asyncio.gather(
        pipe(client_reader, upstream_writer),
        pipe(upstream_reader, client_writer),
        return_exceptions=True,
    )


async def main():
    port = int(os.environ.get("FORWARDER_PORT", "8899"))
    server = await asyncio.start_server(handle, "127.0.0.1", port)
    async with server:
        await server.serve_forever()


asyncio.run(main())

Run it next to the browser and point Selenium at it with --proxy-server=http://127.0.0.1:8899, or the Firefox preferences above with host 127.0.0.1 and port 8899. Properties worth knowing before you rely on it:

  • It does not decrypt anything. HTTPS pages pass through as an opaque CONNECT tunnel, so there is no certificate to install and nothing changes for the target.
  • It binds to localhost only. Keep it that way; a forwarder listening on a public interface is an open proxy that spends your bandwidth.
  • It forces Connection: close on each proxy connection. That keeps the credential injection correct for plain HTTP, at the cost of connection reuse for http:// URLs. HTTPS tunnels are unaffected.
  • It is deliberately minimal. For a long-lived production service, an established proxy server that supports upstream authentication gives you logging, limits and health checks. The design is the same.

We tested this forwarder with Chrome and Firefox under Selenium 4, over both HTTP and HTTPS.

The Firefox alternative: BiDi authentication

Selenium 4's WebDriver BiDi support includes a network authentication handler. With options.enable_bidi = True, driver.network.add_auth_handler(username, password) answers authentication challenges from inside the test. In our tests with Selenium 4.49 and Firefox 156, it answered the gateway's 407 and the page loaded through the proxy. With Chrome 154 and ChromeDriver, the same call did not answer the proxy challenge and the navigation never completed. Treat it as a Firefox option, and re-test when you upgrade either the browser or Selenium.

Why not selenium-wire

selenium-wire was the common answer to this problem for years. Its repository is now archived and its maintainer states it is no longer maintained. It also works by running a local intercepting proxy that decrypts TLS with its own certificate authority, which changes the TLS handshake the target sees and adds a component that no longer receives security fixes. Existing suites that depend on it will keep running until something changes underneath them; new work should use one of the approaches above.

Selenium Grid and remote browsers

On Grid, the browser runs on a node, not on the machine running your test code. Every part of the Selenium proxy setup follows the browser:

  1. The allowlisted IP is the node's egress address. For nodes in containers or behind NAT, that is the NAT gateway's public IP. Allowlisting your laptop or CI runner does nothing for a remote browser.
  2. A forwarder must run where the browser can reach it, normally as a sidecar on each node. A 127.0.0.1 address in the browser options refers to the node itself.
  3. Credentials go to the node environment, not the test code. With a sidecar forwarder, the test only ever sees http://127.0.0.1:8899, and the proxy credentials never cross the Grid protocol.
  4. Options travel with the session request. Proxy arguments and preferences set on the options object are sent to the node as capabilities, so the same options work with webdriver.Remote(command_executor=..., options=options).

If you use a hosted Grid, you usually cannot allowlist its shared egress addresses, and you cannot run a sidecar on its nodes. Check whether the vendor offers a tunnel or proxy setting of its own before designing around it.

Timeouts, retries and verifying the exit IP

Selenium's page load timeout defaults to 300 seconds, which is far too long for proxied traffic. Set it explicitly with driver.set_page_load_timeout(30), and use WebDriverWait for the element you need rather than waiting for full page load.

For retries, treat the WebDriver session as the unit:

  1. On a TimeoutException or a proxy connection error, quit the driver and start a new session, with a new sticky session from your provider if you use one.
  2. On a target-side 403, 429 or challenge page, back off before retrying. Proxy 403 and 429 errors explains how to separate target-side blocks from proxy failures.
  3. Cap attempts per URL and log which proxy session served each attempt.

Browsers open many connections per page, so pair each WebDriver session with a sticky session rather than a per-request rotating endpoint; an IP that changes between the page and its API calls looks suspicious to many sites. Rotating vs sticky proxies covers how long a session should last.

Verify the exit from inside the browser, as the examples do, at the start of each session: load an IP echo page, read the address, and fail fast if it is your own. A curl check from the host proves nothing about a browser that is configured separately.

Which proxy line fits a Selenium workload

Selenium suites are often QA and monitoring rather than large-scale collection, which shapes the choice:

Workload Line Notes
Geo-specific QA of consumer-facing pages Residential Sticky up to 60 minutes per WebDriver session
Stable test identities, allowlisted Grid nodes ISP Static, dedicated addresses, billed per address
Functional tests against your own sites Datacenter Dedicated addresses from our own ASN; cheapest per request
Mobile web and carrier-dependent flows Mobile Sticky up to 30 minutes; pair with a mobile viewport

For ad and landing-page checks across countries, the ad verification use case describes a typical setup.

Running Selenium on ProxyForge

Every ProxyForge line supports IP allowlist authentication alongside username and password, which makes the clean path above available on every line: allowlist your Grid nodes' egress addresses and pass only the host and port. Keep the allowlist entries for QA and production egress addresses documented separately, so removing one environment never cuts off the other.

Pricing is pay-as-you-go from a prepaid wallet, from 1 GB or 1 IP, with a paid trial for ISP and datacenter; see the pricing page. If your suites already run against another provider, the migration process mirrors your current endpoint and authentication format so the suite can run against both during the comparison.

FAQ

Related questions

Can I pass a username and password in Selenium's Proxy capability?

No. The WebDriver proxy capability carries the proxy host and port, not HTTP proxy credentials, and Chrome rejects credentials in its --proxy-server switch. Authenticate with an IP allowlist, a local forwarder, or a browser-level authentication handler.

Is selenium-wire still a good way to add proxy authentication?

Its repository has been archived and its maintainer states it is no longer maintained. It also works by intercepting TLS with its own certificate authority, which changes what the target sees. It is not a sound default for new work.

Which IP do I allowlist when tests run in Docker?

The public address the container's traffic leaves from, which is usually the host's or the NAT gateway's address, not the container IP. Check it by requesting an IP echo service from inside the container without a proxy.

Does headless mode change how the proxy works?

No. Headless Chrome and Firefox use the same proxy settings and the same authentication behavior as headed browsers. The difference is that a headed browser may show a login prompt for an unanswered proxy challenge, while a headless one cannot, so the navigation fails or hangs until the timeout.

Run it on a network you can account for

Order from 1 GB or 1 IP with no monthly minimum, or talk to an engineer about your workload first.

One business day, from a named engineer.