Proxy Authentication for AI Agents: API Keys, Rotating Credentials, and Session Tokens
Most agents fail on proxy auth not because the method is wrong, but because the credential is in the wrong place, in the wrong format, or gets rotated at the wrong time.
Proxy authentication for AI agents has three layers most guides ignore: the format of the credential, where it's stored, and when rotation happens relative to active sessions. Getting any one wrong silently breaks your agent.
- Two proxy auth methods that matter: username/password embedded in the proxy URL, or IP whitelisting. Username/password works from any server. IP whitelisting is cleaner for fixed-IP production infrastructure.
- Special characters in passwords must be URL-encoded. A password containing @, :, /, or # will break the proxy URL silently. The agent makes requests. Nothing gets proxied. No obvious error.
- Session tokens control sticky behavior. Embed a session ID in your credentials to hold the same IP across requests. Critical for multi-step agent workflows where IP changes break task state.
- Rotate credentials at task boundaries, not between requests. In-flight rotation breaks session continuity. Assign one credential per task and hold it for the task's duration.
- Don't store proxy credentials in env vars for production. LangSmith traces can capture env context. Use a secrets manager and inject credentials at runtime.
Proxy authentication for AI agents sounds simple until you hit the first silent failure. The agent runs. Requests go out. Nothing gets blocked outright. But your IP is still your server's IP, your session breaks every ten minutes, or half your agent crew is sharing one credential and fighting over a session ID. The thing is, most proxy auth problems are not about which method you chose. They're about the details of implementation that the docs don't cover.
This guide covers all three layers: the two authentication methods and when to use each, how session tokens work with sticky sessions in agent pipelines, and how to handle rotating credentials without breaking active task state. There's working code throughout.
The Two Proxy Authentication Methods That Actually Matter
Proxy providers offer two ways to verify that a request is authorized to use the proxy. Both work. The choice depends on your infrastructure.
| Method | How It Works | Best For | Watch Out For |
|---|---|---|---|
| Username/Password | Credentials embedded in proxy URL or sent via Proxy-Authorization header | Dynamic deployments, cloud functions, local development, any setup where your IP changes | Special characters in passwords that need URL-encoding; credentials appearing in logs |
| IP Whitelisting | Your server's IP is registered as authorized in the proxy dashboard | Fixed-IP production servers, dedicated VMs, corporate infrastructure | Breaks when your server IP changes; one whitelist per account, so shared pools get complicated |
For most AI agent deployments, username/password is the right starting point. Cloud-hosted agents, containerized pipelines, and local development all involve IP addresses that change. IP whitelisting on a dynamic IP means your agent loses proxy access every time a container restarts on a new host. That said, if you're running agents on a fixed dedicated server where the IP genuinely doesn't change, whitelisting is cleaner. No credentials in the URL, no credential rotation to manage.
The two methods can also be used together. Whitelist your primary production server, use username/password for everything else. Honestly, this is the configuration I'd recommend for any serious deployment.
Username and Password Authentication: The Working Implementation
The Basic URL Format
The standard way to authenticate a proxy via username/password is embedding the credentials directly in the proxy URL. The format is:
# HTTP/HTTPS targets — global residential pool, port 6011
http://username:[email protected]:6011
# SOCKS5 — separate host, port 6014
socks5://username:[email protected]:6014
# With targeting params appended to password (colon-separated)
http://username:password:[email protected]:6011
http://username:password:country-us:[email protected]:6011
In Python's requests library, pass this as the proxies dict value:
import requests
proxies = {
"http": "http://your_username:your_password:[email protected]:6011",
"https": "http://your_username:your_password:[email protected]:6011",
}
response = requests.get("https://httpbin.org/ip", proxies=proxies, timeout=15)
print(response.json()) # Should show the proxy IP, not your server IP
The Special Character Problem
This is the silent failure that trips people up most often. If your password contains any of these characters: @, :, /, ?, #, %, or +, the URL parser splits on them incorrectly and the credential format breaks. The proxy rejects authentication. No meaningful error is thrown. Your requests go through without proxying.
URL-encode the password before embedding it. Python's urllib.parse.quote handles this correctly:
import requests
from urllib.parse import quote
username = "your_username"
password = "p@ss:word/with#special&chars" # This would break a plain URL
# URL-encode ONLY the password, not the full URL
encoded_password = quote(password, safe="")
proxy_url = f"http://{username}:{encoded_password}:[email protected]:6011"
proxies = {"http": proxy_url, "https": proxy_url}
response = requests.get("https://httpbin.org/ip", proxies=proxies, timeout=15)
print(response.json())
Using requests.auth.HTTPProxyAuth
There's an alternative to embedding credentials in the URL. requests.auth.HTTPProxyAuth handles the Proxy-Authorization header directly, which avoids the URL-encoding problem entirely:
import requests
from requests.auth import HTTPProxyAuth
proxies = {
"http": "http://geox.torchproxies.com:6011",
"https": "http://geox.torchproxies.com:6011",
}
auth = HTTPProxyAuth("your_username", "p@ss:word/with#special")
response = requests.get(
"https://httpbin.org/ip",
proxies=proxies,
auth=auth,
timeout=15
)
print(response.json())
The tradeoff: HTTPProxyAuth sends the credential as a Proxy-Authorization header. For HTTPS targets, some proxy implementations don't forward this header correctly through the CONNECT tunnel. If you're seeing 407 errors on HTTPS targets but not HTTP, the embedded URL approach is more reliable.
IP Whitelisting: Setup and When It Breaks
IP whitelisting adds your server's IP address to an approved list in your proxy provider dashboard. Any request arriving from that IP is automatically authorized, no credentials needed in the URL.
The setup from the TorchProxies dashboard: navigate to your proxy settings, find the IP Whitelisting section, add your server's public IP. Requests from that IP use the proxy without credentials. All other IPs get rejected.
import requests
# With IP whitelisting, no username:password in the URL
proxies = {
"http": "http://geox.torchproxies.com:6011",
"https": "http://geox.torchproxies.com:6011",
}
response = requests.get("https://httpbin.org/ip", proxies=proxies, timeout=15)
print(response.json())
The three ways IP whitelisting breaks for AI agents:
First, containerized agents. Docker containers and Kubernetes pods frequently get new host IPs on restart. Your whitelisted IP is the old container's IP. The new container gets a 407. This is the most common failure mode in cloud deployments.
Second, scaling out. If your agent runs on multiple instances, each instance needs its IP whitelisted separately. That's manageable at 2-3 instances. At 50, it becomes a maintenance problem.
Third, cloud function cold starts. AWS Lambda, Google Cloud Functions, and Azure Functions assign IPs from large, rotating pools. Whitelisting is essentially impossible for function-based agent deployments.
For fixed dedicated servers running long-lived agent processes, IP whitelisting is the cleaner option. For anything cloud-native and dynamically scaled, use username/password authentication.
Session Tokens and Sticky Sessions: How They Work in Agent Pipelines
This is the part most proxy auth guides miss completely.
A sticky session tells the proxy server to return the same IP address for multiple requests within a time window. For AI agents doing multi-step tasks (a browser agent navigating checkout, an account manager maintaining a LinkedIn session, a scraper collecting paginated results), this is not optional. IP rotation between steps breaks session state on the target platform and forces re-authentication.
Sticky sessions are enabled by embedding a session identifier in your proxy credentials. The format varies by provider, but the concept is universal: the session ID tells the proxy which pool of sticky sessions to draw from and which specific session to maintain.
TorchProxies Sticky Session Format
On TorchProxies Plan X, sticky sessions require no dashboard setup. The session ID is appended directly to the password, colon-separated, alongside any other targeting parameters. The session ID must be exactly 8 lowercase alphanumeric characters and persists for up to 168 hours:
import requests
import uuid
# Generate a unique session ID for this task
session_id = str(uuid.uuid4()).replace("-", "")[:8] # exactly 8 chars required
# Session ID appended to password, colon-separated
# Format: username:password:country-XX:session-XXXXXXXX@host:port
proxy_username = "your_username"
proxy_password = f"your_password:country-us:session-{session_id}"
proxy_host = "geox.torchproxies.com"
proxy_port = 6011
proxy_url = f"http://{proxy_username}:{proxy_password}@{proxy_host}:{proxy_port}"
proxies = {"http": proxy_url, "https": proxy_url}
# Both requests will use the same IP because they share a session ID
r1 = requests.get("https://httpbin.org/ip", proxies=proxies, timeout=15)
r2 = requests.get("https://httpbin.org/ip", proxies=proxies, timeout=15)
print("First request IP:", r1.json()["origin"])
print("Second request IP:", r2.json()["origin"])
# Both should show the same IP
One Session ID Per Agent Task
The right pattern for multi-agent systems is one unique session ID per task, assigned at task creation and held for the task's duration. Don't share session IDs across agents running different tasks concurrently. Two agents sharing a session ID share an IP, which creates two problems: it links their identity on the target platform, and it means each new request from either agent renews the session timer from the other's perspective, creating unpredictable session expiry behavior.
import requests
import uuid
from dataclasses import dataclass, field
from typing import Optional
@dataclass
class AgentProxyConfig:
username: str
password: str
host: str = "geox.torchproxies.com"
port: int = 6011
session_id: str = field(default_factory=lambda: str(uuid.uuid4()).replace("-", "")[:8]) # 8 chars required
def as_proxies(self) -> dict:
# Session appended to password, colon-separated
proxy_pass = f"{self.password}:country-us:session-{self.session_id}"
url = f"http://{self.username}:{proxy_pass}@{self.host}:{self.port}"
return {"http": url, "https": url}
# Each agent task gets its own session ID automatically
researcher_proxy = AgentProxyConfig(username="your_username", password="your_password")
analyst_proxy = AgentProxyConfig(username="your_username", password="your_password")
# researcher_proxy.session_id != analyst_proxy.session_id
# They will use different IPs from the proxy pool
print("Researcher session:", researcher_proxy.session_id)
print("Analyst session:", analyst_proxy.session_id)
Rotating Credentials Without Breaking Active Sessions
Credential rotation is a security best practice. Rotating proxy credentials periodically limits the damage from a leaked credential. But rotation done wrong breaks agent sessions mid-task, causes cascading re-authentication failures, and wastes LLM API tokens on recovery.
The rule is simple. Rotate at task boundaries, not between requests.
What this looks like in practice: maintain a credential pool. Each task draws one credential from the pool at task start. That credential is held for the entire task duration. When the task completes, the credential is returned to the pool. New tasks draw from the pool again. Rotation happens at the pool level, not during active sessions.
import requests
import threading
import random
from contextlib import contextmanager
class ProxyCredentialPool:
"""Thread-safe credential pool for multi-agent setups."""
def __init__(self, credentials: list[dict]):
# Each credential: {"username": "...", "password": "..."}
self._pool = credentials.copy()
self._in_use = []
self._lock = threading.Lock()
@contextmanager
def acquire(self):
"""Acquire a credential for the duration of a task."""
cred = None
with self._lock:
available = [c for c in self._pool if c not in self._in_use]
if not available:
raise RuntimeError("No proxy credentials available")
cred = random.choice(available)
self._in_use.append(cred)
try:
yield cred
finally:
with self._lock:
if cred in self._in_use:
self._in_use.remove(cred)
def as_proxies(self, cred: dict) -> dict:
user = cred["username"]
pw = cred["password"]
url = f"http://{user}:{pw}:[email protected]:6011"
return {"http": url, "https": url}
# Usage: each agent task acquires and releases one credential
pool = ProxyCredentialPool([
{"username": "user_a", "password": "pass_a"},
{"username": "user_b", "password": "pass_b"},
{"username": "user_c", "password": "pass_c"},
])
with pool.acquire() as cred:
proxies = pool.as_proxies(cred)
r = requests.get("https://httpbin.org/ip", proxies=proxies, timeout=15)
print("Task completed with IP:", r.json()["origin"])
# Credential automatically released when the with block exits
Handling 407 Proxy Authentication Required in Agent Pipelines
HTTP 407 is what the proxy server sends when it rejects your authentication. It's different from 403 (which comes from the target site) and 429 (rate limiting). A 407 means the proxy itself turned away your request before it reached anyone.
The four most common causes for AI agents:
Expired credentials. Proxy provider credentials have TTLs. If your agent has been running for days with a credential that expired overnight, every request after expiry returns 407.
IP not whitelisted. If you use IP whitelisting and your server was re-provisioned, the new IP isn't in the whitelist yet. Switch to username/password auth or add the new IP.
Unencoded special characters. The URL parser splits on @, :, or / in the password. The credential looks malformed to the proxy server.
HTTPS header propagation. Some HTTP libraries don't forward the Proxy-Authorization header through HTTPS CONNECT tunnels correctly. Use the credential-in-URL format rather than HTTPProxyAuth for HTTPS targets.
import requests
import time
from requests.exceptions import ProxyError
def fetch_with_auth_retry(url: str, proxies: dict, max_retries: int = 2) -> str:
for attempt in range(max_retries):
try:
r = requests.get(url, proxies=proxies, timeout=30)
if r.status_code == 407:
# 407 = proxy rejected auth. Not a target-site block.
# Check: credentials expired? IP not whitelisted?
raise ValueError(
f"Proxy authentication failed (407). Check credentials. URL: {url}"
)
if r.status_code == 403:
# 403 = target site blocked the request (bot detection)
# Different problem: IP quality or behavioral detection
if attempt < max_retries - 1:
time.sleep(2 ** attempt)
continue
raise ValueError(f"Target site blocked request (403). URL: {url}")
r.raise_for_status()
return r.text
except ProxyError as e:
# ProxyError wraps connection failures to the proxy itself
raise ConnectionError(f"Cannot reach proxy server: {e}") from e
raise RuntimeError(f"All {max_retries} attempts failed for {url}")
Credential Security: What Actually Goes Wrong in Production
Proxy credentials are less sensitive than your LLM API key, but they still control access to your proxy plan's bandwidth and account. Leaked proxy credentials result in someone else consuming your bandwidth, getting your IPs flagged for abuse, and leaving your agent without proxy access at the worst moment.
The LangSmith Trace Problem
This is one that catches teams off guard. If you're using LangSmith for agent observability and you set HTTP_PROXY or HTTPS_PROXY as environment variables, LangSmith may capture those values as part of the execution context trace, particularly if a tool call includes environment inspection or if an error traceback includes the proxy URL string.
The same applies to any LLM observability platform that captures full environment state: Langfuse, Helicone, and similar tools. A print(os.environ) call anywhere in your agent code will log your proxy credentials to whatever logging infrastructure you're using.
The fix is not to stop using observability. It's to handle proxy credentials the same way you handle API keys:
import os
import boto3 # AWS Secrets Manager
import json
from urllib.parse import quote
def get_proxy_credentials() -> dict:
"""Retrieve proxy credentials from AWS Secrets Manager at runtime."""
client = boto3.client("secretsmanager", region_name="us-east-1")
secret = client.get_secret_value(SecretId="torchproxies/credentials")
return json.loads(secret["SecretString"])
def build_proxy_url(username: str, password: str) -> str:
encoded_pw = quote(password, safe="")
return f"http://{username}:{encoded_pw}@geox.torchproxies.com:6011"
# Retrieve at runtime, not at import time
creds = get_proxy_credentials()
proxy_url = build_proxy_url(creds["username"], creds["password"])
# Only inject into the specific components that need it
# Don't set global env vars - they appear in every subprocess and trace
proxies = {"http": proxy_url, "https": proxy_url}
If you're not yet using a secrets manager, the minimum acceptable approach for development is a .env file with python-dotenv, making sure that file is in .gitignore and is never committed. That keeps credentials out of your source code, but they're still readable by any process with environment access. For anything running in production, AWS Secrets Manager, Google Cloud Secret Manager, or HashiCorp Vault are the right tools.
The August 2025 breach reported by Nango, where stolen OAuth tokens exposed 700+ customer environments, is a reminder that credential security in agent pipelines is a real attack surface, not a theoretical one. Machine identities now outnumber human identities 82 to 1 in enterprise environments. Every agent credential is a machine identity. Treat it accordingly.
Framework-Specific Notes: LangChain and CrewAI
LangChain: Auth via requests_kwargs
The cleanest way to pass proxy auth in LangChain is directly in the component's requests_kwargs rather than env vars. This scopes the credential to exactly the component that needs it and prevents it from appearing in the broader process environment:
from langchain_community.document_loaders import WebBaseLoader
from urllib.parse import quote
username = "your_username"
password = quote("your_password", safe="")
loader = WebBaseLoader(
web_paths=["https://example.com"],
requests_kwargs={
"proxies": {
"http": f"http://{username}:{password}@geox.torchproxies.com:6011",
"https": f"http://{username}:{password}@geox.torchproxies.com:6011",
},
"timeout": 30,
}
)
docs = loader.load()
CrewAI: Per-Agent Credential Assignment
For CrewAI agents that need separate IP identities, pass credentials per tool call rather than via global env vars. This prevents agents from sharing credentials and ensures each has an isolated proxy session:
from crewai_tools import BaseTool
from pydantic import Field
from urllib.parse import quote
import requests
class AuthenticatedWebTool(BaseTool):
name: str = "Authenticated Web Fetcher"
description: str = "Fetches a URL through an authenticated proxy"
username: str = Field(description="Proxy username")
password: str = Field(description="Proxy password (raw, not URL-encoded)")
def _run(self, url: str) -> str:
encoded_pw = quote(self.password, safe="")
proxy_url = f"http://{self.username}:{encoded_pw}@geox.torchproxies.com:6011"
proxies = {"http": proxy_url, "https": proxy_url}
r = requests.get(
url, proxies=proxies, timeout=30,
headers={"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"}
)
r.raise_for_status()
return r.text[:5000]
# Each agent gets its own credentials -- different identities on the proxy
researcher_tool = AuthenticatedWebTool(username="user_a", password="pass_a")
analyst_tool = AuthenticatedWebTool(username="user_b", password="pass_b")
TorchProxies Endpoint and Port Reference
Plan X proxies use pool-specific hosts and ports. Targeting parameters — country, city, state, and session ID — are appended to the password, colon-separated. There is no separate dashboard toggle for these; they go directly in the proxy string at connection time.
http://username:password:[email protected]:6011 Sticky session (session ID appended to password):
http://username:password:country-us:[email protected]:6011 SOCKS5:
socks5://username:password:[email protected]:6014 Session IDs must be exactly 8 lowercase alphanumeric characters. Maximum session duration: 168 hours. Use either city or state targeting — not both together.
Authentication supports both username/password and IP whitelisting. IP whitelisting is configured from the proxy settings page in the dashboard. Credentials for username/password authentication are generated from the same page.